183 lines
6.9 KiB
TypeScript
183 lines
6.9 KiB
TypeScript
/**
|
|
* Tests for src/lib/db/serviceModels.ts
|
|
*
|
|
* Uses an isolated in-memory DB via DATA_DIR override.
|
|
*/
|
|
|
|
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-service-models-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.NODE_ENV = "test";
|
|
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
|
|
|
const core = await import("../../../src/lib/db/core.ts");
|
|
const { getServiceModels, saveServiceModels, markAllUnavailable } =
|
|
await import("../../../src/lib/db/serviceModels.ts");
|
|
|
|
function resetDb() {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
test.beforeEach(() => {
|
|
resetDb();
|
|
});
|
|
|
|
test.after(() => {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
test("getServiceModels — returns [] when no row exists", () => {
|
|
const result = getServiceModels("9router");
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("saveServiceModels + getServiceModels — round-trips a list (with available flag)", () => {
|
|
const models = [
|
|
{ id: "9r/gemma-3n-e4b", name: "Gemma 3n", object: "model", owned_by: "google" },
|
|
{ id: "9r/llama-3.3-70b", name: "Llama 3.3 70B", object: "model", owned_by: "meta" },
|
|
];
|
|
saveServiceModels("9router", models);
|
|
const result = getServiceModels("9router");
|
|
assert.equal(result.length, 2);
|
|
// Models are enriched with available: true on save.
|
|
assert.equal(result[0].id, "9r/gemma-3n-e4b");
|
|
assert.equal(result[0].available, true);
|
|
assert.equal(result[1].id, "9r/llama-3.3-70b");
|
|
assert.equal(result[1].available, true);
|
|
});
|
|
|
|
test("saveServiceModels — incoming model replaces old; old model pruned to available=false", () => {
|
|
saveServiceModels("9router", [{ id: "old-model" }]);
|
|
saveServiceModels("9router", [{ id: "new-model" }]);
|
|
const result = getServiceModels("9router");
|
|
// Both old and new are present — old is pruned (available=false), new is active.
|
|
const byId = Object.fromEntries(result.map((m) => [m.id, m]));
|
|
assert.ok("old-model" in byId, "old-model should persist (soft delete)");
|
|
assert.equal(byId["old-model"].available, false);
|
|
assert.ok("new-model" in byId, "new-model should be present");
|
|
assert.equal(byId["new-model"].available, true);
|
|
});
|
|
|
|
test("saveServiceModels — saving empty list marks previous models as unavailable (no hard delete)", () => {
|
|
saveServiceModels("9router", [{ id: "some-model" }]);
|
|
saveServiceModels("9router", []);
|
|
const result = getServiceModels("9router");
|
|
// Pruning keeps the model around as unavailable rather than deleting.
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].id, "some-model");
|
|
assert.equal(result[0].available, false);
|
|
});
|
|
|
|
test("models are scoped by tool — different tools don't interfere", () => {
|
|
saveServiceModels("9router", [{ id: "nr-model" }]);
|
|
saveServiceModels("cliproxyapi", [{ id: "cli-model" }]);
|
|
|
|
assert.equal(getServiceModels("9router")[0].id, "nr-model");
|
|
assert.equal(getServiceModels("cliproxyapi")[0].id, "cli-model");
|
|
});
|
|
|
|
test("getServiceModels — tolerates corrupt JSON by returning []", () => {
|
|
const db = core.getDbInstance();
|
|
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"serviceModels",
|
|
"9router",
|
|
"not-valid-json{"
|
|
);
|
|
|
|
const result = getServiceModels("9router");
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("getServiceModels — returns [] when stored value is not an array", () => {
|
|
const db = core.getDbInstance();
|
|
db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run(
|
|
"serviceModels",
|
|
"9router",
|
|
JSON.stringify({ id: "not-an-array" })
|
|
);
|
|
|
|
const result = getServiceModels("9router");
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("saveServiceModels — pruning: models missing from new payload have available=false", () => {
|
|
// Initial save with 3 models.
|
|
saveServiceModels("9router", [
|
|
{ id: "9router/model-a" },
|
|
{ id: "9router/model-b" },
|
|
{ id: "9router/model-c" },
|
|
]);
|
|
|
|
// Second save — model-b is missing (pruned), model-c stays, model-d is new.
|
|
saveServiceModels("9router", [{ id: "9router/model-c" }, { id: "9router/model-d" }]);
|
|
|
|
const result = getServiceModels("9router");
|
|
|
|
const byId = Object.fromEntries(result.map((m) => [m.id, m]));
|
|
|
|
// model-a was in first payload but not second, should be pruned
|
|
assert.ok("9router/model-a" in byId, "pruned model-a should still exist (soft delete)");
|
|
assert.equal(byId["9router/model-a"].available, false, "pruned model-a should be unavailable");
|
|
|
|
// model-b was in first payload but not second, should be pruned
|
|
assert.ok("9router/model-b" in byId, "pruned model-b should still exist (soft delete)");
|
|
assert.equal(byId["9router/model-b"].available, false, "pruned model-b should be unavailable");
|
|
|
|
// model-c was in both payloads — should be available
|
|
assert.equal(byId["9router/model-c"].available, true, "model-c should be available");
|
|
|
|
// model-d is new — should be available
|
|
assert.equal(byId["9router/model-d"].available, true, "model-d should be available");
|
|
});
|
|
|
|
test("saveServiceModels — incoming models are marked available=true", () => {
|
|
saveServiceModels("9router", [{ id: "9router/cx/gpt-5-mini", name: "GPT-5 mini" }]);
|
|
const result = getServiceModels("9router");
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].available, true);
|
|
});
|
|
|
|
test("markAllUnavailable — flips all rows for the given tool to available=false", () => {
|
|
saveServiceModels("9router", [{ id: "9router/model-x" }, { id: "9router/model-y" }]);
|
|
|
|
markAllUnavailable("9router");
|
|
|
|
const result = getServiceModels("9router");
|
|
assert.equal(result.length, 2);
|
|
for (const m of result) {
|
|
assert.equal(m.available, false, `${m.id} should be unavailable after markAllUnavailable`);
|
|
}
|
|
});
|
|
|
|
test("markAllUnavailable — does not affect other tools", () => {
|
|
saveServiceModels("9router", [{ id: "9router/model-x" }]);
|
|
saveServiceModels("cliproxy", [{ id: "cliproxy/model-z" }]);
|
|
|
|
markAllUnavailable("9router");
|
|
|
|
const cliproxyModels = getServiceModels("cliproxy");
|
|
assert.equal(cliproxyModels.length, 1);
|
|
// cliproxy models should not have been touched
|
|
assert.notEqual(cliproxyModels[0].available, false, "cliproxy model should not be affected");
|
|
});
|
|
|
|
test("markAllUnavailable — is a no-op when no models stored", () => {
|
|
assert.doesNotThrow(() => markAllUnavailable("9router"));
|
|
const result = getServiceModels("9router");
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
test("available field exists on ServiceModel interface (structural check)", () => {
|
|
saveServiceModels("9router", [{ id: "9router/test-model", available: true }]);
|
|
const result = getServiceModels("9router");
|
|
assert.ok("available" in result[0], "available field should be present in stored model");
|
|
});
|