Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 10 additions & 7 deletions codex-cli/src/utils/agent/sandbox/raw-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,16 @@ export function exec(
abortSignal.addEventListener("abort", abortHandler, { once: true });
}
}
if (!child.pid) {
return Promise.resolve({
stdout: "",
stderr: `likely failed because ${prog} could not be found`,
exitCode: 1,
});
}
// If spawning the child failed (e.g. the executable could not be found)
// `child.pid` will be undefined *and* an `error` event will be emitted on
// the ChildProcess instance. We intentionally do **not** bail out early
// here. Returning prematurely would leave the `error` event without a
// listener which – in Node.js – results in an "Unhandled 'error' event"
// process‑level exception that crashes the CLI. Instead we continue with
// the normal promise flow below where we are guaranteed to attach both the
// `error` and `exit` handlers right away. Either of those callbacks will
// resolve the promise and translate the failure into a regular
// ExecResult object so the rest of the agent loop can carry on gracefully.

const stdoutChunks: Array<Buffer> = [];
const stderrChunks: Array<Buffer> = [];
Expand Down
66 changes: 66 additions & 0 deletions codex-cli/tests/invalid-command-handling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi } from "vitest";

// ---------------------------------------------------------------------------
// Low‑level rawExec test ------------------------------------------------------
// ---------------------------------------------------------------------------

import { exec as rawExec } from "../src/utils/agent/sandbox/raw-exec.js";

describe("rawExec – invalid command handling", () => {
it("resolves with non‑zero exit code when executable is missing", async () => {
const cmd = ["definitely-not-a-command-1234567890"];

const result = await rawExec(cmd, {}, []);

expect(result.exitCode).not.toBe(0);
expect(result.stderr.length).toBeGreaterThan(0);
});
});

// ---------------------------------------------------------------------------
// Higher‑level handleExecCommand test ----------------------------------------
// ---------------------------------------------------------------------------

// Mock approvals and logging helpers so the test focuses on execution flow.
vi.mock("@lib/approvals.js", () => {
return {
__esModule: true,
canAutoApprove: () =>
({ type: "auto-approve", runInSandbox: false } as any),
isSafeCommand: () => null,
};
});

vi.mock("@lib/format-command.js", () => {
return {
__esModule: true,
formatCommandForDisplay: (cmd: Array<string>) => cmd.join(" "),
};
});

vi.mock("../src/utils/agent/log.js", () => ({
__esModule: true,
log: () => {},
isLoggingEnabled: () => false,
}));

import { handleExecCommand } from "../src/utils/agent/handle-exec-command.js";

describe("handleExecCommand – invalid executable", () => {
it("returns non‑zero exit code for 'git show' as a single argv element", async () => {
const execInput = { cmd: ["git show"] } as any;
const config = { model: "any", instructions: "" } as any;
const policy = { mode: "auto" } as any;
const getConfirmation = async () => ({ review: "yes" } as any);

const { outputText, metadata } = await handleExecCommand(
execInput,
config,
policy,
getConfirmation,
);

expect(metadata["exit_code"]).not.toBe(0);
expect(String(outputText).length).toBeGreaterThan(0);
});
});
Loading