Files
llmx/codex-cli/tests/raw-exec-process-group.test.ts
Ilan Bigio 59a180ddec Initial commit
Signed-off-by: Ilan Bigio <ilan@openai.com>
2025-04-16 12:56:08 -04:00

65 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from "vitest";
import { exec as rawExec } from "../src/utils/agent/sandbox/raw-exec.js";
// Regression test: When cancelling an inflight `rawExec()` the implementation
// must terminate *all* processes that belong to the spawned command not just
// the direct child. The original logic only sent `SIGTERM` to the immediate
// child which meant that grandchildren (for instance when running through a
// `bash -c` wrapper) were left running and turned into "zombie" processes.
// Strategy:
// 1. Start a Bash shell that spawns a longrunning `sleep`, prints the PID
// of that `sleep`, and then waits forever. This guarantees we can later
// check if the grandchild is still alive.
// 2. Abort the exec almost immediately.
// 3. After `rawExec()` resolves we probe the previously printed PID with
// `process.kill(pid, 0)`. If the call throws `ESRCH` the process no
// longer exists the desired outcome. Otherwise the test fails.
// The negativePID processgroup trick employed by the fixed implementation is
// POSIXonly. On Windows we skip the test.
describe("rawExec abort kills entire process group", () => {
it("terminates grandchildren spawned via bash", async () => {
if (process.platform === "win32") {
return;
}
const abortController = new AbortController();
// Bash script: spawn `sleep 30` in background, print its PID, then wait.
const script = "sleep 30 & pid=$!; echo $pid; wait $pid";
const cmd = ["bash", "-c", script];
// Kick off the command.
const execPromise = rawExec(cmd, {}, [], abortController.signal);
// Give Bash a tiny bit of time to start and print the PID.
await new Promise((r) => setTimeout(r, 50));
// Cancel the task this should kill *both* bash and the inner sleep.
abortController.abort();
const { exitCode, stdout } = await execPromise;
// We expect a nonzero exit code because the process was killed.
expect(exitCode).not.toBe(0);
// Extract the grandchild PID from stdout.
const pidMatch = /^(\d+)/.exec(stdout.trim());
expect(pidMatch).not.toBeNull();
const sleepPid = Number(pidMatch![1]);
// Verify that the sleep process is no longer alive.
let alive = true;
try {
process.kill(sleepPid, 0); // throws if the process does not exist
alive = true;
} catch {
alive = false;
}
expect(alive).toBe(false);
});
});