mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
test: remove launcher polling waits
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user