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.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 "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", { 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 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 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 "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") ); }); });