import { mkdtemp, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; // We reset the singleton between tests by calling resetSkillRegistryDb // and redirecting PAPERCLIP_HOME to an isolated temp dir describe("skill-registry-schema", () => { let tmpDir: string; let originalPaperclipHome: string | undefined; beforeEach(async () => { tmpDir = await mkdtemp(path.join(os.tmpdir(), "skill-registry-test-")); originalPaperclipHome = process.env.PAPERCLIP_HOME; process.env.PAPERCLIP_HOME = tmpDir; }); afterEach(async () => { // Reset the DB singleton so next test gets a fresh instance 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 }); }); it("Test 1: getSkillRegistryDb() creates registry.db at the resolved path and returns a drizzle instance", async () => { const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const db = await getSkillRegistryDb(); expect(db).toBeDefined(); expect(typeof db.select).toBe("function"); expect(typeof db.insert).toBe("function"); }); it("Test 2: skills table has correct columns", async () => { const { skills } = await import("../services/skill-registry-schema.js"); const cols = Object.keys(skills); // Check the table has the expected name expect((skills as any)[Symbol.for("drizzle:Name") as any] ?? skills._.name).toBeDefined(); const colNames = Object.keys(skills); // Drizzle table object has column accessors expect(skills.id).toBeDefined(); expect(skills.sourceId).toBeDefined(); expect(skills.name).toBeDefined(); expect(skills.description).toBeDefined(); expect(skills.sourceUrl).toBeDefined(); expect(skills.activeVersionId).toBeDefined(); expect(skills.removedAt).toBeDefined(); expect(skills.createdAt).toBeDefined(); expect(skills.updatedAt).toBeDefined(); }); it("Test 3: skill_versions table has correct columns", async () => { const { skillVersions } = await import("../services/skill-registry-schema.js"); expect(skillVersions.id).toBeDefined(); expect(skillVersions.skillId).toBeDefined(); expect(skillVersions.version).toBeDefined(); expect(skillVersions.fetchedAt).toBeDefined(); expect(skillVersions.cacheDir).toBeDefined(); }); it("Test 4: skill_files table has correct columns", async () => { const { skillFiles } = await import("../services/skill-registry-schema.js"); expect(skillFiles.id).toBeDefined(); expect(skillFiles.versionId).toBeDefined(); expect(skillFiles.path).toBeDefined(); expect(skillFiles.kind).toBeDefined(); expect(skillFiles.sizeBytes).toBeDefined(); }); it("Test 5: community_ratings table has correct columns", async () => { const { communityRatings } = await import("../services/skill-registry-schema.js"); expect(communityRatings.id).toBeDefined(); expect(communityRatings.skillId).toBeDefined(); expect(communityRatings.fetchedAt).toBeDefined(); expect(communityRatings.averageRating).toBeDefined(); expect(communityRatings.ratingCount).toBeDefined(); expect(communityRatings.source).toBeDefined(); }); it("Test 6: soft-delete — inserting a skill and setting removed_at keeps the row queryable with a WHERE filter", async () => { const { getSkillRegistryDb } = await import("../services/skill-registry-db.js"); const { skills } = await import("../services/skill-registry-schema.js"); const { eq, isNull, isNotNull } = await import("drizzle-orm"); const db = await getSkillRegistryDb(); const now = Date.now(); await db.insert(skills).values({ id: "test-skill-1", sourceId: "src-1", name: "Test Skill", description: "A test skill", sourceUrl: null, activeVersionId: null, removedAt: null, createdAt: now, updatedAt: now, }); // Before soft-delete: row is visible const before = await db.select().from(skills).where(isNull(skills.removedAt)); expect(before.length).toBe(1); // Apply soft-delete await db.update(skills).set({ removedAt: now + 1000 }).where(eq(skills.id, "test-skill-1")); // After soft-delete: not visible via active filter const activeAfter = await db.select().from(skills).where(isNull(skills.removedAt)); expect(activeAfter.length).toBe(0); // But still visible via removed filter const removedAfter = await db.select().from(skills).where(isNotNull(skills.removedAt)); expect(removedAfter.length).toBe(1); expect(removedAfter[0]!.id).toBe("test-skill-1"); }); it("Test 7: resolveSkillRegistryDbPath() returns path ending in skills/registry.db under instance root", async () => { const { resolveSkillRegistryDbPath } = await import("../home-paths.js"); const dbPath = resolveSkillRegistryDbPath(); expect(dbPath).toMatch(/skills[/\\]registry\.db$/); expect(dbPath.startsWith(tmpDir)).toBe(true); }); it("Test 8: resolveSkillCacheDir returns path ending in skills/cache//", async () => { const { resolveSkillCacheDir } = await import("../home-paths.js"); const cacheDir = resolveSkillCacheDir("my-skill", "abc123"); expect(cacheDir).toMatch(/skills[/\\]cache[/\\]my-skill[/\\]abc123$/); expect(cacheDir.startsWith(tmpDir)).toBe(true); }); });