test(FN-5189): complete Step 4 — add supervision regression test

Fusion-Task-Id: FN-5189
Fusion-Task-Lineage: 4caa3f0a-af81-4c60-88e8-de229ed72e08
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 15:47:47 -07:00
committed by gsxdsm
parent 818db4a714
commit dc8dd33f80

View File

@@ -0,0 +1,136 @@
import { once } from "node:events";
import { spawn } from "node:child_process";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
const repoRoot = resolve(process.cwd(), "..", "..");
const fixturePath = join(repoRoot, "packages", "core", "src", "__tests__", "fixtures", "process-supervisor-child.mjs");
type Scenario = "clean-exit" | "sigterm" | "uncaught-exception";
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === "ESRCH") {
return false;
}
throw error;
}
}
async function waitForDead(pid: number, timeoutMs = 10_000): Promise<void> {
const startedAt = Date.now();
while (isAlive(pid)) {
if (Date.now() - startedAt > timeoutMs) {
throw new Error(`Timed out waiting for supervised child ${pid} to exit`);
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
function buildParentScript(scenario: Scenario): string {
return `
import { superviseSpawn } from "./packages/core/src/index.ts";
const child = superviseSpawn(process.execPath, [${JSON.stringify(fixturePath)}, "keepalive"], {
stdio: "ignore",
killGraceMs: 50,
maxLifetimeMs: 5_000,
});
console.log(String(child.pid));
if (${JSON.stringify(scenario)} === "clean-exit") {
process.exit(0);
}
if (${JSON.stringify(scenario)} === "sigterm") {
process.on("SIGTERM", () => process.exit(0));
setInterval(() => {}, 1_000);
} else {
setTimeout(() => {
throw new Error("FN-5189 uncaught parent failure");
}, 10);
setInterval(() => {}, 1_000);
}
`;
}
async function spawnParent(scenario: Scenario): Promise<{ parent: ReturnType<typeof spawn>; childPid: number }> {
const parent = spawn(process.execPath, ["--import", "tsx/esm", "--input-type=module", "--eval", buildParentScript(scenario)], {
cwd: repoRoot,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
parent.stdout?.on("data", (chunk: Buffer | string) => {
stdout += chunk.toString();
});
const childPid = await new Promise<number>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Timed out waiting for supervised child pid from scenario ${scenario}`));
}, 5_000);
const onData = (chunk: Buffer | string) => {
stdout += chunk.toString();
const pid = Number.parseInt(stdout.trim().split(/\s+/)[0] ?? "", 10);
if (!Number.isFinite(pid)) {
return;
}
clearTimeout(timeout);
parent.stdout?.off("data", onData);
resolve(pid);
};
parent.stdout?.on("data", onData);
parent.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
parent.once("exit", (code, signal) => {
if (!Number.isFinite(Number.parseInt(stdout.trim().split(/\s+/)[0] ?? "", 10))) {
clearTimeout(timeout);
reject(new Error(`Parent exited before printing child pid (code=${code} signal=${signal})`));
}
});
});
return { parent, childPid };
}
describe("reliability interactions: FN-5189 verification spawn supervision", () => {
const spawnedParents = new Set<ReturnType<typeof spawn>>();
afterEach(() => {
for (const parent of spawnedParents) {
if (parent.exitCode === null && parent.signalCode === null) {
try {
parent.kill("SIGKILL");
} catch {
// ignore cleanup failures
}
}
}
spawnedParents.clear();
});
const caseIt = process.platform === "win32" ? it.skip : it;
caseIt.each([
["clean-exit"],
["sigterm"],
["uncaught-exception"],
] satisfies [Scenario][]) ("reaps supervised child after parent %s", async (scenario) => {
const { parent, childPid } = await spawnParent(scenario);
spawnedParents.add(parent);
if (scenario === "sigterm") {
parent.kill("SIGTERM");
}
await once(parent, "exit");
await waitForDead(childPid);
expect(isAlive(childPid)).toBe(false);
});
});