Files
gbrain/test/e2e/postgres-bootstrap.test.ts
T
6966623e0f v0.22.6.1 fix: PGLite/initSchema upgrade-hardening wave (closes 2-year wedge cycle) (#440)
* fix(initSchema): narrow pre-schema bootstrap + v24 PGLite no-op

Closes a 2-year-old wedge cycle that hit users 10+ times across 6 schema
versions (#239, #243, #266, #357, #366, #374, #375, #378, #395, #396).

Bug class: gbrain ships an embedded schema blob (PGLITE_SCHEMA_SQL +
SCHEMA_SQL) that runs before numbered migrations on every initSchema().
The blob references columns that newer migrations introduce. On any
brain older than the migration that adds those columns, the blob crashes
before the migration can run.

Fix: PGLiteEngine.initSchema() and PostgresEngine.initSchema() now call
a new private applyForwardReferenceBootstrap() before the schema blob.
The bootstrap probes for missing forward-referenced state and adds only
what's needed (sources table + pages.source_id, links.link_source +
links.origin_page_id, content_chunks.symbol_name + content_chunks.language).
Fresh installs and modern brains both no-op.

A CI guard test/schema-bootstrap-coverage.test.ts enforces that the
bootstrap covers every forward reference in PGLITE_SCHEMA_SQL. Future
migrations that add column-with-index in the schema blob must extend
the bootstrap; the test fails loudly otherwise.

Migration v24 (rls_backfill_missing_tables) now no-ops on PGLite via
sqlFor.pglite: '' since PGLite has no RLS engine and is single-tenant.
Closes #395.

The plan went through CEO + Eng + Codex review. Codex caught a critical
bug in the original "run all migrations early" approach: it would crash
on v24 trying to ALTER subagent tables that the schema blob hadn't
created yet. The narrow bootstrap shape resolves that.

Wave incorporates community PRs #398 (@vinsew), #399 (@jdcastro2),
#402 (@schnubb-web).

Co-Authored-By: vinsew <yiyangchaishu@gmail.com>
Co-Authored-By: Julián David Castro <juliancastro@Mac-mini-de-Julian.local>
Co-Authored-By: schnubb-web <info@mia-mai.de>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(test): bump beforeAll timeout on minions-shell-pglite for parallel-load flake

Default 5s beforeAll timeout occasionally trips under the parallel test runner
when many test files initialize PGLite concurrently. The same pattern is
documented as a P0 TODO for v0.21 Code Cathedral tests; this is the one
instance the upgrade-hardening wave directly exposed (CPU pressure from new
bootstrap test files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.21.1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.21.1

- CLAUDE.md: PGLite + Postgres engine entries note new
  applyForwardReferenceBootstrap() in initSchema(), v24
  sqlFor.pglite no-op, and the new bootstrap test files
  (test/bootstrap.test.ts, test/schema-bootstrap-coverage.test.ts,
  test/e2e/postgres-bootstrap.test.ts).
- CHANGELOG.md: voice polish on the v0.21.1 headline
  (drop stray ## prefixes so the bold two-line headline
  renders as bold prose, not h2 sub-headers that break
  the version-entry hierarchy).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: correct version slot from v0.22.5 to v0.21.6

Slot allocation correction. v0.21.6 is the actual landing slot for
this wave on the v0.21.x patch line.

VERSION, package.json, CHANGELOG.md (header + table + take-advantage
section), CLAUDE.md (engine entries, migrate.ts entry, test
descriptions) all updated together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: correct version slot to v0.22.7

VERSION, package.json, CHANGELOG.md (header + table + take-advantage
section), CLAUDE.md (engine entries, migrate.ts entry, test descriptions)
all updated together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms.txt + llms-full.txt for v0.22.7

CLAUDE.md changed (engine entries describe the bootstrap, migrate.ts entry
describes the v24 PGLite no-op). The build:llms regen-drift guard caught
the staleness in CI. Running `bun run build:llms` propagates the same
content into the AI-consumable bundles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: change version slot from v0.22.7 to v0.22.6-hotfix.1

PR #483 (fix/mcp-registration-auth) claimed v0.22.7. Moved this wave to
v0.22.6-hotfix.1 to avoid the collision. Note: semver-orders BEFORE
0.22.6 (pre-release suffix), so the hotfix tag is informational, not
ordering-correct. Acceptable here because the wave's content predates
master's 0.22.6 and is being landed as a parallel hotfix slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: change version slot to v0.22.6.1

4-digit hotfix slot under master's v0.22.6. bun + bun:test accept
the format; the build-llms regen-drift guard and bootstrap tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: vinsew <yiyangchaishu@gmail.com>
Co-authored-by: Julián David Castro <juliancastro@Mac-mini-de-Julian.local>
Co-authored-by: schnubb-web <info@mia-mai.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 02:16:23 -07:00

94 lines
4.0 KiB
TypeScript

/**
* E2E test for PostgresEngine forward-reference bootstrap.
*
* Codex caught that `test/e2e/helpers.ts:74` uses the standalone
* `db.initSchema()` from `src/core/db.ts`, which only runs SCHEMA_SQL and
* never calls runMigrations(). A test using that helper would NOT exercise
* `PostgresEngine.initSchema()`'s reordered path, producing false-positive
* coverage. This test deliberately bypasses the standard helper and
* instantiates `PostgresEngine` directly, calling `engine.initSchema()` so
* the bootstrap → SCHEMA_SQL → runMigrations sequence runs end-to-end.
*
* Covers issues #366, #375, #378 — Postgres-side wedges where pre-v0.18
* brains crashed on `column "source_id" does not exist`.
*
* NOTE: snapshot-based historical state simulation is out of scope for this
* wave (would require maintaining historical schema dumps). The test
* mutates a fresh-LATEST brain to a pre-v0.18 shape; codex flagged this as
* approximate. Acceptable here because the bootstrap's contract is narrow:
* "given a brain that lacks the specific forward-references, initSchema
* produces a brain at LATEST." The test exercises exactly that contract.
*
* Run: DATABASE_URL=postgresql://... bun run test:e2e test/e2e/postgres-bootstrap.test.ts
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import { LATEST_VERSION } from '../../src/core/migrate.ts';
const DATABASE_URL = process.env.DATABASE_URL;
const skip = !DATABASE_URL;
describe.skipIf(skip)('PostgresEngine forward-reference bootstrap (E2E)', () => {
let engine: PostgresEngine;
beforeAll(async () => {
engine = new PostgresEngine();
await engine.connect({ database_url: DATABASE_URL! });
});
afterAll(async () => {
await engine.disconnect();
});
test('PostgresEngine.initSchema applies bootstrap → SCHEMA_SQL → migrations on pre-v0.18 brain', async () => {
// First call: bring the test DB to LATEST shape so we have something to mutate.
await engine.initSchema();
// Clear data from prior tests in the suite. Adding a UNIQUE(slug)
// constraint below would fail if multi-source fixtures left rows with
// duplicate slugs across sources (which is valid under the composite
// UNIQUE this test is undoing).
const conn = (engine as any).sql;
await conn.unsafe(`TRUNCATE pages, content_chunks, links, tags, raw_data, timeline_entries, page_versions, ingest_log RESTART IDENTITY CASCADE`);
// Mutate to pre-v0.18 shape: drop source_id and the sources table.
// The advisory lock is released between initSchema calls, so this
// direct DDL won't deadlock.
await conn.unsafe(`
ALTER TABLE pages DROP CONSTRAINT IF EXISTS pages_source_slug_key;
ALTER TABLE pages ADD CONSTRAINT pages_slug_key UNIQUE (slug);
DROP INDEX IF EXISTS idx_pages_source_id;
ALTER TABLE pages DROP COLUMN IF EXISTS source_id CASCADE;
DROP TABLE IF EXISTS sources CASCADE;
`);
await engine.setConfig('version', '20');
// The path under test: full PostgresEngine.initSchema() including the
// bootstrap call, SCHEMA_SQL replay, and runMigrations chain.
await engine.initSchema();
expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));
// Verify the forward-referenced column exists after upgrade.
const colCheck = await conn`
SELECT column_name FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'pages'
AND column_name = 'source_id'
`;
expect(colCheck).toHaveLength(1);
// Verify the default source row was seeded.
const srcCheck = await conn`SELECT id FROM sources WHERE id = 'default'`;
expect(srcCheck).toHaveLength(1);
});
test('PostgresEngine.initSchema is idempotent on a brain already at LATEST', async () => {
// Fresh-LATEST brain. Calling initSchema again must not error and must
// not regress the version.
await engine.initSchema();
expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));
});
});