* fix: 8 root-cause fixes from /investigate wave
Consolidated bundle of bug fixes from /investigate on the 8 deferred bugs.
Each fix was designed to go at the structural gap, not the symptom. Codex
verified 20 load-bearing claims on the plan; 12 triggered plan revisions.
Bug 2 — GBRAIN_POOL_SIZE env knob + init finally blocks (no auto-detect).
Covers both the singleton pool (db.ts) and instance pool (import.ts:140).
Bug 3 — Centralize migration ledger writes in apply-migrations runner.
Removed appendCompletedMigration from v0_11_0, v0_12_0, v0_12_2,
v0_13_0, v0_13_1. Added 3-partial wedge cap + --force-retry reset.
'complete wins' preserved; no partial can regress a completed migration.
Bug 5 — v0.14.0 migration registered. src/commands/migrations/v0_14_0.ts
ships Phase A (ALTER minion_jobs.max_stalled SET DEFAULT 3) + Phase B
(pending-host-work ping for shell-jobs adoption).
Bug 6/10 — jsonb_agg(DISTINCT ...) in legacy traverseGraph (both engines).
Presentation-level dedup; schema still preserves provenance rows.
Bug 7 — doctor --fast reads DB URL source via getDbUrlSource() in config.ts.
Precise message: 'Skipping DB checks (--fast mode, URL present from env)'
replaces the misleading 'No database configured'.
Bug 8 — max_stalled default bumped 1→3 in schema-embedded.ts, pglite-schema.ts,
schema.sql (new installs). v0_14_0 Phase A ALTER for existing installs.
autopilot-cycle handler yields to event loop between phases so the
worker's lock-renewal timer fires on huge brains. (Deep AbortSignal
threading through runEmbedCore/runExtractCore/runBacklinksCore/performSync
deferred to v0.15 queue polish.)
Bug 9 — Gate sync.last_commit on no-failures across all three sync paths
(incremental, full via runImport, gbrain import git continuity).
recordSyncFailures() helper + ~/.gbrain/sync-failures.jsonl with
dedup key path+commit+error-hash. New flags: --skip-failed (ack) +
--retry-failed (re-attempt). Doctor surfaces unacknowledged failures.
Bug 11 — brain_score breakdown fields on BrainHealth (embed_coverage_score,
link_density_score, timeline_coverage_score, no_orphans_score,
no_dead_links_score); sum equals brain_score by construction.
dead_links now on the type (resolves featuresTeaserForDoctor drift).
orphan_pages kept as 'islanded' (no inbound AND no outbound) and
docs updated to match — explicit semantic instead of doc drift.
New tests: test/traverse-graph-dedup.test.ts, test/sync-failures.test.ts,
test/brain-score-breakdown.test.ts, test/migration-resume.test.ts,
test/migrations-v0_14_0.test.ts. Extended: migrate, doctor, apply-migrations.
All 1696 unit tests pass locally. postgres-jsonb E2E regression unchanged
(none of these touch the JSONB write surface).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: v0.14.2 CHANGELOG + CLAUDE.md; align migration-flow E2E with runner-owned ledger
CHANGELOG: v0.14.2 entry in the standard release-summary format
(two-line headline + lead + numbers table + "what this means" +
"To take advantage of v0.14.2" self-repair block + itemized
changes grouped by reliability / observability / graph correctness /
new migration / tests / deferred-to-v0.15).
CLAUDE.md: new "Key commands added in v0.14.2" section covers
--skip-failed, --retry-failed, --force-retry, GBRAIN_POOL_SIZE env,
and the new doctor checks (sync_failures, brain_score breakdown).
Migration orchestrator docs updated to describe v0_14_0.ts + the
runner-owned ledger contract from Bug 3.
test/e2e/migration-flow.test.ts: three assertions updated to match
the Bug 3 contract — orchestrators no longer append to completed.jsonl
directly, so direct-orchestrator E2E calls leave the ledger empty.
Preferences assertions remain (that's still the orchestrator's side
of the contract). Runner's ledger write is covered by the unit suite
(test/apply-migrations.test.ts + test/migration-resume.test.ts).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
7.6 KiB
TypeScript
171 lines
7.6 KiB
TypeScript
/**
|
|
* Bug 3 regression — migration resume semantics.
|
|
*
|
|
* Covers:
|
|
* - statusForVersion prefers 'complete' over 'partial' (never regresses).
|
|
* - Three consecutive 'partial' entries flip a migration to 'wedged'.
|
|
* - 'retry' marker resets the counter; next run treats it as fresh.
|
|
* - appendCompletedMigration no-ops on double 'complete' (idempotency).
|
|
*
|
|
* Infrastructure: point HOME at a tmpdir so the ledger writes don't
|
|
* stomp the real ~/.gbrain/migrations/completed.jsonl.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
|
|
let tmpHome: string;
|
|
const originalHome = process.env.HOME;
|
|
|
|
beforeEach(() => {
|
|
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-migration-resume-'));
|
|
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 3 — statusForVersion semantics', () => {
|
|
test("complete wins over partial regardless of order", async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'complete' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
] as any);
|
|
expect(__testing.statusForVersion('0.13.0', idx)).toBe('complete');
|
|
|
|
const idx2 = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'complete' },
|
|
] as any);
|
|
expect(__testing.statusForVersion('0.13.0', idx2)).toBe('complete');
|
|
});
|
|
|
|
test('two consecutive partials stay at partial', async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
] as any);
|
|
expect(__testing.statusForVersion('0.13.0', idx)).toBe('partial');
|
|
});
|
|
|
|
test('three consecutive partials flip to wedged', async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
] as any);
|
|
expect(__testing.statusForVersion('0.13.0', idx)).toBe('wedged');
|
|
});
|
|
|
|
test("retry marker resets the counter", async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'retry' },
|
|
] as any);
|
|
// After 'retry', the version is pending (fresh start).
|
|
expect(__testing.statusForVersion('0.13.0', idx)).toBe('pending');
|
|
});
|
|
|
|
test('complete after wedge is still complete (terminal)', async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'retry' },
|
|
{ version: '0.13.0', status: 'complete' },
|
|
] as any);
|
|
expect(__testing.statusForVersion('0.13.0', idx)).toBe('complete');
|
|
});
|
|
});
|
|
|
|
describe('Bug 3 — appendCompletedMigration idempotency', () => {
|
|
test('writing complete when last entry is already complete is a no-op', async () => {
|
|
const { appendCompletedMigration, loadCompletedMigrations } = await import('../src/core/preferences.ts');
|
|
appendCompletedMigration({ version: '9.9.9', status: 'complete' });
|
|
const first = loadCompletedMigrations().filter(e => e.version === '9.9.9');
|
|
expect(first.length).toBe(1);
|
|
|
|
appendCompletedMigration({ version: '9.9.9', status: 'complete' });
|
|
const second = loadCompletedMigrations().filter(e => e.version === '9.9.9');
|
|
expect(second.length).toBe(1);
|
|
});
|
|
|
|
test('partial always appends (needed for attempt-cap counter)', async () => {
|
|
const { appendCompletedMigration, loadCompletedMigrations } = await import('../src/core/preferences.ts');
|
|
appendCompletedMigration({ version: '9.9.9', status: 'partial' });
|
|
appendCompletedMigration({ version: '9.9.9', status: 'partial' });
|
|
const entries = loadCompletedMigrations().filter(e => e.version === '9.9.9');
|
|
expect(entries.length).toBe(2);
|
|
});
|
|
|
|
test("'retry' status is accepted", async () => {
|
|
const { appendCompletedMigration, loadCompletedMigrations } = await import('../src/core/preferences.ts');
|
|
appendCompletedMigration({ version: '9.9.9', status: 'retry' } as any);
|
|
const entries = loadCompletedMigrations().filter(e => e.version === '9.9.9');
|
|
expect(entries.length).toBe(1);
|
|
expect(entries[0].status).toBe('retry');
|
|
});
|
|
});
|
|
|
|
describe('Bug 3 — orchestrator no longer writes the ledger directly', () => {
|
|
test('v0_13_0 does not import appendCompletedMigration', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/migrations/v0_13_0.ts', import.meta.url)).text();
|
|
expect(source).not.toContain('import { appendCompletedMigration }');
|
|
});
|
|
test('v0_13_1 does not import appendCompletedMigration', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/migrations/v0_13_1.ts', import.meta.url)).text();
|
|
expect(source).not.toContain('import { appendCompletedMigration }');
|
|
});
|
|
test('v0_12_0 does not import appendCompletedMigration', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/migrations/v0_12_0.ts', import.meta.url)).text();
|
|
expect(source).not.toContain('import { appendCompletedMigration }');
|
|
});
|
|
test('v0_12_2 does not import appendCompletedMigration', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/migrations/v0_12_2.ts', import.meta.url)).text();
|
|
expect(source).not.toContain('import { appendCompletedMigration }');
|
|
});
|
|
test('v0_11_0 does not import appendCompletedMigration', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/migrations/v0_11_0.ts', import.meta.url)).text();
|
|
// Import statement should not reference appendCompletedMigration; the
|
|
// old call site is replaced with a comment.
|
|
expect(source).not.toMatch(/import .*appendCompletedMigration.*from/);
|
|
});
|
|
|
|
test('apply-migrations.ts runner writes the ledger', async () => {
|
|
const source = await Bun.file(new URL('../src/commands/apply-migrations.ts', import.meta.url)).text();
|
|
expect(source).toContain("import { loadCompletedMigrations, appendCompletedMigration");
|
|
expect(source).toContain("appendCompletedMigration({");
|
|
expect(source).toContain("'retry'");
|
|
expect(source).toContain('--force-retry');
|
|
expect(source).toContain('MAX_CONSECUTIVE_PARTIALS');
|
|
});
|
|
});
|
|
|
|
describe('Bug 3 — buildPlan surfaces wedged migrations', () => {
|
|
test('wedged bucket exists in the plan', async () => {
|
|
const { __testing } = await import('../src/commands/apply-migrations.ts');
|
|
const idx = __testing.indexCompleted([
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
{ version: '0.13.0', status: 'partial' },
|
|
] as any);
|
|
const plan = __testing.buildPlan(idx, '0.13.0', '0.13.0'); // filter to just this version
|
|
expect(plan.wedged.length).toBe(1);
|
|
expect(plan.wedged[0].version).toBe('0.13.0');
|
|
expect(plan.pending.length).toBe(0);
|
|
expect(plan.partial.length).toBe(0);
|
|
});
|
|
});
|