test: share scheduled task stop helpers

This commit is contained in:
Peter Steinberger
2026-03-13 23:51:23 +00:00
parent 4fe59edd84
commit e91a5c72de

View File

@@ -22,8 +22,10 @@ vi.mock("../infra/gateway-processes.js", () => ({
const { restartScheduledTask, resolveTaskScriptPath, stopScheduledTask } = const { restartScheduledTask, resolveTaskScriptPath, stopScheduledTask } =
await import("./schtasks.js"); await import("./schtasks.js");
const GATEWAY_PORT = 18789;
const SUCCESS_RESPONSE = { code: 0, stdout: "", stderr: "" } as const;
async function writeGatewayScript(env: Record<string, string>, port = 18789) { async function writeGatewayScript(env: Record<string, string>, port = GATEWAY_PORT) {
const scriptPath = resolveTaskScriptPath(env); const scriptPath = resolveTaskScriptPath(env);
await fs.mkdir(path.dirname(scriptPath), { recursive: true }); await fs.mkdir(path.dirname(scriptPath), { recursive: true });
await fs.writeFile( await fs.writeFile(
@@ -38,16 +40,57 @@ async function writeGatewayScript(env: Record<string, string>, port = 18789) {
); );
} }
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: [],
};
}
async function withPreparedGatewayTask(
run: (context: { env: Record<string, string>; stdout: PassThrough }) => Promise<void>,
) {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => {
await writeGatewayScript(env);
const stdout = new PassThrough();
await run({ env, stdout });
});
}
beforeEach(() => { beforeEach(() => {
resetSchtasksBaseMocks(); resetSchtasksBaseMocks();
findVerifiedGatewayListenerPidsOnPortSync.mockReset(); findVerifiedGatewayListenerPidsOnPortSync.mockReset();
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]); findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
inspectPortUsage.mockResolvedValue({ inspectPortUsage.mockResolvedValue(freePortUsage());
port: 18789,
status: "free",
listeners: [],
hints: [],
});
}); });
afterEach(() => { afterEach(() => {
@@ -56,75 +99,33 @@ afterEach(() => {
describe("Scheduled Task stop/restart cleanup", () => { describe("Scheduled Task stop/restart cleanup", () => {
it("kills lingering verified gateway listeners after schtasks stop", async () => { it("kills lingering verified gateway listeners after schtasks stop", async () => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => { await withPreparedGatewayTask(async ({ env, stdout }) => {
await writeGatewayScript(env); pushSuccessfulSchtasksResponses(3);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]); findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
inspectPortUsage inspectPortUsage
.mockResolvedValueOnce({ .mockResolvedValueOnce(busyPortUsage(4242))
port: 18789, .mockResolvedValueOnce(freePortUsage());
status: "busy",
listeners: [{ pid: 4242, command: "node.exe" }],
hints: [],
})
.mockResolvedValueOnce({
port: 18789,
status: "free",
listeners: [],
hints: [],
});
const stdout = new PassThrough();
await stopScheduledTask({ env, stdout }); await stopScheduledTask({ env, stdout });
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(18789); expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(GATEWAY_PORT);
expect(killProcessTree).toHaveBeenCalledWith(4242, { graceMs: 300 }); expect(killProcessTree).toHaveBeenCalledWith(4242, { graceMs: 300 });
expect(inspectPortUsage).toHaveBeenCalledTimes(2); expect(inspectPortUsage).toHaveBeenCalledTimes(2);
}); });
}); });
it("force-kills remaining busy port listeners when the first stop pass does not free the port", async () => { it("force-kills remaining busy port listeners when the first stop pass does not free the port", async () => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => { await withPreparedGatewayTask(async ({ env, stdout }) => {
await writeGatewayScript(env); pushSuccessfulSchtasksResponses(3);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]); findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([4242]);
inspectPortUsage.mockResolvedValueOnce({ inspectPortUsage.mockResolvedValueOnce(busyPortUsage(4242));
port: 18789,
status: "busy",
listeners: [{ pid: 4242, command: "node.exe" }],
hints: [],
});
for (let i = 0; i < 20; i += 1) { for (let i = 0; i < 20; i += 1) {
inspectPortUsage.mockResolvedValueOnce({ inspectPortUsage.mockResolvedValueOnce(busyPortUsage(4242));
port: 18789,
status: "busy",
listeners: [{ pid: 4242, command: "node.exe" }],
hints: [],
});
} }
inspectPortUsage inspectPortUsage
.mockResolvedValueOnce({ .mockResolvedValueOnce(busyPortUsage(5252))
port: 18789, .mockResolvedValueOnce(freePortUsage());
status: "busy",
listeners: [{ pid: 5252, command: "node.exe" }],
hints: [],
})
.mockResolvedValueOnce({
port: 18789,
status: "free",
listeners: [],
hints: [],
});
const stdout = new PassThrough();
await stopScheduledTask({ env, stdout }); await stopScheduledTask({ env, stdout });
expect(killProcessTree).toHaveBeenNthCalledWith(1, 4242, { graceMs: 300 }); expect(killProcessTree).toHaveBeenNthCalledWith(1, 4242, { graceMs: 300 });
@@ -134,36 +135,18 @@ describe("Scheduled Task stop/restart cleanup", () => {
}); });
it("falls back to inspected gateway listeners when sync verification misses on Windows", async () => { it("falls back to inspected gateway listeners when sync verification misses on Windows", async () => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => { await withPreparedGatewayTask(async ({ env, stdout }) => {
await writeGatewayScript(env); pushSuccessfulSchtasksResponses(3);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]); findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([]);
inspectPortUsage inspectPortUsage
.mockResolvedValueOnce({ .mockResolvedValueOnce(
port: 18789, busyPortUsage(6262, {
status: "busy", commandLine:
listeners: [ '"C:\\Program Files\\nodejs\\node.exe" "C:\\Users\\steipete\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js" gateway --port 18789',
{ }),
pid: 6262, )
command: "node.exe", .mockResolvedValueOnce(freePortUsage());
commandLine:
'"C:\\Program Files\\nodejs\\node.exe" "C:\\Users\\steipete\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js" gateway --port 18789',
},
],
hints: [],
})
.mockResolvedValueOnce({
port: 18789,
status: "free",
listeners: [],
hints: [],
});
const stdout = new PassThrough();
await stopScheduledTask({ env, stdout }); await stopScheduledTask({ env, stdout });
expect(killProcessTree).toHaveBeenCalledWith(6262, { graceMs: 300 }); expect(killProcessTree).toHaveBeenCalledWith(6262, { graceMs: 300 });
@@ -172,35 +155,18 @@ describe("Scheduled Task stop/restart cleanup", () => {
}); });
it("kills lingering verified gateway listeners and waits for port release before restart", async () => { it("kills lingering verified gateway listeners and waits for port release before restart", async () => {
await withWindowsEnv("openclaw-win-stop-", async ({ env }) => { await withPreparedGatewayTask(async ({ env, stdout }) => {
await writeGatewayScript(env); pushSuccessfulSchtasksResponses(4);
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: "", stderr: "" },
);
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([5151]); findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([5151]);
inspectPortUsage inspectPortUsage
.mockResolvedValueOnce({ .mockResolvedValueOnce(busyPortUsage(5151))
port: 18789, .mockResolvedValueOnce(freePortUsage());
status: "busy",
listeners: [{ pid: 5151, command: "node.exe" }],
hints: [],
})
.mockResolvedValueOnce({
port: 18789,
status: "free",
listeners: [],
hints: [],
});
const stdout = new PassThrough();
await expect(restartScheduledTask({ env, stdout })).resolves.toEqual({ await expect(restartScheduledTask({ env, stdout })).resolves.toEqual({
outcome: "completed", outcome: "completed",
}); });
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(18789); expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalledWith(GATEWAY_PORT);
expect(killProcessTree).toHaveBeenCalledWith(5151, { graceMs: 300 }); expect(killProcessTree).toHaveBeenCalledWith(5151, { graceMs: 300 });
expect(inspectPortUsage).toHaveBeenCalledTimes(2); expect(inspectPortUsage).toHaveBeenCalledTimes(2);
expect(schtasksCalls.at(-1)).toEqual(["/Run", "/TN", "OpenClaw Gateway"]); expect(schtasksCalls.at(-1)).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);