feat(FN-4389): complete Step 2 — add schema migration for cache write tokens

Fusion-Task-Id: FN-4389
Fusion-Task-Lineage: 366f9d57-c476-448f-96c0-6e753fc8b090
This commit is contained in:
Fusion
2026-05-13 21:36:06 -07:00
committed by gsxdsm
parent bc226da927
commit a7f4ebe644
6 changed files with 101 additions and 24 deletions

View File

@@ -290,7 +290,13 @@ describe("Database", () => {
}); });
it("seeds schema version", () => { it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
});
it("includes tokenUsageCacheWriteTokens on freshly initialized tasks table", () => {
const columns = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const columnNames = columns.map((column) => column.name);
expect(columnNames).toContain("tokenUsageCacheWriteTokens");
}); });
it("seeds lastModified", () => { it("seeds lastModified", () => {
const ts = db.getLastModified(); const ts = db.getLastModified();
@@ -312,7 +318,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => { it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow(); expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
}); });
it("does not overwrite existing config on re-init", () => { it("does not overwrite existing config on re-init", () => {
// Update the config // Update the config
@@ -1377,7 +1383,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29) // Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1402,11 +1408,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir); const db = new Database(fusionDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
// Re-init should not fail // Re-init should not fail
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
db.close(); db.close();
}); });
@@ -1441,7 +1447,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority"); expect(cols.map((col) => col.name)).toContain("priority");
@@ -1482,13 +1488,14 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name); const colNames = cols.map((col) => col.name);
expect(colNames).toContain("tokenUsageInputTokens"); expect(colNames).toContain("tokenUsageInputTokens");
expect(colNames).toContain("tokenUsageOutputTokens"); expect(colNames).toContain("tokenUsageOutputTokens");
expect(colNames).toContain("tokenUsageCachedTokens"); expect(colNames).toContain("tokenUsageCachedTokens");
expect(colNames).toContain("tokenUsageCacheWriteTokens");
expect(colNames).toContain("tokenUsageTotalTokens"); expect(colNames).toContain("tokenUsageTotalTokens");
expect(colNames).toContain("tokenUsageFirstUsedAt"); expect(colNames).toContain("tokenUsageFirstUsedAt");
expect(colNames).toContain("tokenUsageLastUsedAt"); expect(colNames).toContain("tokenUsageLastUsedAt");
@@ -1498,6 +1505,7 @@ describe("schema migrations", () => {
tokenUsageInputTokens, tokenUsageInputTokens,
tokenUsageOutputTokens, tokenUsageOutputTokens,
tokenUsageCachedTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens,
tokenUsageTotalTokens, tokenUsageTotalTokens,
tokenUsageFirstUsedAt, tokenUsageFirstUsedAt,
tokenUsageLastUsedAt tokenUsageLastUsedAt
@@ -1508,6 +1516,7 @@ describe("schema migrations", () => {
expect(task.tokenUsageInputTokens).toBeNull(); expect(task.tokenUsageInputTokens).toBeNull();
expect(task.tokenUsageOutputTokens).toBeNull(); expect(task.tokenUsageOutputTokens).toBeNull();
expect(task.tokenUsageCachedTokens).toBeNull(); expect(task.tokenUsageCachedTokens).toBeNull();
expect(task.tokenUsageCacheWriteTokens).toBeNull();
expect(task.tokenUsageTotalTokens).toBeNull(); expect(task.tokenUsageTotalTokens).toBeNull();
expect(task.tokenUsageFirstUsedAt).toBeNull(); expect(task.tokenUsageFirstUsedAt).toBeNull();
expect(task.tokenUsageLastUsedAt).toBeNull(); expect(task.tokenUsageLastUsedAt).toBeNull();
@@ -1551,7 +1560,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name); const colNames = cols.map((col) => col.name);
@@ -1791,7 +1800,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments"); expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1865,7 +1874,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>; const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]); expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1889,7 +1898,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>; const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]); expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1993,7 +2002,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 29 // Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -2176,6 +2185,67 @@ describe("schema migrations", () => {
db.close(); db.close();
}); });
it("migration v74 adds tokenUsageCacheWriteTokens without data loss", () => {
tmpDir = makeTmpDir();
const fusionDir = join(tmpDir, ".fusion");
const localDb = new Database(fusionDir);
localDb.exec(`
CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
"column" TEXT NOT NULL,
priority TEXT DEFAULT 'normal',
tokenUsageInputTokens INTEGER,
tokenUsageOutputTokens INTEGER,
tokenUsageCachedTokens INTEGER,
tokenUsageTotalTokens INTEGER,
tokenUsageFirstUsedAt TEXT,
tokenUsageLastUsedAt TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY CHECK (id = 1),
nextId INTEGER DEFAULT 1,
nextWorkflowStepId INTEGER DEFAULT 1,
settings TEXT DEFAULT '{}',
workflowSteps TEXT DEFAULT '[]',
updatedAt TEXT
);
`);
localDb.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '73')");
localDb.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
localDb.exec(`INSERT INTO tasks (id, description, "column", tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, tokenUsageTotalTokens, createdAt, updatedAt) VALUES ('FN-74', 'legacy v73', 'todo', 10, 20, 30, 60, '2026-01-01', '2026-01-01')`);
localDb.init();
expect(localDb.getSchemaVersion()).toBe(74);
const columns = localDb.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(columns.map((column) => column.name)).toContain("tokenUsageCacheWriteTokens");
const row = localDb.prepare(`
SELECT tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, tokenUsageCacheWriteTokens, tokenUsageTotalTokens
FROM tasks
WHERE id = 'FN-74'
`).get() as {
tokenUsageInputTokens: number;
tokenUsageOutputTokens: number;
tokenUsageCachedTokens: number;
tokenUsageCacheWriteTokens: number | null;
tokenUsageTotalTokens: number;
};
expect(row.tokenUsageInputTokens).toBe(10);
expect(row.tokenUsageOutputTokens).toBe(20);
expect(row.tokenUsageCachedTokens).toBe(30);
expect(row.tokenUsageCacheWriteTokens).toBeNull();
expect(row.tokenUsageTotalTokens).toBe(60);
localDb.close();
});
it("SCHEMA_VERSION matches the highest applyMigration target", () => { it("SCHEMA_VERSION matches the highest applyMigration target", () => {
tmpDir = makeTmpDir(); tmpDir = makeTmpDir();
const dbSourcePath = join(dirname(fileURLToPath(import.meta.url)), "..", "db.ts"); const dbSourcePath = join(dirname(fileURLToPath(import.meta.url)), "..", "db.ts");
@@ -2462,7 +2532,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir); const db = createDatabase(fusionDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
expect(db.getLastModified()).toBeGreaterThan(0); expect(db.getLastModified()).toBeGreaterThan(0);
db.close(); db.close();
@@ -2598,7 +2668,7 @@ describe("migration v67 drops orphan project auth tables", () => {
migrated = new Database(fusion); migrated = new Database(fusion);
migrated.init(); migrated.init();
expect(migrated.getSchemaVersion()).toBe(73); expect(migrated.getSchemaVersion()).toBe(74);
const tables = migrated const tables = migrated
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'") .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.all() as Array<{ name: string }>; .all() as Array<{ name: string }>;
@@ -2625,7 +2695,7 @@ describe("migration v67 drops orphan project auth tables", () => {
try { try {
fresh.init(); fresh.init();
expect(fresh.getSchemaVersion()).toBe(73); expect(fresh.getSchemaVersion()).toBe(74);
const tables = fresh const tables = fresh
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'") .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.all() as Array<{ name: string }>; .all() as Array<{ name: string }>;

View File

@@ -886,7 +886,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33) // Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir); const db1 = createDatabase(legacyDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(73); expect(db1.getSchemaVersion()).toBe(74);
db1.close(); db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables // Step 2: Manually downgrade to version 32 and drop insight tables
@@ -921,7 +921,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs"); expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration // Now run init — this triggers the v32→v33 migration
db3.init(); db3.init();
expect(db3.getSchemaVersion()).toBe(73); expect(db3.getSchemaVersion()).toBe(74);
// Step 4: Verify insight tables exist after migration // Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare( const tablesAfter = db3.prepare(
@@ -952,12 +952,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try { try {
const db1 = createDatabase(testDir); const db1 = createDatabase(testDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(73); expect(db1.getSchemaVersion()).toBe(74);
db1.close(); db1.close();
const db2 = createDatabase(testDir); const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow(); expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(73); expect(db2.getSchemaVersion()).toBe(74);
db2.close(); db2.close();
} finally { } finally {
rmSync(testDir, { recursive: true, force: true }); rmSync(testDir, { recursive: true, force: true });
@@ -971,7 +971,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh DB and run migrations // Step 1: Create a fresh DB and run migrations
const db1 = createDatabase(compatDir); const db1 = createDatabase(compatDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(73); expect(db1.getSchemaVersion()).toBe(74);
// Step 2: Strip lifecycle and cancelledAt columns by recreating the // Step 2: Strip lifecycle and cancelledAt columns by recreating the
// table without them. This simulates a DB that was created before the // table without them. This simulates a DB that was created before the

View File

@@ -2639,7 +2639,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => { describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => { it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
}); });
it("mission_features table has loop state columns", () => { it("mission_features table has loop state columns", () => {

View File

@@ -584,7 +584,7 @@ describe("Run Audit", () => {
}); });
it("schema version is bumped to 40", () => { it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
}); });
}); });
}); });

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true); expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true); expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(73); expect(db.getSchemaVersion()).toBe(74);
const index = db const index = db
.prepare( .prepare(

View File

@@ -119,7 +119,7 @@ export function probeFts5(db: DatabaseSync): boolean {
// ── Schema Definition ──────────────────────────────────────────────── // ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 73; const SCHEMA_VERSION = 74;
function normalizeTaskComments( function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined, steeringComments: SteeringComment[] | undefined,
@@ -220,6 +220,7 @@ CREATE TABLE IF NOT EXISTS tasks (
tokenUsageInputTokens INTEGER, tokenUsageInputTokens INTEGER,
tokenUsageOutputTokens INTEGER, tokenUsageOutputTokens INTEGER,
tokenUsageCachedTokens INTEGER, tokenUsageCachedTokens INTEGER,
tokenUsageCacheWriteTokens INTEGER,
tokenUsageTotalTokens INTEGER, tokenUsageTotalTokens INTEGER,
tokenUsageFirstUsedAt TEXT, tokenUsageFirstUsedAt TEXT,
tokenUsageLastUsedAt TEXT, tokenUsageLastUsedAt TEXT,
@@ -3157,6 +3158,12 @@ export class Database {
}); });
} }
if (version < 74) {
this.applyMigration(74, () => {
this.addColumnIfMissing("tasks", "tokenUsageCacheWriteTokens", "INTEGER");
});
}
} }
/** /**