feat(db): migrate from lowdb to SQLite with repos pattern

- Add modular DB layer (adapters, migrations, repos, helpers)
- Replace localDb/usageDb/requestDetailsDb monoliths with repos
- Add Tailscale tunnel integration & status check API
- Add /api/cli-tools/all-statuses aggregated endpoint
- Add settingsStore (Zustand) and mitm/dbReader
- Add DB unit tests (benchmark, concurrent, migration, vs-lowdb)
This commit is contained in:
decolua
2026-05-09 17:48:20 +07:00
parent 145f588cc0
commit bee8dad946
63 changed files with 4223 additions and 2330 deletions
+174
View File
@@ -0,0 +1,174 @@
// Benchmark: SQLite vs lowdb on equivalent workloads.
// Run: cd app/tests && npm test -- db-benchmark
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, beforeAll, afterAll, vi } from "vitest";
const N_ITEMS = 500;
const N_QUERIES = 200;
const originalDataDir = process.env.DATA_DIR;
let tempSqlite, tempLowdb;
let sqliteDb, lowDb;
function fmt(ms) { return `${ms.toFixed(2)}ms`; }
async function bench(label, fn) {
// warmup
await fn();
const t0 = performance.now();
await fn();
const dt = performance.now() - t0;
console.log(` ${label.padEnd(40)} ${fmt(dt)}`);
return dt;
}
beforeAll(async () => {
// SQLite setup
tempSqlite = fs.mkdtempSync(path.join(os.tmpdir(), "9router-bench-sqlite-"));
process.env.DATA_DIR = tempSqlite;
vi.resetModules();
sqliteDb = await import("@/lib/db/index.js");
await sqliteDb.initDb();
// Lowdb setup — direct lowdb usage (mimics legacy behavior)
tempLowdb = fs.mkdtempSync(path.join(os.tmpdir(), "9router-bench-lowdb-"));
const { Low } = await import("lowdb");
const { JSONFile } = await import("lowdb/node");
const dbFile = path.join(tempLowdb, "db.json");
fs.writeFileSync(dbFile, JSON.stringify({ providerConnections: [], usageHistory: [] }));
lowDb = new Low(new JSONFile(dbFile), { providerConnections: [], usageHistory: [] });
await lowDb.read();
});
afterAll(() => {
if (tempSqlite) fs.rmSync(tempSqlite, { recursive: true, force: true });
if (tempLowdb) fs.rmSync(tempLowdb, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
describe("DB Benchmark — SQLite vs Lowdb", () => {
it(`INSERT ${N_ITEMS} provider connections`, async () => {
console.log(`\n[INSERT ${N_ITEMS}]`);
const sqliteTime = await bench("SQLite createProviderConnection", async () => {
for (let i = 0; i < N_ITEMS; i++) {
await sqliteDb.createProviderConnection({
provider: `bench-p${i % 5}`, authType: "apikey",
name: `name-${i}`, apiKey: `k-${i}`,
});
}
});
const lowdbTime = await bench("Lowdb push + write", async () => {
for (let i = 0; i < N_ITEMS; i++) {
lowDb.data.providerConnections.push({
id: `id-${i}`, provider: `bench-p${i % 5}`, authType: "apikey",
name: `name-${i}`, apiKey: `k-${i}`, priority: i + 1, isActive: true,
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
});
await lowDb.write();
}
});
const speedup = (lowdbTime / sqliteTime).toFixed(2);
console.log(` → SQLite is ${speedup}x faster`);
}, 60000);
it(`READ ${N_QUERIES} filtered queries`, async () => {
console.log(`\n[READ ${N_QUERIES} filtered queries]`);
const sqliteTime = await bench("SQLite getProviderConnections(filter)", async () => {
for (let i = 0; i < N_QUERIES; i++) {
await sqliteDb.getProviderConnections({ provider: `bench-p${i % 5}` });
}
});
const lowdbTime = await bench("Lowdb read + filter", async () => {
for (let i = 0; i < N_QUERIES; i++) {
await lowDb.read();
lowDb.data.providerConnections.filter((c) => c.provider === `bench-p${i % 5}`);
}
});
const speedup = (lowdbTime / sqliteTime).toFixed(2);
console.log(` → SQLite is ${speedup}x faster`);
}, 60000);
it(`READ ${N_QUERIES} by id (point lookup)`, async () => {
console.log(`\n[READ ${N_QUERIES} by id]`);
const sqliteAll = await sqliteDb.getProviderConnections();
const ids = sqliteAll.slice(0, N_QUERIES).map((c) => c.id);
const sqliteTime = await bench("SQLite getProviderConnectionById", async () => {
for (const id of ids) await sqliteDb.getProviderConnectionById(id);
});
const lowdbIds = lowDb.data.providerConnections.slice(0, N_QUERIES).map((c) => c.id);
const lowdbTime = await bench("Lowdb find by id", async () => {
for (const id of lowdbIds) {
await lowDb.read();
lowDb.data.providerConnections.find((c) => c.id === id);
}
});
const speedup = (lowdbTime / sqliteTime).toFixed(2);
console.log(` → SQLite is ${speedup}x faster`);
}, 60000);
it(`saveRequestUsage ${N_ITEMS} entries`, async () => {
console.log(`\n[saveRequestUsage ${N_ITEMS}]`);
const sqliteTime = await bench("SQLite saveRequestUsage", async () => {
for (let i = 0; i < N_ITEMS; i++) {
await sqliteDb.saveRequestUsage({
provider: "openai", model: `m-${i % 10}`, connectionId: `c-${i % 5}`,
tokens: { prompt_tokens: 100 + i, completion_tokens: 50 + i },
endpoint: "/v1/chat/completions", status: "ok",
});
}
});
const lowdbTime = await bench("Lowdb push history + write", async () => {
lowDb.data.usageHistory = [];
for (let i = 0; i < N_ITEMS; i++) {
lowDb.data.usageHistory.push({
timestamp: new Date().toISOString(), provider: "openai", model: `m-${i % 10}`,
connectionId: `c-${i % 5}`, tokens: { prompt_tokens: 100 + i, completion_tokens: 50 + i },
endpoint: "/v1/chat/completions", status: "ok", cost: 0,
});
await lowDb.write();
}
});
const speedup = (lowdbTime / sqliteTime).toFixed(2);
console.log(` → SQLite is ${speedup}x faster`);
}, 120000);
it(`getUsageStats(24h) repeat 50x`, async () => {
console.log(`\n[getUsageStats(24h) x 50]`);
const sqliteTime = await bench("SQLite getUsageStats(24h)", async () => {
for (let i = 0; i < 50; i++) await sqliteDb.getUsageStats("24h");
});
const lowdbTime = await bench("Lowdb read + aggregate", async () => {
for (let i = 0; i < 50; i++) {
await lowDb.read();
const cutoff = Date.now() - 86400000;
const hist = lowDb.data.usageHistory.filter((h) => new Date(h.timestamp).getTime() >= cutoff);
const stats = { byProvider: {}, byModel: {} };
for (const e of hist) {
if (!stats.byProvider[e.provider]) stats.byProvider[e.provider] = { requests: 0 };
stats.byProvider[e.provider].requests++;
}
}
});
const speedup = (lowdbTime / sqliteTime).toFixed(2);
console.log(` → SQLite is ${speedup}x faster`);
}, 60000);
});
+171
View File
@@ -0,0 +1,171 @@
// Concurrency stress test — simulate many parallel saveRequestUsage / saveRequestDetail
// to verify atomic counter, no data loss, no race conditions.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
const originalDataDir = process.env.DATA_DIR;
let tempDir;
let db;
beforeAll(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-concurrent-"));
process.env.DATA_DIR = tempDir;
vi.resetModules();
db = await import("@/lib/db/index.js");
await db.initDb();
});
afterAll(() => {
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
describe("DB Concurrency — atomic safety", () => {
it("100 parallel saveRequestUsage → no count loss", async () => {
const N = 100;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.saveRequestUsage({
provider: "openai", model: "gpt-4", connectionId: "c1",
tokens: { prompt_tokens: 10, completion_tokens: 5 },
endpoint: "/v1/chat", status: "ok",
}));
}
await Promise.all(promises);
const stats = await db.getUsageStats("24h");
expect(stats.totalRequests).toBe(N);
expect(stats.byProvider.openai.requests).toBe(N);
expect(stats.byProvider.openai.promptTokens).toBe(N * 10);
const hist = await db.getUsageHistory({ provider: "openai" });
expect(hist.length).toBe(N);
});
it("200 parallel saveRequestDetail → all flushed", async () => {
await db.updateSettings({ enableObservability: true, observabilityBatchSize: 10 });
const N = 200;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.saveRequestDetail({
id: `det-${i}`, provider: "openai", model: "gpt-4",
connectionId: "c1", status: "ok",
tokens: { prompt_tokens: 1 }, request: { i }, response: { ok: true },
}));
}
await Promise.all(promises);
// Wait for any timer-based flush
await new Promise((r) => setTimeout(r, 6000));
const list = await db.getRequestDetails({ provider: "openai", pageSize: 500 });
expect(list.pagination.totalItems).toBeGreaterThanOrEqual(N);
}, 15000);
it("mixed concurrent: usage + details + connections + aliases", async () => {
const ops = [];
for (let i = 0; i < 50; i++) {
ops.push(db.saveRequestUsage({
provider: "anthropic", model: `m-${i % 3}`, connectionId: "c2",
tokens: { prompt_tokens: 20 }, status: "ok",
}));
ops.push(db.setModelAlias(`a-${i}`, `target-${i}`));
ops.push(db.disableModels("openai", [`d-${i}`]));
}
await Promise.all(ops);
const aliases = await db.getModelAliases();
expect(Object.keys(aliases).filter((k) => k.startsWith("a-")).length).toBe(50);
const disabled = await db.getDisabledByProvider("openai");
expect(disabled.length).toBeGreaterThanOrEqual(50);
const stats = await db.getUsageStats("24h");
expect(stats.byProvider.anthropic.requests).toBe(50);
}, 30000);
it("updateSettings parallel → no merge loss", async () => {
const N = 50;
await db.updateSettings({ counter: 0 });
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.updateSettings({ [`field${i}`]: `v${i}` }));
}
await Promise.all(promises);
const s = await db.getSettings();
for (let i = 0; i < N; i++) {
expect(s[`field${i}`]).toBe(`v${i}`); // all updates preserved
}
});
it("OAuth refresh race: parallel updateProviderConnection on same id", async () => {
const conn = await db.createProviderConnection({
provider: "oauth-test", authType: "oauth", email: "x@y.com",
accessToken: "initial", refreshToken: "rt-initial",
});
// 20 parallel updates each with a unique field
const N = 20;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.updateProviderConnection(conn.id, { [`marker${i}`]: i }));
}
await Promise.all(promises);
const after = await db.getProviderConnectionById(conn.id);
for (let i = 0; i < N; i++) {
expect(after[`marker${i}`]).toBe(i); // no field lost
}
expect(after.refreshToken).toBe("rt-initial"); // base preserved
});
it("addCustomModel race: parallel duplicate adds → only 1 inserted", async () => {
const N = 30;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.addCustomModel({ providerAlias: "racep", id: "racemodel", type: "llm", name: "r" }));
}
const results = await Promise.all(promises);
const trueCount = results.filter((r) => r === true).length;
expect(trueCount).toBe(1); // exactly one wins
const all = await db.getCustomModels();
expect(all.filter((m) => m.providerAlias === "racep" && m.id === "racemodel").length).toBe(1);
});
it("updatePricing race: parallel adds different models → all merged", async () => {
const N = 30;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.updatePricing({ "race-prov": { [`m${i}`]: { input: i, output: i * 2 } } }));
}
await Promise.all(promises);
const p = await db.getPricing();
for (let i = 0; i < N; i++) {
expect(p["race-prov"][`m${i}`]).toEqual({ input: i, output: i * 2 });
}
});
it("daily summary aggregates correctly under parallel writes", async () => {
const N = 50;
const promises = [];
for (let i = 0; i < N; i++) {
promises.push(db.saveRequestUsage({
provider: "google", model: "gemini-pro", connectionId: "cG",
tokens: { prompt_tokens: 100, completion_tokens: 50 },
status: "ok",
}));
}
await Promise.all(promises);
const stats = await db.getUsageStats("7d");
const g = stats.byProvider.google;
expect(g).toBeDefined();
expect(g.requests).toBe(N);
expect(g.promptTokens).toBe(N * 100);
expect(g.completionTokens).toBe(N * 50);
});
});
+100
View File
@@ -0,0 +1,100 @@
// Verify schema migration chain runs correctly across versions.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
let tempDir;
const originalDataDir = process.env.DATA_DIR;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-mig-"));
process.env.DATA_DIR = tempDir;
// Reset global singleton so each test gets fresh adapter pointed at tempDir
delete global._dbAdapter;
vi.resetModules();
});
afterEach(() => {
// Close adapter to release file handles before rm
try { global._dbAdapter?.instance?.close?.(); } catch {}
delete global._dbAdapter;
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
describe("Schema migrations", () => {
it("fresh DB → applies migrations & stamps schemaVersion", async () => {
const { getAdapter } = await import("@/lib/db/driver.js");
const { latestVersion } = await import("@/lib/db/migrations/index.js");
const db = await getAdapter();
const row = db.get(`SELECT value FROM _meta WHERE key='schemaVersion'`);
expect(parseInt(row.value, 10)).toBe(latestVersion());
const tables = db.all(`SELECT name FROM sqlite_master WHERE type='table'`).map(t => t.name);
expect(tables).toEqual(expect.arrayContaining([
"_meta", "settings", "providerConnections", "providerNodes",
"proxyPools", "apiKeys", "combos", "kv", "usageHistory", "usageDaily", "requestDetails",
]));
});
it("existing DB at older schemaVersion → re-applies pending migrations on restart", async () => {
// 1st boot
const { getAdapter } = await import("@/lib/db/driver.js");
const db = await getAdapter();
db.run(`INSERT INTO settings(id, data) VALUES(1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, ['{"foo":"bar"}']);
db.run(`UPDATE _meta SET value = '0' WHERE key = 'schemaVersion'`);
db.close?.();
// 2nd boot: full reset to simulate process restart
delete global._dbAdapter;
vi.resetModules();
const { getAdapter: getAdapter2 } = await import("@/lib/db/driver.js");
const { latestVersion } = await import("@/lib/db/migrations/index.js");
const db2 = await getAdapter2();
const row = db2.get(`SELECT value FROM _meta WHERE key='schemaVersion'`);
expect(parseInt(row.value, 10)).toBe(latestVersion());
const settings = db2.get(`SELECT data FROM settings WHERE id=1`);
expect(JSON.parse(settings.data)).toEqual({ foo: "bar" });
});
it("fresh DB + legacy db.json → imports data automatically", async () => {
// Simulate user upgrading: place legacy JSON in DATA_DIR before first boot
const legacy = {
settings: { foo: "legacy-value" },
apiKeys: [{ id: "k1", key: "abc", name: "test", createdAt: new Date().toISOString() }],
modelAliases: { "gpt-4": "gpt-4-turbo" },
};
fs.writeFileSync(path.join(tempDir, "db.json"), JSON.stringify(legacy));
const { getAdapter } = await import("@/lib/db/driver.js");
const db = await getAdapter();
const settings = db.get(`SELECT data FROM settings WHERE id=1`);
expect(JSON.parse(settings.data)).toEqual({ foo: "legacy-value" });
const keys = db.all(`SELECT * FROM apiKeys`);
expect(keys).toHaveLength(1);
expect(keys[0].key).toBe("abc");
const aliases = db.all(`SELECT * FROM kv WHERE scope='modelAliases'`);
expect(aliases).toHaveLength(1);
});
it("auto-sync re-creates missing index when DB lacks it", async () => {
const { getAdapter } = await import("@/lib/db/driver.js");
const db = await getAdapter();
db.exec(`DROP INDEX IF EXISTS idx_pn_type`);
expect(db.all(`PRAGMA index_list(providerNodes)`).map(i => i.name)).not.toContain("idx_pn_type");
db.close?.();
delete global._dbAdapter;
vi.resetModules();
const { getAdapter: getAdapter2 } = await import("@/lib/db/driver.js");
const db2 = await getAdapter2();
const idx = db2.all(`PRAGMA index_list(providerNodes)`).map(i => i.name);
expect(idx).toContain("idx_pn_type");
});
});
+274
View File
@@ -0,0 +1,274 @@
// Compare new SQLite-backed DB layer vs legacy lowdb behavior.
// Verifies: same public API signatures + equivalent results for core operations.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
const originalDataDir = process.env.DATA_DIR;
let tempDir;
let sqliteDb;
beforeAll(async () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-db-compare-"));
process.env.DATA_DIR = tempDir;
vi.resetModules();
sqliteDb = await import("@/lib/db/index.js");
await sqliteDb.initDb();
});
afterAll(() => {
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
describe("DB SQLite layer — public API parity", () => {
it("settings: get → defaults; update → merge", async () => {
const s = await sqliteDb.getSettings();
expect(s).toBeDefined();
expect(s.cloudEnabled).toBe(false);
expect(s.requireLogin).toBe(true);
const updated = await sqliteDb.updateSettings({ cloudEnabled: true, customField: "x" });
expect(updated.cloudEnabled).toBe(true);
expect(updated.customField).toBe("x");
expect(updated.requireLogin).toBe(true); // default preserved
const re = await sqliteDb.getSettings();
expect(re.cloudEnabled).toBe(true);
expect(re.customField).toBe("x");
});
it("isCloudEnabled reflects settings", async () => {
await sqliteDb.updateSettings({ cloudEnabled: true });
expect(await sqliteDb.isCloudEnabled()).toBe(true);
await sqliteDb.updateSettings({ cloudEnabled: false });
expect(await sqliteDb.isCloudEnabled()).toBe(false);
});
it("apiKeys: create/get/validate/delete", async () => {
const k = await sqliteDb.createApiKey("test-key", "machine-abc");
expect(k.id).toBeDefined();
expect(k.key).toMatch(/^sk-/);
expect(k.machineId).toBe("machine-abc");
expect(k.isActive).toBe(true);
const all = await sqliteDb.getApiKeys();
expect(all.find((x) => x.id === k.id)).toBeDefined();
expect(await sqliteDb.validateApiKey(k.key)).toBeTruthy();
expect(await sqliteDb.validateApiKey("invalid")).toBeFalsy();
const deleted = await sqliteDb.deleteApiKey(k.id);
expect(deleted).toBe(true);
expect(await sqliteDb.getApiKeyById(k.id)).toBeNull();
});
it("providerConnections: CRUD + reorder by priority", async () => {
const c1 = await sqliteDb.createProviderConnection({ provider: "test", authType: "apikey", name: "a", apiKey: "k1" });
const c2 = await sqliteDb.createProviderConnection({ provider: "test", authType: "apikey", name: "b", apiKey: "k2" });
const c3 = await sqliteDb.createProviderConnection({ provider: "test", authType: "apikey", name: "c", apiKey: "k3" });
const list = await sqliteDb.getProviderConnections({ provider: "test" });
expect(list).toHaveLength(3);
expect(list[0].priority).toBe(1);
expect(list[1].priority).toBe(2);
expect(list[2].priority).toBe(3);
// Update priority and reorder
await sqliteDb.updateProviderConnection(c3.id, { priority: 1 });
const reordered = await sqliteDb.getProviderConnections({ provider: "test" });
expect(reordered[0].name).toBe("c");
// Delete reorders remaining
await sqliteDb.deleteProviderConnection(c1.id);
const after = await sqliteDb.getProviderConnections({ provider: "test" });
expect(after).toHaveLength(2);
expect(after.every((c) => [1, 2].includes(c.priority))).toBe(true);
});
it("providerConnections: optional fields persisted via JSON column", async () => {
const c = await sqliteDb.createProviderConnection({
provider: "p2", authType: "oauth", email: "x@y.com",
accessToken: "tok", refreshToken: "rtok", expiresAt: 12345,
providerSpecificData: { foo: "bar" },
});
const back = await sqliteDb.getProviderConnectionById(c.id);
expect(back.accessToken).toBe("tok");
expect(back.refreshToken).toBe("rtok");
expect(back.expiresAt).toBe(12345);
expect(back.providerSpecificData).toEqual({ foo: "bar" });
});
it("providerNodes: CRUD", async () => {
const n = await sqliteDb.createProviderNode({ type: "openai", name: "Test", baseUrl: "https://api.test", apiType: "openai" });
expect(n.id).toBeDefined();
expect(n.baseUrl).toBe("https://api.test");
const all = await sqliteDb.getProviderNodes({ type: "openai" });
expect(all.find((x) => x.id === n.id)).toBeDefined();
await sqliteDb.updateProviderNode(n.id, { name: "Test2" });
const updated = await sqliteDb.getProviderNodeById(n.id);
expect(updated.name).toBe("Test2");
await sqliteDb.deleteProviderNode(n.id);
expect(await sqliteDb.getProviderNodeById(n.id)).toBeNull();
});
it("proxyPools: CRUD with sort by updatedAt desc", async () => {
const p1 = await sqliteDb.createProxyPool({ name: "p1", proxyUrl: "http://a", type: "http" });
await new Promise((r) => setTimeout(r, 10));
const p2 = await sqliteDb.createProxyPool({ name: "p2", proxyUrl: "http://b", type: "http" });
const list = await sqliteDb.getProxyPools();
expect(list[0].id).toBe(p2.id); // newest first
await sqliteDb.deleteProxyPool(p1.id);
await sqliteDb.deleteProxyPool(p2.id);
});
it("combos: CRUD", async () => {
const c = await sqliteDb.createCombo({ name: "combo1", models: ["m1", "m2"], kind: "fallback" });
expect(c.id).toBeDefined();
expect(c.models).toEqual(["m1", "m2"]);
const byName = await sqliteDb.getComboByName("combo1");
expect(byName.id).toBe(c.id);
await sqliteDb.updateCombo(c.id, { models: ["m3"] });
const updated = await sqliteDb.getComboById(c.id);
expect(updated.models).toEqual(["m3"]);
expect(await sqliteDb.deleteCombo(c.id)).toBe(true);
});
it("modelAliases: KV ops", async () => {
await sqliteDb.setModelAlias("alias1", "real-model-1");
await sqliteDb.setModelAlias("alias2", "real-model-2");
const all = await sqliteDb.getModelAliases();
expect(all.alias1).toBe("real-model-1");
expect(all.alias2).toBe("real-model-2");
await sqliteDb.deleteModelAlias("alias1");
expect((await sqliteDb.getModelAliases()).alias1).toBeUndefined();
});
it("customModels: add/list/delete with dedupe", async () => {
const ok1 = await sqliteDb.addCustomModel({ providerAlias: "p1", id: "m1", type: "llm", name: "Model 1" });
const dup = await sqliteDb.addCustomModel({ providerAlias: "p1", id: "m1", type: "llm" });
expect(ok1).toBe(true);
expect(dup).toBe(false);
const list = await sqliteDb.getCustomModels();
expect(list.find((m) => m.id === "m1")).toBeDefined();
await sqliteDb.deleteCustomModel({ providerAlias: "p1", id: "m1" });
const after = await sqliteDb.getCustomModels();
expect(after.find((m) => m.id === "m1")).toBeUndefined();
});
it("mitmAlias: get/set per tool", async () => {
await sqliteDb.setMitmAliasAll("cursor", { "gpt-5": "claude-3" });
const a = await sqliteDb.getMitmAlias("cursor");
expect(a["gpt-5"]).toBe("claude-3");
const all = await sqliteDb.getMitmAlias();
expect(all.cursor).toEqual({ "gpt-5": "claude-3" });
});
it("disabledModels: add/remove per provider", async () => {
await sqliteDb.disableModels("openai", ["gpt-3", "gpt-4"]);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(expect.arrayContaining(["gpt-3", "gpt-4"]));
await sqliteDb.enableModels("openai", ["gpt-3"]);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(["gpt-4"]);
await sqliteDb.enableModels("openai", []);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual([]);
});
it("usage: saveRequestUsage + getUsageHistory + getUsageStats", async () => {
await sqliteDb.saveRequestUsage({
provider: "openai", model: "gpt-4", connectionId: "c1",
tokens: { prompt_tokens: 100, completion_tokens: 50 },
endpoint: "/v1/chat/completions", status: "ok",
});
await sqliteDb.saveRequestUsage({
provider: "openai", model: "gpt-4", connectionId: "c1",
tokens: { prompt_tokens: 200, completion_tokens: 100 },
endpoint: "/v1/chat/completions", status: "ok",
});
const hist = await sqliteDb.getUsageHistory({ provider: "openai" });
expect(hist.length).toBeGreaterThanOrEqual(2);
expect(hist[0].tokens.prompt_tokens).toBeDefined();
const stats = await sqliteDb.getUsageStats("24h");
expect(stats.totalRequests).toBeGreaterThanOrEqual(2);
expect(stats.byProvider.openai).toBeDefined();
expect(stats.byProvider.openai.requests).toBeGreaterThanOrEqual(2);
expect(stats.byProvider.openai.promptTokens).toBeGreaterThanOrEqual(300);
});
it("usage: pending tracking in-memory", () => {
sqliteDb.trackPendingRequest("gpt-4", "openai", "c1", true);
expect(global._pendingRequests.byModel["gpt-4 (openai)"]).toBe(1);
sqliteDb.trackPendingRequest("gpt-4", "openai", "c1", false);
expect(global._pendingRequests.byModel["gpt-4 (openai)"]).toBeUndefined();
});
it("requestDetails: save → query with paging", async () => {
// Enable observability first
await sqliteDb.updateSettings({ enableObservability: true, observabilityBatchSize: 1 });
await sqliteDb.saveRequestDetail({
id: "d1", provider: "openai", model: "gpt-4", connectionId: "c1",
status: "ok", tokens: { prompt_tokens: 10 },
request: { method: "POST" }, response: { status: 200 },
});
// Wait for buffer flush
await new Promise((r) => setTimeout(r, 200));
const got = await sqliteDb.getRequestDetailById("d1");
expect(got).toBeDefined();
expect(got.id).toBe("d1");
const list = await sqliteDb.getRequestDetails({ provider: "openai" });
expect(list.details.length).toBeGreaterThanOrEqual(1);
expect(list.pagination.totalItems).toBeGreaterThanOrEqual(1);
});
it("exportDb / importDb roundtrip", async () => {
const exported = await sqliteDb.exportDb();
expect(exported.settings).toBeDefined();
expect(Array.isArray(exported.providerConnections)).toBe(true);
expect(typeof exported.modelAliases).toBe("object");
// Add marker, export, import a different payload, verify reset
await sqliteDb.setModelAlias("marker", "before");
const snap = await sqliteDb.exportDb();
await sqliteDb.setModelAlias("marker", "after");
expect((await sqliteDb.getModelAliases()).marker).toBe("after");
await sqliteDb.importDb(snap);
expect((await sqliteDb.getModelAliases()).marker).toBe("before");
});
it("pricing: user pricing merged with constants", async () => {
await sqliteDb.updatePricing({ openai: { "gpt-test": { input: 1, output: 2 } } });
const p = await sqliteDb.getPricing();
expect(p.openai["gpt-test"]).toEqual({ input: 1, output: 2 });
const single = await sqliteDb.getPricingForModel("openai", "gpt-test");
expect(single).toEqual({ input: 1, output: 2 });
await sqliteDb.resetPricing("openai", "gpt-test");
expect((await sqliteDb.getPricing()).openai?.["gpt-test"]).toBeUndefined();
});
it("getChartData: 24h buckets", async () => {
const data = await sqliteDb.getChartData("24h");
expect(data).toHaveLength(24);
expect(data[0]).toHaveProperty("label");
expect(data[0]).toHaveProperty("tokens");
expect(data[0]).toHaveProperty("cost");
});
it("getChartData: 7d buckets", async () => {
const data = await sqliteDb.getChartData("7d");
expect(data).toHaveLength(7);
});
});