Merge pull request #43 from timothyjlaurent/timothyjlaurent/broken-insights

fix(insights): add missing lifecycle column compatibility check
This commit is contained in:
gsxdsm
2026-05-05 15:08:45 -07:00
committed by GitHub
2 changed files with 72 additions and 0 deletions

View File

@@ -963,4 +963,56 @@ describe("Migration: pre-33 DB upgrade", () => {
rmSync(testDir, { recursive: true, force: true });
}
});
it("ensureInsightRunsSchemaCompatibility adds lifecycle column to legacy table", () => {
const compatDir = mkdtempSync(join(tmpdir(), "fn-insight-compat-"));
try {
// Step 1: Create a fresh DB and run migrations
const db1 = createDatabase(compatDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(62);
// Step 2: Strip lifecycle and cancelledAt columns by recreating the
// table without them. This simulates a DB that was created before the
// lifecycle columns were added and already past v59 when they landed.
db1.exec(`
CREATE TABLE project_insight_runs_legacy AS
SELECT id, projectId, trigger, status, summary, error,
insightsCreated, insightsUpdated, inputMetadata, outputMetadata,
createdAt, startedAt, completedAt
FROM project_insight_runs
`);
db1.exec("DROP TABLE project_insight_runs");
db1.exec("ALTER TABLE project_insight_runs_legacy RENAME TO project_insight_runs");
// Verify lifecycle column is gone
const colsBefore = db1.prepare("PRAGMA table_info(project_insight_runs)").all() as Array<{ name: string }>;
const colNamesBefore = colsBefore.map((c) => c.name);
expect(colNamesBefore).not.toContain("lifecycle");
expect(colNamesBefore).not.toContain("cancelledAt");
db1.close();
// Step 3: Re-open — ensureInsightRunsSchemaCompatibility should add the
// missing columns unconditionally.
const db2 = createDatabase(compatDir);
db2.init();
const colsAfter = db2.prepare("PRAGMA table_info(project_insight_runs)").all() as Array<{ name: string }>;
const colNamesAfter = colsAfter.map((c) => c.name);
expect(colNamesAfter).toContain("lifecycle");
expect(colNamesAfter).toContain("cancelledAt");
// Step 4: Creating a run must not throw — proves the INSERT path works
// with the restored columns.
const s = new InsightStore(db2);
const run = s.createRun("proj", { trigger: "manual" });
expect(run.id).toBeTruthy();
expect(run.lifecycle).toBeDefined();
db2.close();
} finally {
rmSync(compatDir, { recursive: true, force: true });
}
});
});

View File

@@ -1068,6 +1068,7 @@ export class Database {
// Compatibility backfills that must run even when schemaVersion is current.
this.ensureRoutinesSchemaCompatibility();
this.ensureInsightRunsSchemaCompatibility();
// Seed config row idempotently with default settings
const configNow = new Date().toISOString();
@@ -1122,6 +1123,25 @@ export class Database {
this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesScope ON routines(scope)");
}
/**
* Applies idempotent compatibility fixes for the project_insight_runs table.
*
* The `lifecycle` and `cancelledAt` columns were added to SCHEMA_SQL and
* retroactively inserted into migration v33's CREATE TABLE, with a safety-net
* in migration v59. However, databases that were already at v59+ when the
* commit landed never re-run v59, leaving the columns missing. Running this
* unconditionally on every init guarantees the columns exist.
*/
private ensureInsightRunsSchemaCompatibility(): void {
if (!this.hasTable("project_insight_runs")) {
return;
}
this.addColumnIfMissing("project_insight_runs", "lifecycle", "TEXT");
this.addColumnIfMissing("project_insight_runs", "cancelledAt", "TEXT");
this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunsProjectTriggerStatus ON project_insight_runs(projectId, trigger, status)`);
}
private migrate(): void {
const version = this.getSchemaVersion() || 1;