Files
reverse-engineering-anything/src/process/ProcessOwnership.validation.test.ts
T
d8a9673fc4 fix(process): name the Swift compile deadline (#6071)
Name REA_SWIFTC_COMPILE_TIMEOUT_MS, its effective milliseconds, the
1..2147483647 range, and the omitted 120000 ms default when a Darwin
ownership-helper compile times out. A change applies after REA restarts,
on the next ownership-helper compile.

Producer SIGXCPU and SIGXFSZ recovery says REA retains the tighter
inherited limit. Agent-visible fixed caps in this theme say this release
has no runtime override. Numbers are unchanged.

Refs #5910

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-21 23:46:29 +08:00

1305 lines
41 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanupOwnedProcessGroup,
cleanupWindowsProcessTree,
type ProcessOwnershipHost,
type ProcessTableEntry,
type WindowsProcessTreeHost,
} from "./ProcessOwnership.js";
import {
observeOwnedProcessGroups,
observeOwnedProcessLineage,
} from "./ProcessOwnershipObservation.js";
import { createOwnership, host } from "./ProcessOwnership.fixture.js";
let ownership = createOwnership();
beforeEach(() => {
ownership = createOwnership();
});
const launcher: ProcessTableEntry = {
pid: 100,
parentPid: 1,
processGroupId: 100,
state: "S",
command: "fixture",
};
const nonSignalingObserver = (
listProcesses: ProcessOwnershipHost["listProcesses"],
): ProcessOwnershipHost => ({
listProcesses,
environment: () => Promise.resolve({}),
signalGroup: () => {
throw new Error("Observation must never signal processes");
},
});
const expectUnownedLineage = (adapter: ProcessOwnershipHost) =>
expect(observeOwnedProcessLineage(ownership, adapter)).resolves.toEqual({
status: "unavailable",
observedAt: expect.any(String),
runId: "run-token",
launcherPid: 100,
processGroupId: 100,
reason: "process lineage contains an unowned or PID-reused process",
});
describe("owned process-group cleanup validation: ownership and lineage", () => {
it("settles unreadable observations for Darwin processes exiting in the kernel", async () => {
let snapshots = 0;
const child = {
pid: 101,
parentPid: 100,
processGroupId: 100,
state: "?E",
command: "fixture-child",
};
const adapter: ProcessOwnershipHost = {
platform: "darwin",
listProcesses: () => {
snapshots += 1;
return Promise.resolve(
snapshots === 1
? [launcher, child]
: snapshots === 2
? [launcher, child]
: [launcher, { ...child, state: "Z" }],
);
},
environment: (pid) =>
pid === 100
? Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" })
: Promise.reject(new Error("sysctl_errno_22")),
signalGroup: vi.fn(),
};
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
{ cleaned: true, signaled: true },
);
expect(snapshots).toBeGreaterThanOrEqual(3);
});
it("fails closed when a descendant in another process group lacks the token", async () => {
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve([
launcher,
{
pid: 101,
parentPid: 100,
processGroupId: 101,
state: "S",
command: "child-session",
},
]),
environment: (pid) =>
Promise.resolve({
REA_PROCESS_RUN_ID: pid === 100 ? "run-token" : "other-run",
}),
signalGroup: vi.fn(),
};
await expectUnownedLineage(adapter);
});
it("does not publish lineage when any member fails ownership checks", async () => {
const { adapter } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },
101: { REA_PROCESS_RUN_ID: "other-run" },
});
await expectUnownedLineage(adapter);
});
it("fails closed for stale metadata or an unrelated concurrent process", async () => {
const { adapter, signalGroup } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },
101: { REA_PROCESS_RUN_ID: "different-run" },
});
expect(await cleanupOwnedProcessGroup(ownership, adapter)).toEqual({
cleaned: false,
reason: "process ownership token mismatch for PID(s): 101",
failures: [{ pid: 101, reason: "run-token-mismatch" }],
});
expect(signalGroup).not.toHaveBeenCalled();
});
it("checks every member and aggregates ownership read failures", async () => {
const environment = vi.fn((pid: number) => {
if (pid === 100)
return Promise.reject(new Error("transient procfs read"));
return Promise.resolve(
pid === 101
? { REA_PROCESS_RUN_ID: "different-run" }
: { REA_PROCESS_RUN_ID: "run-token" },
);
});
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve(
[100, 101, 102].map((pid) => ({
pid,
parentPid: pid === 100 ? 1 : 100,
processGroupId: 100,
state: "S",
command: "fixture",
})),
),
environment,
signalGroup,
};
expect(await cleanupOwnedProcessGroup(ownership, adapter)).toEqual({
cleaned: false,
reason: "process ownership token mismatch for PID(s): 101",
failures: [
expect.objectContaining({
pid: 100,
reason: "environment-unreadable",
diagnostic: "transient procfs read",
}),
{ pid: 101, reason: "run-token-mismatch" },
],
});
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("owned process-group cleanup liveness rechecks", () => {
it("rechecks all unreadable members after the token phase in one fresh snapshot", async () => {
const members = [100, 101, 102, 103].map((pid) => ({
pid,
parentPid: pid === 100 ? 1 : 100,
processGroupId: 100,
state: "S",
command: "fixture",
}));
const tokenReads: number[] = [];
const listProcesses = vi.fn(async () => {
if (tokenReads.length > 0)
expect(tokenReads).toEqual([100, 101, 102, 103]);
return members;
});
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses,
environment: async (pid) => {
tokenReads.push(pid);
throw new Error(`unreadable ${pid}`);
},
signalGroup,
};
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
{
cleaned: false,
reason:
"process ownership token could not be read for 4 live process(es): other_unavailable=4; live candidates 100=other_unavailable, 101=other_unavailable, 102=other_unavailable, 103=other_unavailable",
failures: members.map(({ pid }) => ({
pid,
reason: "environment-unreadable",
diagnostic: `unreadable ${pid}`,
})),
},
);
expect(listProcesses).toHaveBeenCalledTimes(2);
expect(signalGroup).not.toHaveBeenCalled();
});
it.each([false, true])(
"retains mismatches and fails closed when the liveness recheck fails: %s",
async (recheckFails) => {
const members = [100, 101, 102].map((pid) => ({
pid,
parentPid: pid === 100 ? 1 : 100,
processGroupId: 100,
state: "S",
command: "fixture",
}));
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce(members);
if (recheckFails)
listProcesses.mockRejectedValue(new Error("process table denied"));
else listProcesses.mockResolvedValue([]);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses,
environment: async (pid) => {
if (pid === 101) return { REA_PROCESS_RUN_ID: "other-run" };
throw new Error(`unreadable ${pid}`);
},
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(ownership, adapter),
).resolves.toEqual({
cleaned: false,
reason: "process ownership token mismatch for PID(s): 101",
failures: recheckFails
? [
{
pid: 100,
reason: "environment-unreadable",
diagnostic:
"unreadable 100; process liveness recheck failed: process table denied",
},
{ pid: 101, reason: "run-token-mismatch" },
{
pid: 102,
reason: "environment-unreadable",
diagnostic:
"unreadable 102; process liveness recheck failed: process table denied",
},
]
: [{ pid: 101, reason: "run-token-mismatch" }],
});
expect(listProcesses).toHaveBeenCalledTimes(2);
expect(signalGroup).not.toHaveBeenCalled();
},
);
it("accepts a member that exits during ownership revalidation", async () => {
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([launcher])
.mockResolvedValue([]);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses,
environment: () =>
Promise.reject(new Error("process exited before environment read")),
signalGroup,
};
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
{
cleaned: true,
signaled: false,
},
);
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("owned process-group cleanup validation: refreshed descendant lineage", () => {
it.each(["planning", "signaling"])(
"retains cleanup uncertainty for newly detached descendants during %s",
async (phase) => {
const child = { ...launcher, pid: 101, parentPid: 100 };
const stale = { ...launcher, command: "[MainThread]" };
const signalGroup = vi.fn();
let reads = 0;
const adapter: ProcessOwnershipHost = {
listProcesses: () => {
reads += 1;
if (phase === "signaling" && reads === 1)
return Promise.resolve([launcher, child]);
if (reads === (phase === "planning" ? 1 : 2))
return Promise.resolve([stale, child]);
return Promise.resolve([
{ ...child, parentPid: 1 },
{ ...child, pid: 102, parentPid: 101 },
{ ...child, pid: 103, parentPid: 102, processGroupId: 103 },
]);
},
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
adapter,
),
).resolves.toMatchObject({ cleaned: false });
expect(signalGroup).not.toHaveBeenCalled();
},
);
});
describe("identityless exit refresh signals a token-owned replacement", () => {
it("token-checks a reparented grandchild that appears only after the launcher exits", async () => {
const child = { ...launcher, pid: 101, parentPid: 100 };
const escaped = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "reparented-grandchild",
};
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) =>
Promise.resolve({
REA_PROCESS_RUN_ID: pid === 102 ? ownership.runId : "run-token",
}),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([{ ...launcher, command: "[MainThread]" }, child])
.mockResolvedValue([escaped]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(environment).toHaveBeenCalledWith(102);
expect(signalGroup.mock.calls).toEqual([[102, "SIGKILL"]]);
});
it("does not token-read rows that were already outside the exited snapshot", async () => {
const unrelated = {
...launcher,
pid: 50,
parentPid: 1,
processGroupId: 50,
command: "unrelated",
};
const escaped = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "reparented-grandchild",
};
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) =>
Promise.resolve({
REA_PROCESS_RUN_ID: pid === 102 ? ownership.runId : "other-run",
}),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
unrelated,
])
.mockResolvedValue([unrelated, escaped]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(environment.mock.calls.map(([pid]) => pid)).not.toContain(50);
expect(signalGroup.mock.calls).toEqual([[102, "SIGKILL"]]);
});
});
describe("identityless exit refresh leaves unrelated replacements unresolved", () => {
it.each([
["foreign", "other-run"],
["absent", undefined],
] as const)(
"does not signal a %s replacement row after the launcher exits",
async (_label, runId) => {
const signalGroup = vi.fn();
const environment = vi.fn(() =>
Promise.resolve(
runId === undefined ? {} : { REA_PROCESS_RUN_ID: runId },
),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValue([
{
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "foreign-replacement",
},
]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: false });
expect(environment).toHaveBeenCalledWith(102);
expect(signalGroup).not.toHaveBeenCalled();
},
);
it("keeps cleanup unresolved when a replacement row's token cannot be read", async () => {
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValue([
{
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "unreadable-replacement",
},
]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.reject(new Error("environment unavailable")),
signalGroup,
},
),
).resolves.toMatchObject({
cleaned: false,
reason:
"process ownership could not be revalidated for PID 102: environment unavailable",
});
expect(signalGroup).not.toHaveBeenCalled();
});
it("does not signal a token-owned replacement whose group leader is absent", async () => {
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValue([
{
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 999,
command: "member-without-leader",
},
]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
},
),
).resolves.toMatchObject({
cleaned: false,
reason: "descendant process-group leader identity could not be verified",
});
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("identityless exit refresh keeps later confirmation rows", () => {
it("token-checks a replacement born in the liveness table after an unreadable row exits", async () => {
const unreadable = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "unreadable-replacement",
};
const escaped = {
...launcher,
pid: 103,
parentPid: 1,
processGroupId: 103,
command: "owned-replacement",
};
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) =>
pid === 102
? Promise.reject(new Error("environment unavailable"))
: Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValueOnce([unreadable])
.mockResolvedValue([escaped]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(environment).toHaveBeenCalledWith(103);
expect(signalGroup.mock.calls).toEqual([[103, "SIGKILL"]]);
});
it("signals an escaped group discovered while revalidating the planned group", async () => {
const escaped = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "reparented-grandchild",
};
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([launcher])
.mockResolvedValueOnce([{ ...launcher, command: "[MainThread]" }])
.mockResolvedValue([escaped]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
},
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(signalGroup.mock.calls).toEqual([[102, "SIGKILL"]]);
});
it("stays unresolved when exit-refresh confirmations do not settle", async () => {
const signalGroup = vi.fn();
let nextPid = 200;
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockImplementation(() => {
const pid = nextPid;
nextPid += 1;
return Promise.resolve([
{
...launcher,
pid,
parentPid: 1,
processGroupId: pid,
command: "churn",
},
]);
});
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.reject(new Error("environment unavailable")),
signalGroup,
},
),
).resolves.toMatchObject({
cleaned: false,
reason:
"process ownership could not be revalidated: the exit refresh confirmation limit of 8 reads was exceeded. This release has no runtime override.",
});
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("identityless exit refresh follows a helper that changes groups", () => {
it("signals the group a token-owned member joins during confirmation", async () => {
const leader = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "owned-leader",
};
const member = {
...launcher,
pid: 103,
parentPid: 102,
processGroupId: 102,
command: "owned-member",
};
const moved = { ...member, parentPid: 1, processGroupId: 103 };
const unreadable = {
...launcher,
pid: 104,
parentPid: 1,
processGroupId: 104,
command: "unreadable",
};
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValueOnce([leader, member, unreadable])
.mockResolvedValue([leader, moved]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: (pid) =>
pid === 104
? Promise.reject(new Error("environment unavailable"))
: Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
},
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(signalGroup.mock.calls).toEqual([
[102, "SIGKILL"],
[103, "SIGKILL"],
]);
});
});
describe("identityless exit refresh rechecks a prior PID whose relation changes", () => {
it("token-checks a pre-existing PID after its parent changes", async () => {
const unrelated = {
...launcher,
pid: 50,
parentPid: 999,
processGroupId: 50,
command: "unrelated",
};
const stable = {
...launcher,
pid: 60,
parentPid: 1,
processGroupId: 60,
command: "stable-outsider",
};
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) =>
Promise.resolve({
REA_PROCESS_RUN_ID: pid === 50 ? ownership.runId : "other-run",
}),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
unrelated,
stable,
])
.mockResolvedValue([{ ...unrelated, parentPid: 1 }, stable]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(environment).toHaveBeenCalledWith(50);
expect(environment.mock.calls.map(([pid]) => pid)).not.toContain(60);
expect(signalGroup.mock.calls).toEqual([[50, "SIGKILL"]]);
});
});
describe("identityless exit refresh keeps a token-read helper", () => {
it("rechecks a matched helper after it returns to its original group", async () => {
const helper = {
...launcher,
pid: 50,
parentPid: 1,
processGroupId: 50,
command: "helper",
};
const leader = {
...launcher,
pid: 60,
parentPid: 1,
processGroupId: 60,
command: "owned-leader",
};
const stable = {
...launcher,
pid: 70,
parentPid: 1,
processGroupId: 70,
command: "stable-outsider",
};
const unreadable = {
...launcher,
pid: 104,
parentPid: 1,
processGroupId: 104,
command: "unreadable",
};
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) =>
pid === 104
? Promise.reject(new Error("environment unavailable"))
: Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
);
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
helper,
stable,
])
.mockResolvedValueOnce([
leader,
{ ...helper, parentPid: 60, processGroupId: 60 },
unreadable,
stable,
])
.mockResolvedValue([leader, helper, stable]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(environment.mock.calls.map(([pid]) => pid)).not.toContain(70);
expect(signalGroup.mock.calls).toEqual([
[50, "SIGKILL"],
[60, "SIGKILL"],
]);
});
});
describe("identityless exit refresh re-reads a reused foreign PID", () => {
it("token-checks a PID again after a foreign row exits into an owned replacement", async () => {
const reused = {
...launcher,
pid: 102,
parentPid: 1,
processGroupId: 102,
command: "reused-pid",
};
const unreadable = {
...launcher,
pid: 104,
parentPid: 1,
processGroupId: 104,
command: "unreadable",
};
let reads = 0;
const signalGroup = vi.fn();
const environment = vi.fn((pid: number) => {
if (pid === 104)
return Promise.reject(new Error("environment unavailable"));
if (pid !== 102)
return Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId });
reads += 1;
return Promise.resolve({
REA_PROCESS_RUN_ID: reads === 1 ? "other-run" : ownership.runId,
});
});
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
{ ...launcher, pid: 101, parentPid: 100 },
])
.mockResolvedValueOnce([reused, unreadable])
.mockResolvedValue([reused]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{ listProcesses, environment, signalGroup },
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(reads).toBeGreaterThan(1);
expect(signalGroup.mock.calls).toEqual([[102, "SIGKILL"]]);
});
});
describe("identityless cleanup revalidates a group that reappears", () => {
const leader = (pid: number, command = "fixture"): ProcessTableEntry => ({
...launcher,
pid,
parentPid: pid === 100 ? 1 : 100,
processGroupId: pid,
command,
});
it("signals a token-owned group that reappears after it was already processed", async () => {
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([leader(100), leader(101)])
.mockResolvedValueOnce([leader(100)])
.mockResolvedValueOnce([leader(100, "[MainThread]")])
.mockResolvedValueOnce([leader(102)])
.mockResolvedValueOnce([leader(100, "[MainThread]")])
.mockResolvedValueOnce([leader(101)])
.mockResolvedValue([leader(101)]);
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
},
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(signalGroup.mock.calls).toEqual([
[100, "SIGKILL"],
[101, "SIGKILL"],
]);
});
it("stays unresolved when a processed group keeps reappearing", async () => {
const signalGroup = vi.fn();
const tables = [
[leader(100), leader(101)],
[leader(100)],
[leader(100, "[MainThread]")],
[leader(102)],
[leader(100, "[MainThread]")],
[leader(101)],
[leader(100, "[MainThread]")],
[leader(102)],
[leader(100, "[MainThread]")],
[leader(101)],
];
let reads = 0;
const listProcesses = vi.fn<ProcessOwnershipHost["listProcesses"]>(() => {
const table = tables[reads] ?? [leader(101)];
reads += 1;
return Promise.resolve(table);
});
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
{
listProcesses,
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
signalGroup,
},
),
).resolves.toMatchObject({
cleaned: false,
reason:
"owned process group 101 reappeared after 2 revalidation passes. This release has no runtime override.",
});
expect(signalGroup.mock.calls).toEqual([[100, "SIGKILL"]]);
});
});
describe("owned process-group cleanup validation: launcher exit races", () => {
it.each([101, 999])(
"retains cleanup uncertainty for a detached descendant in group %s after launcher exit",
async (processGroupId) => {
const child = {
...launcher,
pid: 101,
parentPid: 100,
processGroupId: 101,
};
const signalGroup = vi.fn();
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce([
{ ...launcher, command: "[MainThread]" },
child,
])
.mockResolvedValue([{ ...child, parentPid: 1, processGroupId }]);
await expect(
cleanupOwnedProcessGroup(
{
...ownership,
expectedCommand: "fixture",
},
{
listProcesses,
environment: () =>
Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
signalGroup,
},
),
).resolves.toMatchObject({ cleaned: false });
expect(signalGroup).not.toHaveBeenCalled();
},
);
it.each(["planning", "signaling"])(
"settles a stale launcher command during %s without skipping surviving group ownership",
async (phase) => {
for (const remaining of ["absent", "zombie", "owned", "foreign"]) {
const stale = { ...launcher, command: "[MainThread]" };
const child = { ...launcher, pid: 101, command: "child" };
let reads = 0;
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => {
reads += 1;
if (phase === "signaling" && reads === 1)
return Promise.resolve([launcher]);
if (reads === (phase === "planning" ? 1 : 2))
return Promise.resolve([stale]);
return Promise.resolve(
remaining === "absent"
? []
: remaining === "zombie"
? [{ ...stale, state: "Z" }]
: [child],
);
},
environment: () =>
Promise.resolve({
REA_PROCESS_RUN_ID:
remaining === "foreign" && reads > 1
? "other-run"
: "run-token",
}),
signalGroup,
};
const result = await cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
adapter,
);
if (remaining === "foreign")
expect(result).toMatchObject({
cleaned: false,
reason: "process ownership token mismatch for PID(s): 101",
});
else
expect(result).toEqual({
cleaned: true,
signaled: remaining === "owned",
});
if (remaining === "owned")
expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL");
else expect(signalGroup).not.toHaveBeenCalled();
}
},
);
it("fails closed when a mismatched launcher's exit cannot be inspected", async () => {
const { adapter, signalGroup } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },
});
const listProcesses = vi
.fn<ProcessOwnershipHost["listProcesses"]>()
.mockResolvedValueOnce(await adapter.listProcesses())
.mockRejectedValue(new Error("process table unavailable"));
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "/owned/hopper" },
{ ...adapter, listProcesses },
),
).resolves.toMatchObject({
cleaned: false,
reason: expect.stringContaining("command identity did not match"),
});
expect(signalGroup).not.toHaveBeenCalled();
});
it.each(["planning", "signaling"])(
"rejects a surviving launcher PID in another group during %s",
async (phase) => {
let reads = 0;
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => {
reads += 1;
return Promise.resolve([
phase === "signaling" && reads === 1
? launcher
: reads === (phase === "planning" ? 1 : 2)
? { ...launcher, command: "[MainThread]" }
: { ...launcher, processGroupId: 999 },
]);
},
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture" },
adapter,
),
).resolves.toMatchObject({
cleaned: false,
reason: expect.stringContaining("command identity did not match"),
});
expect(signalGroup).not.toHaveBeenCalled();
},
);
});
describe("owned process-group cleanup validation: exited members", () => {
it("preserves cancellation during process-table observation instead of reporting unverifiable ownership", async () => {
const controller = new AbortController();
const reason = new Error("cancelled process observation");
const adapter = nonSignalingObserver(async (signal) => {
controller.abort(reason);
signal?.throwIfAborted();
return [];
});
await expect(
observeOwnedProcessGroups(
ownership.runId,
[ownership.processGroupId],
adapter,
controller.signal,
),
).rejects.toBe(reason);
});
it("does not inspect a process group after observation was cancelled", async () => {
const controller = new AbortController();
const reason = new Error("already cancelled observation");
controller.abort(reason);
const adapter = nonSignalingObserver(() => {
throw new Error("Cancelled observation must not inspect processes");
});
await expect(
observeOwnedProcessGroups(
ownership.runId,
[ownership.processGroupId],
adapter,
controller.signal,
),
).rejects.toBe(reason);
});
it("ignores exited zombie members during live ownership checks", async () => {
const environment = vi.fn((pid: number) =>
Promise.resolve(pid === 101 ? {} : { REA_PROCESS_RUN_ID: "run-token" }),
);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve([
launcher,
{
pid: 101,
parentPid: 100,
processGroupId: 100,
state: "Z",
command: "[node] <defunct>",
},
]),
environment,
signalGroup,
};
expect(await cleanupOwnedProcessGroup(ownership, adapter)).toEqual({
cleaned: true,
signaled: true,
});
expect(environment.mock.calls).toEqual([[100], [100]]);
expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL");
});
it("observes a zombie-only group as settled", async () => {
const environment = vi.fn(() => Promise.resolve({}));
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve([
{
pid: 101,
parentPid: 1,
processGroupId: 100,
state: "Z+",
command: "[node] <defunct>",
},
]),
environment,
signalGroup: vi.fn(),
};
expect(
await observeOwnedProcessGroups(
ownership.runId,
[ownership.processGroupId],
adapter,
),
).toEqual(new Map([[ownership.processGroupId, { state: "empty" }]]));
expect(environment).not.toHaveBeenCalled();
});
it("is idempotent when the owned group has already exited", async () => {
const { adapter } = host({});
expect(await cleanupOwnedProcessGroup(ownership, adapter)).toEqual({
cleaned: true,
signaled: false,
});
});
it("fails closed when the launcher command identity changes", async () => {
const { adapter, signalGroup } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },
});
expect(
await cleanupOwnedProcessGroup(
{
...ownership,
expectedCommand: "/owned/hopper",
expectedParentPid: 1,
},
adapter,
),
).toEqual({
cleaned: false,
reason:
"owned launcher command identity did not match (observed=fixture; expected=/owned/hopper)",
});
expect(signalGroup).not.toHaveBeenCalled();
});
it("fails closed when the launcher parent identity changes", async () => {
const { adapter, signalGroup } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },
});
expect(
await cleanupOwnedProcessGroup(
{ ...ownership, expectedCommand: "fixture", expectedParentPid: 999 },
adapter,
),
).toEqual({
cleaned: false,
reason: "owned launcher parent identity did not match",
});
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("owned process-group cleanup token batches", () => {
const root = {
pid: 100,
parentPid: 1,
processGroupId: 100,
state: "S",
command: "fixture",
};
it("batches validation and falls back only for a missing member row", async () => {
const child = { ...root, pid: 101, parentPid: root.pid };
const runTokens = vi.fn(() =>
Promise.resolve(
new Map([
[root.pid, { state: "readable" as const, runId: ownership.runId }],
]),
),
);
const environment = vi.fn(() =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => Promise.resolve([root, child]),
environment,
runTokens,
signalGroup,
};
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
{ cleaned: true, signaled: true },
);
expect(runTokens).toHaveBeenCalledTimes(2);
expect(runTokens).toHaveBeenNthCalledWith(1, [root, child]);
expect(environment.mock.calls).toEqual([[child.pid], [child.pid]]);
expect(signalGroup).toHaveBeenCalledWith(root.processGroupId, "SIGKILL");
});
it("keeps explicit unreadable rows fail-closed without environment fallback", async () => {
const environment = vi.fn(() =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => Promise.resolve([root]),
environment,
runTokens: () =>
Promise.resolve(
new Map([
[
root.pid,
{ state: "unavailable" as const, reason: "fixture unreadable" },
],
]),
),
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(ownership, adapter),
).resolves.toMatchObject({
cleaned: false,
failures: [
{
pid: root.pid,
reason: "environment-unreadable",
diagnostic: "fixture unreadable",
},
],
});
expect(environment).not.toHaveBeenCalled();
expect(signalGroup).not.toHaveBeenCalled();
});
it("falls back to the environment reader when the batch operation throws", async () => {
const environment = vi.fn(() =>
Promise.resolve({ REA_PROCESS_RUN_ID: ownership.runId }),
);
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => Promise.resolve([root]),
environment,
runTokens: () => Promise.reject(new Error("batch reader unavailable")),
signalGroup,
};
await expect(cleanupOwnedProcessGroup(ownership, adapter)).resolves.toEqual(
{ cleaned: true, signaled: true },
);
expect(environment).toHaveBeenCalledTimes(2);
expect(signalGroup).toHaveBeenCalledWith(root.processGroupId, "SIGKILL");
});
it("retains both failures when batch and individual token reads fail", async () => {
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () => Promise.resolve([root]),
environment: () => Promise.reject(new Error("individual unreadable")),
runTokens: () => Promise.reject(new Error("batch reader unavailable")),
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(ownership, adapter),
).resolves.toMatchObject({
cleaned: false,
failures: [
{
pid: root.pid,
reason: "environment-unreadable",
diagnostic:
"individual unreadable; run-token batch failed: batch reader unavailable",
},
],
});
expect(signalGroup).not.toHaveBeenCalled();
});
});
describe("Windows P0 process-tree cleanup", () => {
it("reports whether taskkill signaled or found an exited tree", async () => {
const terminated: WindowsProcessTreeHost = {
terminateTree: () => Promise.resolve("terminated"),
};
const missing: WindowsProcessTreeHost = {
terminateTree: () => Promise.resolve("missing"),
};
await expect(cleanupWindowsProcessTree(42, terminated)).resolves.toEqual({
cleaned: true,
signaled: true,
});
await expect(cleanupWindowsProcessTree(42, missing)).resolves.toEqual({
cleaned: true,
signaled: false,
});
});
it("keeps invalid identity and termination failures explicit", async () => {
const failing: WindowsProcessTreeHost = {
terminateTree: () => Promise.reject(new Error("taskkill failed")),
};
await expect(cleanupWindowsProcessTree(0, failing)).resolves.toEqual({
cleaned: false,
reason: "Windows process-tree PID is invalid",
});
await expect(cleanupWindowsProcessTree(42, failing)).resolves.toEqual({
cleaned: false,
reason:
"Windows process-tree termination failed for PID 42: taskkill failed",
});
});
});