From b29b2558a7eaf8f143c838db9a6de687db57a11a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 11 May 2026 16:35:23 +0100 Subject: [PATCH] test: remove launcher polling waits --- test/openclaw-launcher.e2e.test.ts | 97 +++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 22 deletions(-) diff --git a/test/openclaw-launcher.e2e.test.ts b/test/openclaw-launcher.e2e.test.ts index 25e83c808f9..489028521ff 100644 --- a/test/openclaw-launcher.e2e.test.ts +++ b/test/openclaw-launcher.e2e.test.ts @@ -1,4 +1,6 @@ import { spawn, spawnSync } from "node:child_process"; +import { once } from "node:events"; +import { watch } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -37,26 +39,71 @@ async function addCompileCacheProbe(fixtureRoot: string): Promise { } async function waitForFile(filePath: string, timeoutMs: number): Promise { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - try { - return await fs.readFile(filePath, "utf8"); - } catch { - await new Promise((resolve) => setTimeout(resolve, 50)); - } + try { + return await fs.readFile(filePath, "utf8"); + } catch { + // Wait below. } - throw new Error(`timed out waiting for ${filePath}`); + + const signal = AbortSignal.timeout(timeoutMs); + return await new Promise((resolve, reject) => { + let settled = false; + let watcher: ReturnType | 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 { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - if (check()) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, 50)); +async function waitForProcessExit( + child: ReturnType, + label: string, + timeoutMs: number, +): Promise<{ code: number | null; signal: NodeJS.Signals | null }> { + if (child.exitCode !== null || child.signalCode !== null) { + 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 { @@ -179,12 +226,14 @@ describe("openclaw launcher", () => { const fixtureRoot = await makeLauncherFixture(fixtureRoots); await addGitMarker(fixtureRoot); const childInfoPath = path.join(fixtureRoot, "child-info.json"); + const signalPath = path.join(fixtureRoot, "sigterm-received.txt"); await fs.writeFile( path.join(fixtureRoot, "dist", "entry.js"), [ 'import { writeFileSync } from "node:fs";', - `writeFileSync(${JSON.stringify(childInfoPath)}, JSON.stringify({ pid: process.pid }) + "\\n");`, '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);", "", ].join("\n"), @@ -206,7 +255,11 @@ describe("openclaw launcher", () => { 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); } finally { if (isProcessAlive(respawnChildPid)) { @@ -253,10 +306,10 @@ describe("openclaw launcher", () => { launcher.kill("SIGTERM"); - await waitUntil( - () => !isProcessAlive(launcher.pid) && !isProcessAlive(respawnChildPid), - 5000, - ); + await expect(waitForProcessExit(launcher, "launcher", 5000)).resolves.toEqual({ + code: 1, + signal: null, + }); expect(isProcessAlive(launcher.pid)).toBe(false); expect(isProcessAlive(respawnChildPid)).toBe(false); } finally {