diff --git a/plugins/codex/scripts/lib/process.mjs b/plugins/codex/scripts/lib/process.mjs index af28d1cf..06f043af 100644 --- a/plugins/codex/scripts/lib/process.mjs +++ b/plugins/codex/scripts/lib/process.mjs @@ -16,7 +16,10 @@ export function runCommand(command, args = [], options = {}) { return { command, args, - status: result.status ?? 0, + // A process killed by a signal reports status === null. Treat that as a + // failure (not success) so runCommandChecked/binaryAvailable don't silently + // accept truncated output; the signal itself is preserved below. + status: result.status ?? (result.signal ? 1 : 0), signal: result.signal ?? null, stdout: result.stdout ?? "", stderr: result.stderr ?? "", diff --git a/tests/process.test.mjs b/tests/process.test.mjs index 80e0715b..26107aa8 100644 --- a/tests/process.test.mjs +++ b/tests/process.test.mjs @@ -1,7 +1,48 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { terminateProcessTree } from "../plugins/codex/scripts/lib/process.mjs"; +import { + formatCommandFailure, + runCommand, + runCommandChecked, + terminateProcessTree +} from "../plugins/codex/scripts/lib/process.mjs"; + +test("runCommand reports a signal-terminated process as a non-zero failure", () => { + const result = runCommand(process.execPath, ["-e", "process.kill(process.pid, 'SIGKILL')"]); + assert.notEqual(result.status, 0); + assert.equal(result.signal, "SIGKILL"); +}); + +test("runCommandChecked throws when the child is killed by a signal", () => { + assert.throws( + () => runCommandChecked(process.execPath, ["-e", "process.kill(process.pid, 'SIGKILL')"]), + /signal=SIGKILL/ + ); +}); + +test("runCommand still reports a clean exit as status 0", () => { + const result = runCommand(process.execPath, ["-e", "process.exit(0)"]); + assert.equal(result.status, 0); + assert.equal(result.signal, null); +}); + +test("runCommand preserves a non-zero exit code", () => { + const result = runCommand(process.execPath, ["-e", "process.exit(3)"]); + assert.equal(result.status, 3); +}); + +test("formatCommandFailure surfaces the signal for a signal-killed command", () => { + const failure = formatCommandFailure({ + command: "git", + args: ["status"], + status: 1, + signal: "SIGKILL", + stdout: "", + stderr: "" + }); + assert.match(failure, /signal=SIGKILL/); +}); test("terminateProcessTree uses taskkill on Windows", () => { let captured = null;