From 794dae319600953d1c4bd334335823f800024ac7 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Sat, 22 Aug 2026 19:04:03 +0000 Subject: [PATCH] fix(FN-149): advance schema ceiling to 0065 so Fusion stops rejecting its own database FN-149 shipped migration 0065_fn_149_review_convergence_stage.sql and registered REVIEW_CONVERGENCE_STAGE_VERSION but left SCHEMA_BASELINE_VERSION at "0064". The first store open applied and recorded 0065; the next open (project store, same boot) hit assertBinaryNotOlderThanDatabase, saw 0065 > 0064 and threw StaleBinarySchemaError, so every startup died with "this binary only knows up to 0064" on fresh and upgraded databases alike. - Bump SCHEMA_BASELINE_VERSION to "0065" (marker only: applies no SQL, touches no data). - Move the DB-free migration-wiring assertions out of the PostgreSQL integration file into src/__tests__/migration-wiring-integrity.test.ts and wire it into test:unit-gate, so the ceiling/migration drift now fails the merge gate instead of main's boot. - Refresh the stale migration-identity expectations (0062-0065). Symptom verification: `node scripts/dev-with-memory.mjs --isolated= --prebuild none` exited 1 with the guard error before; it now boots and serves the dashboard. --- .changeset/fn-schema-baseline-0065.md | 7 +++ packages/core/package.json | 2 +- .../migration-wiring-integrity.test.ts | 55 ++++++++++++++++++ .../__tests__/postgres/schema-applier.test.ts | 57 ++++++++----------- packages/core/src/postgres/schema-applier.ts | 13 ++++- 5 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 .changeset/fn-schema-baseline-0065.md create mode 100644 packages/core/src/__tests__/migration-wiring-integrity.test.ts diff --git a/.changeset/fn-schema-baseline-0065.md b/.changeset/fn-schema-baseline-0065.md new file mode 100644 index 0000000000..e575e1734f --- /dev/null +++ b/.changeset/fn-schema-baseline-0065.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix a startup crash where Fusion rejected the database it had just migrated. +category: fix +dev: FN-149 shipped migration `0065_fn_149_review_convergence_stage.sql` without advancing `SCHEMA_BASELINE_VERSION` (still `"0064"`), so the first store open applied and recorded 0065 and the next open threw `StaleBinarySchemaError` from `assertBinaryNotOlderThanDatabase` ("this binary only knows up to 0064"), exiting 1 on fresh and upgraded databases alike. Bumps the ceiling to `"0065"` (marker only — applies no SQL, touches no data) and moves the DB-free migration-wiring assertions to `packages/core/src/__tests__/migration-wiring-integrity.test.ts`, now wired into `test:unit-gate` so a migration landing without a ceiling bump fails the merge gate. diff --git a/packages/core/package.json b/packages/core/package.json index 7a68f0722b..c11e4caa43 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -64,7 +64,7 @@ "test": "vitest run --silent=passed-only --reporter=dot", "test:embedded-postgres": "vitest run src/__tests__/postgres/embedded-lifecycle.test.ts --silent=passed-only --reporter=dot", "test:pg-gate": "FUSION_PG_TEST_SETUP_PARTICIPANT=1 vitest run --config vitest.pg.config.ts src/__tests__/postgres/handoff-to-review-atomicity.pg.test.ts src/__tests__/postgres/task-lifecycle-e2e.pg.test.ts --silent=passed-only --reporter=dot", - "test:unit-gate": "vitest run src/__tests__/task-merge.test.ts src/__tests__/legacy-adoption.test.ts src/__tests__/no-hardcoded-lifecycle-columns.test.ts src/__tests__/sync-workflow-ir-callsite-allowlist.test.ts --silent=passed-only --reporter=dot" + "test:unit-gate": "vitest run src/__tests__/task-merge.test.ts src/__tests__/legacy-adoption.test.ts src/__tests__/no-hardcoded-lifecycle-columns.test.ts src/__tests__/sync-workflow-ir-callsite-allowlist.test.ts src/__tests__/migration-wiring-integrity.test.ts --silent=passed-only --reporter=dot" }, "devDependencies": { "@earendil-works/pi-coding-agent": "0.84.1", diff --git a/packages/core/src/__tests__/migration-wiring-integrity.test.ts b/packages/core/src/__tests__/migration-wiring-integrity.test.ts new file mode 100644 index 0000000000..62ddfa37e8 --- /dev/null +++ b/packages/core/src/__tests__/migration-wiring-integrity.test.ts @@ -0,0 +1,55 @@ +/* +FNXC:Lifecycle 2026-07-16-22:40: +Migration wiring integrity — the class guard for the FN-8141 crash. Migrations are +registered EXPLICITLY in schema-applier.ts (not auto-discovered), so a new .sql +file that is not wired through a version constant + bookkeeping check silently +never runs (documented hazard). PR #2260 tripped the adjacent trap: it added a +column to the model + 0000 baseline and bumped nothing, so existing DBs never got +it. + +FNXC:ReviewConvergence 2026-08-22-18:58: +These assertions used to live inside src/__tests__/postgres/schema-applier.test.ts, whose comment +claimed they "run in the merge gate" — they did not: the gate runs four named files via +`test:unit-gate` plus two *.pg.test.ts files, and that PostgreSQL-integration file is in neither. +The drift it was meant to catch then landed twice (0064, then FN-149's 0065 with the ceiling left +at 0064), and the second one made every Fusion startup fail: the binary applied 0065, recorded it, +then rejected its own database through assertBinaryNotOlderThanDatabase. + +They are moved here — a file with no PostgreSQL dependency, no fixtures and no timers, reading only +the migrations directory and the applier source — precisely so `test:unit-gate` can run them +deterministically in milliseconds. Gate admission evidence: a bootable `main` is the cheapest thing +this repository can verify, and this exact drift broke it. Keep this file DB-free; anything needing +a live database belongs in the PostgreSQL suite instead. +*/ + +import { describe, it, expect } from "vitest"; +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { SCHEMA_BASELINE_VERSION } from "../postgres/schema-applier.js"; + +describe("schema-applier: migration wiring integrity", () => { + const migrationsDir = fileURLToPath(new URL("../postgres/migrations", import.meta.url)); + const applierSource = readFileSync( + fileURLToPath(new URL("../postgres/schema-applier.ts", import.meta.url)), + "utf8", + ); + const migrationFiles = readdirSync(migrationsDir) + .filter((f) => /^\d{4}_.*\.sql$/.test(f)) + .sort(); + + it("advances SCHEMA_BASELINE_VERSION to the highest-numbered migration file", () => { + const highest = migrationFiles[migrationFiles.length - 1]!.slice(0, 4); + // A new column that ships a migration file must also bump the baseline marker + // (else the "all markers recorded" fast-path and upgrade bookkeeping drift, and + // the stale-binary guard rejects the database this very binary just migrated). + expect(SCHEMA_BASELINE_VERSION).toBe(highest); + }); + + it("wires every migration .sql file into the applier so none silently never runs", () => { + // The applier references each migration by its exact basename in a path + // constant. A file present on disk but absent from the source is unwired. + const unwired = migrationFiles.filter((f) => !applierSource.includes(f)); + expect(unwired).toEqual([]); + }); +}); diff --git a/packages/core/src/__tests__/postgres/schema-applier.test.ts b/packages/core/src/__tests__/postgres/schema-applier.test.ts index 4d008f00f7..7ea92ef9af 100644 --- a/packages/core/src/__tests__/postgres/schema-applier.test.ts +++ b/packages/core/src/__tests__/postgres/schema-applier.test.ts @@ -25,7 +25,7 @@ import { describe, it, expect, afterEach, beforeAll } from "vitest"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import { sql } from "drizzle-orm"; -import { readdirSync, readFileSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { applySchemaBaseline, @@ -104,6 +104,10 @@ import { TASK_SOURCE_AGENT_INDEX_VERSION, WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION, ACTIVITY_LOG_TASK_ID_INDEX_VERSION, + REMOVE_TASK_SUBTASK_SPLITTING_VERSION, + AI_MERGE_REVIEW_RECONCILIATION_VERSION, + TASK_REPOSITORY_SCOPE_VERSION, + REVIEW_CONVERGENCE_STAGE_VERSION, } from "../../postgres/schema-applier.js"; import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js"; import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.js"; @@ -139,7 +143,18 @@ describe("schema-applier: immutable migration identities", () => { expect(TASK_SOURCE_AGENT_INDEX_VERSION).toBe("0059"); expect(WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION).toBe("0060"); expect(ACTIVITY_LOG_TASK_ID_INDEX_VERSION).toBe("0061"); - expect(SCHEMA_BASELINE_VERSION).toBe("0063"); + /* + FNXC:ReviewConvergence 2026-08-22-18:58: + The tail of this list went stale twice in a row (it still asserted 0063 while the ceiling was + 0064), so it stopped being the tripwire it exists to be. Assert every identity through the + current head and keep the ceiling equal to the newest one — FN-149 shipped 0065 with the marker + left at 0064, and that mismatch made the binary reject its own database at startup. + */ + expect(REMOVE_TASK_SUBTASK_SPLITTING_VERSION).toBe("0062"); + expect(AI_MERGE_REVIEW_RECONCILIATION_VERSION).toBe("0063"); + expect(TASK_REPOSITORY_SCOPE_VERSION).toBe("0064"); + expect(REVIEW_CONVERGENCE_STAGE_VERSION).toBe("0065"); + expect(SCHEMA_BASELINE_VERSION).toBe("0065"); }); it("keeps monitor and approval isolation assigned to version 0003", () => { @@ -272,39 +287,13 @@ describe("schema-applier: immutable migration identities", () => { }); /* -FNXC:Lifecycle 2026-07-16-22:40: -Migration wiring integrity — the class guard for the FN-8141 crash. Migrations are -registered EXPLICITLY in schema-applier.ts (not auto-discovered), so a new .sql -file that is not wired through a version constant + bookkeeping check silently -never runs (documented hazard). PR #2260 tripped the adjacent trap: it added a -column to the model + 0000 baseline and bumped nothing, so existing DBs never got -it. These pure (no-PostgreSQL) assertions run in the merge gate and fail fast when -the baseline marker and the on-disk migration set drift out of sync. +FNXC:ReviewConvergence 2026-08-22-18:58: +The pure "migration wiring integrity" assertions (baseline ceiling == highest migration file, and +every .sql wired into the applier) MOVED to src/__tests__/migration-wiring-integrity.test.ts. They +need no PostgreSQL, and living in this integration file kept them out of the merge gate — which is +how FN-149 shipped migration 0065 with the ceiling left at 0064 and made every startup reject its +own database. The new home is wired into `test:unit-gate`. Do not re-add them here. */ -describe("schema-applier: migration wiring integrity", () => { - const migrationsDir = fileURLToPath(new URL("../../postgres/migrations", import.meta.url)); - const applierSource = readFileSync( - fileURLToPath(new URL("../../postgres/schema-applier.ts", import.meta.url)), - "utf8", - ); - const migrationFiles = readdirSync(migrationsDir) - .filter((f) => /^\d{4}_.*\.sql$/.test(f)) - .sort(); - - it("advances SCHEMA_BASELINE_VERSION to the highest-numbered migration file", () => { - const highest = migrationFiles[migrationFiles.length - 1]!.slice(0, 4); - // A new column that ships a migration file must also bump the baseline marker - // (else the "all markers recorded" fast-path and upgrade bookkeeping drift). - expect(SCHEMA_BASELINE_VERSION).toBe(highest); - }); - - it("wires every migration .sql file into the applier so none silently never runs", () => { - // The applier references each migration by its exact basename in a path - // constant. A file present on disk but absent from the source is unwired. - const unwired = migrationFiles.filter((f) => !applierSource.includes(f)); - expect(unwired).toEqual([]); - }); -}); interface TestContext { testUrl: string; diff --git a/packages/core/src/postgres/schema-applier.ts b/packages/core/src/postgres/schema-applier.ts index 2a4226883a..61d7de6e62 100644 --- a/packages/core/src/postgres/schema-applier.ts +++ b/packages/core/src/postgres/schema-applier.ts @@ -66,7 +66,18 @@ capacity-model table drop that landed while this PR was open. /* FNXC:TaskRecommendations 2026-08-13-22:23: upgrades must install the source-agent index before duplicate intake queries it. */ /* FNXC:WorkspaceLease 2026-08-15-12:00: the baseline ceiling must include durable coordination tables so an upgraded database is never rejected by the current binary. */ /* FNXC:ActivityLogTaskSearch 2026-08-20-04:17: advance the schema ceiling so durable central task-ID lookups receive their indexed upgrade. */ -export const SCHEMA_BASELINE_VERSION = "0064"; +/* +FNXC:ReviewConvergence 2026-08-22-18:58: +Advance the ceiling to 0065 for FN-149's review-convergence columns. FN-149 registered +REVIEW_CONVERGENCE_STAGE_VERSION and wired the migration but left this marker at 0064, which is a +SELF-REJECTION rather than a compatibility guard: the first open applies 0065 and records it in +fusion_schema_migrations, then the NEXT open (the project store, in the same boot) hits +assertBinaryNotOlderThanDatabase, sees 0065 > 0064, and throws StaleBinarySchemaError. Every +startup died with "this binary only knows up to 0064" on fresh and upgraded databases alike. This +marker is ONLY the binary's "highest migration I know" claim — bumping it applies no SQL and +touches no data; it must advance in the same change that ships a new migration file. +*/ +export const SCHEMA_BASELINE_VERSION = "0065"; /** FNXC:SymbolLock 2026-07-20-10:00: upgrades need durable task declarations before admission resolves symbols. */ export const TASK_DECLARED_SYMBOLS_VERSION = "0028"; const INITIAL_SCHEMA_VERSION = "0000";