Files
llmx/codex-cli/tests/raw-exec-process-group.test.ts
Sergio 49991bb85a fix: raw-exec-process-group.test improve reliability and error handling (#280)
description:

Makes the test verifying process group termination more
robust against timing variations. It increases a delay slightly 
and correctly handles the scenario where the test process might 
be aborted before it can output the grandchild PID

current:

![image](https://github.com/user-attachments/assets/6dd7a9b4-b578-433d-a3db-c0c8c71950d9)

fixed:

![image](https://github.com/user-attachments/assets/c9a1ffdf-3001-4563-b486-fbefb1830a8b)
2025-04-17 23:00:28 -07:00

75 lines
2.8 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, 100));
// 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);
// Attempt to extract the grandchild PID from stdout.
const pidMatch = /^(\d+)/.exec(stdout.trim());
if (pidMatch) {
const sleepPid = Number(pidMatch[1]);
// Verify that the sleep process is no longer alive.
let alive = true;
try {
process.kill(sleepPid, 0);
} catch (error: any) {
// Check if error is ESRCH (No such process)
if (error.code === "ESRCH") {
alive = false; // Process is dead, as expected.
} else {
throw error;
}
}
expect(alive).toBe(false);
} else {
// If PID was not printed, it implies bash was killed very early.
// The test passes implicitly in this scenario as the abort mechanism
// successfully stopped the command execution quickly.
expect(true).toBe(true);
}
});
});