From 29e0b2652b855c17f74f6c9c74cf4fb68b096fb9 Mon Sep 17 00:00:00 2001 From: Omer Cohen <639682+omercnet@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:21:52 +0000 Subject: [PATCH] test(paseo-omp): cover Windows EOF tree race --- paseo-omp/tests/omp-rpc-process.test.ts | 105 ++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/paseo-omp/tests/omp-rpc-process.test.ts b/paseo-omp/tests/omp-rpc-process.test.ts index a6334690..d4a4f3c3 100644 --- a/paseo-omp/tests/omp-rpc-process.test.ts +++ b/paseo-omp/tests/omp-rpc-process.test.ts @@ -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 = []; let descendantsAlive = true;