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 ORIGINAL_HOME = process.env.HOME; const ORIGINAL_USERPROFILE = process.env.USERPROFILE; const ORIGINAL_DATA_DIR = process.env.DATA_DIR; const ORIGINAL_NEXT_PHASE = process.env.NEXT_PHASE; const TEST_HOME_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-usage-migrations-home-")); const TEST_DATA_DIR = path.join(TEST_HOME_DIR, "data"); process.env.HOME = TEST_HOME_DIR; process.env.USERPROFILE = TEST_HOME_DIR; process.env.DATA_DIR = TEST_DATA_DIR; delete process.env.NEXT_PHASE; const migrations = await import("../../src/lib/usage/migrations.ts"); const { getDbInstance } = await import("../../src/lib/db/core.ts"); const LEGACY_DATA_DIR = path.join(TEST_HOME_DIR, ".omniroute"); const LEGACY_USAGE_JSON_FILE = path.join(LEGACY_DATA_DIR, "usage.json"); const LEGACY_CALL_LOGS_JSON_FILE = path.join(LEGACY_DATA_DIR, "call_logs.json"); const USAGE_JSON_FILE = path.join(TEST_DATA_DIR, "usage.json"); const CALL_LOGS_JSON_FILE = path.join(TEST_DATA_DIR, "call_logs.json"); const CURRENT_REQUEST_LOGS_DIR = path.join(TEST_DATA_DIR, "logs"); const CURRENT_REQUEST_SUMMARY_FILE = path.join(TEST_DATA_DIR, "log.txt"); const MARKER_PATH = migrations.LOG_ARCHIVES_DIR ? path.join(migrations.LOG_ARCHIVES_DIR, "legacy-request-logs.json") : null; function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(value, null, 2)); } function removePath(targetPath) { if (!targetPath) return; fs.rmSync(targetPath, { recursive: true, force: true }); } function resetDbTables() { const db = getDbInstance(); db.prepare("DELETE FROM usage_history").run(); db.prepare("DELETE FROM call_logs").run(); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf8")); } function seedLegacyRequestTargets() { fs.mkdirSync(CURRENT_REQUEST_LOGS_DIR, { recursive: true }); fs.writeFileSync(path.join(CURRENT_REQUEST_LOGS_DIR, "placeholder.txt"), "legacy"); fs.writeFileSync(CURRENT_REQUEST_SUMMARY_FILE, "legacy summary\n"); } test.beforeEach(() => { fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); removePath(LEGACY_DATA_DIR); removePath(USAGE_JSON_FILE); removePath(`${USAGE_JSON_FILE}.migrated`); removePath(CALL_LOGS_JSON_FILE); removePath(`${CALL_LOGS_JSON_FILE}.migrated`); removePath(CURRENT_REQUEST_LOGS_DIR); removePath(CURRENT_REQUEST_SUMMARY_FILE); removePath(migrations.CALL_LOGS_DIR); removePath(migrations.LOG_ARCHIVES_DIR); resetDbTables(); }); test.after(() => { try { const db = getDbInstance(); if (db?.open) db.close(); } catch { // Database may already be closed. } if (ORIGINAL_HOME === undefined) { delete process.env.HOME; } else { process.env.HOME = ORIGINAL_HOME; } if (ORIGINAL_USERPROFILE === undefined) { delete process.env.USERPROFILE; } else { process.env.USERPROFILE = ORIGINAL_USERPROFILE; } if (ORIGINAL_DATA_DIR === undefined) { delete process.env.DATA_DIR; } else { process.env.DATA_DIR = ORIGINAL_DATA_DIR; } if (ORIGINAL_NEXT_PHASE === undefined) { delete process.env.NEXT_PHASE; } else { process.env.NEXT_PHASE = ORIGINAL_NEXT_PHASE; } removePath(TEST_HOME_DIR); }); test("archiveLegacyRequestLogs returns null when there are no targets or the marker already exists", async () => { assert.equal(await migrations.archiveLegacyRequestLogs(), null); seedLegacyRequestTargets(); assert.ok(MARKER_PATH, "marker path should exist in local test mode"); fs.mkdirSync(path.dirname(MARKER_PATH), { recursive: true }); fs.writeFileSync(MARKER_PATH, JSON.stringify({ archiveFilename: "done.zip" }, null, 2)); assert.equal(await migrations.archiveLegacyRequestLogs(), null); assert.equal(fs.existsSync(CURRENT_REQUEST_LOGS_DIR), true); assert.equal(fs.existsSync(CURRENT_REQUEST_SUMMARY_FILE), true); }); test("migrateLegacyUsageFiles copies legacy JSON files once and does not overwrite existing targets", () => { writeJson(LEGACY_USAGE_JSON_FILE, { history: [{ provider: "legacy-openai" }] }); writeJson(LEGACY_CALL_LOGS_JSON_FILE, { logs: [{ id: "legacy-call" }] }); migrations.migrateLegacyUsageFiles(); assert.deepEqual(readJson(USAGE_JSON_FILE), { history: [{ provider: "legacy-openai" }] }); assert.deepEqual(readJson(CALL_LOGS_JSON_FILE), { logs: [{ id: "legacy-call" }] }); writeJson(USAGE_JSON_FILE, { history: [{ provider: "current-openai" }] }); writeJson(CALL_LOGS_JSON_FILE, { logs: [{ id: "current-call" }] }); writeJson(LEGACY_USAGE_JSON_FILE, { history: [{ provider: "legacy-should-not-win" }] }); writeJson(LEGACY_CALL_LOGS_JSON_FILE, { logs: [{ id: "legacy-should-not-win" }] }); migrations.migrateLegacyUsageFiles(); assert.deepEqual(readJson(USAGE_JSON_FILE), { history: [{ provider: "current-openai" }] }); assert.deepEqual(readJson(CALL_LOGS_JSON_FILE), { logs: [{ id: "current-call" }] }); }); test("migrateUsageJsonToSqlite migrates usage history aliases and TTFT fallbacks", () => { writeJson(USAGE_JSON_FILE, { history: [ { provider: "openai", model: "gpt-4o-mini", connectionId: "conn-openai", apiKeyId: "key-1", apiKeyName: "Primary Key", tokens: { prompt_tokens: 11, completion_tokens: 7, cached_tokens: 2, cache_creation_input_tokens: 3, reasoning_tokens: 5, }, status: "error", success: false, latencyMs: "17", errorCode: "timeout", timestamp: "2026-01-01T00:00:00.000Z", }, { provider: "gemini", model: "gemini-2.5-flash", tokens: { input: 9, output: 4, cacheRead: 1, cacheCreation: 2, reasoning: 3, }, latencyMs: "99", timeToFirstTokenMs: "13", timestamp: "2026-01-02T00:00:00.000Z", }, ], }); migrations.migrateUsageJsonToSqlite(); assert.equal(fs.existsSync(`${USAGE_JSON_FILE}.migrated`), true); const db = getDbInstance(); const rows = db .prepare( ` SELECT provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, tokens_cache_read, tokens_cache_creation, tokens_reasoning, status, success, latency_ms, ttft_ms, error_code FROM usage_history ORDER BY timestamp ASC ` ) .all() .map((row) => ({ ...row })); assert.deepEqual(rows, [ { provider: "openai", model: "gpt-4o-mini", connection_id: "conn-openai", api_key_id: "key-1", api_key_name: "Primary Key", tokens_input: 11, tokens_output: 7, tokens_cache_read: 2, tokens_cache_creation: 3, tokens_reasoning: 5, status: "error", success: 0, latency_ms: 17, ttft_ms: 17, error_code: "timeout", }, { provider: "gemini", model: "gemini-2.5-flash", connection_id: null, api_key_id: null, api_key_name: null, tokens_input: 9, tokens_output: 4, tokens_cache_read: 1, tokens_cache_creation: 2, tokens_reasoning: 3, status: null, success: 1, latency_ms: 99, ttft_ms: 13, error_code: null, }, ]); }); test("migrateUsageJsonToSqlite migrates call logs to summary rows and ignores duplicate ids", () => { writeJson(CALL_LOGS_JSON_FILE, { logs: [ { id: "call-1", timestamp: "2026-02-01T00:00:00.000Z", method: "GET", path: "/v1/chat/completions", status: 201, model: "gpt-4o-mini", provider: "openai", account: "acct-a", connectionId: "conn-a", duration: 31, tokens: { in: 12, out: 8 }, sourceFormat: "openai", targetFormat: "openai", apiKeyId: "key-a", apiKeyName: "Key A", comboName: "combo-a", requestBody: { messages: [{ role: "user", content: "hi" }] }, responseBody: { id: "resp-1" }, error: "bad upstream", }, { id: "call-1", timestamp: "2026-02-01T01:00:00.000Z", method: "PATCH", path: "/should-be-ignored", }, { timestamp: "2026-02-02T00:00:00.000Z", requestBody: { foo: "bar" }, }, ], }); migrations.migrateUsageJsonToSqlite(); assert.equal(fs.existsSync(`${CALL_LOGS_JSON_FILE}.migrated`), true); const db = getDbInstance(); const rows = db .prepare( ` SELECT id, method, path, status, provider, account, connection_id, detail_state, artifact_relpath, has_request_body, has_response_body, error_summary FROM call_logs ORDER BY timestamp ASC ` ) .all(); assert.equal(rows.length, 2); assert.deepEqual( { ...rows[0] }, { id: "call-1", method: "GET", path: "/v1/chat/completions", status: 201, provider: "openai", account: "acct-a", connection_id: "conn-a", detail_state: "ready", artifact_relpath: (rows[0] as any).artifact_relpath, has_request_body: 1, has_response_body: 1, error_summary: "bad upstream", } ); assert.equal(typeof rows[0].artifact_relpath, "string"); assert.equal((rows[1] as any).id.length > 0, true); assert.equal((rows[1] as any).method, "POST"); assert.equal((rows[1] as any).path, null); assert.equal((rows[1] as any).status, 0); assert.equal((rows[1] as any).provider, null); assert.equal((rows[1] as any).account, null); assert.equal((rows[1] as any).connection_id, null); assert.equal((rows[1] as any).detail_state, "ready"); assert.equal((rows[1] as any).has_request_body, 1); assert.equal((rows[1] as any).has_response_body, 0); assert.equal((rows[1] as any).error_summary, null); const firstArtifact = JSON.parse( fs.readFileSync(path.join(TEST_DATA_DIR, "call_logs", rows[0].artifact_relpath), "utf8") ); assert.deepEqual(firstArtifact.requestBody, { messages: [{ role: "user", content: "hi" }] }); assert.deepEqual(firstArtifact.responseBody, { id: "resp-1" }); const secondArtifact = JSON.parse( fs.readFileSync( path.join(TEST_DATA_DIR, "call_logs", (rows as any)[1].artifact_relpath), "utf8" ) ); assert.deepEqual(secondArtifact.requestBody, { foo: "bar" }); assert.equal(secondArtifact.responseBody, null); }); test("migrateUsageJsonToSqlite renames empty JSON payloads without inserting rows", () => { writeJson(USAGE_JSON_FILE, { history: [] }); writeJson(CALL_LOGS_JSON_FILE, { logs: [] }); migrations.migrateUsageJsonToSqlite(); assert.equal(fs.existsSync(`${USAGE_JSON_FILE}.migrated`), true); assert.equal(fs.existsSync(`${CALL_LOGS_JSON_FILE}.migrated`), true); const db = getDbInstance(); assert.equal((db.prepare("SELECT COUNT(*) AS count FROM usage_history").get() as any).count, 0); assert.equal((db.prepare("SELECT COUNT(*) AS count FROM call_logs").get() as any).count, 0); }); test("migrateUsageJsonToSqlite leaves malformed JSON files in place and reports both failures", () => { fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); fs.writeFileSync(USAGE_JSON_FILE, "{bad json"); fs.writeFileSync(CALL_LOGS_JSON_FILE, "{bad json"); const errors = []; const originalConsoleError = console.error; console.error = (...args) => { errors.push(args.map(String).join(" ")); }; try { migrations.migrateUsageJsonToSqlite(); } finally { console.error = originalConsoleError; } assert.equal(fs.existsSync(USAGE_JSON_FILE), true); assert.equal(fs.existsSync(`${USAGE_JSON_FILE}.migrated`), false); assert.equal(fs.existsSync(CALL_LOGS_JSON_FILE), true); assert.equal(fs.existsSync(`${CALL_LOGS_JSON_FILE}.migrated`), false); const db = getDbInstance(); assert.equal((db.prepare("SELECT COUNT(*) AS count FROM usage_history").get() as any).count, 0); assert.equal((db.prepare("SELECT COUNT(*) AS count FROM call_logs").get() as any).count, 0); assert.ok(errors.some((entry) => entry.includes("Failed to migrate usage.json"))); assert.ok(errors.some((entry) => entry.includes("Failed to migrate call_logs.json"))); });