mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
- 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
224 lines
7 KiB
TypeScript
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");
|
|
});
|
|
});
|