mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 18:02:17 +03:00
test(db,api): split test files to satisfy file-size gate for account identity work
db-migration-runner.test.ts grew past the frozen 1499-line cap and the new usage-analytics-route.test.ts exceeded the 800-line cap for new files. Split the newly added cases into sibling files instead of rebaselining. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
214
tests/unit/db-migration-runner-account-identity.test.ts
Normal file
214
tests/unit/db-migration-runner-account-identity.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
async function importFresh(modulePath) {
|
||||
const url = pathToFileURL(path.resolve(modulePath)).href;
|
||||
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
||||
}
|
||||
|
||||
function withMockedMigrationFs(files, fn) {
|
||||
const originalExistsSync = fs.existsSync;
|
||||
const originalReaddirSync = fs.readdirSync;
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
|
||||
const isMigrationDir = (target) =>
|
||||
String(target).replaceAll("\\", "/").endsWith("/src/lib/db/migrations") ||
|
||||
String(target).replaceAll("\\", "/").endsWith("/migrations");
|
||||
|
||||
fs.existsSync = (target) => {
|
||||
if (files === null && isMigrationDir(target)) return false;
|
||||
if (files && isMigrationDir(target)) return true;
|
||||
|
||||
const fileName = path.basename(String(target));
|
||||
if (files && Object.hasOwn(files, fileName)) return true;
|
||||
|
||||
return originalExistsSync(target);
|
||||
};
|
||||
|
||||
fs.readdirSync = ((target: string, options?: any) => {
|
||||
if (files && isMigrationDir(target)) {
|
||||
return Object.keys(files);
|
||||
}
|
||||
|
||||
return originalReaddirSync(target, options);
|
||||
}) as any;
|
||||
|
||||
fs.readFileSync = (target, options) => {
|
||||
const fileName = path.basename(String(target));
|
||||
if (files && Object.hasOwn(files, fileName)) {
|
||||
return files[fileName];
|
||||
}
|
||||
|
||||
return originalReadFileSync(target, options);
|
||||
};
|
||||
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
fs.existsSync = originalExistsSync;
|
||||
fs.readdirSync = originalReaddirSync;
|
||||
fs.readFileSync = originalReadFileSync;
|
||||
}
|
||||
}
|
||||
|
||||
function createDb() {
|
||||
return new Database(":memory:");
|
||||
}
|
||||
|
||||
test("migration 123 backfills account snapshots and creates its index", async () => {
|
||||
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
const db = createDb();
|
||||
const migrationSql = fs.readFileSync(
|
||||
"src/lib/db/migrations/127_usage_history_account_identity.sql",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider TEXT,
|
||||
auth_type TEXT,
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
provider_specific_data TEXT
|
||||
);
|
||||
CREATE TABLE usage_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider TEXT,
|
||||
connection_id TEXT,
|
||||
account_key TEXT,
|
||||
account_label TEXT,
|
||||
account_label_priority INTEGER DEFAULT 0
|
||||
);
|
||||
INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, provider_specific_data)
|
||||
VALUES
|
||||
('empty-provider', '', 'oauth', 'member@example.com', '{"username":"member"}');
|
||||
INSERT INTO usage_history (provider, connection_id)
|
||||
VALUES ('codex', 'orphan-id'), ('', 'empty-provider');
|
||||
`);
|
||||
|
||||
const count = withMockedMigrationFs(
|
||||
{ "127_usage_history_account_identity.sql": migrationSql },
|
||||
() => runner.runMigrations(db)
|
||||
);
|
||||
const rows = db
|
||||
.prepare("SELECT account_key, account_label FROM usage_history ORDER BY id")
|
||||
.all();
|
||||
const index = db
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_uh_account_key'"
|
||||
)
|
||||
.get();
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(rows, [
|
||||
{
|
||||
account_key: '["connection","codex","orphan-id"]',
|
||||
account_label: "orphan-id",
|
||||
},
|
||||
{
|
||||
account_key: '["oauth","unknown","email","member@example.com","username","member"]',
|
||||
account_label: "member@example.com",
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(index, { name: "idx_uh_account_key" });
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("migration 123 preserves exact dedup identity and ignores non-string JSON scalars", async () => {
|
||||
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
const db = createDb();
|
||||
const sql = fs.readFileSync(
|
||||
"src/lib/db/migrations/127_usage_history_account_identity.sql",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider TEXT,
|
||||
auth_type TEXT,
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
provider_specific_data TEXT
|
||||
);
|
||||
CREATE TABLE usage_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider TEXT,
|
||||
connection_id TEXT,
|
||||
account_key TEXT,
|
||||
account_label TEXT,
|
||||
account_label_priority INTEGER DEFAULT 0
|
||||
);
|
||||
INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, provider_specific_data)
|
||||
VALUES
|
||||
('generic', 'openai', 'oauth', ' Member@example.com ', '{"username":" Member "}'),
|
||||
('workspace', 'codex', 'oauth', ' Member@example.com ', '{"workspaceId":" Workspace "}'),
|
||||
('numeric-workspace', 'codex', 'oauth', 'member@example.com', '{"workspaceId":42}'),
|
||||
('object-user', 'codex', 'oauth', 'member@example.com', '{"chatgptUserId":{"id":"user-a"}}'),
|
||||
('array-username', 'openai', 'oauth', 'member@example.com', '{"username":["member"]}'),
|
||||
('malformed-json', 'openai', 'oauth', 'member@example.com', '{bad json');
|
||||
INSERT INTO usage_history (provider, connection_id) VALUES
|
||||
('openai', 'generic'),
|
||||
('codex', 'workspace'),
|
||||
('codex', 'numeric-workspace'),
|
||||
('codex', 'object-user'),
|
||||
('openai', 'array-username'),
|
||||
('openai', 'malformed-json'),
|
||||
('', '');
|
||||
`);
|
||||
|
||||
const count = withMockedMigrationFs({ "127_usage_history_account_identity.sql": sql }, () =>
|
||||
runner.runMigrations(db)
|
||||
);
|
||||
const rows = db
|
||||
.prepare("SELECT connection_id, account_key FROM usage_history ORDER BY id")
|
||||
.all();
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(rows, [
|
||||
{
|
||||
connection_id: "generic",
|
||||
account_key: '["oauth","openai","email"," Member@example.com ","username"," Member "]',
|
||||
},
|
||||
{
|
||||
connection_id: "workspace",
|
||||
account_key: '["oauth","codex","workspace"," Workspace ","email"," Member@example.com "]',
|
||||
},
|
||||
{
|
||||
connection_id: "numeric-workspace",
|
||||
account_key: '["connection","codex","numeric-workspace"]',
|
||||
},
|
||||
{
|
||||
connection_id: "object-user",
|
||||
account_key: '["connection","codex","object-user"]',
|
||||
},
|
||||
{
|
||||
connection_id: "array-username",
|
||||
account_key: '["oauth","openai","email","member@example.com"]',
|
||||
},
|
||||
{
|
||||
connection_id: "malformed-json",
|
||||
account_key: '["oauth","openai","email","member@example.com"]',
|
||||
},
|
||||
{
|
||||
connection_id: "",
|
||||
account_key: '["connection","unknown","unknown"]',
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
@@ -61,160 +61,6 @@ function createDb() {
|
||||
return new Database(":memory:");
|
||||
}
|
||||
|
||||
test("migration 123 backfills account snapshots and creates its index", async () => {
|
||||
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
const db = createDb();
|
||||
const migrationSql = fs.readFileSync(
|
||||
"src/lib/db/migrations/127_usage_history_account_identity.sql",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider TEXT,
|
||||
auth_type TEXT,
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
provider_specific_data TEXT
|
||||
);
|
||||
CREATE TABLE usage_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider TEXT,
|
||||
connection_id TEXT,
|
||||
account_key TEXT,
|
||||
account_label TEXT,
|
||||
account_label_priority INTEGER DEFAULT 0
|
||||
);
|
||||
INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, provider_specific_data)
|
||||
VALUES
|
||||
('empty-provider', '', 'oauth', 'member@example.com', '{"username":"member"}');
|
||||
INSERT INTO usage_history (provider, connection_id)
|
||||
VALUES ('codex', 'orphan-id'), ('', 'empty-provider');
|
||||
`);
|
||||
|
||||
const count = withMockedMigrationFs(
|
||||
{ "127_usage_history_account_identity.sql": migrationSql },
|
||||
() => runner.runMigrations(db)
|
||||
);
|
||||
const rows = db
|
||||
.prepare("SELECT account_key, account_label FROM usage_history ORDER BY id")
|
||||
.all();
|
||||
const index = db
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_uh_account_key'"
|
||||
)
|
||||
.get();
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(rows, [
|
||||
{
|
||||
account_key: '["connection","codex","orphan-id"]',
|
||||
account_label: "orphan-id",
|
||||
},
|
||||
{
|
||||
account_key: '["oauth","unknown","email","member@example.com","username","member"]',
|
||||
account_label: "member@example.com",
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(index, { name: "idx_uh_account_key" });
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("migration 123 preserves exact dedup identity and ignores non-string JSON scalars", async () => {
|
||||
const runner = await importFresh("src/lib/db/migrationRunner.ts");
|
||||
const db = createDb();
|
||||
const sql = fs.readFileSync(
|
||||
"src/lib/db/migrations/127_usage_history_account_identity.sql",
|
||||
"utf8"
|
||||
);
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE provider_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider TEXT,
|
||||
auth_type TEXT,
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
display_name TEXT,
|
||||
provider_specific_data TEXT
|
||||
);
|
||||
CREATE TABLE usage_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider TEXT,
|
||||
connection_id TEXT,
|
||||
account_key TEXT,
|
||||
account_label TEXT,
|
||||
account_label_priority INTEGER DEFAULT 0
|
||||
);
|
||||
INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, provider_specific_data)
|
||||
VALUES
|
||||
('generic', 'openai', 'oauth', ' Member@example.com ', '{"username":" Member "}'),
|
||||
('workspace', 'codex', 'oauth', ' Member@example.com ', '{"workspaceId":" Workspace "}'),
|
||||
('numeric-workspace', 'codex', 'oauth', 'member@example.com', '{"workspaceId":42}'),
|
||||
('object-user', 'codex', 'oauth', 'member@example.com', '{"chatgptUserId":{"id":"user-a"}}'),
|
||||
('array-username', 'openai', 'oauth', 'member@example.com', '{"username":["member"]}'),
|
||||
('malformed-json', 'openai', 'oauth', 'member@example.com', '{bad json');
|
||||
INSERT INTO usage_history (provider, connection_id) VALUES
|
||||
('openai', 'generic'),
|
||||
('codex', 'workspace'),
|
||||
('codex', 'numeric-workspace'),
|
||||
('codex', 'object-user'),
|
||||
('openai', 'array-username'),
|
||||
('openai', 'malformed-json'),
|
||||
('', '');
|
||||
`);
|
||||
|
||||
const count = withMockedMigrationFs({ "127_usage_history_account_identity.sql": sql }, () =>
|
||||
runner.runMigrations(db)
|
||||
);
|
||||
const rows = db
|
||||
.prepare("SELECT connection_id, account_key FROM usage_history ORDER BY id")
|
||||
.all();
|
||||
|
||||
assert.equal(count, 1);
|
||||
assert.deepEqual(rows, [
|
||||
{
|
||||
connection_id: "generic",
|
||||
account_key: '["oauth","openai","email"," Member@example.com ","username"," Member "]',
|
||||
},
|
||||
{
|
||||
connection_id: "workspace",
|
||||
account_key: '["oauth","codex","workspace"," Workspace ","email"," Member@example.com "]',
|
||||
},
|
||||
{
|
||||
connection_id: "numeric-workspace",
|
||||
account_key: '["connection","codex","numeric-workspace"]',
|
||||
},
|
||||
{
|
||||
connection_id: "object-user",
|
||||
account_key: '["connection","codex","object-user"]',
|
||||
},
|
||||
{
|
||||
connection_id: "array-username",
|
||||
account_key: '["oauth","openai","email","member@example.com"]',
|
||||
},
|
||||
{
|
||||
connection_id: "malformed-json",
|
||||
account_key: '["oauth","openai","email","member@example.com"]',
|
||||
},
|
||||
{
|
||||
connection_id: "",
|
||||
account_key: '["connection","unknown","unknown"]',
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
function createSqlJsLikeDb() {
|
||||
const db = createDb();
|
||||
|
||||
|
||||
558
tests/unit/usage-analytics-route-extra.test.ts
Normal file
558
tests/unit/usage-analytics-route-extra.test.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
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-usage-analytics-route-extra-")
|
||||
);
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
const ORIGINAL_API_KEY_SECRET = process.env.API_KEY_SECRET;
|
||||
process.env.API_KEY_SECRET = "test-usage-analytics-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const localDb = await import("../../src/lib/localDb.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
|
||||
const analyticsRoute = await import("../../src/app/api/usage/analytics/route.ts");
|
||||
|
||||
const clearPendingRequests = usageHistory.clearPendingRequests;
|
||||
const EXPECTED_TOTAL_COST = 0.020925;
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
clearPendingRequests();
|
||||
}
|
||||
|
||||
async function seedAnalyticsData() {
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const timestamp = new Date(now.getTime() - i * 60 * 60 * 1000).toISOString();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
i % 2 === 0 ? "openai" : "anthropic",
|
||||
i % 2 === 0 ? "gpt-4o" : "claude-sonnet",
|
||||
"test-conn",
|
||||
"test-key",
|
||||
"Primary Key",
|
||||
100 + i,
|
||||
50 + i,
|
||||
1,
|
||||
200 + i * 10,
|
||||
timestamp
|
||||
);
|
||||
}
|
||||
db.prepare(
|
||||
`INSERT INTO call_logs (provider, model, requested_model, connection_id, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "gpt-4o-mini", "test-conn", new Date().toISOString());
|
||||
}
|
||||
|
||||
function makeRequest(url: string) {
|
||||
return new Request(url, { method: "GET" });
|
||||
}
|
||||
|
||||
function assertClose(actual: number, expected: number, epsilon = 0.000001) {
|
||||
assert.ok(
|
||||
Math.abs(actual - expected) <= epsilon,
|
||||
`expected ${actual} to be within ${epsilon} of ${expected}`
|
||||
);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
await localDb.updatePricing({
|
||||
openai: { "gpt-4o": { input: 2.5, output: 10 } },
|
||||
anthropic: { "claude-sonnet": { input: 3, output: 15 } },
|
||||
});
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
|
||||
if (ORIGINAL_API_KEY_SECRET === undefined) {
|
||||
delete process.env.API_KEY_SECRET;
|
||||
} else {
|
||||
process.env.API_KEY_SECRET = ORIGINAL_API_KEY_SECRET;
|
||||
}
|
||||
});
|
||||
test("GET /api/usage/analytics joins legacy workspace usage after Codex re-login", async () => {
|
||||
await localDb.updatePricing({
|
||||
codex: { "gpt-5.5": { input: 1, output: 2 } },
|
||||
});
|
||||
|
||||
const db = core.getDbInstance();
|
||||
const connectionId = "legacy-workspace-connection";
|
||||
db.prepare(
|
||||
`INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, display_name, provider_specific_data, created_at, updated_at)
|
||||
VALUES (?, 'codex', 'oauth', NULL, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
|
||||
).run(connectionId, "Production Codex", JSON.stringify({ workspaceId: "workspace-production" }));
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const relogged = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
accessToken: "new-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
assert.equal(relogged.id, connectionId);
|
||||
const repaired = db
|
||||
.prepare(
|
||||
"SELECT account_key, account_label, account_label_priority FROM usage_history WHERE connection_id = ?"
|
||||
)
|
||||
.get(connectionId);
|
||||
assert.deepEqual(repaired, {
|
||||
account_key:
|
||||
'["oauth","codex","workspace","workspace-production","email","member@example.com"]',
|
||||
account_label: "Production Codex",
|
||||
account_label_priority: 4,
|
||||
});
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-02T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Production Codex");
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assertClose(body.byAccount[0].cost, 0.004);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics preserves a Codex account across deletion and re-login", async () => {
|
||||
await localDb.updatePricing({
|
||||
codex: { "gpt-5.5": { input: 1, output: 2 } },
|
||||
});
|
||||
|
||||
const original = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
displayName: "Production Codex",
|
||||
accessToken: "old-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId: original.id as string,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
await providersDb.deleteProviderConnection(original.id as string);
|
||||
|
||||
const recreated = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
accessToken: "new-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
assert.notEqual(recreated.id, original.id);
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId: recreated.id as string,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-02T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Production Codex");
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assertClose(body.byAccount[0].cost, 0.004);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics keeps provider and Codex workspace account identities distinct", async () => {
|
||||
const accountSpecs = [
|
||||
{ provider: "codex", workspaceId: "workspace-a" },
|
||||
{ provider: "codex", workspaceId: "workspace-b" },
|
||||
{ provider: "openai", workspaceId: undefined },
|
||||
];
|
||||
|
||||
for (const [index, spec] of accountSpecs.entries()) {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: spec.provider,
|
||||
authType: "oauth",
|
||||
email: "shared@example.com",
|
||||
accessToken: `secret-${index}`,
|
||||
providerSpecificData: spec.workspaceId ? { workspaceId: spec.workspaceId } : {},
|
||||
});
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: spec.provider,
|
||||
model: spec.provider === "codex" ? "gpt-5.5" : "gpt-4o",
|
||||
connectionId: connection.id as string,
|
||||
tokens: { input: 100 + index, output: 50 + index },
|
||||
timestamp: `2026-01-0${index + 1}T00:00:00.000Z`,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 3);
|
||||
assert.ok(body.byAccount.every((row) => !("accountKey" in row)));
|
||||
assert.deepEqual(body.byAccount.map((row) => row.account).sort(), [
|
||||
"shared@example.com",
|
||||
"shared@example.com",
|
||||
"shared@example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics keeps an honest UUID label for an orphaned legacy account", async () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("codex", "gpt-5.5", "deleted-legacy-uuid", 10, 5, 1, 25, "2026-01-01T00:00:00.000Z");
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "deleted-legacy-uuid");
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics uses the newest equal-priority label for one account", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const accountKey = '["oauth","codex","user","user-a","email","member@example.com"]';
|
||||
const now = Date.now();
|
||||
const insert = db.prepare(
|
||||
`INSERT INTO usage_history
|
||||
(provider, model, connection_id, account_key, account_label, account_label_priority,
|
||||
tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insert.run(
|
||||
"codex",
|
||||
"gpt-5.5",
|
||||
"old-uuid",
|
||||
accountKey,
|
||||
"Zulu before rename",
|
||||
4,
|
||||
10,
|
||||
5,
|
||||
1,
|
||||
20,
|
||||
new Date(now - 60_000).toISOString()
|
||||
);
|
||||
insert.run(
|
||||
"codex",
|
||||
"gpt-5.5",
|
||||
"new-uuid",
|
||||
accountKey,
|
||||
"Alpha after rename",
|
||||
4,
|
||||
20,
|
||||
10,
|
||||
1,
|
||||
30,
|
||||
new Date(now).toISOString()
|
||||
);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.uniqueAccounts, 1);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Alpha after rename");
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics includes cost by API key", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(body.byApiKey));
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, "test-key");
|
||||
assert.equal(body.byApiKey[0].apiKeyName, "Primary Key");
|
||||
assertClose(body.byApiKey[0].cost, body.summary.totalCost);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not double-count raw and aggregated rows", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split("T")[0];
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 30);
|
||||
const olderDate = new Date(cutoffDate);
|
||||
olderDate.setDate(olderDate.getDate() - 1);
|
||||
const olderDateStr = olderDate.toISOString().split("T")[0];
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "raw-current", 100, 50, 1, 200, today.toISOString());
|
||||
|
||||
const insertSummary = db.prepare(
|
||||
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insertSummary.run("openai", "gpt-4o", todayStr, 99, 9900, 9900, 0);
|
||||
insertSummary.run("openai", "gpt-4o", olderDateStr, 1, 25, 10, 0);
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?range=all")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 2);
|
||||
assert.equal(body.summary.totalTokens, 185);
|
||||
assert.equal(body.summary.uniqueAccounts, 1);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics omits global aggregates when filtering by API key", async () => {
|
||||
const apiKey = await apiKeysDb.createApiKey("Scoped Key", "machine1234567890");
|
||||
const db = core.getDbInstance();
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"scoped-conn",
|
||||
apiKey.id,
|
||||
"Scoped Key",
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
200,
|
||||
new Date().toISOString()
|
||||
);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "2024-01-01", 99, 9900, 9900, 0);
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?range=all&apiKeyIds=${apiKey.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 1);
|
||||
assert.equal(body.summary.totalTokens, 150);
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics groups renamed API key usage by stable ID", async () => {
|
||||
const apiKey = await apiKeysDb.createApiKey("Averyanov", "machine1234567890");
|
||||
await apiKeysDb.updateApiKeyPermissions(apiKey.id, { name: "Alexander Averyanov" });
|
||||
|
||||
const db = core.getDbInstance();
|
||||
const now = Date.now();
|
||||
const insertUsage = db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insertUsage.run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"test-conn",
|
||||
apiKey.id,
|
||||
"Averyanov",
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
200,
|
||||
new Date(now - 60_000).toISOString()
|
||||
);
|
||||
insertUsage.run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"test-conn",
|
||||
apiKey.id,
|
||||
"Desktop",
|
||||
200,
|
||||
100,
|
||||
1,
|
||||
250,
|
||||
new Date(now).toISOString()
|
||||
);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.uniqueApiKeys, 1);
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
|
||||
assert.equal(body.byApiKey[0].apiKeyName, "Alexander Averyanov");
|
||||
assert.deepEqual(body.byApiKey[0].historicalApiKeyNames.sort(), ["Averyanov", "Desktop"]);
|
||||
assert.equal(body.byApiKey[0].requests, 2);
|
||||
assert.equal(body.byApiKey[0].promptTokens, 300);
|
||||
assert.equal(body.byApiKey[0].completionTokens, 150);
|
||||
|
||||
const filteredResponse = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?apiKeyIds=${apiKey.id}`)
|
||||
);
|
||||
const filteredBody = await filteredResponse.json();
|
||||
|
||||
assert.equal(filteredResponse.status, 200);
|
||||
assert.equal(filteredBody.summary.totalRequests, 2);
|
||||
assert.equal(filteredBody.byApiKey.length, 1);
|
||||
assert.equal(filteredBody.byApiKey[0].apiKeyId, apiKey.id);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not persist guessed API key attribution", async () => {
|
||||
await localDb.updatePricing({
|
||||
openai: { "gpt-4o": { input: 2.5, output: 10 } },
|
||||
});
|
||||
await apiKeysDb.createApiKey("Unrestricted Key", "machine1234567890");
|
||||
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "legacy-conn", null, null, 100, 50, 1, 200, new Date().toISOString());
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byApiKey.length, 0);
|
||||
|
||||
const row = db
|
||||
.prepare("SELECT api_key_id, api_key_name FROM usage_history WHERE connection_id = ?")
|
||||
.get("legacy-conn") as { api_key_id: string | null; api_key_name: string | null };
|
||||
assert.equal(row.api_key_id, null);
|
||||
assert.equal(row.api_key_name, null);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics returns weeklyPattern for the costs dashboard", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(body.weeklyPattern));
|
||||
assert.equal(body.weeklyPattern.length, 7);
|
||||
assert.deepEqual(
|
||||
body.weeklyPattern.map((row) => row.day),
|
||||
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
);
|
||||
assert.ok(body.weeklyPattern.some((row) => row.totalTokens > 0 && row.avgTokens > 0));
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics includes activityMap for heatmap", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(typeof body.activityMap === "object");
|
||||
assert.ok(Object.keys(body.activityMap).length > 0);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics returns 500 on database errors", async () => {
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(body.summary.totalRequests === 0);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not throw Unknown named parameter on short range (needsAggregated=false)", async () => {
|
||||
// Regression: shared params object leaked agg-only bindings (@sinceDate, @rawCutoffDate)
|
||||
// into queries that don't reference them, causing better-sqlite3 to throw.
|
||||
// A short range (1h) triggers needsAggregated=false because the entire window
|
||||
// falls within the raw-data-only period.
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "test-conn", 100, 50, 1, 200, now.toISOString());
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?range=1h")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 1);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not throw Unknown named parameter with apiKey filter on long range", async () => {
|
||||
// Regression: Object.assign(presetParams, params) leaked all main-query bindings
|
||||
// into preset queries that only reference preset-prefixed placeholders.
|
||||
const apiKey = await apiKeysDb.createApiKey("Preset Key", "machine-preset1234");
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
|
||||
// Seed data old enough to trigger aggregated + preset path
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ts = new Date(now.getTime() - (35 + i) * 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "test-conn", apiKey.id, apiKey.name, 100, 50, 1, 200, ts);
|
||||
}
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?range=60d&apiKeyId=${apiKey.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
// Core regression check: no "Unknown named parameter" error.
|
||||
// The exact count depends on raw-vs-aggregated boundary; we only need to
|
||||
// confirm the endpoint returns 200 without throwing.
|
||||
assert.ok(typeof body.summary.totalRequests === "number");
|
||||
});
|
||||
@@ -333,473 +333,3 @@ test("GET /api/usage/analytics includes byAccount array with cost data", async (
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics joins legacy workspace usage after Codex re-login", async () => {
|
||||
await localDb.updatePricing({
|
||||
codex: { "gpt-5.5": { input: 1, output: 2 } },
|
||||
});
|
||||
|
||||
const db = core.getDbInstance();
|
||||
const connectionId = "legacy-workspace-connection";
|
||||
db.prepare(
|
||||
`INSERT INTO provider_connections
|
||||
(id, provider, auth_type, email, display_name, provider_specific_data, created_at, updated_at)
|
||||
VALUES (?, 'codex', 'oauth', NULL, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`
|
||||
).run(connectionId, "Production Codex", JSON.stringify({ workspaceId: "workspace-production" }));
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const relogged = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
accessToken: "new-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
assert.equal(relogged.id, connectionId);
|
||||
const repaired = db
|
||||
.prepare(
|
||||
"SELECT account_key, account_label, account_label_priority FROM usage_history WHERE connection_id = ?"
|
||||
)
|
||||
.get(connectionId);
|
||||
assert.deepEqual(repaired, {
|
||||
account_key:
|
||||
'["oauth","codex","workspace","workspace-production","email","member@example.com"]',
|
||||
account_label: "Production Codex",
|
||||
account_label_priority: 4,
|
||||
});
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-02T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Production Codex");
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assertClose(body.byAccount[0].cost, 0.004);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics preserves a Codex account across deletion and re-login", async () => {
|
||||
await localDb.updatePricing({
|
||||
codex: { "gpt-5.5": { input: 1, output: 2 } },
|
||||
});
|
||||
|
||||
const original = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
displayName: "Production Codex",
|
||||
accessToken: "old-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId: original.id as string,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
await providersDb.deleteProviderConnection(original.id as string);
|
||||
|
||||
const recreated = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "member@example.com",
|
||||
accessToken: "new-secret",
|
||||
providerSpecificData: { workspaceId: "workspace-production" },
|
||||
});
|
||||
assert.notEqual(recreated.id, original.id);
|
||||
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: "codex",
|
||||
model: "gpt-5.5",
|
||||
connectionId: recreated.id as string,
|
||||
tokens: { input: 1000, output: 500 },
|
||||
timestamp: "2026-01-02T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Production Codex");
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assertClose(body.byAccount[0].cost, 0.004);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics keeps provider and Codex workspace account identities distinct", async () => {
|
||||
const accountSpecs = [
|
||||
{ provider: "codex", workspaceId: "workspace-a" },
|
||||
{ provider: "codex", workspaceId: "workspace-b" },
|
||||
{ provider: "openai", workspaceId: undefined },
|
||||
];
|
||||
|
||||
for (const [index, spec] of accountSpecs.entries()) {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: spec.provider,
|
||||
authType: "oauth",
|
||||
email: "shared@example.com",
|
||||
accessToken: `secret-${index}`,
|
||||
providerSpecificData: spec.workspaceId ? { workspaceId: spec.workspaceId } : {},
|
||||
});
|
||||
await usageHistory.saveRequestUsage({
|
||||
provider: spec.provider,
|
||||
model: spec.provider === "codex" ? "gpt-5.5" : "gpt-4o",
|
||||
connectionId: connection.id as string,
|
||||
tokens: { input: 100 + index, output: 50 + index },
|
||||
timestamp: `2026-01-0${index + 1}T00:00:00.000Z`,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 3);
|
||||
assert.ok(body.byAccount.every((row) => !("accountKey" in row)));
|
||||
assert.deepEqual(body.byAccount.map((row) => row.account).sort(), [
|
||||
"shared@example.com",
|
||||
"shared@example.com",
|
||||
"shared@example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics keeps an honest UUID label for an orphaned legacy account", async () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("codex", "gpt-5.5", "deleted-legacy-uuid", 10, 5, 1, 25, "2026-01-01T00:00:00.000Z");
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?startDate=2026-01-01T00:00:00.000Z")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "deleted-legacy-uuid");
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics uses the newest equal-priority label for one account", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const accountKey = '["oauth","codex","user","user-a","email","member@example.com"]';
|
||||
const now = Date.now();
|
||||
const insert = db.prepare(
|
||||
`INSERT INTO usage_history
|
||||
(provider, model, connection_id, account_key, account_label, account_label_priority,
|
||||
tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insert.run(
|
||||
"codex",
|
||||
"gpt-5.5",
|
||||
"old-uuid",
|
||||
accountKey,
|
||||
"Zulu before rename",
|
||||
4,
|
||||
10,
|
||||
5,
|
||||
1,
|
||||
20,
|
||||
new Date(now - 60_000).toISOString()
|
||||
);
|
||||
insert.run(
|
||||
"codex",
|
||||
"gpt-5.5",
|
||||
"new-uuid",
|
||||
accountKey,
|
||||
"Alpha after rename",
|
||||
4,
|
||||
20,
|
||||
10,
|
||||
1,
|
||||
30,
|
||||
new Date(now).toISOString()
|
||||
);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.uniqueAccounts, 1);
|
||||
assert.equal(body.byAccount.length, 1);
|
||||
assert.equal(body.byAccount[0].account, "Alpha after rename");
|
||||
assert.equal(body.byAccount[0].requests, 2);
|
||||
assert.equal("accountKey" in body.byAccount[0], false);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics includes cost by API key", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(body.byApiKey));
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, "test-key");
|
||||
assert.equal(body.byApiKey[0].apiKeyName, "Primary Key");
|
||||
assertClose(body.byApiKey[0].cost, body.summary.totalCost);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not double-count raw and aggregated rows", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split("T")[0];
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 30);
|
||||
const olderDate = new Date(cutoffDate);
|
||||
olderDate.setDate(olderDate.getDate() - 1);
|
||||
const olderDateStr = olderDate.toISOString().split("T")[0];
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "raw-current", 100, 50, 1, 200, today.toISOString());
|
||||
|
||||
const insertSummary = db.prepare(
|
||||
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insertSummary.run("openai", "gpt-4o", todayStr, 99, 9900, 9900, 0);
|
||||
insertSummary.run("openai", "gpt-4o", olderDateStr, 1, 25, 10, 0);
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?range=all")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 2);
|
||||
assert.equal(body.summary.totalTokens, 185);
|
||||
assert.equal(body.summary.uniqueAccounts, 1);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics omits global aggregates when filtering by API key", async () => {
|
||||
const apiKey = await apiKeysDb.createApiKey("Scoped Key", "machine1234567890");
|
||||
const db = core.getDbInstance();
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"scoped-conn",
|
||||
apiKey.id,
|
||||
"Scoped Key",
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
200,
|
||||
new Date().toISOString()
|
||||
);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO daily_usage_summary (provider, model, date, total_requests, total_input_tokens, total_output_tokens, total_cost)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "2024-01-01", 99, 9900, 9900, 0);
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?range=all&apiKeyIds=${apiKey.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 1);
|
||||
assert.equal(body.summary.totalTokens, 150);
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics groups renamed API key usage by stable ID", async () => {
|
||||
const apiKey = await apiKeysDb.createApiKey("Averyanov", "machine1234567890");
|
||||
await apiKeysDb.updateApiKeyPermissions(apiKey.id, { name: "Alexander Averyanov" });
|
||||
|
||||
const db = core.getDbInstance();
|
||||
const now = Date.now();
|
||||
const insertUsage = db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
);
|
||||
insertUsage.run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"test-conn",
|
||||
apiKey.id,
|
||||
"Averyanov",
|
||||
100,
|
||||
50,
|
||||
1,
|
||||
200,
|
||||
new Date(now - 60_000).toISOString()
|
||||
);
|
||||
insertUsage.run(
|
||||
"openai",
|
||||
"gpt-4o",
|
||||
"test-conn",
|
||||
apiKey.id,
|
||||
"Desktop",
|
||||
200,
|
||||
100,
|
||||
1,
|
||||
250,
|
||||
new Date(now).toISOString()
|
||||
);
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.uniqueApiKeys, 1);
|
||||
assert.equal(body.byApiKey.length, 1);
|
||||
assert.equal(body.byApiKey[0].apiKeyId, apiKey.id);
|
||||
assert.equal(body.byApiKey[0].apiKeyName, "Alexander Averyanov");
|
||||
assert.deepEqual(body.byApiKey[0].historicalApiKeyNames.sort(), ["Averyanov", "Desktop"]);
|
||||
assert.equal(body.byApiKey[0].requests, 2);
|
||||
assert.equal(body.byApiKey[0].promptTokens, 300);
|
||||
assert.equal(body.byApiKey[0].completionTokens, 150);
|
||||
|
||||
const filteredResponse = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?apiKeyIds=${apiKey.id}`)
|
||||
);
|
||||
const filteredBody = await filteredResponse.json();
|
||||
|
||||
assert.equal(filteredResponse.status, 200);
|
||||
assert.equal(filteredBody.summary.totalRequests, 2);
|
||||
assert.equal(filteredBody.byApiKey.length, 1);
|
||||
assert.equal(filteredBody.byApiKey[0].apiKeyId, apiKey.id);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not persist guessed API key attribution", async () => {
|
||||
await localDb.updatePricing({
|
||||
openai: { "gpt-4o": { input: 2.5, output: 10 } },
|
||||
});
|
||||
await apiKeysDb.createApiKey("Unrestricted Key", "machine1234567890");
|
||||
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "legacy-conn", null, null, 100, 50, 1, 200, new Date().toISOString());
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.byApiKey.length, 0);
|
||||
|
||||
const row = db
|
||||
.prepare("SELECT api_key_id, api_key_name FROM usage_history WHERE connection_id = ?")
|
||||
.get("legacy-conn") as { api_key_id: string | null; api_key_name: string | null };
|
||||
assert.equal(row.api_key_id, null);
|
||||
assert.equal(row.api_key_name, null);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics returns weeklyPattern for the costs dashboard", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(Array.isArray(body.weeklyPattern));
|
||||
assert.equal(body.weeklyPattern.length, 7);
|
||||
assert.deepEqual(
|
||||
body.weeklyPattern.map((row) => row.day),
|
||||
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
);
|
||||
assert.ok(body.weeklyPattern.some((row) => row.totalTokens > 0 && row.avgTokens > 0));
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics includes activityMap for heatmap", async () => {
|
||||
await seedAnalyticsData();
|
||||
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(typeof body.activityMap === "object");
|
||||
assert.ok(Object.keys(body.activityMap).length > 0);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics returns 500 on database errors", async () => {
|
||||
const response = await analyticsRoute.GET(makeRequest("http://localhost/api/usage/analytics"));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(body.summary.totalRequests === 0);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not throw Unknown named parameter on short range (needsAggregated=false)", async () => {
|
||||
// Regression: shared params object leaked agg-only bindings (@sinceDate, @rawCutoffDate)
|
||||
// into queries that don't reference them, causing better-sqlite3 to throw.
|
||||
// A short range (1h) triggers needsAggregated=false because the entire window
|
||||
// falls within the raw-data-only period.
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "test-conn", 100, 50, 1, 200, now.toISOString());
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest("http://localhost/api/usage/analytics?range=1h")
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.summary.totalRequests, 1);
|
||||
});
|
||||
|
||||
test("GET /api/usage/analytics does not throw Unknown named parameter with apiKey filter on long range", async () => {
|
||||
// Regression: Object.assign(presetParams, params) leaked all main-query bindings
|
||||
// into preset queries that only reference preset-prefixed placeholders.
|
||||
const apiKey = await apiKeysDb.createApiKey("Preset Key", "machine-preset1234");
|
||||
const db = core.getDbInstance();
|
||||
const now = new Date();
|
||||
|
||||
// Seed data old enough to trigger aggregated + preset path
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ts = new Date(now.getTime() - (35 + i) * 24 * 60 * 60 * 1000).toISOString();
|
||||
db.prepare(
|
||||
`INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name, tokens_input, tokens_output, success, latency_ms, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
).run("openai", "gpt-4o", "test-conn", apiKey.id, apiKey.name, 100, 50, 1, 200, ts);
|
||||
}
|
||||
|
||||
const response = await analyticsRoute.GET(
|
||||
makeRequest(`http://localhost/api/usage/analytics?range=60d&apiKeyId=${apiKey.id}`)
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
// Core regression check: no "Unknown named parameter" error.
|
||||
// The exact count depends on raw-vs-aggregated boundary; we only need to
|
||||
// confirm the endpoint returns 200 without throwing.
|
||||
assert.ok(typeof body.summary.totalRequests === "number");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user