mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
71ae02c6ef
Retain original parent/child scope and prior native-generation proof across exit settlement and token retries. Keep historical scope separate from signal authority, count each supplied snapshot once, and share one existing native-exit deadline throughout cleanup. Validated 387 affected tests (one Darwin-only skip), five controls repeated three times, and 30 synced-main integration tests (one platform skip). Synced-head static, architecture, docs and discovery checks pass; real GDB qualification passes 44 CLI/MCP cases. Native-observer edge controls are simulated. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
191 lines
6.0 KiB
TypeScript
191 lines
6.0 KiB
TypeScript
import { performance } from "node:perf_hooks";
|
|
import { expect, it, vi } from "vitest";
|
|
import {
|
|
cleanupOwnedProcessGroup,
|
|
verifyNoTokenOwnedProcesses,
|
|
type ProcessIdentityObservation,
|
|
type ProcessOwnershipHost,
|
|
type ProcessRunTokenObservation,
|
|
type ProcessTableEntry,
|
|
} from "./ProcessOwnership.js";
|
|
import { ProcessOwnershipEvidenceOwner } from "./ProcessOwnershipEvidence.js";
|
|
|
|
const row = (pid: number, parentPid: number): ProcessTableEntry => ({
|
|
pid,
|
|
parentPid,
|
|
processGroupId: pid,
|
|
state: "R",
|
|
command: "/fixture/node",
|
|
});
|
|
const identity = (
|
|
process: ProcessTableEntry,
|
|
exiting: boolean,
|
|
): ProcessIdentityObservation => ({
|
|
state: "readable",
|
|
identity: `start:${process.pid}`,
|
|
lifecycle: {
|
|
state: "observed",
|
|
lifecycle: exiting ? "exiting" : "non-zombie",
|
|
},
|
|
relation: {
|
|
state: "observed",
|
|
parentPid: process.parentPid,
|
|
processGroupId: process.processGroupId,
|
|
},
|
|
});
|
|
|
|
it.each(["readable", "retried", "reused", "retained-parent"] as const)(
|
|
"keeps child scope tied to its native generation after parent exit (%s)",
|
|
async (mode) => {
|
|
const parent = row(200, 1);
|
|
const child = row(300, parent.pid);
|
|
const owner = new ProcessOwnershipEvidenceOwner();
|
|
if (mode === "retained-parent")
|
|
owner.reconcile(
|
|
[
|
|
{
|
|
process: parent,
|
|
state: "same-generation",
|
|
identityBefore: identity(parent, false),
|
|
token: { state: "readable", runId: "run" },
|
|
identityAfter: identity(parent, false),
|
|
},
|
|
],
|
|
{
|
|
runId: "run",
|
|
listedRows: 1,
|
|
listedPids: new Set([parent.pid]),
|
|
leaderPid: 100,
|
|
processGroupIds: new Set([100]),
|
|
},
|
|
);
|
|
const priorSnapshots = owner.coverage.snapshotCount;
|
|
let parentReads = 0;
|
|
let childTokenReads = 0;
|
|
let reparented = false;
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: async () => [parent, child],
|
|
environment: async (pid) =>
|
|
pid === parent.pid ? { REA_PROCESS_RUN_ID: "run" } : {},
|
|
runTokens: async (selected) =>
|
|
new Map<number, ProcessRunTokenObservation>(
|
|
selected.map((process) => [
|
|
process.pid,
|
|
process.pid === parent.pid
|
|
? mode === "retained-parent"
|
|
? { state: "unavailable", reason: "EACCES" }
|
|
: { state: "readable", runId: "run" }
|
|
: ++childTokenReads === 1 && mode !== "readable"
|
|
? { state: "unavailable", reason: "EACCES" }
|
|
: { state: "readable", runId: undefined },
|
|
]),
|
|
),
|
|
processIdentities: async (selected) =>
|
|
new Map<number, ProcessIdentityObservation>(
|
|
selected.map((process) => {
|
|
if (process.pid === parent.pid && ++parentReads > 2) {
|
|
reparented = true;
|
|
return [
|
|
process.pid,
|
|
{ state: "absent", reason: "not-in-snapshot" },
|
|
];
|
|
}
|
|
const observed =
|
|
process.pid === child.pid && reparented
|
|
? { ...process, parentPid: 1 }
|
|
: process;
|
|
const current = identity(
|
|
observed,
|
|
process.pid === parent.pid && parentReads === 2,
|
|
);
|
|
return [
|
|
process.pid,
|
|
process.pid === child.pid &&
|
|
reparented &&
|
|
mode === "reused" &&
|
|
current.state === "readable"
|
|
? { ...current, identity: "start:replacement" }
|
|
: current,
|
|
];
|
|
}),
|
|
),
|
|
signalGroup: vi.fn(),
|
|
};
|
|
const result = await verifyNoTokenOwnedProcesses("run", host, undefined, {
|
|
leaderPid: 100,
|
|
processGroupId: 100,
|
|
evidenceOwner: owner,
|
|
});
|
|
if (mode === "reused") {
|
|
// A replacement process cannot inherit the original child's history.
|
|
expect(result.cleaned).toBe(true);
|
|
expect(owner.candidates).not.toContainEqual(
|
|
expect.objectContaining({ pid: child.pid }),
|
|
);
|
|
} else {
|
|
// The original child stays live even though its parent has exited.
|
|
expect(result.cleaned).toBe(false);
|
|
expect(owner.candidates).toContainEqual(
|
|
expect.objectContaining({
|
|
pid: child.pid,
|
|
identity: "start:300",
|
|
applicability: "related-ownership-unresolved",
|
|
}),
|
|
);
|
|
}
|
|
expect(host.signalGroup).not.toHaveBeenCalled();
|
|
expect(owner.coverage.snapshotCount).toBe(priorSnapshots + 1);
|
|
},
|
|
);
|
|
|
|
it("shares native exit waiting before signaling healthy groups in one cleanup attempt", async () => {
|
|
const root = row(100, 1);
|
|
const rows = [
|
|
root,
|
|
row(200, root.pid),
|
|
row(300, root.pid),
|
|
row(400, root.pid),
|
|
];
|
|
let clock = 0;
|
|
let firstSignalAt: number | undefined;
|
|
const nativeClock = vi
|
|
.spyOn(performance, "now")
|
|
.mockImplementation(() => clock);
|
|
const host: ProcessOwnershipHost = {
|
|
listProcesses: async () => rows,
|
|
environment: async () => ({ REA_PROCESS_RUN_ID: "run" }),
|
|
processIdentities: async (selected) => {
|
|
clock += 25;
|
|
return new Map<number, ProcessIdentityObservation>(
|
|
selected.map((process) => [
|
|
process.pid,
|
|
identity(process, process.pid !== root.pid),
|
|
]),
|
|
);
|
|
},
|
|
signalGroup: vi.fn((group) => {
|
|
expect(group).toBe(root.pid);
|
|
firstSignalAt ??= clock;
|
|
}),
|
|
};
|
|
try {
|
|
const result = await cleanupOwnedProcessGroup(
|
|
{
|
|
runId: "run",
|
|
leaderPid: root.pid,
|
|
processGroupId: root.pid,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
},
|
|
host,
|
|
);
|
|
expect(result.cleaned).toBe(false);
|
|
expect(firstSignalAt).toBeDefined();
|
|
// Controlled native-observation time, not a wall-clock timeout: three
|
|
// stuck helpers must not each add another full second before root signal.
|
|
expect(firstSignalAt).toBeLessThan(2_000);
|
|
} finally {
|
|
nativeClock.mockRestore();
|
|
}
|
|
});
|