blob: a023f7b89c21c019bf6d87257ce4b659338d7727 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
import { test } from 'node:test';
import { migrateSqliteUsageDatabase } from '../sqlite-usage-schema.js';
import {
MODEL_CALL_NOW as NOW,
modelCallAttempt as attempt,
wideModelCallAttempt as wideAttempt,
} from './fixtures/model-call-attempt.js';
import { MODEL_CALL_COLUMNS } from '../sqlite-usage-schema.js';
/** A ledger as it stood before the record was spread into columns. */
function blobLedger(database: DatabaseSync): void {
database.exec(`
CREATE TABLE usage_model_call_attempts (
attempt_id TEXT PRIMARY KEY,
completed_at INTEGER NOT NULL,
record_json TEXT NOT NULL,
session_id TEXT
);
`);
}
function insertBlob(
database: DatabaseSync,
attemptId: string,
record: unknown,
sessionId?: string,
) {
database
.prepare('INSERT INTO usage_model_call_attempts VALUES (?, ?, ?, ?)')
.run(
attemptId,
NOW - 500,
typeof record === 'string' ? record : JSON.stringify(record),
sessionId ?? null,
);
}
function storedRow(database: DatabaseSync, attemptId: string): Record<string, unknown> {
return database
.prepare('SELECT * FROM usage_model_call_attempts WHERE attempt_id = ?')
.get(attemptId) as Record<string, unknown>;
}
test('usage migration backfills Session identity for existing ledger rows', () => {
const database = new DatabaseSync(':memory:');
try {
database.exec(`
CREATE TABLE usage_llm_calls (
storage_key TEXT PRIMARY KEY,
id TEXT NOT NULL,
ts INTEGER NOT NULL,
record_json TEXT NOT NULL
);
CREATE TABLE usage_model_call_attempts (
attempt_id TEXT PRIMARY KEY,
completed_at INTEGER NOT NULL,
record_json TEXT NOT NULL
);
INSERT INTO usage_llm_calls(storage_key, id, ts, record_json)
VALUES ('legacy', 'legacy', 1, '{"sessionId":"session-a"}');
INSERT INTO usage_model_call_attempts(attempt_id, completed_at, record_json)
VALUES ('canonical', 1, '{"sessionId":"session-b"}');
`);
migrateSqliteUsageDatabase(database);
assert.equal(
database.prepare("SELECT session_id FROM usage_llm_calls WHERE id = 'legacy'").get()
?.session_id,
'session-a',
);
assert.equal(
database
.prepare("SELECT session_id FROM usage_model_call_attempts WHERE attempt_id = 'canonical'")
.get()?.session_id,
'session-b',
);
assert.ok(
database
.prepare(
"SELECT 1 FROM sqlite_schema WHERE type = 'index' AND name = 'usage_llm_calls_session_ts'",
)
.get(),
);
assert.ok(
database
.prepare(
"SELECT 1 FROM sqlite_schema WHERE type = 'index' AND name = 'usage_model_call_attempts_session_completed_at'",
)
.get(),
);
} finally {
database.close();
}
});
test('the migration spreads a stored record into the columns a cost answer sums', () => {
const database = new DatabaseSync(':memory:');
try {
blobLedger(database);
const wide = wideAttempt();
insertBlob(database, wide.attemptId, wide, wide.sessionId);
migrateSqliteUsageDatabase(database);
const row = storedRow(database, wide.attemptId);
assert.deepEqual(Object.keys(row), [...MODEL_CALL_COLUMNS]);
// Every number a Usage total is built from reads the same after the spread.
assert.equal(row.cost_usd, 0.004);
assert.equal(row.cost_basis, 'priced');
assert.equal(row.input_tokens, 100);
assert.equal(row.output_tokens, 20);
assert.equal(row.provider_id, 'anthropic');
assert.equal(row.session_id, 'session-1');
} finally {
database.close();
}
});
test('a record the migration cannot read whole keeps its identity and loses the rest', () => {
// A row that ends up half-filled would make the table's own CHECK
// unsatisfiable and take the whole migration with it, so conversion is
// all-or-nothing per row. What is left says a call happened and its cost is
// gone — which is what a read reports as unreadable.
const database = new DatabaseSync(':memory:');
try {
blobLedger(database);
insertBlob(database, 'damaged', '{"schemaVersion":1,', 'session-1');
insertBlob(database, 'alien', { sessionId: 'session-2' });
const priced = attempt({ attemptId: 'priced' });
insertBlob(database, priced.attemptId, priced, priced.sessionId);
migrateSqliteUsageDatabase(database);
assert.deepEqual(
database
.prepare(
'SELECT attempt_id, session_id FROM usage_model_call_attempts WHERE cost_basis IS NULL ORDER BY attempt_id',
)
.all()
.map((row) => ({ ...row })),
[
{ attempt_id: 'alien', session_id: 'session-2' },
{ attempt_id: 'damaged', session_id: 'session-1' },
],
);
assert.equal(storedRow(database, 'priced').cost_usd, 0.004);
} finally {
database.close();
}
});
test('the migration is a no-op once the ledger already holds columns', () => {
const database = new DatabaseSync(':memory:');
try {
blobLedger(database);
const wide = wideAttempt();
insertBlob(database, wide.attemptId, wide, wide.sessionId);
migrateSqliteUsageDatabase(database);
const once = storedRow(database, wide.attemptId);
migrateSqliteUsageDatabase(database);
assert.deepEqual(storedRow(database, wide.attemptId), once);
} finally {
database.close();
}
});
test('the migration converts every row, however many a workspace holds', () => {
const database = new DatabaseSync(':memory:');
try {
blobLedger(database);
for (let index = 0; index < 1_200; index += 1) {
const row = attempt({ attemptId: `attempt-${String(index).padStart(5, '0')}` });
insertBlob(database, row.attemptId, row, row.sessionId);
}
migrateSqliteUsageDatabase(database);
assert.equal(
database
.prepare('SELECT COUNT(*) AS count FROM usage_model_call_attempts WHERE cost_basis IS NULL')
.get()?.count,
0,
);
} finally {
database.close();
}
});
test('the ledger refuses a row that would make a total dishonest', () => {
const database = new DatabaseSync(':memory:');
try {
migrateSqliteUsageDatabase(database);
const insert = (values: Record<string, unknown>) => {
const columns = Object.keys(values);
database
.prepare(
`INSERT INTO usage_model_call_attempts(${columns.join(', ')}) VALUES (${columns
.map(() => '?')
.join(', ')})`,
)
.run(...(Object.values(values) as (string | number | null)[]));
};
const base = {
completed_at: NOW,
logical_call_id: 'call-1',
turn_id: 'turn-1',
call_kind: 'main',
provider_id: 'anthropic',
model_id: 'claude-opus-5',
latency_ms: 10,
status: 'completed',
usage_basis: 'reported',
};
// A price nobody could resolve must never surface as an amount.
assert.throws(() =>
insert({ ...base, attempt_id: 'a', cost_basis: 'unpriced', cost_usd: 0.004 }),
);
// A priced call must carry one; zero is legal and means genuinely free.
assert.throws(() => insert({ ...base, attempt_id: 'b', cost_basis: 'priced' }));
// "No usage reported" and "zero tokens" are different facts.
assert.throws(() =>
insert({
...base,
attempt_id: 'c',
usage_basis: 'missing',
input_tokens: 0,
cost_basis: 'priced',
cost_usd: 0,
}),
);
// Half a record is not a record.
assert.throws(() => insert({ attempt_id: 'd', completed_at: NOW, cost_basis: 'unpriced' }));
} finally {
database.close();
}
});
test('Usage title revision triggers migrate existing metadata and roll back with title changes', () => {
const database = new DatabaseSync(':memory:');
try {
// A pre-existing metadata table, as installed before the Usage migration.
database.exec(
'CREATE TABLE session_metadata (session_id TEXT PRIMARY KEY, name TEXT, is_flagged INTEGER)',
);
database.exec("INSERT INTO session_metadata VALUES ('session', 'Before', 0)");
migrateSqliteUsageDatabase(database);
const revision = () =>
database.prepare('SELECT revision FROM usage_screen_revision').get()!.revision;
const before = revision();
database.exec("UPDATE session_metadata SET is_flagged = 1 WHERE session_id = 'session'");
assert.equal(revision(), before);
database.exec('BEGIN');
database.exec("UPDATE session_metadata SET name = 'Rolled back' WHERE session_id = 'session'");
assert.notEqual(revision(), before);
database.exec('ROLLBACK');
assert.equal(revision(), before);
database.exec("UPDATE session_metadata SET name = 'After' WHERE session_id = 'session'");
const renamed = revision();
assert.notEqual(renamed, before);
database.exec("DELETE FROM session_metadata WHERE session_id = 'session'");
const deleted = revision();
assert.notEqual(deleted, renamed);
database.exec("INSERT INTO session_metadata VALUES ('session', 'Restored', 0)");
assert.notEqual(revision(), deleted);
} finally {
database.close();
}
});