From 9950091e0df3fa6034c19d1cd935a2f31ca73378 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Wed, 1 Apr 2026 01:04:57 +0200 Subject: [PATCH] test(09-02): add failing tests for skill-registry-fetcher - 7 tests covering fetch from Anthropic marketplace and GitHub tree sources - Tests DB insertion, file caching, idempotency, and BUILT_IN_SOURCES config - All tests fail with ERR_MODULE_NOT_FOUND (expected TDD RED state) --- .../__tests__/skill-registry-fetch.test.ts | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 server/src/__tests__/skill-registry-fetch.test.ts diff --git a/server/src/__tests__/skill-registry-fetch.test.ts b/server/src/__tests__/skill-registry-fetch.test.ts new file mode 100644 index 00000000..bf10c3bd --- /dev/null +++ b/server/src/__tests__/skill-registry-fetch.test.ts @@ -0,0 +1,404 @@ +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(); + } + }); +});