test(process): use owned host-aware settlement fixtures

This commit is contained in:
morluto
2026-10-15 03:37:16 +08:00
parent 170bfc7b3f
commit 9b7ec809e6
@@ -1,5 +1,10 @@
import { spawn } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
spawnOwnedProviderProcess,
type ProviderProcessHandle,
} from "../../../src/process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { observeSettlement } from "../../../src/process/capture/ProcessCaptureLifecycle.js";
import { observeOwnedProcessGroups } from "../../../src/process/ProcessOwnershipObservation.js";
@@ -9,65 +14,121 @@ const ABSENT_GROUP_ID = 4_294_967_295;
const observeGroup = async (groupId: number) =>
observeOwnedProcessGroups("settlement-deadline-probe", [groupId]);
describe("settlement deadline classification", () => {
it("does not report a single empty observation as alive_at_deadline", async () => {
expect(
(await observeGroup(ABSENT_GROUP_ID)).get(ABSENT_GROUP_ID)?.state,
).toBe("empty");
const settlement = await observeSettlement(
"settlement-single-empty",
[ABSENT_GROUP_ID],
0,
() => undefined,
"linux",
);
expect(settlement.state).toBe("unverifiable");
const waitForOwnedExit = (
child: ProviderProcessHandle,
timeoutMs: number,
): Promise<boolean> =>
new Promise((resolve) => {
if (child.exitCode !== null || child.signalCode !== null) {
resolve(true);
return;
}
const onExit = (): void => {
clearTimeout(timer);
resolve(true);
};
const timer = setTimeout(() => {
child.off("exit", onExit);
resolve(false);
}, timeoutMs);
child.once("exit", onExit);
});
it("still reports observed liveness as alive_at_deadline", async () => {
const runId = "settlement-live-group";
// A detached child leads its own process group and carries the run token,
// so the group observes as a genuinely owned live member.
const child = spawn("sleep", ["30"], {
detached: true,
stdio: "ignore",
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
});
child.unref();
try {
// Wait until the child is actually listed before asserting on it.
const started = Date.now();
while (
(await observeOwnedProcessGroups(runId, [child.pid!])).get(child.pid!)
?.state !== "alive"
) {
if (Date.now() - started > 5_000)
throw new Error("live fixture group never observed as alive");
await new Promise((resolve) => setTimeout(resolve, 50));
}
const stopOwnedFixture = async (
child: ProviderProcessHandle,
ownership: Parameters<typeof cleanupOwnedProcessGroup>[0],
): Promise<void> => {
let cleanupFailure: string | undefined;
try {
const result = await cleanupOwnedProcessGroup(ownership);
if (!result.cleaned) cleanupFailure = result.reason;
} catch (cause: unknown) {
cleanupFailure = cause instanceof Error ? cause.message : String(cause);
}
if (child.exitCode === null && child.signalCode === null)
child.kill("SIGKILL");
if (!(await waitForOwnedExit(child, 5_000)))
throw new Error("owned settlement fixture did not exit after cleanup");
if (cleanupFailure !== undefined)
throw new Error(
`owned settlement fixture cleanup failed: ${cleanupFailure}`,
);
};
// Windows Job Object ownership has a separate settlement path from POSIX PGIDs.
describe.skipIf(process.platform === "win32")(
"settlement deadline classification",
() => {
it("does not report a single empty observation as alive_at_deadline", async () => {
expect(
(await observeGroup(ABSENT_GROUP_ID)).get(ABSENT_GROUP_ID)?.state,
).toBe("empty");
const settlement = await observeSettlement(
runId,
[child.pid!],
"settlement-single-empty",
[ABSENT_GROUP_ID],
0,
() => undefined,
"linux",
);
expect(settlement.state).toBe("alive_at_deadline");
} finally {
child.kill("SIGKILL");
}
});
expect(settlement.state).toBe("unverifiable");
});
it("still reports two consecutive empty observations as quiesced", async () => {
const settlement = await observeSettlement(
"settlement-two-empty",
[ABSENT_GROUP_ID],
1_000,
() => undefined,
"linux",
);
expect(settlement.state).toBe("quiesced");
});
});
it("still reports observed owned liveness as alive_at_deadline", async () => {
const runId = "settlement-live-group";
const launched = await spawnOwnedProviderProcess({
command: process.execPath,
arguments: ["-e", "setInterval(() => undefined, 1000)"],
runId,
});
try {
const started = Date.now();
let observation = (
await observeOwnedProcessGroups(runId, [
launched.ownership.processGroupId,
])
).get(launched.ownership.processGroupId);
while (
observation?.state === "empty" &&
Date.now() - started <= 5_000
) {
await new Promise((resolve) => setTimeout(resolve, 50));
observation = (
await observeOwnedProcessGroups(runId, [
launched.ownership.processGroupId,
])
).get(launched.ownership.processGroupId);
}
expect(
observation?.state,
observation?.state === "unverifiable"
? observation.reason
: undefined,
).toBe("alive");
const settlement = await observeSettlement(
runId,
[launched.ownership.processGroupId],
0,
() => undefined,
process.platform,
);
expect(settlement.state).toBe("alive_at_deadline");
} finally {
await stopOwnedFixture(launched.process, launched.ownership);
}
});
it("still reports two consecutive empty observations as quiesced", async () => {
const settlement = await observeSettlement(
"settlement-two-empty",
[ABSENT_GROUP_ID],
1_000,
() => undefined,
"linux",
);
expect(settlement.state).toBe("quiesced");
});
},
);