import { describe, it, expect, vi, beforeEach } from "vitest"; import { createHash } from "node:crypto"; vi.mock("../lib/manifest.js", () => ({ readManifest: vi.fn(), })); vi.mock("../lib/install-path.js", () => ({ resolveInstallPath: vi.fn(), detectAllTargets: vi.fn(() => []), ALL_TARGETS: [], parseTargets: vi.fn(() => []), getPlatform: vi.fn(), })); vi.mock("node:fs", () => ({ existsSync: vi.fn(), readFileSync: vi.fn(), })); import { verifyCommand, extractInstructionsBody, computeContentHash, } from "./verify.js"; import { readManifest } from "../lib/manifest.js"; import { resolveInstallPath } from "../lib/install-path.js"; import { existsSync, readFileSync } from "node:fs"; const mockReadManifest = vi.mocked(readManifest); const mockResolveInstallPath = vi.mocked(resolveInstallPath); const mockExistsSync = vi.mocked(existsSync); const mockReadFileSync = vi.mocked(readFileSync); function makeHash(body: string): string { return createHash("sha256").update(body, "utf-8").digest("hex"); } function makeSkillMd(body: string): string { return `---\nname: Test\ndescription: A skill\nversion: 1.0.0\ncategory: testing\n---\n\n${body}\n`; } beforeEach(() => { vi.clearAllMocks(); mockResolveInstallPath.mockReturnValue({ path: "/home/user/.claude/skills", type: "claude-code", } as any); }); describe("extractInstructionsBody", () => { it("extracts body after second --- delimiter", () => { const body = "Do something useful."; const file = makeSkillMd(body); expect(extractInstructionsBody(file)).toBe(body); }); it("returns trimmed content when no frontmatter", () => { const content = " no frontmatter here "; expect(extractInstructionsBody(content)).toBe("no frontmatter here"); }); }); describe("computeContentHash", () => { it("returns sha256 hex of the input", () => { const text = "hello world"; const expected = createHash("sha256").update(text, "utf-8").digest("hex"); expect(computeContentHash(text)).toBe(expected); }); }); describe("verify command — all pass", () => { it("exits 0 when all hashes match", async () => { const body = "Do something useful."; const hash = makeHash(body); mockReadManifest.mockReturnValue({ version: 1, skills: { "my-skill": { version: "1.0.0", installedAt: "", platform: "claude-code", contentHash: hash, }, }, }); mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(makeSkillMd(body) as any); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify"], { from: "node" }); expect(exitSpy).not.toHaveBeenCalledWith(1); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("PASS"); expect(output).toContain("my-skill"); log.mockRestore(); exitSpy.mockRestore(); }); }); describe("verify command — hash mismatch (FAIL)", () => { it("exits 1 when hash does not match", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "tampered-skill": { version: "1.0.0", installedAt: "", platform: "claude-code", contentHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, }, }); mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(makeSkillMd("actual content") as any); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify"], { from: "node" }); expect(exitSpy).toHaveBeenCalledWith(1); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("FAIL"); log.mockRestore(); exitSpy.mockRestore(); }); }); describe("verify command — missing file (MISSING)", () => { it("exits 1 when installed file does not exist", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "missing-skill": { version: "1.0.0", installedAt: "", platform: "claude-code", contentHash: "aabbcc", }, }, }); mockExistsSync.mockReturnValue(false); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify"], { from: "node" }); expect(exitSpy).toHaveBeenCalledWith(1); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("MISSING"); log.mockRestore(); exitSpy.mockRestore(); }); }); describe("verify command — no hash (UNVERIFIED)", () => { it("does not fail when no contentHash recorded", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: { "old-skill": { version: "1.0.0", installedAt: "", platform: "claude-code", }, }, }); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify"], { from: "node" }); expect(exitSpy).not.toHaveBeenCalledWith(1); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("UNVERIFIED"); log.mockRestore(); exitSpy.mockRestore(); }); }); describe("verify command — single slug filter", () => { it("verifies only the specified slug", async () => { const body = "targeted skill body"; const hash = makeHash(body); mockReadManifest.mockReturnValue({ version: 1, skills: { "skill-a": { version: "1.0.0", installedAt: "", platform: "claude-code", contentHash: hash, }, "skill-b": { version: "1.0.0", installedAt: "", platform: "claude-code", contentHash: "other", }, }, }); mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(makeSkillMd(body) as any); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify", "skill-a"], { from: "node", }); expect(exitSpy).not.toHaveBeenCalledWith(1); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("skill-a"); expect(output).not.toContain("skill-b"); log.mockRestore(); exitSpy.mockRestore(); }); it("exits 1 if slug not in manifest", async () => { mockReadManifest.mockReturnValue({ version: 1, skills: {} }); const exitSpy = vi .spyOn(process, "exit") .mockImplementation((() => {}) as any); const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); await verifyCommand.parseAsync(["node", "verify", "ghost-skill"], { from: "node", }); expect(exitSpy).toHaveBeenCalledWith(1); errSpy.mockRestore(); exitSpy.mockRestore(); }); });