feat(S-202): Test-drive and implement src/detect.ts
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { detectClients } from "../src/detect.js";
|
||||
|
||||
function mockSpawnSync(
|
||||
responses: Map<string, { status: number; stdout: string }>
|
||||
): 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<string>
|
||||
): 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<string, { status: number; stdout: string }>();
|
||||
|
||||
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<string, { status: number; stdout: string }>();
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user