import { describe, it } from "node:test"; import assert from "node:assert"; import { main } from "../src/cli.js"; import { ClientNotFoundError } from "../src/types.js"; import type { ClientInfo, ExecResult, ClientName } from "../src/types.js"; function captureOutput() { const logs: string[] = []; const errors: string[] = []; const origLog = console.log; const origError = console.error; console.log = (...args: unknown[]) => { logs.push(args.map(String).join(" ")); }; console.error = (...args: unknown[]) => { errors.push(args.map(String).join(" ")); }; return { logs, errors, restore() { console.log = origLog; console.error = origError; }, }; } describe("main", () => { const mockClients: ClientInfo[] = [ { name: "codex", found: true, path: "/usr/bin/codex", version: "1.0.0" }, { name: "claude", found: true, path: "/usr/bin/claude", version: "2.0.0" }, { name: "opencode", found: false }, ]; const mockResult: ExecResult = { stdout: "output", stderr: "", exitCode: 0, }; it("returns 0 for --help and prints usage", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); assert.ok(out.logs.some((l) => l.includes("Usage:"))); } finally { out.restore(); } }); it("prints usage and returns 1 for bare invocation", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts"]); assert.strictEqual(code, 1); assert.ok(out.logs.some((l) => l.includes("Usage:"))); } finally { out.restore(); } }); it("returns 1 for unknown command", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "bogus"]); assert.strictEqual(code, 1); const err = out.errors[0] ?? out.logs[0]; assert.ok(String(err).includes("Unknown command")); } finally { out.restore(); } }); it("list prints JSON array of clients by default", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "list"], { detectClients: () => mockClients, }); assert.strictEqual(code, 0); assert.strictEqual(out.logs.length, 1); const parsed = JSON.parse(out.logs[0]); assert.deepStrictEqual(parsed, mockClients); } finally { out.restore(); } }); it("list prints human-readable output with --text", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "list", "--text"], { detectClients: () => mockClients, }); assert.strictEqual(code, 0); assert.ok(out.logs.some((l) => l.includes("codex:"))); assert.ok(out.logs.some((l) => l.includes("claude:"))); assert.ok(out.logs.some((l) => l.includes("opencode:"))); } finally { out.restore(); } }); it("run executes client with prompt and prints JSON result", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, } ); assert.strictEqual(code, 0); assert.strictEqual(executed.length, 1); assert.strictEqual(executed[0].client, "codex"); assert.strictEqual(executed[0].prompt, "hello"); const parsed = JSON.parse(out.logs[0]); assert.deepStrictEqual(parsed, mockResult); } finally { out.restore(); } }); it("run prints text output with --text", async () => { const out = captureOutput(); const stdoutChunks: string[] = []; const stderrChunks: string[] = []; try { const code = await main( [ "node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--text", ], { detectClients: () => mockClients, executePrompt: async () => ({ stdout: "hello-out", stderr: "hello-err", exitCode: 0, }), stdoutWrite: (chunk) => stdoutChunks.push(chunk), stderrWrite: (chunk) => stderrChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stdoutChunks.join(""), "hello-out"); assert.strictEqual(stderrChunks.join(""), "hello-err"); } finally { out.restore(); } }); it("run returns 1 and prints error JSON when client is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "run", "--prompt", "hello"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("client")); } finally { out.restore(); } }); it("run returns 1 and prints error text when client is missing in text mode", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "run", "--prompt", "hello", "--text"], { detectClients: () => mockClients, } ); assert.strictEqual(code, 1); assert.ok(out.errors[0].includes("client")); } finally { out.restore(); } }); it("run returns 1 and prints error JSON when prompt is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "run", "--client", "codex"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("prompt")); } finally { out.restore(); } }); it("run returns 1 for unknown client", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "run", "--client", "bogus", "--prompt", "hello"], { detectClients: () => mockClients, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("client")); } finally { out.restore(); } }); it("run returns 1 and prints error JSON when client not found", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, executePrompt: async () => { throw new ClientNotFoundError("codex"); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not found")); } finally { out.restore(); } }); it("dispatch routes positional prompt and prints JSON result", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "use claude to write tests"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {} }), } ); assert.strictEqual(code, 0); assert.strictEqual(executed.length, 1); assert.strictEqual(executed[0].client, "claude"); assert.strictEqual(executed[0].prompt, "use claude to write tests"); const parsed = JSON.parse(out.logs[0]); assert.deepStrictEqual(parsed, mockResult); } finally { out.restore(); } }); it("dispatch routes --prompt flag and prints JSON result", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "--prompt", "use codex to refactor"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {} }), } ); assert.strictEqual(code, 0); assert.strictEqual(executed.length, 1); assert.strictEqual(executed[0].prompt, "use codex to refactor"); } finally { out.restore(); } }); it("dispatch respects --client override", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "do something", "--client", "opencode"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: (_p, cfg) => cfg?.client ?? null, resolveConfig: () => ({ paths: {}, defaultClient: "claude" }), } ); assert.strictEqual(code, 0); assert.strictEqual(executed[0].client, "opencode"); } finally { out.restore(); } }); it("dispatch returns 1 when prompt cannot be resolved", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "dispatch", "do something"], { detectClients: () => mockClients, executePrompt: async () => mockResult, resolveClient: () => null, resolveConfig: () => ({ paths: {} }), } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("resolve")); } finally { out.restore(); } }); it("dispatch returns 1 when resolved client is not found", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "dispatch", "do something"], { detectClients: () => mockClients, executePrompt: async () => { throw new ClientNotFoundError("claude"); }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {} }), } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not found")); } finally { out.restore(); } }); it("dispatch returns 1 when no prompt is given", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "dispatch"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("prompt")); } finally { out.restore(); } }); it("dispatch prints text output with --text", async () => { const out = captureOutput(); const stdoutChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "dispatch", "do something", "--text"], { detectClients: () => mockClients, executePrompt: async () => ({ stdout: "done", stderr: "", exitCode: 0, }), resolveClient: () => "codex", resolveConfig: () => ({ paths: {} }), stdoutWrite: (chunk) => stdoutChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stdoutChunks.join(""), "done"); } finally { out.restore(); } }); });