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, Job, JobStatus, DebugInfo } from "../src/types.js"; function mockJob(overrides: Partial & { id: string; status: JobStatus }): Job { return { client: "codex", prompt: "hello", startedAt: "2024-01-01T00:00:00Z", ...overrides, } as Job; } 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, client: "codex", durationMs: 42, }; 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 with --sync", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--sync"], { 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 and --sync", async () => { const out = captureOutput(); const stdoutChunks: string[] = []; const stderrChunks: string[] = []; try { const code = await main( [ "node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--text", "--sync", ], { detectClients: () => mockClients, executePrompt: async () => ({ stdout: "hello-out", stderr: "hello-err", exitCode: 0, client: "codex", durationMs: 42, }), 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 with --sync", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--sync"], { 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 with --sync", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "use claude to write tests", "--sync"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); 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 with --sync", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "--prompt", "use codex to refactor", "--sync"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); 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 with --sync", async () => { const out = captureOutput(); try { const executed: { client: ClientName; prompt: string }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "do something", "--client", "opencode", "--sync"], { detectClients: () => mockClients, executePrompt: async (client, prompt) => { executed.push({ client, prompt }); return mockResult; }, resolveClient: (_p, cfg) => cfg?.client ?? null, resolveConfig: () => ({ paths: {}, defaultClient: "claude", timeout: 600_000 }), } ); 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: {}, timeout: 600_000 }), } ); 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 with --sync", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "dispatch", "do something", "--sync"], { detectClients: () => mockClients, executePrompt: async () => { throw new ClientNotFoundError("claude"); }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); 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 and --sync", async () => { const out = captureOutput(); const stdoutChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "dispatch", "do something", "--text", "--sync"], { detectClients: () => mockClients, executePrompt: async () => ({ stdout: "done", stderr: "", exitCode: 0, client: "codex", durationMs: 42, }), resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), stdoutWrite: (chunk) => stdoutChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stdoutChunks.join(""), "done"); } finally { out.restore(); } }); it("run prints debug diagnostic JSON to stderr with --debug and --sync", async () => { const out = captureOutput(); const stderrChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--debug", "--sync"], { detectClients: () => mockClients, executePrompt: async (_client, _prompt, options?) => { options?.onDebug?.({ command: "codex", args: ["exec", "--yolo", "hello"], pid: 12345, exitCode: 0, exitSignal: null, durationMs: 42, stderrLength: 0, stdoutLength: 6, noisySuccess: false, } satisfies DebugInfo); return { stdout: "output", stderr: "", exitCode: 0, client: "codex", durationMs: 42, }; }, stderrWrite: (chunk) => stderrChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stderrChunks.length, 1); const diag = JSON.parse(stderrChunks[0]); assert.strictEqual(diag.command, "codex"); assert.deepStrictEqual(diag.args, ["exec", "--yolo", "hello"]); assert.strictEqual(diag.pid, 12345); assert.strictEqual(diag.exitCode, 0); assert.strictEqual(diag.exitSignal, null); assert.strictEqual(diag.durationMs, 42); assert.strictEqual(diag.stderrLength, 0); } finally { out.restore(); } }); it("run ignores non-numeric --timeout and falls back to config timeout with --sync", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--timeout", "not-a-number", "--sync"], { detectClients: () => mockClients, executePrompt: async (_client, _prompt, options?) => { receivedTimeout = options?.timeoutMs; return mockResult; }, resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 600_000); } finally { out.restore(); } }); it("dispatch ignores non-numeric --timeout and falls back to config timeout with --sync", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "dispatch", "do something", "--timeout", "bad", "--sync"], { detectClients: () => mockClients, executePrompt: async (_client, _prompt, options?) => { receivedTimeout = options?.timeoutMs; return mockResult; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 600_000); } finally { out.restore(); } }); it("dispatch prints debug diagnostic JSON to stderr with --debug and --sync", async () => { const out = captureOutput(); const stderrChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "dispatch", "do something", "--debug", "--sync"], { detectClients: () => mockClients, executePrompt: async (_client, _prompt, options?) => { options?.onDebug?.({ command: "codex", args: ["exec", "--yolo", "do something"], pid: 12345, exitCode: 0, exitSignal: null, durationMs: 42, stderrLength: 0, stdoutLength: 6, noisySuccess: false, } satisfies DebugInfo); return { stdout: "output", stderr: "", exitCode: 0, client: "codex", durationMs: 42, }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), stderrWrite: (chunk) => stderrChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stderrChunks.length, 1); const diag = JSON.parse(stderrChunks[0]); assert.strictEqual(diag.command, "codex"); assert.strictEqual(diag.durationMs, 42); } finally { out.restore(); } }); // Async default tests (S-301) it("run async default starts job and returns job ID JSON immediately", async () => { const out = captureOutput(); try { const started: { client: ClientName; prompt: string; options?: any }[] = []; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, startJob: async (client, prompt, options) => { started.push({ client, prompt, options }); return { id: "job-123", client, prompt, status: "running", startedAt: new Date().toISOString() }; }, } ); assert.strictEqual(code, 0); assert.strictEqual(started.length, 1); assert.strictEqual(started[0].client, "codex"); assert.strictEqual(started[0].prompt, "hello"); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.jobId, "job-123"); assert.strictEqual(parsed.client, "codex"); assert.strictEqual(parsed.status, "running"); } finally { out.restore(); } }); it("dispatch async default starts job and returns job ID JSON immediately", async () => { const out = captureOutput(); try { const started: { client: ClientName; prompt: string; options?: any }[] = []; const code = await main( ["node", "cli.ts", "dispatch", "use claude to write tests"], { detectClients: () => mockClients, startJob: async (client, prompt, options) => { started.push({ client, prompt, options }); return { id: "job-456", client, prompt, status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(started.length, 1); assert.strictEqual(started[0].client, "claude"); assert.strictEqual(started[0].prompt, "use claude to write tests"); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.jobId, "job-456"); assert.strictEqual(parsed.client, "claude"); assert.strictEqual(parsed.status, "running"); } finally { out.restore(); } }); it("run async passes --timeout to startJob", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--timeout", "30000"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-789", client: "codex", prompt: "hello", status: "running", startedAt: new Date().toISOString() }; }, } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 30000); } finally { out.restore(); } }); it("run async ignores non-numeric --timeout and falls back to config timeout", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--timeout", "not-a-number"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-t1", client: "codex", prompt: "hello", status: "running", startedAt: new Date().toISOString() }; }, resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 600_000); } finally { out.restore(); } }); it("dispatch async passes --timeout to startJob", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "dispatch", "do something", "--timeout", "45000"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-abc", client: "codex", prompt: "do something", status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 45000); } finally { out.restore(); } }); it("dispatch async ignores non-numeric --timeout and falls back to config timeout", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "dispatch", "do something", "--timeout", "bad"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-t2", client: "codex", prompt: "do something", status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 600_000); } finally { out.restore(); } }); it("run async passes --debug to startJob", async () => { const out = captureOutput(); const stderrChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello", "--debug"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { options?.onDebug?.({ command: "codex", args: ["exec", "--yolo", "hello"], pid: 12345, exitCode: 0, exitSignal: null, durationMs: 42, stderrLength: 0, stdoutLength: 6, noisySuccess: false, } satisfies DebugInfo); return { id: "job-def", client: "codex", prompt: "hello", status: "running", startedAt: new Date().toISOString() }; }, stderrWrite: (chunk) => stderrChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stderrChunks.length, 1); const diag = JSON.parse(stderrChunks[0]); assert.strictEqual(diag.command, "codex"); assert.deepStrictEqual(diag.args, ["exec", "--yolo", "hello"]); assert.strictEqual(diag.pid, 12345); } finally { out.restore(); } }); it("run async returns 1 and prints error JSON when startJob throws", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, startJob: async () => { throw new Error("spawn ENOENT"); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("ENOENT")); } finally { out.restore(); } }); it("dispatch async returns 1 and prints error JSON when startJob throws", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "dispatch", "do something"], { detectClients: () => mockClients, startJob: async () => { throw new Error("spawn ENOENT"); }, resolveClient: () => "claude", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("ENOENT")); } finally { out.restore(); } }); it("run async uses config timeout when --timeout is not provided", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-nt1", client: "codex", prompt: "hello", status: "running", startedAt: new Date().toISOString() }; }, resolveConfig: () => ({ paths: {}, timeout: 300_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 300_000); } finally { out.restore(); } }); it("dispatch async uses config timeout when --timeout is not provided", async () => { const out = captureOutput(); try { let receivedTimeout: number | undefined; const code = await main( ["node", "cli.ts", "dispatch", "do something"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedTimeout = options?.timeoutMs; return { id: "job-nt2", client: "codex", prompt: "do something", status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 300_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedTimeout, 300_000); } finally { out.restore(); } }); it("run async without --debug does not pass onDebug to startJob", async () => { const out = captureOutput(); let receivedOptions: any; try { const code = await main( ["node", "cli.ts", "run", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedOptions = options; return { id: "job-nd1", client: "codex", prompt: "hello", status: "running", startedAt: new Date().toISOString() }; }, } ); assert.strictEqual(code, 0); assert.strictEqual(receivedOptions.onDebug, undefined); } finally { out.restore(); } }); it("dispatch async without --debug does not pass onDebug to startJob", async () => { const out = captureOutput(); let receivedOptions: any; try { const code = await main( ["node", "cli.ts", "dispatch", "do something"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { receivedOptions = options; return { id: "job-nd2", client: "codex", prompt: "do something", status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), } ); assert.strictEqual(code, 0); assert.strictEqual(receivedOptions.onDebug, undefined); } finally { out.restore(); } }); it("dispatch async passes --debug to startJob", async () => { const out = captureOutput(); const stderrChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "dispatch", "do something", "--debug"], { detectClients: () => mockClients, startJob: async (_client, _prompt, options) => { options?.onDebug?.({ command: "codex", args: ["exec", "--yolo", "do something"], pid: 12345, exitCode: 0, exitSignal: null, durationMs: 42, stderrLength: 0, stdoutLength: 6, noisySuccess: false, } satisfies DebugInfo); return { id: "job-ghi", client: "codex", prompt: "do something", status: "running", startedAt: new Date().toISOString() }; }, resolveClient: () => "codex", resolveConfig: () => ({ paths: {}, timeout: 600_000 }), stderrWrite: (chunk) => stderrChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stderrChunks.length, 1); const diag = JSON.parse(stderrChunks[0]); assert.strictEqual(diag.command, "codex"); assert.strictEqual(diag.durationMs, 42); } finally { out.restore(); } }); // Job lifecycle subcommands (S-302) it("start starts job and returns job ID JSON", async () => { const out = captureOutput(); try { const started: { client: ClientName; prompt: string; options?: any }[] = []; const code = await main( ["node", "cli.ts", "start", "--client", "codex", "--prompt", "hello"], { detectClients: () => mockClients, startJob: async (client, prompt, options) => { started.push({ client, prompt, options }); return { id: "job-start-1", client, prompt, status: "running", startedAt: new Date().toISOString() }; }, } ); assert.strictEqual(code, 0); assert.strictEqual(started.length, 1); assert.strictEqual(started[0].client, "codex"); assert.strictEqual(started[0].prompt, "hello"); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.jobId, "job-start-1"); assert.strictEqual(parsed.client, "codex"); assert.strictEqual(parsed.status, "running"); } finally { out.restore(); } }); it("start returns 1 when client is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "start", "--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("start returns 1 when prompt is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "start", "--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("status returns running job JSON", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "status", "job-123"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, status: "running" }), } ); assert.strictEqual(code, 0); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.id, "job-123"); assert.strictEqual(parsed.status, "running"); assert.strictEqual(parsed.client, "codex"); } finally { out.restore(); } }); it("status returns completed job JSON", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "status", "job-456"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, client: "claude", prompt: "write tests", status: "completed", completedAt: "2024-01-01T00:01:00Z", result: { stdout: "ok", stderr: "", exitCode: 0, client: "claude", durationMs: 100 }, }), } ); assert.strictEqual(code, 0); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.status, "completed"); assert.strictEqual(parsed.result.exitCode, 0); } finally { out.restore(); } }); it("status returns 1 and prints error JSON for nonexistent job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "status", "missing-job"], { detectClients: () => mockClients, getJob: () => { throw new Error('Job "missing-job" not found.'); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not found")); } finally { out.restore(); } }); it("status returns 1 when job-id is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "status"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("job-id")); } finally { out.restore(); } }); it("status prints text output with --text", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "status", "job-123", "--text"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, status: "running" }), } ); assert.strictEqual(code, 0); assert.ok(out.logs[0].includes("running")); assert.ok(out.logs[0].includes("job-123")); } finally { out.restore(); } }); it("results returns ExecResult JSON for completed job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "results", "job-789"], { detectClients: () => mockClients, getJobResult: () => ({ stdout: "output", stderr: "", exitCode: 0, client: "codex", durationMs: 42, }), } ); assert.strictEqual(code, 0); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.stdout, "output"); assert.strictEqual(parsed.exitCode, 0); } finally { out.restore(); } }); it("results returns 1 and prints error JSON for still-running job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "results", "job-running"], { detectClients: () => mockClients, getJobResult: () => { throw new Error('Job "job-running" result is not available (status: running).'); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not available")); } finally { out.restore(); } }); it("results returns 1 and prints error JSON for nonexistent job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "results", "missing-job"], { detectClients: () => mockClients, getJobResult: () => { throw new Error('Job "missing-job" not found.'); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not found")); } finally { out.restore(); } }); it("results returns 1 when job-id is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "results"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("job-id")); } finally { out.restore(); } }); it("results prints text output with --text", async () => { const out = captureOutput(); const stdoutChunks: string[] = []; try { const code = await main( ["node", "cli.ts", "results", "job-789", "--text"], { detectClients: () => mockClients, getJobResult: () => ({ stdout: "hello-out", stderr: "hello-err", exitCode: 0, client: "codex", durationMs: 42, }), stdoutWrite: (chunk) => stdoutChunks.push(chunk), } ); assert.strictEqual(code, 0); assert.strictEqual(stdoutChunks.join(""), "hello-out"); } finally { out.restore(); } }); it("cancel returns cancelled confirmation for running job", async () => { const out = captureOutput(); try { let cancelledJobId: string | undefined; const code = await main( ["node", "cli.ts", "cancel", "job-abc"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, status: "running" }), cancelJob: (jobId) => { cancelledJobId = jobId; }, } ); assert.strictEqual(code, 0); assert.strictEqual(cancelledJobId, "job-abc"); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.jobId, "job-abc"); assert.strictEqual(parsed.cancelled, true); } finally { out.restore(); } }); it("cancel returns 1 and prints error JSON for completed job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "cancel", "job-done"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, status: "completed", completedAt: "2024-01-01T00:01:00Z" }), } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("running")); } finally { out.restore(); } }); it("cancel returns 1 and prints error JSON for nonexistent job", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "cancel", "missing-job"], { detectClients: () => mockClients, getJob: () => { throw new Error('Job "missing-job" not found.'); }, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("not found")); } finally { out.restore(); } }); it("cancel returns 1 when job-id is missing", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "cancel"], { detectClients: () => mockClients, }); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("job-id")); } finally { out.restore(); } }); it("cancel prints text output with --text", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "cancel", "job-abc", "--text"], { detectClients: () => mockClients, getJob: (jobId) => mockJob({ id: jobId, status: "running" }), cancelJob: () => {}, } ); assert.strictEqual(code, 0); assert.ok(out.logs[0].includes("cancelled")); assert.ok(out.logs[0].includes("job-abc")); } finally { out.restore(); } }); it("list-jobs returns array of all jobs", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "list-jobs"], { detectClients: () => mockClients, listJobs: () => [ { id: "job-1", client: "codex", prompt: "p1", status: "running", startedAt: "2024-01-01T00:00:00Z" }, { id: "job-2", client: "claude", prompt: "p2", status: "completed", startedAt: "2024-01-01T00:01:00Z" }, ], } ); assert.strictEqual(code, 0); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.length, 2); assert.strictEqual(parsed[0].id, "job-1"); assert.strictEqual(parsed[1].id, "job-2"); } finally { out.restore(); } }); it("list-jobs filters by --status", async () => { const out = captureOutput(); try { let receivedFilter: string | undefined; const code = await main( ["node", "cli.ts", "list-jobs", "--status", "running"], { detectClients: () => mockClients, listJobs: (options) => { receivedFilter = options?.filter; return [ { id: "job-1", client: "codex", prompt: "p1", status: "running", startedAt: "2024-01-01T00:00:00Z" }, ]; }, } ); assert.strictEqual(code, 0); assert.strictEqual(receivedFilter, "running"); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.length, 1); assert.strictEqual(parsed[0].status, "running"); } finally { out.restore(); } }); it("list-jobs prints text output with --text", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "list-jobs", "--text"], { detectClients: () => mockClients, listJobs: () => [ { id: "job-1", client: "codex", prompt: "p1", status: "running", startedAt: "2024-01-01T00:00:00Z" }, ], } ); assert.strictEqual(code, 0); assert.ok(out.logs[0].includes("job-1")); assert.ok(out.logs[0].includes("codex")); assert.ok(out.logs[0].includes("running")); } finally { out.restore(); } }); it("cleanup-jobs returns count of cleaned jobs", async () => { const out = captureOutput(); try { let callCount = 0; const jobs = [ { id: "job-1", client: "codex", prompt: "p1", status: "completed", startedAt: "2024-01-01T00:00:00Z" }, { id: "job-2", client: "claude", prompt: "p2", status: "completed", startedAt: "2024-01-01T00:01:00Z" }, ]; const code = await main( ["node", "cli.ts", "cleanup-jobs"], { detectClients: () => mockClients, listJobs: () => { callCount++; return callCount === 1 ? jobs : []; }, cleanupJobs: () => {}, } ); assert.strictEqual(code, 0); const parsed = JSON.parse(out.logs[0]); assert.strictEqual(parsed.count, 2); } finally { out.restore(); } }); it("cleanup-jobs with --max-age parses hours", async () => { const out = captureOutput(); try { let receivedMaxAgeMs: number | undefined; const code = await main( ["node", "cli.ts", "cleanup-jobs", "--max-age", "12h"], { detectClients: () => mockClients, listJobs: () => [], cleanupJobs: (options) => { receivedMaxAgeMs = options?.maxAgeMs; }, } ); assert.strictEqual(code, 0); assert.strictEqual(receivedMaxAgeMs, 12 * 60 * 60 * 1000); } finally { out.restore(); } }); it("cleanup-jobs returns 1 for invalid --max-age format", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "cleanup-jobs", "--max-age", "abc"], { detectClients: () => mockClients, } ); assert.strictEqual(code, 1); const parsed = JSON.parse(out.errors[0]); assert.ok(parsed.error.includes("max-age")); } finally { out.restore(); } }); it("cleanup-jobs prints text output with --text", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "cleanup-jobs", "--text"], { detectClients: () => mockClients, listJobs: () => [], cleanupJobs: () => {}, } ); assert.strictEqual(code, 0); assert.ok(out.logs[0].includes("Cleaned")); assert.ok(out.logs[0].includes("0")); } finally { out.restore(); } }); // S-303: help output and CLI integration smoke tests it("--help output documents start subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("start"), "help should mention start"); } finally { out.restore(); } }); it("--help output documents status subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("status"), "help should mention status"); } finally { out.restore(); } }); it("--help output documents results subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("results"), "help should mention results"); } finally { out.restore(); } }); it("--help output documents cancel subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("cancel"), "help should mention cancel"); } finally { out.restore(); } }); it("--help output documents list-jobs subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("list-jobs"), "help should mention list-jobs"); } finally { out.restore(); } }); it("--help output documents cleanup-jobs subcommand", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("cleanup-jobs"), "help should mention cleanup-jobs"); } finally { out.restore(); } }); it("--help output documents --sync flag", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("--sync"), "help should mention --sync"); } finally { out.restore(); } }); it("--help output documents --timeout flag", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("--timeout"), "help should mention --timeout"); } finally { out.restore(); } }); it("--help output documents --debug flag", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("--debug"), "help should mention --debug"); } finally { out.restore(); } }); it("--help output documents --max-age flag", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("--max-age"), "help should mention --max-age"); } finally { out.restore(); } }); it("--help output documents --status flag for list-jobs", async () => { const out = captureOutput(); try { const code = await main(["node", "cli.ts", "--help"]); assert.strictEqual(code, 0); const help = out.logs.join("\n"); assert.ok(help.includes("--status"), "help should mention --status"); } finally { out.restore(); } }); it("start returns 1 for unknown client", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "start", "--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("start prints text output with --text", async () => { const out = captureOutput(); try { const code = await main( ["node", "cli.ts", "start", "--client", "codex", "--prompt", "hello", "--text"], { detectClients: () => mockClients, startJob: async (client, prompt) => ({ id: "job-start-txt", client, prompt, status: "running", startedAt: new Date().toISOString(), }), } ); assert.strictEqual(code, 0); assert.ok(out.logs[0].includes("job-start-txt")); assert.ok(out.logs[0].includes("codex")); assert.ok(out.logs[0].includes("running")); } finally { out.restore(); } }); });