skillhub/skillhub-cli/tests/install.test.ts
chenbaowang 6c0d4f4f16 feat(skillhub-cli): add skillhub CLI with interactive multi-agent support
- Interactive agent selection for install/uninstall/list
- Namespace search selection for unspecified namespace
- --skill-version option to skip interactive selection
- --from option for local/github source installation
- Remove deprecated add command (use install --from instead)
- Path-based grouping for uninstall results
2026-04-14 10:21:09 +08:00

224 lines
7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
const mockSuccess = vi.fn();
const mockError = vi.fn();
const mockInfo = vi.fn();
vi.mock("../src/utils/logger.js", () => ({
success: mockSuccess,
error: mockError,
info: mockInfo,
dim: vi.fn(),
}));
describe("install command", () => {
let tempDir: string;
beforeEach(() => {
tempDir = join(tmpdir(), "install-test-" + Date.now());
mkdirSync(tempDir, { recursive: true });
mockSuccess.mockClear();
mockError.mockClear();
mockInfo.mockClear();
});
afterEach(() => {
try { rmSync(tempDir, { recursive: true, force: true }); } catch {}
vi.restoreAllMocks();
});
describe("source auto-detection", () => {
it("should detect git source: owner/repo format", () => {
const source = "vercel-labs/agent-skills";
const isGitSource = /^[\w-]+\/[\w-]+$/.test(source);
expect(isGitSource).toBe(true);
});
it("should detect git source: GitHub URL", () => {
const source = "https://github.com/vercel-labs/agent-skills";
expect(source.includes("github.com")).toBe(true);
});
it("should detect git source: GitLab URL", () => {
const source = "https://gitlab.com/vercel-labs/agent-skills";
expect(source.includes("gitlab.com")).toBe(true);
});
it("should detect local source: relative path", () => {
const source = "./my-skill";
const isLocal = source.startsWith(".") || source.startsWith("/") || source.startsWith("~");
expect(isLocal).toBe(true);
});
it("should detect local source: absolute path", () => {
const source = "/Users/me/skills/my-skill";
expect(source.startsWith("/")).toBe(true);
});
it("should detect registry source: plain slug", () => {
const source = "my-skill";
const isGit = /^[\w-]+\/[\w-]+$/.test(source) || source.includes("github.com") || source.includes("gitlab.com");
const isLocal = source.startsWith(".") || source.startsWith("/") || source.startsWith("~");
expect(isGit || isLocal).toBe(false);
});
it("should detect registry source: namespace--slug format", () => {
const source = "global--my-skill";
const isScoped = source.includes("--");
expect(isScoped).toBe(true);
});
});
describe("--list option", () => {
it("should list skills without installing", () => {
const skills = [
{ name: "skill-one", description: "First skill" },
{ name: "skill-two", description: "Second skill" },
];
expect(skills.length).toBe(2);
expect(skills[0].name).toBe("skill-one");
});
});
describe("registry install", () => {
it("should construct correct download URL", () => {
const ns = "global";
const slug = "my-skill";
const downloadUrl = `/api/v1/skills/${ns}/${slug}/download`;
expect(downloadUrl).toBe("/api/v1/skills/global/my-skill/download");
});
it("should use default namespace when not specified", () => {
const defaultNs = "global";
expect(defaultNs).toBe("global");
});
});
describe("git install", () => {
it("should parse owner/repo correctly", () => {
const input = "vercel-labs/skills";
const parts = input.split("/");
expect(parts[0]).toBe("vercel-labs");
expect(parts[1]).toBe("skills");
});
it("should construct GitHub clone URL", () => {
const owner = "vercel-labs";
const repo = "skills";
const cloneUrl = `https://github.com/${owner}/${repo}.git`;
expect(cloneUrl).toBe("https://github.com/vercel-labs/skills.git");
});
});
describe("agent selection", () => {
it("should detect installed agents", () => {
const allAgents = [
{ key: "claude-code", name: "Claude Code" },
{ key: "cursor", name: "Cursor" },
];
expect(allAgents.length).toBe(2);
});
it("should filter by specified agent keys", () => {
const allAgents = [
{ key: "claude-code", name: "Claude Code" },
{ key: "cursor", name: "Cursor" },
];
const selected = allAgents.filter((a) => ["claude-code"].includes(a.key));
expect(selected.length).toBe(1);
expect(selected[0].key).toBe("claude-code");
});
});
describe("install modes", () => {
it("should support symlink mode (default)", () => {
const mode = "symlink";
expect(mode).toBe("symlink");
});
it("should support copy mode with --copy flag", () => {
const useCopy = true;
const mode = useCopy ? "copy" : "symlink";
expect(mode).toBe("copy");
});
it("should support global scope with --global flag", () => {
const isGlobal = true;
expect(isGlobal).toBe(true);
});
it("should support project scope (default)", () => {
const isGlobal = false;
expect(isGlobal).toBe(false);
});
});
});
describe("--skill option", () => {
it("should select all skills when '*' is specified", () => {
const allSkills = [
{ name: "skill-one", description: "First" },
{ name: "skill-two", description: "Second" },
{ name: "skill-three", description: "Third" },
];
const skillNames = ["*"] as string[];
let selectedSkills;
if (skillNames.includes("*")) {
selectedSkills = allSkills;
}
expect(selectedSkills).toEqual(allSkills);
expect(selectedSkills.length).toBe(3);
});
it("should filter skills by exact name match", () => {
const allSkills = [
{ name: "skill-one", description: "First" },
{ name: "skill-two", description: "Second" },
{ name: "skill-three", description: "Third" },
];
const skillNames = ["skill-one", "skill-three"] as string[];
const selectedSkills = allSkills.filter((s) => skillNames.includes(s.name));
expect(selectedSkills.length).toBe(2);
expect(selectedSkills[0].name).toBe("skill-one");
expect(selectedSkills[1].name).toBe("skill-three");
});
it("should return empty array when no skills match", () => {
const allSkills = [
{ name: "skill-one", description: "First" },
{ name: "skill-two", description: "Second" },
];
const skillNames = ["non-existent"] as string[];
const selectedSkills = allSkills.filter((s) => skillNames.includes(s.name));
expect(selectedSkills.length).toBe(0);
});
it("should handle case-sensitive name matching", () => {
const allSkills = [
{ name: "OpenSpec", description: "OpenSpec skill" },
{ name: "openspec", description: "lowercase" },
];
const skillNames = ["openspec"] as string[];
const selectedSkills = allSkills.filter((s) => skillNames.includes(s.name));
expect(selectedSkills.length).toBe(1);
expect(selectedSkills[0].name).toBe("openspec");
});
});
describe("add command alias", () => {
it("should be equivalent to install --source git", () => {
const installSource = "git";
expect(installSource).toBe("git");
});
});