import { describe, it } from "node:test"; import assert from "node:assert"; import { detectClients } from "../src/detect.js"; function mockSpawnSync( responses: Map ): any { return (cmd: string, args: string[], _opts: unknown) => { const key = [cmd, ...args].join(" "); const hit = responses.get(key); if (hit) { return { status: hit.status, stdout: hit.stdout, stderr: "" }; } return { status: 1, stdout: "", stderr: "" }; }; } function mockExistsSync( allowedPaths: Set ): any { return (p: string) => allowedPaths.has(p); } describe("detectClients", () => { it("returns all three clients when found on PATH", () => { const responses = new Map([ ["which codex", { status: 0, stdout: "/usr/local/bin/codex\n" }], ["which claude", { status: 0, stdout: "/usr/local/bin/claude\n" }], ["which opencode", { status: 0, stdout: "/usr/local/bin/opencode\n" }], ["/usr/local/bin/codex --version", { status: 0, stdout: "1.0.0\n" }], ["/usr/local/bin/claude --version", { status: 0, stdout: "1.0.0\n" }], ["/usr/local/bin/opencode --version", { status: 0, stdout: "1.0.0\n" }], ]); const clients = detectClients({ pathEnv: "/usr/local/bin", spawnSync: mockSpawnSync(responses), existsSync: () => false, }); assert.strictEqual(clients.length, 3); for (const c of clients) { assert.strictEqual(c.found, true); assert.ok(c.path?.includes(c.name)); assert.strictEqual(c.version, "1.0.0"); } }); it("returns found: false for a missing client", () => { const responses = new Map([ ["which codex", { status: 0, stdout: "/usr/local/bin/codex\n" }], ["which claude", { status: 0, stdout: "/usr/local/bin/claude\n" }], ["which opencode", { status: 1, stdout: "" }], ["/usr/local/bin/codex --version", { status: 0, stdout: "1.0.0\n" }], ["/usr/local/bin/claude --version", { status: 0, stdout: "1.0.0\n" }], ]); const clients = detectClients({ pathEnv: "/usr/local/bin", spawnSync: mockSpawnSync(responses), existsSync: () => false, }); assert.strictEqual(clients.length, 3); const opencode = clients.find((c) => c.name === "opencode"); assert.ok(opencode); assert.strictEqual(opencode!.found, false); assert.strictEqual(opencode!.path, undefined); assert.strictEqual(opencode!.version, undefined); const codex = clients.find((c) => c.name === "codex"); assert.ok(codex); assert.strictEqual(codex!.found, true); }); it("returns found: false for all when none are on PATH", () => { const responses = new Map(); const clients = detectClients({ pathEnv: "/usr/local/bin", spawnSync: mockSpawnSync(responses), existsSync: () => false, }); assert.strictEqual(clients.length, 3); for (const c of clients) { assert.strictEqual(c.found, false); assert.strictEqual(c.path, undefined); assert.strictEqual(c.version, undefined); } }); it("handles PATH with duplicate entries via fallback walk", () => { const responses = new Map(); // which fails for all, so fallback walk is used const allowedPaths = new Set([ "/usr/bin/codex", "/usr/bin/claude", "/usr/bin/opencode", ]); const clients = detectClients({ pathEnv: "/usr/bin:/usr/bin:/usr/local/bin", spawnSync: mockSpawnSync(responses), existsSync: mockExistsSync(allowedPaths), }); assert.strictEqual(clients.length, 3); for (const c of clients) { assert.strictEqual(c.found, true); assert.strictEqual(c.path, `/usr/bin/${c.name}`); } }); it("parses version string from noisy output", () => { const responses = new Map([ ["which codex", { status: 0, stdout: "/opt/bin/codex\n" }], ["which claude", { status: 0, stdout: "/opt/bin/claude\n" }], ["which opencode", { status: 0, stdout: "/opt/bin/opencode\n" }], [ "/opt/bin/codex --version", { status: 0, stdout: "codex version 1.2.3 (build abc)\n" }, ], [ "/opt/bin/claude --version", { status: 0, stdout: "Claude Code 0.4.5-beta\n" }, ], ["/opt/bin/opencode --version", { status: 0, stdout: "v2.0.0\n" }], ]); const clients = detectClients({ pathEnv: "/opt/bin", spawnSync: mockSpawnSync(responses), existsSync: () => false, }); assert.strictEqual(clients.find((c) => c.name === "codex")!.version, "1.2.3"); assert.strictEqual( clients.find((c) => c.name === "claude")!.version, "0.4.5-beta" ); assert.strictEqual(clients.find((c) => c.name === "opencode")!.version, "v2.0.0"); }); });