mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-10 12:07:09 +02:00
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
import type { ChildProcess } from "node:child_process";
|
|
import { expect, it } from "vitest";
|
|
import { createProcessTreeOwner } from "./process-tree-owner.js";
|
|
import type { ProcessObservation } from "./process-tree.js";
|
|
|
|
function row(pid: number, parentPid: number, processGroupId: number, started = `start-${pid}`, state = "S"): ProcessObservation {
|
|
return { pid, parentPid, processGroupId, started, state, kind: "node" };
|
|
}
|
|
function fixture(initial: ProcessObservation[]) {
|
|
let table = [row(process.pid, 1, 10), ...initial];
|
|
const root = { pid: 100, exitCode: null as number | null, signalCode: null as NodeJS.Signals | null };
|
|
const signals: Array<[number, NodeJS.Signals]> = [];
|
|
const owner = createProcessTreeOwner(root as ChildProcess, {
|
|
readTable: async () => table,
|
|
signalGroup: (pid, signal) => { signals.push([pid, signal]); },
|
|
});
|
|
return { root, owner, signals, replace: (rows: ProcessObservation[]) => { table = [row(process.pid, 1, 10), ...rows]; } };
|
|
}
|
|
|
|
it.skipIf(process.platform === "win32")("revalidates PID/start before signaling and refuses a recycled group", async () => {
|
|
const f = fixture([row(100, process.pid, 100), row(200, 100, 200)]);
|
|
try {
|
|
await f.owner.observe();
|
|
f.replace([row(100, process.pid, 100), row(200, 1, 200, "new-start")]);
|
|
await expect(f.owner.signal("SIGKILL")).rejects.toThrow("identity became uncertain");
|
|
expect(f.signals).toEqual([]);
|
|
} finally { f.owner.stopObserving(); }
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")("retains observed descendants when the root exits and excludes the caller group", async () => {
|
|
const f = fixture([row(100, process.pid, 100), row(200, 100, 200), row(300, 100, 10), row(999, process.pid, 999)]);
|
|
try {
|
|
await f.owner.observe();
|
|
f.root.exitCode = 1;
|
|
f.replace([row(200, 1, 200), row(300, 1, 10), row(999, process.pid, 999)]);
|
|
await f.owner.signal("SIGTERM");
|
|
expect(f.signals).toEqual([[200, "SIGTERM"]]);
|
|
} finally { f.owner.stopObserving(); }
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")("treats an unreaped zombie as stopped without signaling a replacement", async () => {
|
|
const f = fixture([row(100, process.pid, 100), row(200, 100, 200)]);
|
|
try {
|
|
await f.owner.observe();
|
|
f.root.exitCode = 1;
|
|
f.replace([row(200, 1, 200, "start-200", "Z")]);
|
|
await f.owner.signal("SIGKILL");
|
|
expect(f.owner.liveGroups()).toEqual([]);
|
|
expect(f.signals).toEqual([]);
|
|
} finally { f.owner.stopObserving(); }
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")("sends launcher grace only to the root while retaining nested groups for escalation", async () => {
|
|
const f = fixture([row(100, process.pid, 100), row(200, 100, 200), row(300, 200, 300)]);
|
|
try {
|
|
await f.owner.observe();
|
|
await f.owner.signal("SIGTERM", f.owner.gracefulRoots());
|
|
expect(f.signals).toEqual([[100, "SIGTERM"]]);
|
|
f.root.exitCode = 1;
|
|
f.replace([row(200, 1, 200), row(300, 200, 300)]);
|
|
await f.owner.observe();
|
|
expect([...f.owner.gracefulRoots()]).toEqual([200]);
|
|
await f.owner.signal("SIGKILL");
|
|
expect(f.signals.slice(1).map(([pid]) => pid).sort()).toEqual([200, 300]);
|
|
} finally { f.owner.stopObserving(); }
|
|
});
|