mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
test(process): use owned host-aware settlement fixtures
This commit is contained in:
@@ -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");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user