mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
Keep a run-scoped evidence owner from acquisition through settlement and cleanup retry. Bind token observations to native generations and retain related generations and unresolved identity lookups across table omissions, reparenting and process-group changes without treating PID hints as authority. Use native effective UID facts for withholding decisions, corroborate Linux procfs absence, and share the Linux stat parser with sampling and debugger identity checks. Signal only after current token and group revalidation. Complete swept cleanup across all groups with one bounded final family check; retain unresolved owners and successful-peer completion across retries. Validation: npm run check, docs:check and test discovery passed. Focused ownership/consumer coverage totals 513 passed and 1 skipped across 35 files, including the corrected Electron cancellation test rerun independently. Darwin helper compilation, real owned Node child cleanup, CLI/MCP capture and retained cleanup retries ran. Real Electron applications, native Linux races/PID reuse and Windows cleanup were not qualified. The original Linux reports remain open for that work. Fixes #5238 Refs #4322, #5206
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
|
|
const filesystem = vi.hoisted(() => ({
|
|
readFile: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("node:fs/promises")>();
|
|
return { ...original, readFile: filesystem.readFile };
|
|
});
|
|
|
|
import { createSystemProcessOwnershipHost } from "./ProcessOwnershipObservation.js";
|
|
import type { ProcessTableEntry } from "./ProcessOwnership.js";
|
|
|
|
const entry: ProcessTableEntry = {
|
|
pid: 42,
|
|
parentPid: 1,
|
|
processGroupId: 42,
|
|
state: "S",
|
|
command: "fixture",
|
|
};
|
|
|
|
const filesystemError = (code: string): NodeJS.ErrnoException =>
|
|
Object.assign(new Error(code), { code });
|
|
|
|
it("reports absence only when /proc lookup and signal-zero both report ESRCH", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
const signalProbe = vi.spyOn(process, "kill").mockImplementation(() => {
|
|
throw filesystemError("ESRCH");
|
|
});
|
|
try {
|
|
for (const code of ["ENOENT", "ESRCH"]) {
|
|
filesystem.readFile.mockRejectedValueOnce(filesystemError(code));
|
|
await expect(host.processIdentities?.([entry])).resolves.toEqual(
|
|
new Map([
|
|
[
|
|
42,
|
|
{
|
|
state: "absent",
|
|
reason: "not-found-in-caller-pid-namespace",
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
}
|
|
expect(signalProbe).toHaveBeenCalledTimes(2);
|
|
expect(signalProbe).toHaveBeenNthCalledWith(1, 42, 0);
|
|
} finally {
|
|
signalProbe.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("keeps hidden or permission-denied live PIDs unavailable", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
const signalProbe = vi.spyOn(process, "kill");
|
|
try {
|
|
filesystem.readFile.mockRejectedValueOnce(filesystemError("ENOENT"));
|
|
signalProbe.mockReturnValueOnce(true);
|
|
await expect(host.processIdentities?.([entry])).resolves.toEqual(
|
|
new Map([
|
|
[
|
|
42,
|
|
{
|
|
state: "unavailable",
|
|
reason: "proc_stat_missing_but_process_signal_probe_succeeded",
|
|
},
|
|
],
|
|
]),
|
|
);
|
|
|
|
filesystem.readFile.mockRejectedValueOnce(filesystemError("ENOENT"));
|
|
signalProbe.mockImplementationOnce(() => {
|
|
throw filesystemError("EPERM");
|
|
});
|
|
const denied = await host.processIdentities?.([entry]);
|
|
const deniedIdentity = denied?.get(42);
|
|
expect(deniedIdentity).toMatchObject({ state: "unavailable" });
|
|
expect(
|
|
deniedIdentity?.state === "unavailable" ? deniedIdentity.reason : "",
|
|
).toContain("EPERM");
|
|
} finally {
|
|
signalProbe.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("keeps permission, parser, and unexpected failures unavailable", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
const signalProbe = vi.spyOn(process, "kill");
|
|
try {
|
|
for (const code of ["EACCES", "EPERM", "EIO"]) {
|
|
filesystem.readFile.mockRejectedValueOnce(filesystemError(code));
|
|
const result = await host.processIdentities?.([entry]);
|
|
expect(result?.get(42)?.state).toBe("unavailable");
|
|
}
|
|
|
|
filesystem.readFile.mockResolvedValueOnce("42 (fixture) S malformed");
|
|
await expect(host.processIdentities?.([entry])).resolves.toEqual(
|
|
new Map([[42, { state: "unavailable", reason: "malformed_proc_stat" }]]),
|
|
);
|
|
expect(signalProbe).not.toHaveBeenCalled();
|
|
} finally {
|
|
signalProbe.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("rejects invalid PIDs before procfs lookup or signal probing", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
const signalProbe = vi.spyOn(process, "kill");
|
|
filesystem.readFile.mockClear();
|
|
try {
|
|
await expect(
|
|
host.processIdentities?.([{ ...entry, pid: 0 }]),
|
|
).resolves.toEqual(
|
|
new Map([[0, { state: "unavailable", reason: "invalid_process_id" }]]),
|
|
);
|
|
expect(filesystem.readFile).not.toHaveBeenCalled();
|
|
expect(signalProbe).not.toHaveBeenCalled();
|
|
} finally {
|
|
signalProbe.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("preserves cancellation before and after the signal-zero probe", async () => {
|
|
const host = createSystemProcessOwnershipHost("linux", {});
|
|
const controller = new AbortController();
|
|
const reason = new Error("cancelled");
|
|
controller.abort(reason);
|
|
filesystem.readFile.mockRejectedValueOnce(
|
|
Object.assign(new Error("aborted"), { name: "AbortError" }),
|
|
);
|
|
await expect(
|
|
host.processIdentities?.([entry], controller.signal),
|
|
).rejects.toMatchObject({ name: "AbortError" });
|
|
|
|
const afterProbe = new AbortController();
|
|
const probeAbort = vi.spyOn(process, "kill").mockImplementation(() => {
|
|
afterProbe.abort(reason);
|
|
throw filesystemError("ESRCH");
|
|
});
|
|
try {
|
|
filesystem.readFile.mockRejectedValueOnce(filesystemError("ENOENT"));
|
|
await expect(
|
|
host.processIdentities?.([entry], afterProbe.signal),
|
|
).rejects.toBe(reason);
|
|
} finally {
|
|
probeAbort.mockRestore();
|
|
}
|
|
});
|