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": "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: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: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": {
|
"devDependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "0.84.1",
|
"@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 postgres from "postgres";
|
||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { readdirSync, readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import {
|
import {
|
||||||
applySchemaBaseline,
|
applySchemaBaseline,
|
||||||
@@ -104,6 +104,10 @@ import {
|
|||||||
TASK_SOURCE_AGENT_INDEX_VERSION,
|
TASK_SOURCE_AGENT_INDEX_VERSION,
|
||||||
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
|
||||||
ACTIVITY_LOG_TASK_ID_INDEX_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";
|
} from "../../postgres/schema-applier.js";
|
||||||
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
|
||||||
import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.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(TASK_SOURCE_AGENT_INDEX_VERSION).toBe("0059");
|
||||||
expect(WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION).toBe("0060");
|
expect(WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION).toBe("0060");
|
||||||
expect(ACTIVITY_LOG_TASK_ID_INDEX_VERSION).toBe("0061");
|
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", () => {
|
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:
|
FNXC:ReviewConvergence 2026-08-22-18:58:
|
||||||
Migration wiring integrity — the class guard for the FN-8141 crash. Migrations are
|
The pure "migration wiring integrity" assertions (baseline ceiling == highest migration file, and
|
||||||
registered EXPLICITLY in schema-applier.ts (not auto-discovered), so a new .sql
|
every .sql wired into the applier) MOVED to src/__tests__/migration-wiring-integrity.test.ts. They
|
||||||
file that is not wired through a version constant + bookkeeping check silently
|
need no PostgreSQL, and living in this integration file kept them out of the merge gate — which is
|
||||||
never runs (documented hazard). PR #2260 tripped the adjacent trap: it added a
|
how FN-149 shipped migration 0065 with the ceiling left at 0064 and made every startup reject its
|
||||||
column to the model + 0000 baseline and bumped nothing, so existing DBs never got
|
own database. The new home is wired into `test:unit-gate`. Do not re-add them here.
|
||||||
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.
|
|
||||||
*/
|
*/
|
||||||
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 {
|
interface TestContext {
|
||||||
testUrl: string;
|
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: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: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. */
|
/* 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. */
|
/** FNXC:SymbolLock 2026-07-20-10:00: upgrades need durable task declarations before admission resolves symbols. */
|
||||||
export const TASK_DECLARED_SYMBOLS_VERSION = "0028";
|
export const TASK_DECLARED_SYMBOLS_VERSION = "0028";
|
||||||
const INITIAL_SCHEMA_VERSION = "0000";
|
const INITIAL_SCHEMA_VERSION = "0000";
|
||||||
|
|||||||
Reference in New Issue
Block a user