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)
This commit is contained in:
Mikkel Georgsen 2026-04-01 01:04:57 +02:00 committed by Nexus Dev
parent b629235a02
commit 9950091e0d

View file

@ -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<typeof vi.fn>;
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 <instance-root>/skills/cache/<skill-id>/<sha>/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();
}
});
});