import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-skills-registry-")); process.env.DATA_DIR = TEST_DATA_DIR; const coreDb = await import("../../src/lib/db/core.ts"); const { skillRegistry } = await import("../../src/lib/skills/registry.ts"); function resetRegistryState() { skillRegistry["registeredSkills"].clear(); skillRegistry["versionCache"].clear(); skillRegistry["loadedAll"] = false; skillRegistry["loadedApiKeyIds"].clear(); skillRegistry.invalidateCache(); } async function resetStorage() { resetRegistryState(); coreDb.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } test.beforeEach(async () => { await resetStorage(); }); test.after(() => { resetRegistryState(); coreDb.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); test("skillRegistry registers, lists, sorts and resolves versions", async () => { await skillRegistry.register({ name: "echo", version: "1.0.0", description: "stable", schema: { input: { text: "string" }, output: { result: "string" } }, handler: "echo-handler", apiKeyId: "key-a", }); const latest = await skillRegistry.register({ name: "echo", version: "1.2.0", description: "latest", schema: { input: { text: "string" }, output: { result: "string" } }, handler: "echo-handler", apiKeyId: "key-a", }); await skillRegistry.register({ name: "summarize", description: "defaults version", schema: { input: {}, output: {} }, handler: "summarize-handler", apiKeyId: "key-b", }); assert.equal(skillRegistry.list("key-a").length, 2); assert.equal(skillRegistry.list().length, 3); assert.equal(skillRegistry.getSkill("echo@1.2.0")?.description, "latest"); assert.equal(skillRegistry.getSkill("echo", "key-a")?.description, "latest"); const versions = skillRegistry.getSkillVersions("echo").map((skill) => skill.version); assert.deepEqual(versions, ["1.2.0", "1.0.0"]); assert.equal(skillRegistry.resolveVersion("echo", "^1.1.0")?.id, latest.id); assert.equal(skillRegistry.resolveVersion("echo", "~1.0.0")?.version, "1.0.0"); assert.equal(skillRegistry.resolveVersion("echo", "1.2.0")?.version, "1.2.0"); assert.equal(skillRegistry.resolveVersion("missing", "^1.0.0"), undefined); }); test("skillRegistry keeps same name/version isolated per API key", async () => { const first = await skillRegistry.register({ name: "shared-skill", version: "1.0.0", description: "key a", schema: { input: {}, output: {} }, handler: "shared-handler", apiKeyId: "key-a", }); const second = await skillRegistry.register({ name: "shared-skill", version: "1.0.0", description: "key b", schema: { input: {}, output: {} }, handler: "shared-handler", apiKeyId: "key-b", }); assert.equal(skillRegistry.list().length, 2); assert.equal(skillRegistry.getSkill("shared-skill", "key-a")?.id, first.id); assert.equal(skillRegistry.getSkill("shared-skill", "key-b")?.id, second.id); assert.deepEqual( skillRegistry.getSkillVersions("shared-skill", "key-a").map((skill) => skill.id), [first.id] ); }); test("skillRegistry can reload persisted skills from SQLite", async () => { const first = await skillRegistry.register({ name: "file-read", version: "2.0.0", description: "reads files", schema: { input: { path: "string" }, output: { content: "string" } }, handler: "file-read-handler", apiKeyId: "key-a", }); const second = await skillRegistry.register({ name: "file-write", version: "1.0.0", description: "writes files", schema: { input: { path: "string", content: "string" }, output: { status: "boolean" } }, handler: "file-write-handler", apiKeyId: "key-b", }); resetRegistryState(); assert.equal(skillRegistry.list().length, 0); await skillRegistry.loadFromDatabase("key-a"); assert.deepEqual( skillRegistry.list().map((skill) => skill.name), ["file-read"] ); await skillRegistry.loadFromDatabase(); assert.deepEqual( skillRegistry .list() .map((skill) => skill.name) .sort(), ["file-read", "file-write"] ); resetRegistryState(); await skillRegistry.loadFromDatabase(); const loadedNames = skillRegistry .list() .map((skill) => skill.name) .sort(); assert.deepEqual(loadedNames, ["file-read", "file-write"]); assert.equal(skillRegistry.getSkill(`${first.name}@${first.version}`)?.apiKeyId, "key-a"); assert.equal(skillRegistry.getSkill(`${second.name}@${second.version}`)?.apiKeyId, "key-b"); }); test("skillRegistry updates enabled state by ID without duplicate registration", async () => { const skill = await skillRegistry.register({ name: "toggle-skill", version: "1.0.0", description: "toggles", schema: { input: {}, output: {} }, handler: "toggle-handler", enabled: false, apiKeyId: "key-a", }); const updated = await skillRegistry.setEnabledById(skill.id, "key-a", true); assert.equal(updated?.enabled, true); assert.equal(updated?.mode, "on"); assert.equal(skillRegistry.list("key-a").length, 1); assert.equal(skillRegistry.getSkill(skill.id, "key-a")?.enabled, true); assert.equal(await skillRegistry.setEnabledById(skill.id, "key-b", false), undefined); }); test("skillRegistry unregisters by version, by name/apiKey and by id", async () => { const exact = await skillRegistry.register({ name: "translate", version: "1.0.0", description: "translate en to pt", schema: { input: { text: "string" }, output: { text: "string" } }, handler: "translate-handler", apiKeyId: "key-a", }); await skillRegistry.register({ name: "translate", version: "1.1.0", description: "translate v2", schema: { input: { text: "string" }, output: { text: "string" } }, handler: "translate-handler", apiKeyId: "key-a", }); const otherKey = await skillRegistry.register({ name: "translate", version: "2.0.0", description: "translate external", schema: { input: { text: "string" }, output: { text: "string" } }, handler: "translate-handler", apiKeyId: "key-b", }); assert.equal(await skillRegistry.unregister("translate", "1.0.0", "key-a"), true); assert.equal(skillRegistry.getSkill("translate@1.0.0"), undefined); assert.equal(await skillRegistry.unregister("translate", undefined, "key-a"), true); assert.deepEqual( skillRegistry.list().map((skill) => skill.id), [otherKey.id] ); assert.equal(await skillRegistry.unregisterById(otherKey.id), true); assert.equal(await skillRegistry.unregisterById(exact.id), false); assert.equal(await skillRegistry.unregister("translate", undefined, "key-a"), false); }); test("skillRegistry rejects invalid payloads from schema validation", async () => { await assert.rejects( skillRegistry.register({ name: "bad-skill", version: "not-semver", description: "invalid version", schema: { input: {}, output: {} }, handler: "bad-handler", apiKeyId: "key-a", }), /Invalid string/ ); await assert.rejects( skillRegistry.register({ name: "", version: "1.0.0", description: "missing name", schema: { input: {}, output: {} }, handler: "bad-handler", apiKeyId: "key-a", }), /Too small/ ); }); // --------------------------------------------------------------------------- // TTL Cache tests // --------------------------------------------------------------------------- const skillPayload = (name = "cache-skill", version = "1.0.0", apiKeyId = "key-cache") => ({ name, version, description: `${name} desc`, schema: { input: {}, output: {} }, handler: `${name}-handler`, apiKeyId, }); test("cache hit — loadFromDatabase within TTL returns same reference (no DB re-query)", async () => { const originalNow = Date.now; try { // Register a skill so the DB has data await skillRegistry.register(skillPayload("cache-hit", "1.0.0")); // Force cache stale, then load to prime it skillRegistry.invalidateCache(); let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; await skillRegistry.loadFromDatabase(); const loadedOnce = skillRegistry["lastLoaded"]; // Advance time by less than cacheTTL (e.g. 30s) fakeTime += 30_000; // Second load should short-circuit (lastLoaded unchanged) await skillRegistry.loadFromDatabase(); const loadedTwice = skillRegistry["lastLoaded"]; assert.equal(loadedOnce, loadedTwice, "lastLoaded should NOT update on cache hit"); } finally { Date.now = originalNow; } }); test("cache miss on expiry — after TTL passes, loadFromDatabase re-queries", async () => { const originalNow = Date.now; try { await skillRegistry.register(skillPayload("cache-expiry", "1.0.0")); skillRegistry.invalidateCache(); let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; await skillRegistry.loadFromDatabase(); const firstLoad = skillRegistry["lastLoaded"]; // Advance past TTL (cacheTTL = 60_000) fakeTime += 61_000; await skillRegistry.loadFromDatabase(); const secondLoad = skillRegistry["lastLoaded"]; assert.notEqual(firstLoad, secondLoad, "lastLoaded should update after TTL expiry"); assert.equal(secondLoad, fakeTime, "lastLoaded should reflect current time after reload"); } finally { Date.now = originalNow; } }); test("cache invalidated on register — calling register() clears the cache", async () => { const originalNow = Date.now; try { let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; // Prime cache skillRegistry.invalidateCache(); await skillRegistry.loadFromDatabase(); const loadedBefore = skillRegistry["lastLoaded"]; assert.ok(loadedBefore > 0, "cache should be primed"); // Register a new skill — should invalidate await skillRegistry.register(skillPayload("cache-inv-reg", "1.0.0")); assert.equal(skillRegistry["lastLoaded"], 0, "lastLoaded should be 0 after register()"); } finally { Date.now = originalNow; } }); test("cache invalidated on unregister — calling unregister() clears the cache", async () => { const originalNow = Date.now; try { let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; const skill = await skillRegistry.register(skillPayload("cache-inv-unreg", "1.0.0")); // Prime cache skillRegistry.invalidateCache(); await skillRegistry.loadFromDatabase(); assert.ok(skillRegistry["lastLoaded"] > 0, "cache should be primed"); // Unregister — should invalidate await skillRegistry.unregister("cache-inv-unreg", "1.0.0", "key-cache"); assert.equal(skillRegistry["lastLoaded"], 0, "lastLoaded should be 0 after unregister()"); } finally { Date.now = originalNow; } }); test("cache invalidated on unregisterById — calling unregisterById() clears the cache", async () => { const originalNow = Date.now; try { let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; const skill = await skillRegistry.register(skillPayload("cache-inv-byid", "1.0.0")); // Prime cache skillRegistry.invalidateCache(); await skillRegistry.loadFromDatabase(); assert.ok(skillRegistry["lastLoaded"] > 0, "cache should be primed"); // Unregister by id — should invalidate await skillRegistry.unregisterById(skill.id); assert.equal(skillRegistry["lastLoaded"], 0, "lastLoaded should be 0 after unregisterById()"); } finally { Date.now = originalNow; } }); test("concurrent loadFromDatabase calls during cache miss only query DB once (no stampede)", async () => { const originalNow = Date.now; try { await skillRegistry.register(skillPayload("cache-concurrent", "1.0.0")); // Force stale skillRegistry.invalidateCache(); let fakeTime = originalNow.call(Date); Date.now = () => fakeTime; // Fire multiple concurrent loads const results = await Promise.all([ skillRegistry.loadFromDatabase(), skillRegistry.loadFromDatabase(), skillRegistry.loadFromDatabase(), ]); // After all settle, lastLoaded should be set exactly to fakeTime const lastLoaded = skillRegistry["lastLoaded"]; assert.equal(lastLoaded, fakeTime, "lastLoaded should be set after concurrent loads"); // Advance time but stay within TTL fakeTime += 10_000; // Additional call should NOT re-query (cache hit) await skillRegistry.loadFromDatabase(); assert.equal( skillRegistry["lastLoaded"], lastLoaded, "cache should still be fresh after concurrent loads settled" ); } finally { Date.now = originalNow; } });