import { describe, it } from "node:test"; import assert from "node:assert"; import { EventEmitter } from "node:events"; import { Readable } from "node:stream"; import { executePrompt } from "../src/execute.js"; import { ClientNotFoundError, ExecError } from "../src/types.js"; import type { ClientName } from "../src/types.js"; interface MockScenario { stdout?: string; stderr?: string; exitCode?: number; error?: NodeJS.ErrnoException; hang?: boolean; } function createMockChildProcess(scenario: MockScenario): any { const child = new EventEmitter() as any; child.pid = 12345; child.stdout = Readable.from( scenario.stdout !== undefined ? [scenario.stdout] : [] ); child.stderr = Readable.from( scenario.stderr !== undefined ? [scenario.stderr] : [] ); child.killed = false; child.kill = () => { child.killed = true; process.nextTick(() => { child.emit("exit", null, "SIGTERM"); child.emit("close", null, "SIGTERM"); }); return true; }; let stdoutEnded = scenario.stdout === undefined; let stderrEnded = scenario.stderr === undefined; child.stdout.on("end", () => { stdoutEnded = true; maybeClose(); }); child.stderr.on("end", () => { stderrEnded = true; maybeClose(); }); function maybeClose() { if (stdoutEnded && stderrEnded && !scenario.hang) { child.emit("exit", scenario.exitCode ?? 0, null); child.emit("close", scenario.exitCode ?? 0, null); } } process.nextTick(() => { if (scenario.error) { child.emit("error", scenario.error); } }); return child; } function mockSpawn( scenarios: Map ): any { return (cmd: string, args: string[], _opts: any): any => { const key = [cmd, ...args].join(" "); const scenario = scenarios.get(key); if (!scenario) { return createMockChildProcess({ error: Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }), }); } return createMockChildProcess(scenario); }; } function mockExistsSync(allowedPaths: Set): any { return (p: string) => allowedPaths.has(p); } describe("executePrompt", () => { it("captures stdout for a successful codex execution", async () => { const scenarios = new Map([ ['codex exec --yolo "hello world"', { stdout: "result\n", exitCode: 0 }], ]); const result = await executePrompt("codex", '"hello world"', { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.stdout, "result\n"); assert.strictEqual(result.stderr, ""); assert.strictEqual(result.exitCode, 0); }); it("captures stderr for a successful claude execution", async () => { const scenarios = new Map([ ["claude -p hello --dangerously-skip-permissions", { stdout: "", stderr: "warning\n", exitCode: 0 }], ]); const result = await executePrompt("claude", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.stdout, ""); assert.strictEqual(result.stderr, "warning\n"); assert.strictEqual(result.exitCode, 0); }); it("returns non-zero exit code without throwing", async () => { const scenarios = new Map([ ["opencode run --dangerously-skip-permissions fail", { stdout: "", stderr: "error\n", exitCode: 1 }], ]); const result = await executePrompt("opencode", "fail", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.exitCode, 1); assert.strictEqual(result.stderr, "error\n"); }); it("throws ClientNotFoundError when binary emits ENOENT", async () => { const scenarios = new Map(); await assert.rejects( executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }), (err: unknown) => err instanceof ClientNotFoundError ); }); it("throws ClientNotFoundError when explicit clientPath does not exist", async () => { await assert.rejects( executePrompt("claude", "hello", { clientPath: "/nonexistent/claude", existsSync: mockExistsSync(new Set()), }), (err: unknown) => err instanceof ClientNotFoundError ); }); it("rejects with ExecError when timeout is exceeded", async () => { const scenarios = new Map([ ["codex exec --yolo slow", { hang: true }], ]); await assert.rejects( executePrompt("codex", "slow", { spawn: mockSpawn(scenarios), existsSync: () => true, timeoutMs: 10, }), (err: unknown) => err instanceof ExecError && err.message.includes("timed out") && err.result.exitCode === -1 ); }); it("passes prompts with special characters unchanged", async () => { const scenarios = new Map([ [ 'codex exec --yolo "quotes\nnewlines"', { stdout: "ok", exitCode: 0 }, ], ]); const result = await executePrompt("codex", '"quotes\nnewlines"', { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.stdout, "ok"); assert.strictEqual(result.exitCode, 0); }); it("rejects empty prompt", async () => { await assert.rejects( executePrompt("codex", "", { spawn: mockSpawn(new Map()), existsSync: () => true, }), (err: unknown) => err instanceof ExecError && err.message.includes("empty") ); }); it("rejects whitespace-only prompt", async () => { await assert.rejects( executePrompt("claude", " ", { spawn: mockSpawn(new Map()), existsSync: () => true, }), (err: unknown) => err instanceof ExecError && err.message.includes("empty") ); }); it("throws ExecError for invalid client at runtime", async () => { await assert.rejects( executePrompt("bogus" as unknown as ClientName, "hello", { spawn: mockSpawn(new Map()), existsSync: () => true, }), (err: unknown) => err instanceof ExecError && err.message.includes("Unknown client") ); }); it("includes client and durationMs in result", async () => { const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", exitCode: 0 }], ]); const result = await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.client, "codex"); assert.strictEqual(typeof result.durationMs, "number"); assert.ok(result.durationMs >= 0); }); it("rejects with ExecError containing custom timeout value", async () => { const scenarios = new Map([ ["codex exec --yolo slow", { hang: true }], ]); await assert.rejects( executePrompt("codex", "slow", { spawn: mockSpawn(scenarios), existsSync: () => true, timeoutMs: 50, }), (err: unknown) => err instanceof ExecError && err.message === "Execution timed out after 50ms" && err.result.exitCode === -1 && err.result.client === "codex" && typeof err.result.durationMs === "number" ); }); it("uses default timeout of 600000 when timeoutMs is not provided", async () => { const delays: number[] = []; const origSetTimeout = global.setTimeout; (global as any).setTimeout = function(callback: any, delay: number) { delays.push(delay); return origSetTimeout(callback, delay); }; const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", exitCode: 0 }], ]); try { await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(delays[0], 600_000); } finally { global.setTimeout = origSetTimeout; } }); it("emits debug info via onDebug when debug is true for successful execution", async () => { const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", stderr: "warn", exitCode: 0 }], ]); const debugInfos: any[] = []; const result = await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(result.exitCode, 0); assert.strictEqual(debugInfos.length, 1); const info = debugInfos[0]; assert.strictEqual(info.command, "codex"); assert.deepStrictEqual(info.args, ["exec", "--yolo", "hello"]); assert.strictEqual(info.pid, 12345); assert.strictEqual(info.exitCode, 0); assert.strictEqual(info.exitSignal, null); assert.strictEqual(info.stderrLength, 4); assert.strictEqual(info.stdoutLength, 2); assert.strictEqual(typeof info.durationMs, "number"); assert.ok(info.durationMs >= 0); }); it("emits debug info via onDebug when debug is true for failed execution", async () => { const scenarios = new Map([ ["codex exec --yolo fail", { stdout: "", stderr: "error", exitCode: 1 }], ]); const debugInfos: any[] = []; const result = await executePrompt("codex", "fail", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(result.exitCode, 1); assert.strictEqual(debugInfos.length, 1); assert.strictEqual(debugInfos[0].exitCode, 1); assert.strictEqual(debugInfos[0].stderrLength, 5); assert.strictEqual(debugInfos[0].stdoutLength, 0); }); it("emits debug info via onDebug for spawn errors", async () => { const scenarios = new Map(); const debugInfos: any[] = []; await assert.rejects( executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }), (err: unknown) => err instanceof ClientNotFoundError ); assert.strictEqual(debugInfos.length, 1); assert.strictEqual(debugInfos[0].command, "codex"); assert.deepStrictEqual(debugInfos[0].args, ["exec", "--yolo", "hello"]); assert.strictEqual(debugInfos[0].exitCode, null); assert.strictEqual(debugInfos[0].exitSignal, null); }); it("reports noisySuccess=true when stderr is non-empty and exitCode is 0", async () => { const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", stderr: "warn", exitCode: 0 }], ]); const debugInfos: any[] = []; await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(debugInfos[0].noisySuccess, true); }); it("reports noisySuccess=false when stderr is empty and exitCode is 0", async () => { const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", stderr: "", exitCode: 0 }], ]); const debugInfos: any[] = []; await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(debugInfos[0].noisySuccess, false); }); it("reports noisySuccess=false when exitCode is non-zero even if stderr is non-empty", async () => { const scenarios = new Map([ ["codex exec --yolo fail", { stdout: "", stderr: "error", exitCode: 1 }], ]); const debugInfos: any[] = []; await executePrompt("codex", "fail", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(debugInfos[0].noisySuccess, false); }); it("filters codex ReasoningSummary noise from stderr on exit code 0", async () => { const noisyStderr = [ '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryPartAdded without active item', '2026-05-20T18:33:03.281713Z ERROR codex_core::util: ReasoningSummaryDelta without active item', '2026-05-20T18:33:03.348247Z ERROR codex_core::util: ReasoningSummaryDelta without active item', ].join('\n'); const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "Hello world!", stderr: noisyStderr, exitCode: 0 }], ]); const result = await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.exitCode, 0); assert.strictEqual(result.stderr, ""); assert.strictEqual(result.stdout, "Hello world!"); }); it("preserves real error stderr from codex on non-zero exit code", async () => { const noisyStderr = [ '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item', 'Error: something actually went wrong', ].join('\n'); const scenarios = new Map([ ["codex exec --yolo fail", { stdout: "", stderr: noisyStderr, exitCode: 1 }], ]); const result = await executePrompt("codex", "fail", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.exitCode, 1); assert.ok(result.stderr.includes("ReasoningSummaryDelta")); assert.ok(result.stderr.includes("something actually went wrong")); }); it("provides rawStderr in debug info when noise is filtered", async () => { const noisyStderr = '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item\n'; const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", stderr: noisyStderr, exitCode: 0 }], ]); const debugInfos: any[] = []; const result = await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(result.exitCode, 0); assert.strictEqual(result.stderr, ""); assert.strictEqual(debugInfos[0].rawStderr, noisyStderr); }); it("does not set rawStderr when no noise filtering occurred", async () => { const scenarios = new Map([ ["codex exec --yolo hello", { stdout: "ok", stderr: "", exitCode: 0 }], ]); const debugInfos: any[] = []; await executePrompt("codex", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, debug: true, onDebug: (info) => debugInfos.push(info), }); assert.strictEqual(debugInfos[0].rawStderr, undefined); }); it("does not filter stderr for non-codex clients", async () => { const noisyStderr = '2026-05-20T18:33:01.969310Z ERROR codex_core::util: ReasoningSummaryDelta without active item\n'; const scenarios = new Map([ ["claude -p hello --dangerously-skip-permissions", { stdout: "ok", stderr: noisyStderr, exitCode: 0 }], ]); const result = await executePrompt("claude", "hello", { spawn: mockSpawn(scenarios), existsSync: () => true, }); assert.strictEqual(result.exitCode, 0); assert.strictEqual(result.stderr, noisyStderr); }); });