Files
rea/tests/fixtures/providerProcess.ts
morluto 7016951394 fix(process): retain diagnostics through bounded output finalization
Separate verified process cleanup from output closure. Bound additional retention from owned stop entry through cleanup retry, drain before detaching capture, and mark unreadable or abandoned output incomplete. Release owned event-loop references on drain timeout without changing the proven cleanup disposition; preserve borrowed handle reference state.

Cover held and failed cleanup, late tail, bounded drain, stream errors, borrowed authority, and a real POSIX inherited-stderr release barrier. The real pipe control drops its final ten bytes on the baseline and retains them with this change. Focused consumer checks passed, including simulated Windows ownership; native Windows and real provider failure scope remain unverified.

Android still constructs its startup error snapshot before retirement; that consumer correction remains separate work.

Fixes #4840
2026-10-18 01:03:19 +08:00

116 lines
3.4 KiB
TypeScript

import { spawn, type ChildProcess } from "node:child_process";
import { fileURLToPath } from "node:url";
import type { ProviderProcessHandle } from "../../src/process/ProviderProcess.js";
const fixturePath = fileURLToPath(
new URL("./providerProcess.mjs", import.meta.url),
);
/** Spawn one deterministic process-lifecycle fixture with piped diagnostics. */
export const spawnProviderProcessFixture = (
mode:
| "burst"
| "detached-child"
| "exit"
| "graceful"
| "invalid-utf8"
| "valid-multibyte"
| "late-stderr"
| "stubborn",
value?: number,
): ChildProcess =>
spawn(
process.execPath,
[fixturePath, mode, ...(value === undefined ? [] : [String(value)])],
{
stdio:
mode === "late-stderr"
? ["ignore", "pipe", "pipe", "pipe"]
: ["ignore", "pipe", "pipe"],
},
);
/** Wait until a long-lived fixture has installed its signal behavior. */
export const waitForProviderProcessReady = (
child: ProviderProcessHandle,
timeoutMs = 2_000,
): Promise<void> =>
new Promise((resolve, reject) => {
const stdout = child.stdout;
if (stdout === null) {
reject(new Error("Provider process fixture stdout is unavailable"));
return;
}
let output = "";
const detach = (): void => {
clearTimeout(timer);
stdout.off("data", onData);
child.off("exit", onExit);
};
const onData = (chunk: Buffer | string): void => {
output += chunk.toString();
if (output.includes("ready\n")) {
detach();
resolve();
}
};
const onExit = (): void => {
detach();
reject(new Error("Provider process fixture exited before readiness"));
};
const timer = setTimeout(() => {
detach();
reject(new Error("Provider process fixture readiness timed out"));
}, timeoutMs);
stdout.on("data", onData);
child.once("exit", onExit);
});
/** Read the distinct process-group leader created by the detached-child mode. */
export const waitForDetachedProviderChild = (
child: ProviderProcessHandle,
timeoutMs = 2_000,
): Promise<number> =>
new Promise((resolve, reject) => {
const stdout = child.stdout;
if (stdout === null) {
reject(new Error("Provider process fixture stdout is unavailable"));
return;
}
let output = "";
const detach = (): void => {
clearTimeout(timer);
stdout.off("data", onData);
child.off("exit", onExit);
};
const onData = (chunk: Buffer | string): void => {
output += chunk.toString();
const match = /(?:^|\n)ready:(\d+)\n/u.exec(output);
if (match?.[1] === undefined) return;
detach();
resolve(Number(match[1]));
};
const onExit = (): void => {
detach();
reject(new Error("Provider process fixture exited before readiness"));
};
const timer = setTimeout(() => {
detach();
reject(new Error("Provider process fixture readiness timed out"));
}, timeoutMs);
stdout.on("data", onData);
child.once("exit", onExit);
});
/** Kill a fixture if necessary and wait for its process handle to settle. */
export const stopProviderProcessFixture = async (
child: ProviderProcessHandle,
): Promise<void> => {
if (child.exitCode !== null || child.signalCode !== null) return;
const exited = new Promise<void>((resolve) => {
child.once("exit", () => resolve());
});
child.kill("SIGKILL");
await exited;
};