Files
gbrain/test/migrations-v0_14_0.test.ts
Garry Tan ff10796a00 fix(wave): v0.15.1 - 4 hot issues + scope expansion (#248)
* fix(wave): 4 hot issues + 3 scope expansions (v0.13.1)

Addresses four user-filed regressions after v0.13.0 plus three adjacent
footgun closures.

* #170 — CREATE INDEX [CONCURRENTLY] IF NOT EXISTS idx_pages_updated_at_desc
  on pages (updated_at DESC). Engine-aware migration v12 with invalid-index
  cleanup on Postgres, plain CREATE on PGLite. ~700x on 30k+ row brains.
  Contributed by @fuleinist (#215).

* #219 — Minions schema default max_stalled 1 -> 5. v13 migration ALTERs
  the default and UPDATEs existing non-terminal rows (waiting/active/
  delayed/waiting-children/paused) so live queues get rescued on upgrade.
  Adds MinionJobInput.max_stalled with [1,100] clamp. New --max-stalled
  CLI flag on `jobs submit`. Reported by @macbotmini-eng.

* #218 — package.json postinstall surfaces errors instead of silencing.
  trustedDependencies whitelists @electric-sql/pglite. doctor
  schema_version check fails loudly when migrations never ran and links
  to #218. README + INSTALL_FOR_AGENTS warn against `bun install -g`.
  Reported by @gopalpatel.

* #223 — @electric-sql/pglite pinned to exactly 0.4.3 (was ^0.4.4).
  PGLiteEngine.connect() wraps PGlite.create() errors with a message
  pointing at the issue + gbrain doctor. Does NOT suggest 'missing
  migrations' as a cause (create-time abort happens before migrations
  run). Pin is unverified against macOS 26.3; error-wrap is the safety
  net. Reported by @AndreLYL.

* Scope: `gbrain jobs submit` gains --backoff-type/--backoff-delay/
  --backoff-jitter/--timeout-ms/--idempotency-key (MinionJobInput audit).
* Scope: `gbrain jobs smoke --sigkill-rescue` regression case (opt-in,
  CI-only) that simulates a killed worker and asserts the new default
  rescues.
* Scope: `gbrain doctor --index-audit` reports zero-scan Postgres indexes
  as drop candidates (informational; no auto-drop).

Infrastructure:
* Migration interface extended with sqlFor: { postgres?, pglite? } and
  transaction: boolean. Runner picks the engine-specific branch and
  bypasses engine.transaction() when transaction:false (required for
  CONCURRENTLY). BrainEngine.kind readonly discriminator added.
* scripts/check-jsonb-pattern.sh CI guard extended to block
  `max_stalled DEFAULT 1` from regressing.

Tests:
* 15 new unit tests: v12/v13 structural + behavioral assertions,
  max_stalled default/clamp/backfill, PGLite error-wrap source guard,
  engine kind discriminator.
* 3 regression tests pinned by IRON RULE.
* Full unit suite: 1416 pass.
* Full E2E suite against Postgres 16 + pgvector: 126 pass.

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

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

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

* docs: sync documentation for v0.13.1

CLAUDE.md "Key files" and "Commands" sections refreshed to match the
v0.13.1 fix wave:

- Note `BrainEngine.kind` discriminator on engine.ts
- Document v0.13.1 connect() error-wrap on pglite-engine.ts
- Refresh src/core/minions/ layout (no shell handler, no protected-names,
  no quiet-hours/stagger — that was v0.13-development scaffolding that
  did not ship)
- Add src/core/migrate.ts entry with `Migration` interface extensions
  (`sqlFor`, `transaction: false`)
- Document new `gbrain jobs submit` flags (--max-stalled, --backoff-type,
  --backoff-delay, --backoff-jitter, --timeout-ms, --idempotency-key)
- Document `gbrain jobs smoke --sigkill-rescue` regression guard
- Document `gbrain doctor --index-audit` and the schema_version=0
  surface that catches #218 postinstall failures
- Extend check-jsonb-pattern.sh note with the max_stalled DEFAULT 1
  regression guard
- Touch up test file blurbs for migrate.test.ts, pglite-engine.test.ts,
  minions.test.ts with v0.13.1 coverage

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

* test(e2e): run files sequentially to eliminate shared-DB race

The E2E suite was flaky. ~3 of every 5 runs had 4-10 failures clustered
in Links, Timeline, Versions, Minions resilience, Parallel Import, and
Page CRUD tests. Symptoms included "expected 16 pages, got 8" (half),
"expected 1 link inserted, got 0", timeline entries missing after
round-trip, and similar data-shape mismatches.

Root cause: bun test runs test FILES in parallel (each in a worker
process). 13 E2E files share one DATABASE_URL, and `setupDB()` in
`test/e2e/helpers.ts` does `TRUNCATE ... CASCADE` on all tables before
each file's `importFixtures()`. File A's TRUNCATE would race with file
B's in-flight INSERT stream, producing the observed half-populated or
wrong-count states.

An earlier attempt used a Postgres advisory lock held on a dedicated
single-connection client for the lifetime of each file's run. It broke
because bun's default 5000 ms hook timeout fires on queued beforeAll()
calls: with 13 files serializing through the lock, files 2-13 would
time out waiting for file 1 to finish.

This commit switches to sequential file execution at the harness level
via scripts/run-e2e.sh, which loops through test/e2e/*.test.ts one at
a time, tracks aggregate pass/fail counts, and exits non-zero on the
first failing file. No lock, no timeout issues, no changes to any test
file. package.json test:e2e points at the new script.

Verified: 5 back-to-back runs against the same Postgres container,
each completing in ~5 min. Every run: 13 files, 138 tests, 0 fails.

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

* chore: bump version to 0.15.1 (fix wave locked to MINOR line)

Master v0.14.2 was the last /investigate root-cause wave on the
v0.14.x line. This fix wave opens v0.15.x: four hot issues (#170,
#218, #219, #223) close v0.13.x regressions that v0.14.x didn't
cover, so the MINOR bump reflects the semantic shift — new schema
migrations (v14, v15), a new CLI surface (`--max-stalled`,
`--sigkill-rescue`, `--index-audit`), a new BrainEngine contract
(`kind` discriminator + extended `Migration` interface), and a new
install-time contract (PGLite 0.4.3 pin + `trustedDependencies`).

Locked to 0.15.1 in advance: other work may land before/after this
PR, but the version is fixed so reviewers can cite a stable number.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:19:23 -07:00

108 lines
4.6 KiB
TypeScript

/**
* Bug 5 + Bug 8 — v0_14_0 orchestrator regression.
*
* The migration ships:
* - Phase A (schema): ALTER minion_jobs.max_stalled SET DEFAULT 3
* - Phase B (host-work): append skill-ping entry to
* ~/.gbrain/migrations/pending-host-work.jsonl
*
* Both phases are idempotent — re-running the migration is a no-op after
* the first successful pass.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
let tmpHome: string;
const originalHome = process.env.HOME;
beforeEach(() => {
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-v0_14_0-'));
process.env.HOME = tmpHome;
});
afterEach(() => {
if (originalHome) process.env.HOME = originalHome;
else delete process.env.HOME;
try { rmSync(tmpHome, { recursive: true, force: true }); } catch { /* ignore */ }
});
describe('Bug 5 + Bug 8 — v0_14_0 module shape', () => {
test('v0_14_0 is registered in migrations/index.ts', async () => {
const { migrations } = await import('../src/commands/migrations/index.ts');
const m = migrations.find(x => x.version === '0.14.0');
expect(m).toBeDefined();
expect(m!.featurePitch.headline).toBeTruthy();
});
test('v0_14_0 does NOT write the ledger directly', async () => {
const source = await Bun.file(new URL('../src/commands/migrations/v0_14_0.ts', import.meta.url)).text();
expect(source).not.toContain('appendCompletedMigration');
});
test('orchestrator returns complete when phase A is skipped (no config)', async () => {
const { v0_14_0 } = await import('../src/commands/migrations/v0_14_0.ts');
// No loadConfig() backing → phaseASchema reports skipped (no brain).
// Phase B still emits the host-work ping.
const result = await v0_14_0.orchestrator({
yes: true,
dryRun: false,
noAutopilotInstall: true,
});
expect(['complete', 'partial']).toContain(result.status);
expect(result.version).toBe('0.14.0');
const hostWork = result.phases.find(p => p.name === 'host-work');
expect(hostWork).toBeDefined();
});
});
describe('Bug 5 — Phase B host-work entry dedup', () => {
test('first run writes the entry, second run is a skip', async () => {
const { v0_14_0 } = await import('../src/commands/migrations/v0_14_0.ts');
const first = await v0_14_0.orchestrator({ yes: true, dryRun: false, noAutopilotInstall: true });
const hostPath = join(tmpHome, '.gbrain', 'migrations', 'pending-host-work.jsonl');
expect(existsSync(hostPath)).toBe(true);
const beforeLines = readFileSync(hostPath, 'utf-8').split('\n').filter(l => l.trim()).length;
expect(beforeLines).toBe(1);
// Second run — Phase B should skip, not duplicate.
await v0_14_0.orchestrator({ yes: true, dryRun: false, noAutopilotInstall: true });
const afterLines = readFileSync(hostPath, 'utf-8').split('\n').filter(l => l.trim()).length;
expect(afterLines).toBe(1);
const entry = JSON.parse(readFileSync(hostPath, 'utf-8').split('\n')[0]);
expect(entry.migration).toBe('0.14.0');
expect(entry.skill).toBe('skills/migrations/v0.14.0.md');
});
test('dry-run writes nothing', async () => {
const { v0_14_0 } = await import('../src/commands/migrations/v0_14_0.ts');
await v0_14_0.orchestrator({ yes: true, dryRun: true, noAutopilotInstall: true });
const hostPath = join(tmpHome, '.gbrain', 'migrations', 'pending-host-work.jsonl');
expect(existsSync(hostPath)).toBe(false);
});
});
describe('Bug 8 — max_stalled default bumped in schema files', () => {
// v0.14.2 bumped schema default 1 -> 3 via Bug 8. v0.14.3 (#219 fix wave) further
// bumps to 5 for extra flaky-deploy headroom, plus adds UPDATE backfill of
// non-terminal rows via migration v15. These structural assertions track the
// current schema source state (not historical).
test('schema-embedded.ts has max_stalled DEFAULT 5', async () => {
const source = await Bun.file(new URL('../src/core/schema-embedded.ts', import.meta.url)).text();
expect(source).toContain('max_stalled INTEGER NOT NULL DEFAULT 5');
});
test('pglite-schema.ts has max_stalled DEFAULT 5', async () => {
const source = await Bun.file(new URL('../src/core/pglite-schema.ts', import.meta.url)).text();
expect(source).toContain('max_stalled INTEGER NOT NULL DEFAULT 5');
});
test('schema.sql has max_stalled DEFAULT 5', async () => {
const source = await Bun.file(new URL('../src/schema.sql', import.meta.url)).text();
expect(source).toContain('max_stalled INTEGER NOT NULL DEFAULT 5');
});
});