Files
reverse-engineering-anything/tests/boundary/process/processFixtureLifecycle.test.ts
T
morluto 60149282fc test: strengthen integration oracles and verification (#8295)
* test: strengthen integration oracles and verification

* test: bound lifecycle gates and strengthen workflow checks

* test: verify complete paged and extracted results

* test: preserve bytecode and cancellation evidence

* test: strengthen retained evidence and fixture cleanup

* test: require cross-projection bridge identities

* test: retain fixture owners through cleanup failures

* test: verify graph examples and project selectors

* test: tighten provider and lifecycle regressions

* test: consolidate hex coverage and bound cutter cleanup

* test: bound subprocess output waits and strengthen graph checks

* test: check mcp recovery and preserve cleanup owners

* test: verify structured mcp argument failures

* test: preserve artifact cleanup and gdb limit evidence

* test: preserve evidence identities and bound provider gates

* test: retain browser and comparison cleanup owners

* test: bound startup and strengthen fixture oracles

* test: bound lifecycle gates and verify recovered evidence

* test: verify public outcomes and lifecycle ownership

* test: assert precise evidence and release lifecycle gates

* test: verify selected facts and bound cleanup waits

* test: check retained evidence and decoded records

* test: verify final reports and retain cleanup ownership

* test: bound reviewed startup gates and retain cleanup

* test: verify PE errors and preserve MCP cleanup

* test: preserve MCP fixture cleanup ownership

* test: release provider gates before late startup
2026-11-01 00:15:28 +08:00

252 lines
7.8 KiB
TypeScript

import { EventEmitter, once } from "node:events";
import { createServer, type Socket } from "node:net";
import { setTimeout as delay } from "node:timers/promises";
import { PassThrough } from "node:stream";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { expect, it, vi } from "vitest";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
import { createTestProcessRegistry } from "../../support/process/processFixture.js";
const within = async <T>(
operation: Promise<T>,
timeoutMs: number,
timeoutValue: string,
): Promise<T | string> => {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
operation,
new Promise<string>((resolve) => {
timer = setTimeout(() => resolve(timeoutValue), timeoutMs);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
};
it("bounds a timed-out run when the leader exits but output remains open", async () => {
vi.useFakeTimers();
const child = Object.assign(new EventEmitter(), {
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
pid: 123,
exitCode: null as number | null,
signalCode: null as NodeJS.Signals | null,
kill: vi.fn(() => false),
});
const processes = createTestProcessRegistry(
() => child as unknown as ChildProcessWithoutNullStreams,
);
const pending = processes.run("fixture", [], { timeoutMs: 100 });
const outcome = pending.then(
() => "resolved",
(error: unknown) => error,
);
try {
child.stdout.write("partial output");
child.exitCode = 0;
child.emit("exit", 0, null);
await vi.advanceTimersByTimeAsync(2_101);
const result = await Promise.race([
outcome,
Promise.resolve("still pending"),
]);
expect(result).toBeInstanceOf(Error);
expect(child.kill).not.toHaveBeenCalled();
expect(result).toMatchObject({
message: "Test process exceeded its 100ms deadline",
cause: { exitCode: 0, signal: null, stdout: "partial output" },
});
} finally {
child.emit("close", 0, null);
await outcome;
await processes.shutdown();
vi.useRealTimers();
}
});
it("retains an unconfirmed child for shutdown retry after the run deadline", async () => {
vi.useFakeTimers();
const child = Object.assign(new EventEmitter(), {
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
pid: 456,
exitCode: null as number | null,
signalCode: null as NodeJS.Signals | null,
kill: vi.fn(() => true),
});
const processes = createTestProcessRegistry(
() => child as unknown as ChildProcessWithoutNullStreams,
);
const pending = processes.run("fixture", [], { timeoutMs: 100 });
const outcome = pending.then(
() => "resolved",
(error: unknown) => error,
);
try {
await vi.advanceTimersByTimeAsync(2_101);
expect(await outcome).toMatchObject({
message: "Test process exceeded its 100ms deadline",
cause: { exitCode: null, signal: null },
});
const firstShutdown = processes.shutdown().then(
() => "resolved",
(error: unknown) => error,
);
await vi.advanceTimersByTimeAsync(2_001);
expect(await firstShutdown).toMatchObject({
message: "Test child process shutdown failed",
errors: [
expect.objectContaining({
message: "test child process 456 did not exit after SIGKILL",
}),
expect.any(Error),
],
});
child.kill.mockImplementation(() => {
child.exitCode = 0;
child.emit("exit", 0, null);
return true;
});
await expect(processes.shutdown()).resolves.toBeUndefined();
expect(child.kill).toHaveBeenCalledTimes(5);
} finally {
child.exitCode = 0;
child.emit("exit", 0, null);
child.emit("close", 0, null);
await outcome;
await processes.shutdown();
vi.useRealTimers();
}
});
it("returns normal completion without a timeout and preserves child errors", async () => {
const child = Object.assign(new EventEmitter(), {
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
pid: 789,
exitCode: null as number | null,
signalCode: null as NodeJS.Signals | null,
kill: vi.fn(() => true),
});
const processes = createTestProcessRegistry(
() => child as unknown as ChildProcessWithoutNullStreams,
);
const run = processes.run("fixture", []);
child.stdout.write("completed");
child.exitCode = 0;
child.emit("exit", 0, null);
child.emit("close", 0, null);
await expect(run).resolves.toEqual({
exitCode: 0,
signal: null,
stdout: "completed",
stderr: "",
});
await processes.shutdown();
const failure = new Error("spawn failed");
child.exitCode = null;
const failedRun = processes.run("fixture", []);
child.emit("error", failure);
child.emit("close", null, null);
await expect(failedRun).rejects.toBe(failure);
await processes.shutdown();
});
const releaseDescendant = async (peer: Socket | undefined): Promise<void> => {
if (peer === undefined) {
// The child has a finite self-expiry even if it never connects.
await delay(9_500);
return;
}
if (peer.destroyed) return;
const acknowledgement = once(peer, "data");
const closed = once(peer, "close");
peer.write("stop");
const result = await within(
Promise.all([acknowledgement, closed]),
10_000,
"still open",
);
if (result === "still open") {
peer.destroy();
throw new Error("descendant connection did not close");
}
expect(String(result[0][0])).toBe("stopping");
};
it("times out when a real descendant holds the exited leader's output open", async () => {
const server = createServer();
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Expected TCP listener");
let peer: Socket | undefined;
let descendantPid: number | undefined;
const connected = new Promise<void>((resolve) => {
server.once("connection", (socket) => {
peer = socket;
socket.once("data", (data: Buffer) => {
descendantPid = Number(data.toString());
resolve();
});
});
});
const descendant = `
const net = require("node:net");
const socket = net.connect(Number(process.argv[1]), "127.0.0.1", () => {
process.stdout.write("descendant output\\n");
socket.write(String(process.pid));
});
socket.on("data", () => socket.write("stopping", () => process.exit(0)));
setTimeout(() => process.exit(0), 9000);
`;
const leader = `
const { spawn } = require("node:child_process");
const child = spawn(process.execPath, ["-e", process.argv[1], process.argv[2]], {
detached: true, stdio: ["ignore", "inherit", "inherit"],
});
child.unref();
`;
const processes = createTestProcessRegistry();
const pending = processes.run(
process.execPath,
["-e", leader, descendant, String(address.port)],
{ timeoutMs: 3000 },
);
const outcome = pending.then(
() => "resolved",
(error: unknown) => error,
);
try {
await awaitAcknowledgement(connected, outcome, "descendant connection");
const result = await within(outcome, 7_000, "still pending");
expect(result).toMatchObject({
message: "Test process exceeded its 3000ms deadline",
cause: {
exitCode: 0,
signal: null,
stdout: expect.stringContaining("descendant output"),
},
});
expect(descendantPid).toBeGreaterThan(0);
} finally {
try {
await releaseDescendant(peer);
} finally {
await outcome;
await processes.shutdown();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
}
}, 17_000);