Files
Ray Arayilakath 986557d4cc fix(cli): align install agent selection with the Vercel skills CLI (#939)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 02:11:07 -04:00

132 lines
5.3 KiB
TypeScript

import { tmpdir } from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
import {
computeDefaultSelectedAgents,
detectAvailableAgents,
} from "../src/cli/utils/detect-agents.js";
import * as fs from "node:fs";
const writeExecutable = (binDir: string, binaryName: string): void => {
const binaryPath = path.join(binDir, binaryName);
fs.writeFileSync(binaryPath, "#!/bin/sh\nexit 0\n");
fs.chmodSync(binaryPath, 0o755);
};
// HACK: detectAvailableAgents unions our PATH detection with
// agent-install's filesystem detection. agent-install captures
// `homedir()` at module load, so we can't rewire its detection from a
// `beforeEach` hook — these tests therefore exercise PATH detection in
// isolation by clearing $PATH down to a single fake bin dir, and rely on
// agent-install's own test suite for filesystem-detection coverage. The
// only thing we cross-check is "agents found via either signal end up
// in the result, deduped, in stable order".
describe.skipIf(process.platform === "win32")("detectAvailableAgents (PATH detection)", () => {
let fakeBinDirectory: string;
let originalPath: string | undefined;
beforeEach(() => {
fakeBinDirectory = fs.mkdtempSync(path.join(tmpdir(), "react-doctor-detect-"));
originalPath = process.env.PATH;
process.env.PATH = fakeBinDirectory;
});
afterEach(() => {
process.env.PATH = originalPath;
fs.rmSync(fakeBinDirectory, { recursive: true, force: true });
});
it("returns at least the PATH-detected agents (claude binary present)", async () => {
writeExecutable(fakeBinDirectory, "claude");
expect(await detectAvailableAgents()).toContain("claude-code");
});
it("detects droid via the `droid` binary on PATH", async () => {
writeExecutable(fakeBinDirectory, "droid");
expect(await detectAvailableAgents()).toContain("droid");
});
it("detects pi via either of its `pi` / `omegon` binary aliases", async () => {
writeExecutable(fakeBinDirectory, "omegon");
expect(await detectAvailableAgents()).toContain("pi");
});
it("detects cursor via its `agent` binary alias", async () => {
writeExecutable(fakeBinDirectory, "agent");
expect(await detectAvailableAgents()).toContain("cursor");
});
it("results are unique (a given agent appears at most once)", async () => {
writeExecutable(fakeBinDirectory, "claude");
writeExecutable(fakeBinDirectory, "droid");
const result = await detectAvailableAgents();
expect(new Set(result).size).toBe(result.length);
});
it("never returns the synthetic `universal` install target", async () => {
writeExecutable(fakeBinDirectory, "claude");
expect(await detectAvailableAgents()).not.toContain("universal");
});
it("ignores non-executable files with matching names (PATH detection only)", async () => {
const nonExecutablePath = path.join(fakeBinDirectory, "claude");
fs.writeFileSync(nonExecutablePath, "not executable");
fs.chmodSync(nonExecutablePath, 0o644);
// HACK: agent-install's FS detection might still return claude-code
// if the host running the tests has ~/.claude. Just assert that PATH
// detection alone didn't add it.
const result = await detectAvailableAgents();
if (result.includes("claude-code")) {
// If it's there, it can only be from FS detection, not PATH.
// We can't disable FS detection mid-test, so this branch passes
// silently. The negative assertion is meaningful only on a CI
// box without ~/.claude.
return;
}
expect(result).not.toContain("claude-code");
});
it("ignores directories with matching binary names (PATH detection only)", async () => {
fs.mkdirSync(path.join(fakeBinDirectory, "claude"));
const result = await detectAvailableAgents();
if (result.includes("claude-code")) return;
expect(result).not.toContain("claude-code");
});
});
describe("computeDefaultSelectedAgents", () => {
it("pre-selects the remembered agents (intersected with what's detected)", () => {
expect(
computeDefaultSelectedAgents(["claude-code", "cursor", "goose"], ["goose", "crush"]),
).toEqual(["goose"]);
});
it("prefers the remembered selection over the curated defaults", () => {
expect(computeDefaultSelectedAgents(["claude-code", "cursor", "goose"], ["goose"])).toEqual([
"goose",
]);
});
it("falls back to the curated popular defaults when nothing is remembered", () => {
expect(computeDefaultSelectedAgents(["claude-code", "cursor", "goose", "crush"], [])).toEqual([
"claude-code",
"cursor",
]);
});
it("falls back to a lone detected agent when neither remembered nor a default matches", () => {
expect(computeDefaultSelectedAgents(["goose"], [])).toEqual(["goose"]);
});
it("selects nothing when several agents are detected but none is remembered or a default", () => {
expect(computeDefaultSelectedAgents(["goose", "crush", "kilo"], [])).toEqual([]);
});
it("never pre-selects a remembered or default agent that isn't detected", () => {
// `codex` is a curated default but isn't installed here; `claude-code` is
// remembered but also not detected — neither should leak into the result.
expect(computeDefaultSelectedAgents(["goose", "crush"], ["claude-code"])).toEqual([]);
});
});