Files
OmniRoute/tests/unit/skills-registry.test.ts
Burak Bayır 16fc433a4f fix(skills): make marketplace installs available to API keys (#10854)
Dashboard-installed SkillsMP and skills.sh skills now store under the canonical global skill scope and merge into every API-key-scoped lookup, so marketplace installs actually reach API keys instead of staying invisible outside the installing session. Tenant-owned skill overrides stay isolated; existing skillsmp/skillssh rows are recognized without a migration, with canonical rows preferred on identity overlap. Closes #9716.

Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 9 files):
- 94/94 skills-*.test.ts tests pass, including the tenant-isolation regression coverage in skills-injection.test.ts (global skills reach a different API key without leaking another tenant's skills).
- check-file-size, check-changelog-integrity: OK.
- typecheck:core: clean.
- check-complexity / check-cognitive-complexity: OK, both under baseline.

Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com>
2026-08-21 00:13:31 -03:00

464 lines
15 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 { GLOBAL_SKILL_OWNER_ID, 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 exposes global skills without crossing API-key scopes", async () => {
const global = await skillRegistry.register({
name: "marketplace-skill",
version: "1.0.0",
description: "global version",
schema: { input: {}, output: {} },
handler: "global-handler",
apiKeyId: GLOBAL_SKILL_OWNER_ID,
});
const globalOnly = await skillRegistry.register({
name: "global-only",
version: "1.0.0",
description: "global skill",
schema: { input: {}, output: {} },
handler: "global-only-handler",
apiKeyId: GLOBAL_SKILL_OWNER_ID,
});
const legacyOnly = await skillRegistry.register({
name: "legacy-marketplace-skill",
version: "1.0.0",
description: "legacy SkillsMP install",
schema: { input: {}, output: {} },
handler: "legacy-handler",
apiKeyId: "skillsmp",
});
await skillRegistry.register({
name: "global-only",
version: "1.0.0",
description: "legacy duplicate",
schema: { input: {}, output: {} },
handler: "legacy-duplicate-handler",
apiKeyId: "skillssh",
});
const keyOverride = await skillRegistry.register({
name: "marketplace-skill",
version: "1.0.0",
description: "key override",
schema: { input: {}, output: {} },
handler: "key-handler",
apiKeyId: "key-a",
});
await skillRegistry.register({
name: "private-skill",
version: "1.0.0",
description: "key b only",
schema: { input: {}, output: {} },
handler: "private-handler",
apiKeyId: "key-b",
});
resetRegistryState();
await skillRegistry.loadFromDatabase("key-a");
assert.deepEqual(
skillRegistry.list("key-a").map((skill) => skill.id),
[keyOverride.id, globalOnly.id, legacyOnly.id]
);
assert.equal(skillRegistry.getSkill("marketplace-skill", "key-a")?.id, keyOverride.id);
assert.equal(skillRegistry.getSkill(global.id, "key-a"), undefined);
assert.equal(skillRegistry.getSkill(globalOnly.id, "key-a")?.id, globalOnly.id);
assert.equal(skillRegistry.getSkill("private-skill", "key-a"), undefined);
});
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;
}
});