Files
openclaw/src/daemon/schtasks.stop.test.ts
Onur 900e291f31 CI: expand native release validation coverage (#67144)
* Actions: grant reusable release checks actions read

* Actions: use read-all for reusable release checks

* CI: add native cross-OS release checks

* CI: wire Discord smoke secrets for cross-OS checks

* CI: fix native cross-OS installer compatibility

* CI: skip empty pnpm cache saves in matrix jobs

* CI: honor workflow runner override envs

* CI: finish native cross-OS update checks

* CI: fix native cross-OS workflow regressions

* Installer: capture Windows npm stderr safely

* CI: harden cross-OS release checks

* CI: resolve reusable workflow harness ref

* CI: stabilize cross-OS dev update lanes

* CI: tighten release-check workflow semantics

* CI: repoint repaired git CLI on POSIX

* CI: repair native dev-update shell handoff

* CI: preserve real updater semantics

* CI: harden supported release-check refs

* CI: harden release-check refs and fresh mode

* CI: skip dev-update for immutable tag refs

* CI: repair fresh installer release checks

* CI: fix native release check installer lanes

* CI: install release checks from candidate artifacts

* CI: use Windows cmd shims in release checks

* Installer: run Windows npm shim via PowerShell

* CI: pin dev update verification to candidate sha

* CI: pin reusable harness and published installers

* CI: isolate Windows dev-update PATH validation

* CI: align Windows dev-update bootstrap validation

* CI: avoid Windows installer gateway flake

* CI: run cross-OS release checks via TypeScript

* CI: bootstrap tsx for release-check workflow

* CI: fix native release-check follow-ups

* CI: tighten dev-update release checks

* CI: peel annotated workflow refs

* CI: harden native release checks

* CI: fix release-check verifier drift

* CI: fix release-check workflow drift

* CI: fix release-check ref resolution

* CI: harden Windows release-check gateway startup

* CI: fix release-check fallback validation

* CI: harden cross-os release checks

* CI: pin dev-update release checks to candidate SHA

* CI: resolve remote dev target refs

* CI: detect cloned dev-update checkouts

* CI: harden Windows release-check launcher

* Windows: harden task fallback and runner overrides

* Release checks: preserve Windows PATH and baseline version reads

* CI: add release validation live lanes

* CI: expand live and e2e release coverage

* CI: add branch dispatch for live and e2e checks
2026-04-16 19:58:19 +02:00

206 lines
6.6 KiB
TypeScript

import { PassThrough } from "node:stream";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "./test-helpers/schtasks-base-mocks.js";
import {
inspectPortUsage,
killProcessTree,
resetSchtasksBaseMocks,
schtasksCalls,
schtasksResponses,
withWindowsEnv,
writeGatewayScript,
} from "./test-helpers/schtasks-fixtures.js";
const findVerifiedGatewayListenerPidsOnPortSync = vi.hoisted(() =>
vi.fn<(port: number) => number[]>(() => []),
);
const timeState = vi.hoisted(() => ({ now: 0 }));
const sleepMock = vi.hoisted(() =>
vi.fn(async (ms: number) => {
timeState.now += ms;
}),
);
vi.mock("../infra/gateway-processes.js", () => ({
findVerifiedGatewayListenerPidsOnPortSync: (port: number) =>
findVerifiedGatewayListenerPidsOnPortSync(port),
}));
vi.mock("../utils.js", async () => {
const actual = await vi.importActual<typeof import("../utils.js")>("../utils.js");
return {
...actual,
sleep: (ms: number) => sleepMock(ms),
};
});
const { restartScheduledTask, stopScheduledTask } = await import("./schtasks.js");
const GATEWAY_PORT = 18789;
const SUCCESS_RESPONSE = { code: 0, stdout: "", stderr: "" } as const;
function pushSuccessfulSchtasksResponses(count: number) {
for (let i = 0; i < count; i += 1) {
schtasksResponses.push({ ...SUCCESS_RESPONSE });
}
}
function freePortUsage() {
return {
port: GATEWAY_PORT,
status: "free" as const,
listeners: [],
hints: [],
};
}
function busyPortUsage(
pid: number,
options: {
command?: string;
commandLine?: string;
} = {},
) {
return {
port: GATEWAY_PORT,
status: "busy" as const,
listeners: [
{
pid,
command: options.command ?? "node.exe",
...(options.commandLine ? { commandLine: options.commandLine } : {}),
},
],
hints: [],
};
}
function expectGatewayTermination(pid: number) {
if (process.platform === "win32") {
expect(killProcessTree).not.toHaveBeenCalled();
return;
}
expect(killProcessTree).toHaveBeenCalledWith(pid, { graceMs: 300 });
}
async function withPreparedGatewayTask(
run: (context: { env: Record<string, string>; stdout: PassThrough }) => Promise<void>,
) {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => {
await writeGatewayScript(env, GATEWAY_PORT);
const stdout = new PassThrough();
await run({ env, stdout });
});
}
beforeEach(() => {
resetSchtasksBaseMocks();
findVerifiedGatewayListenerPidsOnPortSync.mockReset();
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
timeState.now = 0;
vi.spyOn(Date, "now").mockImplementation(() => timeState.now);
sleepMock.mockReset();
sleepMock.mockImplementation(async (ms: number) => {
timeState.now += ms;
});
inspectPortUsage.mockResolvedValue(freePortUsage());
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Scheduled Task stop/restart cleanup", () => {
it("kills lingering verified gateway listeners after schtasks stop", async () => {
await withPreparedGatewayTask(async ({ env, stdout }) => {
pushSuccessfulSchtasksResponses(3);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
inspectPortUsage
.mockResolvedValueOnce(busyPortUsage(4242))
.mockResolvedValueOnce(freePortUsage());
await stopScheduledTask({ env, stdout });
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(GATEWAY_PORT);
expectGatewayTermination(4242);
expect(inspectPortUsage).toHaveBeenCalledTimes(2);
});
});
it("force-kills remaining busy port listeners when the first stop pass does not free the port", async () => {
await withPreparedGatewayTask(async ({ env, stdout }) => {
pushSuccessfulSchtasksResponses(3);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
inspectPortUsage.mockResolvedValueOnce(busyPortUsage(4242));
for (let i = 0; i < 20; i += 1) {
inspectPortUsage.mockResolvedValueOnce(busyPortUsage(4242));
}
inspectPortUsage
.mockResolvedValueOnce(busyPortUsage(5252))
.mockResolvedValueOnce(freePortUsage());
await stopScheduledTask({ env, stdout });
if (process.platform !== "win32") {
expect(killProcessTree).toHaveBeenNthCalledWith(1, 4242, { graceMs: 300 });
expect(killProcessTree).toHaveBeenNthCalledWith(2, expect.any(Number), { graceMs: 300 });
} else {
expect(killProcessTree).not.toHaveBeenCalled();
}
expect(inspectPortUsage.mock.calls.length).toBeGreaterThanOrEqual(22);
});
});
it("falls back to inspected gateway listeners when sync verification misses on Windows", async () => {
await withPreparedGatewayTask(async ({ env, stdout }) => {
pushSuccessfulSchtasksResponses(3);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
inspectPortUsage
.mockResolvedValueOnce(
busyPortUsage(6262, {
commandLine:
'"C:\\Program Files\\nodejs\\node.exe" "C:\\Users\\steipete\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js" gateway --port 18789',
}),
)
.mockResolvedValueOnce(freePortUsage());
await stopScheduledTask({ env, stdout });
expectGatewayTermination(6262);
expect(inspectPortUsage).toHaveBeenCalledTimes(2);
});
});
it("kills lingering verified gateway listeners and waits for port release before restart", async () => {
await withPreparedGatewayTask(async ({ env, stdout }) => {
pushSuccessfulSchtasksResponses(4);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([5151]);
inspectPortUsage
.mockResolvedValueOnce(busyPortUsage(5151))
.mockResolvedValueOnce(freePortUsage());
await expect(restartScheduledTask({ env, stdout })).resolves.toEqual({
outcome: "completed",
});
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(GATEWAY_PORT);
expectGatewayTermination(5151);
expect(inspectPortUsage).toHaveBeenCalledTimes(2);
expect(schtasksCalls).toContainEqual(["/Run", "/TN", "OpenClaw Gateway"]);
});
});
it("throws when /Run fails during restart", async () => {
await withPreparedGatewayTask(async ({ env, stdout }) => {
schtasksResponses.push(
{ ...SUCCESS_RESPONSE },
{ ...SUCCESS_RESPONSE },
{ ...SUCCESS_RESPONSE },
{ code: 1, stdout: "", stderr: "ERROR: Access is denied." },
);
await expect(restartScheduledTask({ env, stdout })).rejects.toThrow(
"schtasks run failed: ERROR: Access is denied.",
);
expect(schtasksCalls.at(-1)).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
});
});
});