|
| 1 | +import { vi, describe, expect, it, beforeEach } from "vite-plus/test"; |
| 2 | + |
| 3 | +vi.mock("node:fs", () => ({ |
| 4 | + existsSync: vi.fn(), |
| 5 | + rmSync: vi.fn(), |
| 6 | +})); |
| 7 | + |
| 8 | +vi.mock("agent-install/skill", () => ({ |
| 9 | + add: vi.fn(), |
| 10 | + getCanonicalSkillsDir: vi.fn(), |
| 11 | + getSkillAgentConfig: vi.fn(), |
| 12 | + getSkillAgentDir: vi.fn(), |
| 13 | + isUniversalSkillAgent: vi.fn(), |
| 14 | +})); |
| 15 | + |
| 16 | +vi.mock("../src/utils/detect-agents.js", () => ({ |
| 17 | + detectAvailableAgents: vi.fn(), |
| 18 | +})); |
| 19 | + |
| 20 | +import { existsSync, rmSync } from "node:fs"; |
| 21 | +import { |
| 22 | + add, |
| 23 | + getCanonicalSkillsDir, |
| 24 | + getSkillAgentDir, |
| 25 | + isUniversalSkillAgent, |
| 26 | +} from "agent-install/skill"; |
| 27 | +import { detectAvailableAgents } from "../src/utils/detect-agents.js"; |
| 28 | +import { installSkill, removeSkill } from "../src/utils/install-skill.js"; |
| 29 | + |
| 30 | +const mockExistsSync = vi.mocked(existsSync); |
| 31 | +const mockRmSync = vi.mocked(rmSync); |
| 32 | +const mockAdd = vi.mocked(add); |
| 33 | +const mockGetCanonicalSkillsDir = vi.mocked(getCanonicalSkillsDir); |
| 34 | +const mockGetSkillAgentDir = vi.mocked(getSkillAgentDir); |
| 35 | +const mockIsUniversalSkillAgent = vi.mocked(isUniversalSkillAgent); |
| 36 | +const mockDetectAvailableAgents = vi.mocked(detectAvailableAgents); |
| 37 | + |
| 38 | +beforeEach(() => { |
| 39 | + vi.clearAllMocks(); |
| 40 | + mockIsUniversalSkillAgent.mockReturnValue(false); |
| 41 | + mockGetSkillAgentDir.mockImplementation((agent, options) => `${options.cwd}/.${agent}`); |
| 42 | + mockGetCanonicalSkillsDir.mockImplementation((_global, cwd) => `${cwd}/.agents/skills`); |
| 43 | + mockAdd.mockResolvedValue({ installed: [], failed: [] } as never); |
| 44 | +}); |
| 45 | + |
| 46 | +describe("installSkill", () => { |
| 47 | + it("installs to detected agents by default in copy mode", async () => { |
| 48 | + mockDetectAvailableAgents.mockResolvedValue(["claude-code", "cursor"] as never); |
| 49 | + |
| 50 | + await installSkill({ cwd: "/app" }); |
| 51 | + |
| 52 | + expect(mockDetectAvailableAgents).toHaveBeenCalledTimes(1); |
| 53 | + expect(mockAdd).toHaveBeenCalledWith( |
| 54 | + expect.objectContaining({ |
| 55 | + source: expect.stringContaining("skills/react-grab"), |
| 56 | + agents: ["claude-code", "cursor"], |
| 57 | + global: false, |
| 58 | + cwd: "/app", |
| 59 | + mode: "copy", |
| 60 | + }), |
| 61 | + ); |
| 62 | + }); |
| 63 | + |
| 64 | + it("uses explicit agents without running detection", async () => { |
| 65 | + await installSkill({ agents: ["codex"] as never, cwd: "/app", global: true }); |
| 66 | + |
| 67 | + expect(mockDetectAvailableAgents).not.toHaveBeenCalled(); |
| 68 | + expect(mockAdd).toHaveBeenCalledWith( |
| 69 | + expect.objectContaining({ agents: ["codex"], global: true, cwd: "/app", mode: "copy" }), |
| 70 | + ); |
| 71 | + }); |
| 72 | +}); |
| 73 | + |
| 74 | +describe("removeSkill", () => { |
| 75 | + it("removes skill directories that exist and returns the removed agents", async () => { |
| 76 | + mockDetectAvailableAgents.mockResolvedValue(["claude-code", "cursor"] as never); |
| 77 | + mockExistsSync.mockImplementation((path) => `${path}`.includes(".claude-code")); |
| 78 | + |
| 79 | + const removed = await removeSkill({ cwd: "/app" }); |
| 80 | + |
| 81 | + expect(removed).toEqual(["claude-code"]); |
| 82 | + expect(mockRmSync).toHaveBeenCalledTimes(1); |
| 83 | + expect(mockRmSync).toHaveBeenCalledWith("/app/.claude-code/react-grab", { |
| 84 | + recursive: true, |
| 85 | + force: true, |
| 86 | + }); |
| 87 | + }); |
| 88 | + |
| 89 | + it("returns an empty array and removes nothing when no skill is installed", async () => { |
| 90 | + mockDetectAvailableAgents.mockResolvedValue(["claude-code"] as never); |
| 91 | + mockExistsSync.mockReturnValue(false); |
| 92 | + |
| 93 | + const removed = await removeSkill({ cwd: "/app" }); |
| 94 | + |
| 95 | + expect(removed).toEqual([]); |
| 96 | + expect(mockRmSync).not.toHaveBeenCalled(); |
| 97 | + }); |
| 98 | + |
| 99 | + it("resolves universal agents to the canonical skills directory", async () => { |
| 100 | + mockDetectAvailableAgents.mockResolvedValue(["universal"] as never); |
| 101 | + mockIsUniversalSkillAgent.mockReturnValue(true); |
| 102 | + mockExistsSync.mockReturnValue(true); |
| 103 | + |
| 104 | + const removed = await removeSkill({ cwd: "/app", global: true }); |
| 105 | + |
| 106 | + expect(removed).toEqual(["universal"]); |
| 107 | + expect(mockGetCanonicalSkillsDir).toHaveBeenCalledWith(true, "/app"); |
| 108 | + expect(mockRmSync).toHaveBeenCalledWith("/app/.agents/skills/react-grab", { |
| 109 | + recursive: true, |
| 110 | + force: true, |
| 111 | + }); |
| 112 | + }); |
| 113 | +}); |
0 commit comments