* 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>
108 lines
4.6 KiB
TypeScript
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');
|
|
});
|
|
});
|