fix(test): handle extension memory spawn errors

This commit is contained in:
Vincent Koc
2026-05-28 14:27:59 +02:00
parent 17c1b06cc7
commit e5a687f115
2 changed files with 81 additions and 19 deletions
+45 -10
View File
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { ensureExtensionMemoryBuild } from "./ensure-extension-memory-build.mjs";
import { formatErrorMessage } from "./lib/error-format.mjs";
@@ -168,9 +169,17 @@ function summarizeStderr(stderr, lines = 8, maxChars = STDERR_PREVIEW_MAX_CHARS)
)}`;
}
async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
export async function runCase({
repoRoot,
env,
hookPath,
name,
body,
timeoutMs,
spawnImpl = spawn,
}) {
return await new Promise((resolve) => {
const child = spawn(
const child = spawnImpl(
process.execPath,
["--import", hookPath, "--input-type=module", "--eval", body],
{
@@ -185,10 +194,21 @@ async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
let stderrRssTail = "";
let maxRssMb = null;
let timedOut = false;
let settled = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGKILL");
}, timeoutMs);
timer.unref?.();
function settle(result) {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(result);
}
child.stdout.on("data", (chunk) => {
stdout = appendBoundedOutput(stdout, chunk);
@@ -199,14 +219,27 @@ async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
maxRssMb = rssScan.maxRssMb;
stderr = appendBoundedOutput(stderr, chunk);
});
child.on("close", (code, signal) => {
clearTimeout(timer);
child.on("error", (error) => {
const stderrText = formatCapturedOutput(stderr);
resolve({
settle({
name,
code: null,
signal: null,
timedOut,
error: formatErrorMessage(error),
stdout: formatCapturedOutput(stdout),
stderr: stderrText,
maxRssMb: maxRssMb ?? parseMaxRssMb(stderrText),
});
});
child.on("close", (code, signal) => {
const stderrText = formatCapturedOutput(stderr);
settle({
name,
code,
signal,
timedOut,
error: null,
stdout: formatCapturedOutput(stdout),
stderr: stderrText,
maxRssMb: maxRssMb ?? parseMaxRssMb(stderrText),
@@ -411,9 +444,11 @@ async function main() {
}
}
try {
await main();
} catch (error) {
console.error(`[extension-memory] ${formatErrorMessage(error)}`);
process.exit(1);
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
await main();
} catch (error) {
console.error(`[extension-memory] ${formatErrorMessage(error)}`);
process.exit(1);
}
}
+36 -9
View File
@@ -1,8 +1,10 @@
import { spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { runCase } from "../../scripts/profile-extension-memory.mjs";
const SCRIPT_PATH = path.resolve("scripts/profile-extension-memory.mjs");
@@ -59,15 +61,7 @@ describe("scripts/profile-extension-memory", () => {
);
const result = runProfileExtensionMemory(
[
"--extension",
"noisy",
"--skip-combined",
"--concurrency",
"1",
"--json",
reportPath,
],
["--extension", "noisy", "--skip-combined", "--concurrency", "1", "--json", reportPath],
root,
);
@@ -85,4 +79,37 @@ describe("scripts/profile-extension-memory", () => {
rmSync(root, { recursive: true, force: true });
}
});
it("resolves spawn errors without waiting for the timeout", async () => {
const startedAt = Date.now();
const result = await runCase({
repoRoot: process.cwd(),
env: process.env,
hookPath: "missing-hook.mjs",
name: "spawn-error",
body: "",
timeoutMs: 30_000,
spawnImpl: () => {
const child = new EventEmitter() as EventEmitter & {
kill: () => boolean;
stderr: EventEmitter;
stdout: EventEmitter;
};
child.stderr = new EventEmitter();
child.stdout = new EventEmitter();
child.kill = () => true;
queueMicrotask(() => child.emit("error", new Error("spawn denied")));
return child;
},
});
expect(Date.now() - startedAt).toBeLessThan(1000);
expect(result).toMatchObject({
code: null,
error: "spawn denied",
name: "spawn-error",
signal: null,
timedOut: false,
});
});
});