import { mkdtemp, rm, readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; // --------------------------------------------------------------------------- // Helpers for building mock responses // --------------------------------------------------------------------------- function mockMarketplaceJson(skills: Array<{ path: string }>) { return JSON.stringify({ skills }); } function mockGitHubTree(paths: string[]) { return JSON.stringify({ tree: paths.map((p) => ({ path: p, type: "blob", size: 100 })), }); } function mockSkillMd(name: string, description: string) { return `--- name: ${name} description: ${description} --- # ${name} A skill for testing. `; } function mockCommitSha(sha = "abc1234567890abcdef1234567890abcdef123456") { return JSON.stringify({ sha }); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("skill-registry-fetch", () => { let tmpDir: string; let originalPaperclipHome: string | undefined; let fetchSpy: ReturnType; beforeEach(async () => { tmpDir = await mkdtemp(path.join(os.tmpdir(), "skill-fetch-test-")); originalPaperclipHome = process.env.PAPERCLIP_HOME; process.env.PAPERCLIP_HOME = tmpDir; // Mock global.fetch fetchSpy = vi.fn(); vi.stubGlobal("fetch", fetchSpy); }); afterEach(async () => { vi.restoreAllMocks(); vi.unstubAllGlobals(); const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js"); resetSkillRegistryDb(); if (originalPaperclipHome === undefined) { delete process.env.PAPERCLIP_HOME; } else { process.env.PAPERCLIP_HOME = originalPaperclipHome; } await rm(tmpDir, { recursive: true, force: true }); }); // ------------------------------------------------------------------------- // Test 1: Anthropic marketplace source inserts skills rows with sourceId // ------------------------------------------------------------------------- it("Test 1: fetchAllSources() with mocked Anthropic marketplace.json inserts skills rows with sourceId='anthropic-official'", async () => { const sha = "abc1234567890abcdef1234567890abcdef123456"; // Mock all fetch calls fetchSpy.mockImplementation(async (url: string, opts?: RequestInit) => { const urlStr = String(url); // marketplace.json if (urlStr.includes("marketplace.json")) { return { ok: true, text: async () => mockMarketplaceJson([{ path: "coding/my-skill" }]), json: async () => ({ skills: [{ path: "coding/my-skill" }] }), }; } // commit SHA lookup if (urlStr.includes("/commits/")) { return { ok: true, text: async () => mockCommitSha(sha), json: async () => ({ sha }), }; } // SKILL.md raw content if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => mockSkillMd("My Skill", "A test skill from Anthropic"), json: async () => ({}), }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skills } = await import("../services/skill-registry-schema.js"); // Fetch only the Anthropic source const anthropicSource = BUILT_IN_SOURCES.find((s) => s.id === "anthropic-official")!; const result = await fetchAllSources([anthropicSource]); expect(result.errors).toHaveLength(0); expect(result.fetched).toBeGreaterThan(0); const db = await getSkillRegistryDb(); const rows = await db.select().from(skills); expect(rows.length).toBeGreaterThan(0); expect(rows[0]!.sourceId).toBe("anthropic-official"); }); // ------------------------------------------------------------------------- // Test 2: Community GitHub source inserts skills rows with correct sourceId // ------------------------------------------------------------------------- it("Test 2: fetchAllSources() with mocked GitHub tree API for community repo inserts skills rows with correct sourceId", async () => { const sha = "deadbeef1234567890abcdef1234567890abcdef"; fetchSpy.mockImplementation(async (url: string) => { const urlStr = String(url); if (urlStr.includes("/git/trees/")) { return { ok: true, text: async () => mockGitHubTree(["code-review/SKILL.md", "code-review/rules/rules.md"]), json: async () => ({ tree: [ { path: "code-review/SKILL.md", type: "blob", size: 200 }, { path: "code-review/rules/rules.md", type: "blob", size: 500 }, ], }), }; } if (urlStr.includes("/commits/")) { return { ok: true, text: async () => mockCommitSha(sha), json: async () => ({ sha }), }; } if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => mockSkillMd("Code Review", "Reviews code for quality"), json: async () => ({}), }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skills } = await import("../services/skill-registry-schema.js"); const communitySource = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!; const result = await fetchAllSources([communitySource]); expect(result.errors).toHaveLength(0); const db = await getSkillRegistryDb(); const rows = await db.select().from(skills); expect(rows.length).toBeGreaterThan(0); expect(rows[0]!.sourceId).toBe("schwepps-skills"); }); // ------------------------------------------------------------------------- // Test 3: Each fetched skill has a skill_versions row with commit SHA // ------------------------------------------------------------------------- it("Test 3: Each fetched skill has a skill_versions row with the commit SHA as version", async () => { const sha = "cafebabe1234567890abcdef1234567890abcdef"; fetchSpy.mockImplementation(async (url: string) => { const urlStr = String(url); if (urlStr.includes("/git/trees/")) { return { ok: true, json: async () => ({ tree: [{ path: "test-skill/SKILL.md", type: "blob", size: 100 }], }), }; } if (urlStr.includes("/commits/")) { return { ok: true, json: async () => ({ sha }), }; } if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => mockSkillMd("Test Skill", "A test skill"), }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skillVersions } = await import("../services/skill-registry-schema.js"); const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!; await fetchAllSources([source]); const db = await getSkillRegistryDb(); const versions = await db.select().from(skillVersions); expect(versions.length).toBeGreaterThan(0); expect(versions[0]!.version).toBe(sha); }); // ------------------------------------------------------------------------- // Test 4: SKILL.md written to cache dir // ------------------------------------------------------------------------- it("Test 4: Each fetched skill's SKILL.md content is written to cache dir at /skills/cache///SKILL.md", async () => { const sha = "feedfeed1234567890abcdef1234567890abcdef"; const skillMdContent = mockSkillMd("Cached Skill", "Written to disk"); fetchSpy.mockImplementation(async (url: string) => { const urlStr = String(url); if (urlStr.includes("/git/trees/")) { return { ok: true, json: async () => ({ tree: [{ path: "cached-skill/SKILL.md", type: "blob", size: 100 }], }), }; } if (urlStr.includes("/commits/")) { return { ok: true, json: async () => ({ sha }), }; } if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => skillMdContent, }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { resolveSkillCacheDir } = await import("../home-paths.js"); const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!; await fetchAllSources([source]); // The skill id should be schwepps-skills/cached-skill const skillId = "schwepps-skills/cached-skill"; const cacheDir = resolveSkillCacheDir(skillId, sha); const cachedPath = path.join(cacheDir, "SKILL.md"); expect(existsSync(cachedPath)).toBe(true); const content = await readFile(cachedPath, "utf-8"); expect(content).toBe(skillMdContent); }); // ------------------------------------------------------------------------- // Test 5: skill_files rows inserted for each cached file // ------------------------------------------------------------------------- it("Test 5: skill_files rows are inserted for each cached file with path, kind, and size_bytes", async () => { const sha = "aabbccdd1234567890abcdef1234567890abcdef"; fetchSpy.mockImplementation(async (url: string) => { const urlStr = String(url); if (urlStr.includes("/git/trees/")) { return { ok: true, json: async () => ({ tree: [{ path: "files-skill/SKILL.md", type: "blob", size: 350 }], }), }; } if (urlStr.includes("/commits/")) { return { ok: true, json: async () => ({ sha }), }; } if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => mockSkillMd("Files Skill", "Tests skill_files rows"), }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skillFiles } = await import("../services/skill-registry-schema.js"); const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!; await fetchAllSources([source]); const db = await getSkillRegistryDb(); const files = await db.select().from(skillFiles); expect(files.length).toBeGreaterThan(0); const skillMdFile = files.find((f) => f.path.endsWith("SKILL.md")); expect(skillMdFile).toBeDefined(); expect(skillMdFile!.kind).toBe("skill"); expect(skillMdFile!.sizeBytes).toBeGreaterThan(0); }); // ------------------------------------------------------------------------- // Test 6: Re-fetching with same SHA is idempotent // ------------------------------------------------------------------------- it("Test 6: Re-fetching with same SHA skips re-download (idempotent)", async () => { const sha = "idemidemidem1234567890abcdef1234567890ab"; let fetchCallCount = 0; fetchSpy.mockImplementation(async (url: string) => { const urlStr = String(url); fetchCallCount++; if (urlStr.includes("/git/trees/")) { return { ok: true, json: async () => ({ tree: [{ path: "idem-skill/SKILL.md", type: "blob", size: 100 }], }), }; } if (urlStr.includes("/commits/")) { return { ok: true, json: async () => ({ sha }), }; } if (urlStr.includes("SKILL.md")) { return { ok: true, text: async () => mockSkillMd("Idem Skill", "Tests idempotency"), }; } return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) }; }); const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skillVersions } = await import("../services/skill-registry-schema.js"); const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!; // First fetch await fetchAllSources([source]); const firstCount = fetchCallCount; // Second fetch with same SHA — should not re-download SKILL.md fetchCallCount = 0; await fetchAllSources([source]); const db = await getSkillRegistryDb(); const versions = await db.select().from(skillVersions); // Only 1 version row for the same SHA (idempotent insert) const idemVersions = versions.filter((v) => v.version === sha); expect(idemVersions.length).toBe(1); // Second run should have fewer fetch calls (no SKILL.md re-download) expect(fetchCallCount).toBeLessThan(firstCount); }); // ------------------------------------------------------------------------- // Test 7: BUILT_IN_SOURCES has exactly 3 entries // ------------------------------------------------------------------------- it("Test 7: BUILT_IN_SOURCES contains 3 entries (anthropic-official, schwepps-skills, daymade-skills)", async () => { const { BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js"); expect(BUILT_IN_SOURCES).toHaveLength(3); const ids = BUILT_IN_SOURCES.map((s) => s.id); expect(ids).toContain("anthropic-official"); expect(ids).toContain("schwepps-skills"); expect(ids).toContain("daymade-skills"); // Verify all sources have required fields for (const source of BUILT_IN_SOURCES) { expect(source.id).toBeTruthy(); expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/); expect(source.owner).toBeTruthy(); expect(source.repo).toBeTruthy(); expect(source.ref).toBeTruthy(); expect(source.label).toBeTruthy(); } }); });