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=<tmp> --prebuild none` exited 1 with the guard error before; it now boots and serves the dashboard.
This commit is contained in:
7
.changeset/fn-schema-baseline-0065.md
Normal file
7
.changeset/fn-schema-baseline-0065.md
Normal file
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user