- Install @libsql/client@^0.17.2 to server package - Create skill-registry-schema.ts with 4 sqliteTable definitions (skills, skillVersions, skillFiles, communityRatings) - Create skill-registry-db.ts with lazy singleton getSkillRegistryDb() and resetSkillRegistryDb() - Add resolveSkillRegistryDbPath() and resolveSkillCacheDir() to home-paths.ts - Add skill-registry-schema.test.ts with 8 passing tests (TDD green)
136 lines
5.5 KiB
TypeScript
136 lines
5.5 KiB
TypeScript
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/<skillId>/<versionId>", 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);
|
|
});
|
|
});
|