import { describe, expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, parseProcessEnvironment, selectCapturedProcessGroupIds, type ProcessOwnershipHost, } from "../src/application/ProcessOwnership.js"; const ownership = { runId: "run-token", leaderPid: 100, processGroupId: 100, }; const host = ( environments: Readonly>>>, ): { readonly adapter: ProcessOwnershipHost; readonly signalGroup: ReturnType; } => { const signalGroup = vi.fn(); return { adapter: { listMembers: () => Promise.resolve( Object.keys(environments).map((pid) => ({ pid: Number(pid), parentPid: Number(pid) === 100 ? 1 : 100, processGroupId: 100, command: "fixture", })), ), environment: (pid) => Promise.resolve(environments[pid] ?? {}), signalGroup, }, signalGroup, }; }; describe("owned process-group cleanup", () => { it("excludes sampled groups whose leader was outside the captured tree", () => { expect( selectCapturedProcessGroupIds(100, [ { pid: 100, process_group_id: 42 }, { pid: 100, process_group_id: 100 }, { pid: 101, process_group_id: 42 }, { pid: 102, process_group_id: 102 }, { pid: 103, process_group_id: 102 }, ]), ).toEqual([100, 102]); }); it("drops nameless Linux environment entries", () => { expect( parseProcessEnvironment("=ignored\0REA_PROCESS_RUN_ID=owned\0EMPTY=\0"), ).toEqual({ REA_PROCESS_RUN_ID: "owned", EMPTY: "" }); }); it("signals only a group whose every member carries the run token", async () => { const { adapter, signalGroup } = host({ 100: { REA_PROCESS_RUN_ID: "run-token" }, 101: { REA_PROCESS_RUN_ID: "run-token" }, }); expect(await cleanupOwnedProcessGroup(ownership, adapter)).toEqual({ cleaned: true, signaled: true, }); expect(signalGroup).toHaveBeenCalledWith(100, "SIGKILL"); }); 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 group contains an unowned or PID-reused process", }); expect(signalGroup).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", }); 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(); }); });