import { describe, it, expect, vi, beforeEach } from "vitest"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; vi.mock("node:fs", () => ({ existsSync: vi.fn(() => false), mkdirSync: vi.fn(), writeFileSync: vi.fn(), })); vi.mock("../lib/install-path.js", () => ({ detectInstallTarget: vi.fn(() => ({ type: "claude-code", path: "/home/user/.claude/skills", })), resolveInstallPath: vi.fn(() => ({ type: "claude-code", path: "/home/user/.claude/skills", })), ALL_TARGETS: [ "claude-code", "cursor", "windsurf", "cline", "codex", "copilot", "opencode", ], })); vi.mock("../lib/config.js", () => ({ getConfig: vi.fn(() => ({ apiUrl: "https://api.skills-hub.ai" })), getAuthHeader: vi.fn(() => ({})), saveConfig: vi.fn(), })); vi.mock("../lib/api-client.js", () => ({ apiRequest: vi.fn(), })); import { installBundleCommand, installBundle } from "./install-bundle.js"; import { apiRequest } from "../lib/api-client.js"; import { writeFileSync as realWriteFileSync } from "node:fs"; const mockApiRequest = vi.mocked(apiRequest); const mockWriteFileSync = vi.mocked(realWriteFileSync); const mockExistsSync = vi.mocked(existsSync); const mockBundle = { slug: "full-stack-bundle", name: "Full Stack Bundle", description: "All skills for full-stack development", author: { username: "tho", avatarUrl: null }, skillCount: 3, installCount: 42, createdAt: "2026-03-04T00:00:00.000Z", updatedAt: "2026-03-04T00:00:00.000Z", skills: [ { slug: "lint-check", name: "Lint Check", description: "Linting skill", installCount: 100, }, { slug: "security-scan", name: "Security Scan", description: "Security skill", installCount: 50, }, { slug: "code-review", name: "Code Review", description: "Review skill", installCount: 75, }, ], }; const makeSkillDetail = (slug: string, name: string) => ({ id: slug, slug, name, description: `${name} description`, instructions: `${name} instructions`, latestVersion: "1.0.0", category: { name: "Build", slug: "build" }, author: { username: "tho" }, }); function mockApiByPath(overrides: Record = {}) { mockApiRequest.mockImplementation((path: string) => { if (overrides[path] instanceof Error) return Promise.reject(overrides[path]); if (overrides[path] !== undefined) return Promise.resolve(overrides[path]); if (path === "/api/v1/bundles/full-stack-bundle") return Promise.resolve(mockBundle); if (path === "/api/v1/skills/lint-check") return Promise.resolve(makeSkillDetail("lint-check", "Lint Check")); if (path === "/api/v1/skills/security-scan") return Promise.resolve(makeSkillDetail("security-scan", "Security Scan")); if (path === "/api/v1/skills/code-review") return Promise.resolve(makeSkillDetail("code-review", "Code Review")); if (path.endsWith("/install")) return Promise.resolve(undefined); return Promise.resolve(undefined); }); } beforeEach(() => { vi.clearAllMocks(); mockExistsSync.mockReturnValue(false); }); describe("install-bundle", () => { it("installs all skills from a bundle", async () => { mockApiByPath(); const result = await installBundle("full-stack-bundle", {}); expect(result.bundle.name).toBe("Full Stack Bundle"); expect(result.results).toHaveLength(3); expect(result.results.every((r) => r.status === "installed")).toBe(true); expect(mockWriteFileSync).toHaveBeenCalledTimes(3); }); it("skips already-installed skills", async () => { mockApiByPath(); mockExistsSync.mockImplementation((p) => { return String(p) === "/home/user/.claude/skills/lint-check/SKILL.md"; }); const result = await installBundle("full-stack-bundle", {}); expect(result.results[0]).toEqual({ slug: "lint-check", status: "skipped", }); expect(result.results[1]).toEqual({ slug: "security-scan", status: "installed", }); expect(result.results[2]).toEqual({ slug: "code-review", status: "installed", }); expect(mockWriteFileSync).toHaveBeenCalledTimes(2); }); it("continues when one skill fails", async () => { mockApiByPath({ "/api/v1/skills/security-scan": new Error("API error: 404"), }); const result = await installBundle("full-stack-bundle", {}); expect(result.results[0]).toEqual({ slug: "lint-check", status: "installed", }); expect(result.results[1]).toEqual({ slug: "security-scan", status: "failed", error: "API error: 404", }); expect(result.results[2]).toEqual({ slug: "code-review", status: "installed", }); }); it("fetches bundle from correct API endpoint", async () => { mockApiByPath(); await installBundle("full-stack-bundle", {}); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/bundles/full-stack-bundle", ); }); it("handles bundle not found error", async () => { mockApiRequest.mockRejectedValue(new Error("API error: 404 Not Found")); await expect(installBundle("nonexistent", {})).rejects.toThrow( "API error: 404", ); }); it("writes SKILL.md for each installed skill", async () => { mockApiByPath(); await installBundle("full-stack-bundle", {}); expect(mockWriteFileSync).toHaveBeenCalledWith( "/home/user/.claude/skills/lint-check/SKILL.md", expect.stringContaining("Lint Check instructions"), ); expect(mockWriteFileSync).toHaveBeenCalledWith( "/home/user/.claude/skills/security-scan/SKILL.md", expect.stringContaining("Security Scan instructions"), ); expect(mockWriteFileSync).toHaveBeenCalledWith( "/home/user/.claude/skills/code-review/SKILL.md", expect.stringContaining("Code Review instructions"), ); }); it("installBundleCommand exposes --target option", () => { const opt = installBundleCommand.options.find((o) => o.long === "--target"); expect(opt).toBeDefined(); }); });