Files
harness-tools/extensions/fast-browser/fixture-process.test.ts
T
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

182 lines
8.2 KiB
TypeScript

import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import test from "node:test";
import {
runOwnedFixtureHook,
startOwnedFixtureProcess,
type FixtureChildProcess,
type FixtureProcessAdapter,
type FixtureProcessIdentity,
} from "./fixture-process.ts";
class FakeChild extends EventEmitter implements FixtureChildProcess {
readonly pid: number;
readonly stdout = new EventEmitter();
readonly stderr = new EventEmitter();
constructor(pid: number) { super(); this.pid = pid; }
}
class FakeAdapter implements FixtureProcessAdapter {
readonly platform = "linux" as const;
readonly signals: Array<{ group: number; signal: string }> = [];
readonly identities = new Map<number, FixtureProcessIdentity>();
readonly groups = new Map<number, FixtureProcessIdentity[]>();
readonly child: FakeChild;
termLeavesDescendant = false;
autoExit = false;
spawnCount = 0;
constructor(pid = 40_001) {
this.child = new FakeChild(pid);
const root = { pid, pgrp: pid, session: pid, startTime: "root-start", state: "R" };
this.identities.set(pid, root);
this.groups.set(pid, [root]);
}
spawn(): FixtureChildProcess {
this.spawnCount += 1;
queueMicrotask(() => {
this.child.emit("spawn");
if (this.autoExit) this.child.emit("exit", 0, null);
});
return this.child;
}
identity(pid: number): FixtureProcessIdentity | undefined { return this.identities.get(pid); }
groupMembers(group: number): readonly FixtureProcessIdentity[] | undefined { return this.groups.get(group); }
signalGroup(group: number, signal: "SIGTERM" | "SIGKILL"): void {
this.signals.push({ group, signal });
if (signal === "SIGTERM" && this.termLeavesDescendant) {
const root = this.groups.get(group)?.find((member) => member.pid === this.child.pid);
this.groups.set(group, root ? [{ ...root, state: "Z" }, { pid: group + 1, pgrp: group, session: group, startTime: "descendant", state: "R" }] : []);
this.identities.delete(this.child.pid);
this.child.emit("exit", 143, "SIGTERM");
} else {
this.groups.set(group, []);
this.identities.delete(this.child.pid);
this.child.emit("exit", 137, "SIGKILL");
}
}
}
const signal = () => new AbortController().signal;
function start(adapter: FakeAdapter, overrides: Partial<Parameters<typeof startOwnedFixtureProcess>[0]> = {}) {
return startOwnedFixtureProcess({ argv: ["fixture", "--foreground"], cwd: "/tmp/fixture", env: { FIXTURE: "1" }, adapter, termTimeoutMs: 20, killTimeoutMs: 20, ...overrides }, signal());
}
test("a successful one-shot cleans a descendant after its root exits", async () => {
const adapter = new FakeAdapter();
adapter.termLeavesDescendant = true;
adapter.autoExit = true;
const result = await runOwnedFixtureHook({ argv: ["fixture"], cwd: "/tmp/fixture", env: {}, adapter, timeoutMs: 100, termTimeoutMs: 10, killTimeoutMs: 10 }, signal());
assert.equal(result.exit.code, 0);
assert.deepEqual(adapter.signals.map((item) => item.signal), ["SIGTERM", "SIGKILL"]);
assert.equal(adapter.groups.get(adapter.child.pid)?.length, 0);
});
test("cancellation uses bounded owned cleanup and does not expose the abort reason", async () => {
const adapter = new FakeAdapter();
adapter.termLeavesDescendant = true;
const controller = new AbortController();
const running = runOwnedFixtureHook({ argv: ["fixture", "--secret-token"], cwd: "/tmp/fixture", env: { TOKEN: "private" }, adapter, timeoutMs: 100, termTimeoutMs: 10, killTimeoutMs: 10 }, controller.signal);
controller.abort(new Error("private secret and raw argv must not escape"));
await assert.rejects(running, (error: unknown) => {
assert.match(String(error), /fixture hook cancelled/);
assert.doesNotMatch(String(error), /private|secret-token/);
return true;
});
assert.deepEqual(adapter.signals.map((item) => item.signal), ["SIGTERM", "SIGKILL"]);
});
test("deadline escalates from SIGTERM to SIGKILL", async () => {
const adapter = new FakeAdapter();
adapter.termLeavesDescendant = true;
await assert.rejects(runOwnedFixtureHook({ argv: ["fixture"], cwd: "/tmp/fixture", env: {}, adapter, timeoutMs: 5, termTimeoutMs: 10, killTimeoutMs: 10 }, signal()), /fixture hook exceeded its deadline/);
assert.deepEqual(adapter.signals.map((item) => item.signal), ["SIGTERM", "SIGKILL"]);
});
test("invalid, self, and non-integer PIDs are rejected before identity probes or signals", async () => {
for (const pid of [0, -1, 1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, process.pid]) {
const adapter = new FakeAdapter(pid);
let probes = 0;
const identity = adapter.identity.bind(adapter);
adapter.identity = (candidate) => { probes += 1; return identity(candidate); };
const handle = start(adapter);
adapter.child.emit("exit", 0, null);
await handle.stop();
assert.equal(probes, 0, `identity was probed for pid ${String(pid)}`);
assert.deepEqual(adapter.signals, []);
}
});
test("PID reuse or an unrelated process-group member fails closed without signaling", async () => {
const adapter = new FakeAdapter();
const handle = start(adapter);
adapter.groups.set(adapter.child.pid, [{ pid: adapter.child.pid, pgrp: adapter.child.pid, session: adapter.child.pid, startTime: "reused", state: "R" }]);
adapter.identities.set(adapter.child.pid, { pid: adapter.child.pid, pgrp: adapter.child.pid, session: adapter.child.pid, startTime: "reused", state: "R" });
adapter.child.emit("exit", 0, null);
await assert.rejects(handle.stop(), /ownership could not be confirmed/);
assert.deepEqual(adapter.signals, []);
});
test("uncertain process-group inspection fails closed without signaling", async () => {
const adapter = new FakeAdapter();
const original = adapter.groupMembers.bind(adapter);
adapter.groupMembers = () => undefined;
const handle = start(adapter);
adapter.child.emit("exit", 0, null);
await assert.rejects(handle.stop(), /liveness could not be confirmed/);
assert.deepEqual(adapter.signals, []);
adapter.groupMembers = original;
});
test("an unrelated process-group identity fails closed", async () => {
const adapter = new FakeAdapter();
const handle = start(adapter);
adapter.groups.set(adapter.child.pid, [
{ pid: adapter.child.pid, pgrp: adapter.child.pid, session: adapter.child.pid, startTime: "root-start", state: "Z" },
{ pid: 40_999, pgrp: adapter.child.pid, session: adapter.child.pid + 1, startTime: "foreign", state: "R" },
]);
adapter.identities.delete(adapter.child.pid);
adapter.child.emit("exit", 0, null);
await assert.rejects(handle.stop(), /ownership could not be confirmed/);
assert.deepEqual(adapter.signals, []);
});
test("missing executable is safe and does not leak output to process stdout", async () => {
const adapter = new FakeAdapter();
adapter.spawn = function spawn() {
this.spawnCount += 1;
queueMicrotask(() => {
this.groups.set(this.child.pid, []);
this.identities.delete(this.child.pid);
this.child.emit("error", new Error("ENOENT /secret/private-executable"));
});
return this.child;
};
const result = await assert.rejects(runOwnedFixtureHook({ argv: ["missing", "raw-secret"], cwd: "/tmp/fixture", env: { PASSWORD: "private" }, adapter, timeoutMs: 100 }, signal()), /fixture hook failed to start/);
assert.equal(result, undefined);
assert.deepEqual(adapter.signals, []);
});
test("stdout and stderr are captured with finite bounds", async () => {
const adapter = new FakeAdapter();
const handle = start(adapter, { maxStdoutBytes: 4, maxStderrBytes: 3 });
adapter.child.stdout.emit("data", "abcdef");
adapter.child.stderr.emit("data", "wxyz");
adapter.groups.set(adapter.child.pid, []);
adapter.identities.delete(adapter.child.pid);
adapter.child.emit("exit", 0, null);
await handle.stop();
assert.deepEqual(await handle.stdout, { text: "abcd", truncated: true });
assert.deepEqual(await handle.stderr, { text: "wxy", truncated: true });
});
test("unsupported platforms fail before spawning", () => {
const adapter = new FakeAdapter() as FakeAdapter & { platform: NodeJS.Platform };
Object.defineProperty(adapter, "platform", { value: "win32" });
assert.throws(() => startOwnedFixtureProcess({ argv: ["fixture"], cwd: "/tmp", env: {}, adapter }, signal()), /unavailable on this platform/);
assert.equal(adapter.spawnCount, 0);
});