Files
fusion/packages/dashboard/scripts/run-vitest-with-heap.mjs
gsxdsm ed80f98216 FN-5918: reap dashboard vitest workers on wrapper exit
Prevent dashboard test wrappers from leaving orphaned Vitest subprocesses after interruption or timeout.

- run dashboard vitest wrappers in a detached process group and forward shutdown signals to the whole group
- add an exit-time cleanup path and spawn override seam for process-lifecycle handling without launching real vitest
- cover SIGINT/SIGTERM orphan reaping and keep the dashboard test config guard aligned with the new script test

Files changed:
 .../scripts/__tests__/run-vitest-with-heap.test.ts | 172 +++++++++++++++++++++
 .../dashboard/scripts/run-vitest-with-heap.mjs     |  89 ++++++++++-
 .../__tests__/dashboard-test-config-guard.test.ts  |   1 +
 packages/dashboard/vitest.config.ts                |   1 +
 4 files changed, 256 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-5918

Fusion-Task-Lineage: 5c695b3b-14c5-4749-ad9c-eb9f33d5ecd5
2026-06-02 19:51:12 -07:00

117 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
/* global clearInterval, console, process, setInterval */
import { spawn } from "node:child_process";
const rawArgs = process.argv.slice(2);
const heapArg = rawArgs.find((arg) => arg.startsWith("--heap="));
const heapMb = heapArg?.slice("--heap=".length) || "6144";
const vitestArgs = rawArgs.filter((arg) => !arg.startsWith("--heap="));
if (vitestArgs.length === 0) {
console.error("Usage: node scripts/run-vitest-with-heap.mjs [--heap=6144] <vitest args...>");
process.exit(1);
}
const nodeOptions = [`--max-old-space-size=${heapMb}`, process.env.NODE_OPTIONS || ""]
.join(" ")
.trim();
function resolveSpawnCommand() {
const override = process.env.FUSION_RUN_VITEST_SPAWN_OVERRIDE;
if (!override) {
return { command: "pnpm", args: ["exec", "vitest", ...vitestArgs] };
}
const parsedOverride = JSON.parse(override);
if (
!parsedOverride ||
typeof parsedOverride.command !== "string" ||
parsedOverride.command.length === 0 ||
!Array.isArray(parsedOverride.args) ||
parsedOverride.args.some((arg) => typeof arg !== "string")
) {
throw new Error(
"FUSION_RUN_VITEST_SPAWN_OVERRIDE must be valid JSON with string command and string[] args",
);
}
// Test seam for process-lifecycle coverage without launching real vitest.
return { command: parsedOverride.command, args: parsedOverride.args };
}
const { command, args } = resolveSpawnCommand();
// process-supervisor-allowlist: foreground wrapper signals the entire vitest process group on death/timeout; not a background daemon
const child = spawn(command, args, {
detached: true,
stdio: "inherit",
env: { ...process.env, NODE_OPTIONS: nodeOptions },
});
const heartbeat = setInterval(() => {
console.log(`[dashboard-vitest] still running: ${vitestArgs.join(" ")}`);
}, 5_000);
function clearHeartbeat() {
clearInterval(heartbeat);
}
function forwardSignal(signal) {
clearHeartbeat();
try {
process.kill(-child.pid, signal);
return;
} catch (error) {
if (!(error instanceof Error) || !("code" in error)) {
throw error;
}
if (error.code !== "ESRCH" && error.code !== "EPERM") {
throw error;
}
}
try {
child.kill(signal);
} catch (error) {
if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH") {
throw error;
}
}
}
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
process.on(signal, () => forwardSignal(signal));
}
process.on("exit", () => {
clearHeartbeat();
try {
process.kill(-child.pid, "SIGTERM");
} catch (error) {
if (
!(error instanceof Error) ||
!("code" in error) ||
(error.code !== "ESRCH" && error.code !== "EPERM")
) {
throw error;
}
}
});
child.on("error", (error) => {
clearHeartbeat();
console.error(error);
process.exit(1);
});
child.on("close", (code, signal) => {
clearHeartbeat();
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});