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", () => {
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", () => {
const ts = db.getLastModified();
@@ -312,7 +318,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
});
it("does not overwrite existing config on re-init", () => {
// Update the config
@@ -1377,7 +1383,7 @@ describe("schema migrations", () => {
db.init();
// 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
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);
db.init();
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
db.close();
});
@@ -1441,7 +1447,7 @@ describe("schema migrations", () => {
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 }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -1482,13 +1488,14 @@ describe("schema migrations", () => {
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 colNames = cols.map((col) => col.name);
expect(colNames).toContain("tokenUsageInputTokens");
expect(colNames).toContain("tokenUsageOutputTokens");
expect(colNames).toContain("tokenUsageCachedTokens");
expect(colNames).toContain("tokenUsageCacheWriteTokens");
expect(colNames).toContain("tokenUsageTotalTokens");
expect(colNames).toContain("tokenUsageFirstUsedAt");
expect(colNames).toContain("tokenUsageLastUsedAt");
@@ -1498,6 +1505,7 @@ describe("schema migrations", () => {
tokenUsageInputTokens,
tokenUsageOutputTokens,
tokenUsageCachedTokens,
tokenUsageCacheWriteTokens,
tokenUsageTotalTokens,
tokenUsageFirstUsedAt,
tokenUsageLastUsedAt
@@ -1508,6 +1516,7 @@ describe("schema migrations", () => {
expect(task.tokenUsageInputTokens).toBeNull();
expect(task.tokenUsageOutputTokens).toBeNull();
expect(task.tokenUsageCachedTokens).toBeNull();
expect(task.tokenUsageCacheWriteTokens).toBeNull();
expect(task.tokenUsageTotalTokens).toBeNull();
expect(task.tokenUsageFirstUsedAt).toBeNull();
expect(task.tokenUsageLastUsedAt).toBeNull();
@@ -1551,7 +1560,7 @@ describe("schema migrations", () => {
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 colNames = cols.map((col) => col.name);
@@ -1791,7 +1800,7 @@ describe("schema migrations", () => {
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 }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1865,7 +1874,7 @@ describe("schema migrations", () => {
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 }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1889,7 +1898,7 @@ describe("schema migrations", () => {
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 }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1993,7 +2002,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -2176,6 +2185,67 @@ describe("schema migrations", () => {
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", () => {
tmpDir = makeTmpDir();
const dbSourcePath = join(dirname(fileURLToPath(import.meta.url)), "..", "db.ts");
@@ -2462,7 +2532,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();
@@ -2598,7 +2668,7 @@ describe("migration v67 drops orphan project auth tables", () => {
migrated = new Database(fusion);
migrated.init();
expect(migrated.getSchemaVersion()).toBe(73);
expect(migrated.getSchemaVersion()).toBe(74);
const tables = migrated
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.all() as Array<{ name: string }>;
@@ -2625,7 +2695,7 @@ describe("migration v67 drops orphan project auth tables", () => {
try {
fresh.init();
expect(fresh.getSchemaVersion()).toBe(73);
expect(fresh.getSchemaVersion()).toBe(74);
const tables = fresh
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.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)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(73);
expect(db1.getSchemaVersion()).toBe(74);
db1.close();
// 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");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(73);
expect(db3.getSchemaVersion()).toBe(74);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -952,12 +952,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(73);
expect(db1.getSchemaVersion()).toBe(74);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(73);
expect(db2.getSchemaVersion()).toBe(74);
db2.close();
} finally {
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
const db1 = createDatabase(compatDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(73);
expect(db1.getSchemaVersion()).toBe(74);
// Step 2: Strip lifecycle and cancelledAt columns by recreating 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)", () => {
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", () => {

View File

@@ -584,7 +584,7 @@ describe("Run Audit", () => {
});
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_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(73);
expect(db.getSchemaVersion()).toBe(74);
const index = db
.prepare(

View File

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