Files
2026-07-13 13:39:12 +08:00

401 lines
13 KiB
TypeScript

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;
}
});