test: remove launcher polling waits

This commit is contained in:
Peter Steinberger
2026-05-11 16:35:23 +01:00
parent 80175cfb74
commit b29b2558a7

View File

@@ -1,4 +1,6 @@
import { spawn, spawnSync } from "node:child_process"; import { spawn, spawnSync } from "node:child_process";
import { once } from "node:events";
import { watch } from "node:fs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
@@ -37,26 +39,71 @@ async function addCompileCacheProbe(fixtureRoot: string): Promise<void> {
} }
async function waitForFile(filePath: string, timeoutMs: number): Promise<string> { async function waitForFile(filePath: string, timeoutMs: number): Promise<string> {
const start = Date.now(); try {
while (Date.now() - start < timeoutMs) { return await fs.readFile(filePath, "utf8");
try { } catch {
return await fs.readFile(filePath, "utf8"); // Wait below.
} catch {
await new Promise((resolve) => setTimeout(resolve, 50));
}
} }
throw new Error(`timed out waiting for ${filePath}`);
const signal = AbortSignal.timeout(timeoutMs);
return await new Promise<string>((resolve, reject) => {
let settled = false;
let watcher: ReturnType<typeof watch> | undefined;
const fileName = path.basename(filePath);
const cleanup = () => {
if (settled) {
return;
}
settled = true;
watcher?.close();
};
const tryRead = async () => {
try {
const content = await fs.readFile(filePath, "utf8");
cleanup();
resolve(content);
} catch {
// Keep watching until the deadline aborts.
}
};
signal.addEventListener(
"abort",
() => {
cleanup();
reject(new Error(`timed out waiting for ${filePath}`));
},
{ once: true },
);
watcher = watch(path.dirname(filePath), { signal }, (_event, changedFileName) => {
if (!changedFileName || changedFileName.toString() === fileName) {
void tryRead();
}
});
void tryRead();
});
} }
async function waitUntil(check: () => boolean, timeoutMs: number): Promise<boolean> { async function waitForProcessExit(
const start = Date.now(); child: ReturnType<typeof spawn>,
while (Date.now() - start < timeoutMs) { label: string,
if (check()) { timeoutMs: number,
return true; ): Promise<{ code: number | null; signal: NodeJS.Signals | null }> {
} if (child.exitCode !== null || child.signalCode !== null) {
await new Promise((resolve) => setTimeout(resolve, 50)); return { code: child.exitCode, signal: child.signalCode };
}
const signal = AbortSignal.timeout(timeoutMs);
try {
const [code, exitSignal] = (await once(child, "exit", { signal })) as [
number | null,
NodeJS.Signals | null,
];
return { code, signal: exitSignal };
} catch (error) {
throw new Error(`timed out waiting for ${label} to exit`, { cause: error });
} }
return check();
} }
function isProcessAlive(pid: number | undefined): boolean { function isProcessAlive(pid: number | undefined): boolean {
@@ -179,12 +226,14 @@ describe("openclaw launcher", () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots); const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addGitMarker(fixtureRoot); await addGitMarker(fixtureRoot);
const childInfoPath = path.join(fixtureRoot, "child-info.json"); const childInfoPath = path.join(fixtureRoot, "child-info.json");
const signalPath = path.join(fixtureRoot, "sigterm-received.txt");
await fs.writeFile( await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"), path.join(fixtureRoot, "dist", "entry.js"),
[ [
'import { writeFileSync } from "node:fs";', 'import { writeFileSync } from "node:fs";',
`writeFileSync(${JSON.stringify(childInfoPath)}, JSON.stringify({ pid: process.pid }) + "\\n");`,
'process.title = "openclaw-launcher-sigterm-test-child";', 'process.title = "openclaw-launcher-sigterm-test-child";',
`process.on("SIGTERM", () => { writeFileSync(${JSON.stringify(signalPath)}, "SIGTERM\\n"); process.exit(0); });`,
`writeFileSync(${JSON.stringify(childInfoPath)}, JSON.stringify({ pid: process.pid }) + "\\n");`,
"setInterval(() => {}, 1000);", "setInterval(() => {}, 1000);",
"", "",
].join("\n"), ].join("\n"),
@@ -206,7 +255,11 @@ describe("openclaw launcher", () => {
launcher.kill("SIGTERM"); launcher.kill("SIGTERM");
await waitUntil(() => !isProcessAlive(respawnChildPid), 5000); await expect(waitForProcessExit(launcher, "launcher", 5000)).resolves.toEqual({
code: 0,
signal: null,
});
await expect(fs.readFile(signalPath, "utf8")).resolves.toBe("SIGTERM\n");
expect(isProcessAlive(respawnChildPid)).toBe(false); expect(isProcessAlive(respawnChildPid)).toBe(false);
} finally { } finally {
if (isProcessAlive(respawnChildPid)) { if (isProcessAlive(respawnChildPid)) {
@@ -253,10 +306,10 @@ describe("openclaw launcher", () => {
launcher.kill("SIGTERM"); launcher.kill("SIGTERM");
await waitUntil( await expect(waitForProcessExit(launcher, "launcher", 5000)).resolves.toEqual({
() => !isProcessAlive(launcher.pid) && !isProcessAlive(respawnChildPid), code: 1,
5000, signal: null,
); });
expect(isProcessAlive(launcher.pid)).toBe(false); expect(isProcessAlive(launcher.pid)).toBe(false);
expect(isProcessAlive(respawnChildPid)).toBe(false); expect(isProcessAlive(respawnChildPid)).toBe(false);
} finally { } finally {