import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("node:fs", () => ({ existsSync: vi.fn(() => false), mkdirSync: vi.fn(), writeFileSync: vi.fn(), readFileSync: 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(), })); vi.mock("../lib/manifest.js", () => ({ readManifest: vi.fn(), addToManifest: vi.fn(), writeManifest: vi.fn(), removeFromManifest: vi.fn(), })); import { restoreCommand } from "./restore.js"; import { apiRequest } from "../lib/api-client.js"; import { readManifest, addToManifest } from "../lib/manifest.js"; const mockApiRequest = vi.mocked(apiRequest); const mockReadManifest = vi.mocked(readManifest); const mockAddToManifest = vi.mocked(addToManifest); const makeSkill = (slug: string, name: string, version = "1.0.0") => ({ id: slug, slug, name, description: `${name} desc`, instructions: `${name} instructions`, latestVersion: version, category: { name: "Build", slug: "build" }, author: { username: "tho" }, }); beforeEach(() => { vi.clearAllMocks(); }); describe("restore", () => { it("shows message when no manifest or empty", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: {} }); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await restoreCommand.parseAsync(["node", "restore"], { from: "node" }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("No .skills.json"); log.mockRestore(); }); it("installs all skills from manifest", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "code-review": { version: "2.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "claude-code", }, "lint-check": { version: "1.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "claude-code", }, }, }); mockApiRequest.mockImplementation((path: string) => { if (path.includes("code-review") && path.includes("versions")) return Promise.resolve({ instructions: "Code Review instructions", version: "2.0.0", }); if (path.includes("lint-check") && path.includes("versions")) return Promise.resolve({ instructions: "Lint Check instructions", version: "1.0.0", }); if (path.includes("code-review") && !path.includes("install")) return Promise.resolve( makeSkill("code-review", "Code Review", "2.0.0"), ); if (path.includes("lint-check") && !path.includes("install")) return Promise.resolve(makeSkill("lint-check", "Lint Check", "1.0.0")); return Promise.resolve(undefined); }); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await restoreCommand.parseAsync(["node", "restore"], { from: "node" }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/skills/code-review"); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/skills/lint-check"); expect(mockAddToManifest).toHaveBeenCalledTimes(2); log.mockRestore(); }); it("continues when one skill fails and sets exit code", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "broken-skill": { version: "1.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "claude-code", }, "good-skill": { version: "1.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "claude-code", }, }, }); mockApiRequest.mockImplementation((path: string) => { if (path.includes("broken-skill")) return Promise.reject(new Error("API error: 404")); if (path.includes("good-skill") && path.includes("versions")) return Promise.resolve({ instructions: "Good Skill instructions", version: "1.0.0", }); if (path.includes("good-skill") && !path.includes("install")) return Promise.resolve(makeSkill("good-skill", "Good Skill")); return Promise.resolve(undefined); }); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await restoreCommand.parseAsync(["node", "restore"], { from: "node" }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/skills/good-skill"); expect(mockAddToManifest).toHaveBeenCalledTimes(1); expect(mockAddToManifest).toHaveBeenCalledWith( "good-skill", "1.0.0", "claude-code", ); expect(process.exitCode).toBe(1); log.mockRestore(); process.exitCode = undefined; }); it("passes platform from manifest entry to addToManifest", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "my-skill": { version: "1.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "cursor", }, }, }); mockApiRequest.mockImplementation((path: string) => { if (path.includes("my-skill") && path.includes("versions")) { return Promise.resolve({ instructions: "My Skill instructions", version: "1.0.0", }); } if (path.includes("my-skill") && !path.includes("install")) { return Promise.resolve(makeSkill("my-skill", "My Skill", "1.0.0")); } return Promise.resolve(undefined); }); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await restoreCommand.parseAsync(["node", "restore"], { from: "node" }); expect(mockAddToManifest).toHaveBeenCalledWith( "my-skill", "1.0.0", "cursor", ); log.mockRestore(); }); // dry-run must be last because Commander.js option state persists on the singleton it("dry-run lists skills without installing", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "code-review": { version: "2.0.0", installedAt: "2026-03-07T00:00:00Z", platform: "claude-code", }, }, }); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await restoreCommand.parseAsync(["node", "restore", "--dry-run"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("code-review"); expect(mockApiRequest).not.toHaveBeenCalled(); log.mockRestore(); }); });