import { describe, it, expect, vi, beforeEach } from "vitest"; import { readFileSync, writeFileSync } from "node:fs"; vi.mock("node:fs", () => ({ readFileSync: vi.fn(), writeFileSync: vi.fn(), })); import { readManifest, writeManifest, addToManifest, removeFromManifest, } from "./manifest.js"; const mockReadFileSync = vi.mocked(readFileSync); const mockWriteFileSync = vi.mocked(writeFileSync); beforeEach(() => { vi.clearAllMocks(); }); describe("readManifest", () => { it("returns empty manifest when file does not exist", () => { mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); }); const result = readManifest(); expect(result).toEqual({ version: 1, skills: {} }); }); it("parses valid manifest file", () => { const data = { version: 1, skills: { "code-review": { version: "1.0.0", installedAt: "2026-03-07T00:00:00.000Z", platform: "claude-code", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(data)); const result = readManifest(); expect(result).toEqual(data); }); it("returns empty manifest for invalid JSON", () => { mockReadFileSync.mockReturnValue("not json"); const result = readManifest(); expect(result).toEqual({ version: 1, skills: {} }); }); it("returns empty manifest when skills field is missing", () => { mockReadFileSync.mockReturnValue(JSON.stringify({ other: "data" })); const result = readManifest(); expect(result).toEqual({ version: 1, skills: {} }); }); it("reads from .skills.json in cwd", () => { mockReadFileSync.mockReturnValue( JSON.stringify({ version: 1, skills: {} }), ); readManifest(); expect(mockReadFileSync).toHaveBeenCalledWith( expect.stringContaining(".skills.json"), "utf-8", ); }); it("migrates old format without version field", () => { const data = { skills: { "code-review": { version: "1.0.0", installedAt: "2026-03-07T00:00:00.000Z", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(data)); const result = readManifest(); expect(result.version).toBe(1); }); it("migrates old format without platform field", () => { const data = { version: 1, skills: { "code-review": { version: "1.0.0", installedAt: "2026-03-07T00:00:00.000Z", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(data)); const result = readManifest(); expect(result.skills["code-review"].platform).toBe("claude-code"); }); }); describe("writeManifest", () => { it("writes formatted JSON with trailing newline and version field", () => { const manifest = { version: 1, skills: { "my-skill": { version: "2.0.0", installedAt: "2026-03-07T00:00:00.000Z", platform: "claude-code" as const, }, }, }; writeManifest(manifest); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.version).toBe(1); expect(written.skills["my-skill"].version).toBe("2.0.0"); expect(mockWriteFileSync).toHaveBeenCalledWith( expect.stringContaining(".skills.json"), expect.stringContaining('"version": 1'), ); }); }); describe("addToManifest", () => { it("adds a new skill to an empty manifest", () => { mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); }); vi.spyOn(Date.prototype, "toISOString").mockReturnValue( "2026-03-07T12:00:00.000Z", ); addToManifest("code-review", "1.0.0"); expect(mockWriteFileSync).toHaveBeenCalledWith( expect.stringContaining(".skills.json"), expect.stringContaining('"code-review"'), ); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.version).toBe(1); expect(written.skills["code-review"]).toEqual({ version: "1.0.0", installedAt: "2026-03-07T12:00:00.000Z", platform: "claude-code", }); vi.restoreAllMocks(); }); it("adds a skill with custom platform", () => { mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); }); vi.spyOn(Date.prototype, "toISOString").mockReturnValue( "2026-03-07T12:00:00.000Z", ); addToManifest("code-review", "1.0.0", "cursor"); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.skills["code-review"].platform).toBe("cursor"); vi.restoreAllMocks(); }); it("adds a skill to an existing manifest preserving other entries", () => { const existing = { version: 1, skills: { "existing-skill": { version: "1.0.0", installedAt: "2026-03-01T00:00:00.000Z", platform: "claude-code", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(existing)); vi.spyOn(Date.prototype, "toISOString").mockReturnValue( "2026-03-07T12:00:00.000Z", ); addToManifest("new-skill", "2.0.0"); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.skills["existing-skill"]).toEqual({ version: "1.0.0", installedAt: "2026-03-01T00:00:00.000Z", platform: "claude-code", }); expect(written.skills["new-skill"]).toEqual({ version: "2.0.0", installedAt: "2026-03-07T12:00:00.000Z", platform: "claude-code", }); vi.restoreAllMocks(); }); it("overwrites version when reinstalling same skill", () => { const existing = { version: 1, skills: { "code-review": { version: "1.0.0", installedAt: "2026-03-01T00:00:00.000Z", platform: "claude-code", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(existing)); vi.spyOn(Date.prototype, "toISOString").mockReturnValue( "2026-03-07T12:00:00.000Z", ); addToManifest("code-review", "2.0.0"); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.skills["code-review"].version).toBe("2.0.0"); expect(written.skills["code-review"].installedAt).toBe( "2026-03-07T12:00:00.000Z", ); vi.restoreAllMocks(); }); }); describe("removeFromManifest", () => { it("removes an existing skill and returns true", () => { const existing = { skills: { "code-review": { version: "1.0.0", installedAt: "2026-03-01T00:00:00.000Z", }, "lint-check": { version: "1.0.0", installedAt: "2026-03-01T00:00:00.000Z", }, }, }; mockReadFileSync.mockReturnValue(JSON.stringify(existing)); const result = removeFromManifest("code-review"); expect(result).toBe(true); const written = JSON.parse(mockWriteFileSync.mock.calls[0][1] as string); expect(written.skills["code-review"]).toBeUndefined(); expect(written.skills["lint-check"]).toBeDefined(); }); it("returns false when skill is not in manifest", () => { mockReadFileSync.mockImplementation(() => { throw new Error("ENOENT"); }); const result = removeFromManifest("nonexistent"); expect(result).toBe(false); expect(mockWriteFileSync).not.toHaveBeenCalled(); }); });