mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
* 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>
166 lines
6.7 KiB
TypeScript
166 lines
6.7 KiB
TypeScript
/**
|
|
* Tests for `gbrain apply-migrations` — the migration runner CLI.
|
|
*
|
|
* Unit-scope: exercises the pure helpers (parseArgs, indexCompleted, buildPlan,
|
|
* statusForVersion). End-to-end integration against real orchestrators is
|
|
* covered by test/e2e/migration-flow.test.ts (Lane C-5).
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { __testing } from '../src/commands/apply-migrations.ts';
|
|
import type { CompletedMigrationEntry } from '../src/core/preferences.ts';
|
|
|
|
const { parseArgs, indexCompleted, buildPlan, statusForVersion } = __testing;
|
|
|
|
describe('parseArgs', () => {
|
|
test('default flags', () => {
|
|
const a = parseArgs([]);
|
|
expect(a.list).toBe(false);
|
|
expect(a.dryRun).toBe(false);
|
|
expect(a.yes).toBe(false);
|
|
expect(a.nonInteractive).toBe(false);
|
|
expect(a.mode).toBeUndefined();
|
|
expect(a.specificMigration).toBeUndefined();
|
|
expect(a.hostDir).toBeUndefined();
|
|
expect(a.noAutopilotInstall).toBe(false);
|
|
});
|
|
|
|
test('--list / --dry-run / --yes / --non-interactive', () => {
|
|
expect(parseArgs(['--list']).list).toBe(true);
|
|
expect(parseArgs(['--dry-run']).dryRun).toBe(true);
|
|
expect(parseArgs(['--yes']).yes).toBe(true);
|
|
expect(parseArgs(['--non-interactive']).nonInteractive).toBe(true);
|
|
});
|
|
|
|
test('--mode accepts valid values', () => {
|
|
expect(parseArgs(['--mode', 'always']).mode).toBe('always');
|
|
expect(parseArgs(['--mode', 'pain_triggered']).mode).toBe('pain_triggered');
|
|
expect(parseArgs(['--mode', 'off']).mode).toBe('off');
|
|
});
|
|
|
|
test('--migration and --host-dir parse values', () => {
|
|
const a = parseArgs(['--migration', '0.11.0', '--host-dir', '/tmp/abc']);
|
|
expect(a.specificMigration).toBe('0.11.0');
|
|
expect(a.hostDir).toBe('/tmp/abc');
|
|
});
|
|
|
|
test('--no-autopilot-install flips flag', () => {
|
|
expect(parseArgs(['--no-autopilot-install']).noAutopilotInstall).toBe(true);
|
|
});
|
|
|
|
test('--help sets help flag', () => {
|
|
expect(parseArgs(['--help']).help).toBe(true);
|
|
expect(parseArgs(['-h']).help).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('indexCompleted + statusForVersion', () => {
|
|
test('no entries → pending', () => {
|
|
const idx = indexCompleted([]);
|
|
expect(statusForVersion('0.11.0', idx)).toBe('pending');
|
|
});
|
|
|
|
test('one complete entry → complete', () => {
|
|
const entries: CompletedMigrationEntry[] = [
|
|
{ version: '0.11.0', status: 'complete', mode: 'always' },
|
|
];
|
|
const idx = indexCompleted(entries);
|
|
expect(statusForVersion('0.11.0', idx)).toBe('complete');
|
|
});
|
|
|
|
test('only partial entries → partial', () => {
|
|
const entries: CompletedMigrationEntry[] = [
|
|
{ version: '0.11.0', status: 'partial', apply_migrations_pending: true },
|
|
];
|
|
const idx = indexCompleted(entries);
|
|
expect(statusForVersion('0.11.0', idx)).toBe('partial');
|
|
});
|
|
|
|
test('partial then complete → complete (stopgap then v0.11.1 apply-migrations)', () => {
|
|
const entries: CompletedMigrationEntry[] = [
|
|
{ version: '0.11.0', status: 'partial', apply_migrations_pending: true },
|
|
{ version: '0.11.0', status: 'complete', mode: 'always' },
|
|
];
|
|
const idx = indexCompleted(entries);
|
|
expect(statusForVersion('0.11.0', idx)).toBe('complete');
|
|
});
|
|
|
|
test('only looks at the queried version', () => {
|
|
const entries: CompletedMigrationEntry[] = [
|
|
{ version: '0.10.0', status: 'complete' },
|
|
];
|
|
const idx = indexCompleted(entries);
|
|
expect(statusForVersion('0.11.0', idx)).toBe('pending');
|
|
expect(statusForVersion('0.10.0', idx)).toBe('complete');
|
|
});
|
|
});
|
|
|
|
describe('buildPlan — diff against completed + installed VERSION', () => {
|
|
test('fresh install (no entries) — v0.11.0 is pending when installed ≥ 0.11.0', () => {
|
|
const idx = indexCompleted([]);
|
|
const plan = buildPlan(idx, '0.11.1');
|
|
expect(plan.applied).toEqual([]);
|
|
expect(plan.partial).toEqual([]);
|
|
expect(plan.pending.map(m => m.version)).toContain('0.11.0');
|
|
// Future migrations (registered but newer than installed VERSION) land in
|
|
// skippedFuture until the binary catches up. v0.13.0 = frontmatter graph
|
|
// (master), v0.13.1 = Knowledge Runtime grandfather, v0.14.0 = shell
|
|
// jobs + autopilot cooperative.
|
|
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.0', '0.12.2', '0.13.0', '0.13.1', '0.14.0']);
|
|
});
|
|
|
|
test('already applied → v0.11.0 lands in `applied` bucket, not pending', () => {
|
|
const idx = indexCompleted([{ version: '0.11.0', status: 'complete' }]);
|
|
const plan = buildPlan(idx, '0.11.1');
|
|
expect(plan.applied.map(m => m.version)).toContain('0.11.0');
|
|
expect(plan.pending).toEqual([]);
|
|
});
|
|
|
|
test('stopgap wrote partial → v0.11.0 lands in `partial` bucket (resumable)', () => {
|
|
const idx = indexCompleted([
|
|
{ version: '0.11.0', status: 'partial', apply_migrations_pending: true },
|
|
]);
|
|
const plan = buildPlan(idx, '0.11.1');
|
|
expect(plan.partial.map(m => m.version)).toContain('0.11.0');
|
|
expect(plan.applied).toEqual([]);
|
|
expect(plan.pending).toEqual([]);
|
|
});
|
|
|
|
test('Codex H9 regression: installed older than migration → skippedFuture, not skipped silently', () => {
|
|
// Running a v0.10.x binary that somehow loaded a v0.11.0 migration registry:
|
|
// migration is skippedFuture (wait for a newer install), NOT ignored.
|
|
const idx = indexCompleted([]);
|
|
const plan = buildPlan(idx, '0.10.5');
|
|
expect(plan.skippedFuture.map(m => m.version)).toContain('0.11.0');
|
|
expect(plan.pending).toEqual([]);
|
|
});
|
|
|
|
test('Codex H9 regression: installed > migration version → still runs (not skipped)', () => {
|
|
// This is the critical bug Codex caught: the plan was "apply when version >
|
|
// installed", which would SKIP v0.11.0 when running v0.11.1. The correct
|
|
// rule is "apply when not in completed.jsonl AND version ≤ installed".
|
|
const idx = indexCompleted([]);
|
|
const plan = buildPlan(idx, '0.12.0');
|
|
expect(plan.pending.map(m => m.version)).toContain('0.11.0');
|
|
// v0.12.2, v0.13.0, v0.13.1, and v0.14.0 were added later; installed=0.12.0
|
|
// means they belong in skippedFuture, not pending. v0.11.0 and v0.12.0
|
|
// stay pending despite being ≤ installed — that is the H9 invariant.
|
|
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.2', '0.13.0', '0.13.1', '0.14.0']);
|
|
});
|
|
|
|
test('--migration filter narrows to one version', () => {
|
|
const idx = indexCompleted([]);
|
|
const plan = buildPlan(idx, '0.11.1', '0.11.0');
|
|
expect(plan.pending.map(m => m.version)).toEqual(['0.11.0']);
|
|
});
|
|
|
|
test('--migration filter for unknown version → empty plan', () => {
|
|
const idx = indexCompleted([]);
|
|
const plan = buildPlan(idx, '0.11.1', '99.99.99');
|
|
expect(plan.applied).toEqual([]);
|
|
expect(plan.pending).toEqual([]);
|
|
expect(plan.partial).toEqual([]);
|
|
expect(plan.skippedFuture).toEqual([]);
|
|
});
|
|
});
|