| |
| 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; |
| |
| delete global._dbAdapter; |
| vi.resetModules(); |
| }); |
|
|
| afterEach(() => { |
| |
| 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 () => { |
| |
| 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?.(); |
|
|
| |
| 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 () => { |
| |
| 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"); |
| }); |
| }); |
|
|