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

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