fix: complete gate cost micro-usd migration

This commit is contained in:
Mikael Hugo 2026-05-07 05:07:57 +02:00
parent 7c39165c81
commit c0973ac287
2 changed files with 110 additions and 14 deletions

View file

@ -731,7 +731,8 @@ function initSchema(db, fileBacked) {
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT '',
duration_ms INTEGER DEFAULT NULL
duration_ms INTEGER DEFAULT NULL,
cost_micro_usd INTEGER DEFAULT NULL
)
`);
db.exec(`
@ -936,11 +937,6 @@ function migrateCostUsdToMicroUsd(db) {
// Purpose: Enable accurate cost tracking at scale without rounding errors
// Consumer: gate_runs cost tracking, cost analytics, budget checks
// Check if cost_micro_usd already exists (avoid re-running migration)
if (columnExists(db, "gate_runs", "cost_micro_usd")) {
return;
}
// Add cost_micro_usd column if it doesn't exist
if (!columnExists(db, "gate_runs", "cost_micro_usd")) {
db.exec(
@ -955,6 +951,7 @@ function migrateCostUsdToMicroUsd(db) {
UPDATE gate_runs
SET cost_micro_usd = CAST(ROUND(cost_usd * 1000000) AS INTEGER)
WHERE cost_usd IS NOT NULL
AND cost_micro_usd IS NULL
`).run();
}
@ -1551,12 +1548,13 @@ function migrateSchema(db) {
rationale TEXT NOT NULL DEFAULT '',
findings TEXT NOT NULL DEFAULT '',
attempt INTEGER NOT NULL DEFAULT 1,
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT '',
duration_ms INTEGER DEFAULT NULL
)
`);
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT '',
duration_ms INTEGER DEFAULT NULL,
cost_micro_usd INTEGER DEFAULT NULL
)
`);
db.exec(`
CREATE TABLE IF NOT EXISTS turn_git_transactions (
trace_id TEXT NOT NULL,

View file

@ -10,7 +10,12 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, test } from "vitest";
import { closeDatabase, getDatabase, openDatabase } from "../sf-db.js";
import {
closeDatabase,
getDatabase,
insertGateRun,
openDatabase,
} from "../sf-db.js";
const tmpDirs = [];
@ -131,6 +136,58 @@ function makeLegacyV27Db() {
return dbPath;
}
function makeLegacyV35GateRunsDb() {
const dir = mkdtempSync(join(tmpdir(), "sf-legacy-v35-gates-"));
tmpDirs.push(dir);
const sfDir = join(dir, ".sf");
mkdirSync(sfDir, { recursive: true });
const dbPath = join(sfDir, "sf.db");
const db = new DatabaseSync(dbPath);
db.exec(`
CREATE TABLE schema_version (
version INTEGER NOT NULL,
applied_at TEXT NOT NULL
);
INSERT INTO schema_version (version, applied_at)
VALUES (35, '2026-05-07T00:00:00.000Z');
CREATE TABLE gate_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
gate_id TEXT NOT NULL,
gate_type TEXT NOT NULL DEFAULT '',
unit_type TEXT DEFAULT NULL,
unit_id TEXT DEFAULT NULL,
milestone_id TEXT DEFAULT NULL,
slice_id TEXT DEFAULT NULL,
task_id TEXT DEFAULT NULL,
outcome TEXT NOT NULL DEFAULT 'pass',
failure_class TEXT NOT NULL DEFAULT 'none',
rationale TEXT NOT NULL DEFAULT '',
findings TEXT NOT NULL DEFAULT '',
attempt INTEGER NOT NULL DEFAULT 1,
max_attempts INTEGER NOT NULL DEFAULT 1,
retryable INTEGER NOT NULL DEFAULT 0,
evaluated_at TEXT NOT NULL DEFAULT '',
duration_ms INTEGER DEFAULT NULL,
cost_usd REAL DEFAULT NULL
);
INSERT INTO gate_runs (
trace_id, turn_id, gate_id, gate_type, unit_type, unit_id,
milestone_id, slice_id, task_id, outcome, failure_class, rationale,
findings, attempt, max_attempts, retryable, evaluated_at, duration_ms,
cost_usd
) VALUES (
'trace-1', 'turn-1', 'cost-gate', 'policy', 'execute-task',
'M001/S01/T01', 'M001', 'S01', 'T01', 'pass', 'none', 'ok',
'', 1, 1, 0, '2026-05-07T00:00:00.000Z', 12, 0.123456
);
`);
db.close();
return dbPath;
}
test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill", () => {
const dbPath = makeLegacyV27Db();
@ -142,7 +199,7 @@ test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill",
const version = db
.prepare("SELECT MAX(version) AS version FROM schema_version")
.get();
assert.equal(version.version, 35);
assert.equal(version.version, 36);
const taskSpec = db
.prepare(
"SELECT milestone_id, slice_id, task_id, verify FROM task_specs WHERE task_id = 'T01'",
@ -155,3 +212,44 @@ test("openDatabase_migrates_v27_tasks_without_created_at_through_spec_backfill",
verify: "go test ./portal",
});
});
test("openDatabase_when_fresh_db_supports_gate_run_micro_usd", () => {
assert.equal(openDatabase(":memory:"), true);
insertGateRun({
traceId: "trace-1",
turnId: "turn-1",
gateId: "cost-gate",
gateType: "policy",
outcome: "pass",
failureClass: "none",
rationale: "ok",
attempt: 1,
maxAttempts: 1,
retryable: false,
evaluatedAt: "2026-05-07T00:00:00.000Z",
durationMs: 12,
costMicroUsd: 123_456,
});
const row = getDatabase()
.prepare("SELECT cost_micro_usd FROM gate_runs WHERE gate_id = 'cost-gate'")
.get();
assert.equal(row.cost_micro_usd, 123_456);
});
test("openDatabase_migrates_v35_gate_cost_usd_to_micro_usd", () => {
const dbPath = makeLegacyV35GateRunsDb();
assert.equal(openDatabase(dbPath), true);
const db = getDatabase();
const columns = db.prepare("PRAGMA table_info(gate_runs)").all();
assert.ok(columns.some((row) => row.name === "cost_micro_usd"));
const row = db
.prepare(
"SELECT cost_usd, cost_micro_usd FROM gate_runs WHERE gate_id = 'cost-gate'",
)
.get();
assert.equal(row.cost_usd, 0.123456);
assert.equal(row.cost_micro_usd, 123_456);
});