Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions paseo-omp/tests/omp-rpc-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,111 @@ process.stdout.write(String(descendant.pid) + "\\n", () => {
);
});

testOnWindows(
"keeps the Windows tree root alive until descendant cleanup completes",
async () => {
const script = `
const { spawn } = require("node:child_process");
const descendant = spawn(
process.execPath,
["-e", "Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0)"],
{ stdio: "ignore", windowsHide: true },
);
descendant.unref();
process.stdout.write(JSON.stringify({
type: "ready",
protocolVersion: 1,
supportedProtocolVersions: [1, 2],
maxFrameBytes: 1048576,
maxReassembledFrameBytes: 67108864,
}) + "\\n");
let input = "";
process.stdin.on("data", chunk => {
input += String(chunk);
while (true) {
const newline = input.indexOf("\\n");
if (newline < 0) return;
const line = input.slice(0, newline);
input = input.slice(newline + 1);
if (!line) continue;
const command = JSON.parse(line);
if (command.type === "negotiate_protocol") {
process.stdout.write(JSON.stringify({
type: "response",
id: command.id,
command: "negotiate_protocol",
success: true,
data: { protocolVersion: 2 },
}) + "\\n");
continue;
}
if (command.type !== "get_state") continue;
process.stdout.write(JSON.stringify({
type: "notice",
level: "info",
message: String(descendant.pid),
}) + "\\n");
process.stdout.write(JSON.stringify({
type: "response",
id: command.id,
success: true,
data: {
model: null,
isStreaming: false,
isCompacting: false,
sessionId: "windows-tree",
},
}) + "\\n");
}
});
process.stdin.on("end", () => process.exit(0));
`;
let leader: ChildProcessWithoutNullStreams | undefined;
let descendantPid: number | undefined;
const runtime = new OmpRpcRuntime({
spawnProcess(request) {
leader = spawn(process.execPath, ["-e", script], {
cwd: request.cwd,
env: { ...process.env, ...request.env },
detached: request.detached,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
return leader;
},
async terminateProcessTree(pid) {
const root = leader;
if (!root) return false;
// Model a taskkill launch that loses the tree root when EOF is sent concurrently.
await Promise.race([once(root, "exit"), sleep(250)]);
return await terminateSpawnedProcessTree(pid, "win32");
},
environment: TEST_RUNTIME_ENV,
});

try {
const session = await runtime.startSession({ cwd: process.cwd(), mode: "full" });
const descendantPidEvent = nextEvent((listener) => session.onEvent(listener));
await session.getState();
const notice = await descendantPidEvent;
if (notice.type !== "notice") throw new Error("Expected descendant PID notice");
descendantPid = Number(notice.message);
if (!Number.isSafeInteger(descendantPid) || descendantPid < 1) {
throw new Error("Windows process-tree fixture did not report a valid descendant PID");
}
const pid = descendantPid;
expect(() => process.kill(pid, 0)).not.toThrow();

await session.close();

expect(() => process.kill(pid, 0)).toThrow(expect.objectContaining({ code: "ESRCH" }));
} finally {
if (leader?.pid) await terminateSpawnedProcessTree(leader.pid, "win32");
if (descendantPid) await terminateSpawnedProcessTree(descendantPid, "win32");
}
},
);

test("terminates a surviving POSIX process group after its leader exited", async () => {
const signals: Array<NodeJS.Signals | 0> = [];
let descendantsAlive = true;
Expand Down
Loading