Files
gbrain/test/cli-dream-engine-warn.test.ts
T
f56cc619ba v0.41.13.0 fix(sync): infiniteGameExp + foxhoundinc 5-bug wave (#1422, #1433, #1434, #1309, #1436) (#1456)
* fix(sync): infiniteGameExp + foxhoundinc 5-bug wave (#1422, #1433, #1434, #1309, #1436)

Five real production bugs from infiniteGameExp (PostgreSQL onboarding) and
foxhoundinc (dream-cycle reproduction), each silent-failure shape where gbrain
told the user the operation succeeded when it didn't.

* #1422 — `gbrain dream` swallowed connectEngine errors. Bind the caught
  error and surface `[dream] WARNING: could not connect to DB (...)` on
  stderr before falling through to filesystem-only phases. runDream(null)
  no-DB fallback preserved.

* #1433 — `gbrain sync` deleted previously-indexed log.md / schema.md /
  index.md / README.md pages on every re-sync. Refactor isSyncable
  through private classifySync helper; expose unsyncableReason (companion
  returning the same tagged reason) and SYNC_SKIP_FILES named export.
  Cleanup loop guards on reason === 'metafile' before deleting.

* #1434 — `gbrain sync` without --source on single-vault brains routed
  to source_id='default' (zero pages) and silently failed. Add resolver
  tier 5.5 'sole_non_default' AFTER brain_default (explicit user intent
  wins). Wire runSync + runImport to call resolveSourceWithTier
  unconditionally so the tier actually fires. Stderr nudge on tier hit;
  suppress with GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1.

* #1309 — overlapping ingest roots created duplicate pages. New
  BrainEngine.findDuplicatePage?(sourceId, {hash, frontmatterId}) with
  identity-based posture: SKIP when frontmatter.id matches (true
  external duplicate), WARN-ALWAYS on content_hash collision with
  different/missing fm.id, FAIL CLOSED on lookup error. Migration v95
  adds partial index pages_dedup_idx (Postgres CONCURRENTLY, PGLite
  plain CREATE).

* #1436 — MCP fuzzy get_page returned slug candidates from sources
  outside caller's scope. resolveSlugs signature extended with
  {sourceId?, sourceIds?} matching the sourceScopeOpts helper output;
  operations.ts threads it through. Both engines preserve unscoped
  back-compat for internal CLI callers.

Plus a stable tiebreaker on searchVector ORDER BY (score DESC, page_id
ASC, chunk_id ASC) in both engines. Caught while wiring the index above
— basis-vector eval fixtures with tied scores depend on planner row
order, which any new index on pages could flip. Pins eval-replay-gate
ranking determinism against future index changes.

Per codex review of the original plan: caught 6 load-bearing gaps that
the engineering review missed (runSync bypass, #1436 misclassified as
fixed, dedup fail-open, content-hash-alone too aggressive, soft-delete
filter missing, tier-ordering contradiction). All folded in pre-merge.

Tests: 65 new wave cases across 7 new files + 1 extended; all green.

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

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

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-05-25 19:53:59 -07:00

97 lines
4.2 KiB
TypeScript

/**
* v0.41.13 (#1422) — `gbrain dream` surfaces engine-connect failures on stderr.
*
* Pre-fix bug (foxhoundinc): dream wrapped `connectEngine()` in `try {} catch {}`
* that silently swallowed the failure. The cycle then ran with `engine: null`,
* every DB phase reported "No database connection: connect() has not been
* called", and the user saw exit 0 + "lint + backlinks done" with no clue why.
*
* Post-fix: the catch binds the error and writes a single `[dream] WARNING:`
* line to stderr naming the connect failure and the consequence (DB phases
* will be skipped). The cycle still runs filesystem phases honestly — no
* behavior change for that path.
*
* Subprocess test so we exercise the real cli.ts dispatch end-to-end.
*/
import { describe, test, expect } from 'bun:test';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
function runDream(args: string[], extraEnv: Record<string, string> = {}): {
stdout: string;
stderr: string;
status: number;
} {
const result = spawnSync('bun', ['run', 'src/cli.ts', 'dream', ...args], {
cwd: process.cwd(),
encoding: 'utf8',
env: { ...process.env, ...extraEnv },
timeout: 30_000,
});
return {
stdout: result.stdout ?? '',
stderr: result.stderr ?? '',
status: result.status ?? -1,
};
}
describe('#1422 — dream surfaces connectEngine failures', () => {
test('connect failure prints WARNING line on stderr (does not swallow silently)', () => {
const tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-dream-warn-'));
const tmpBrain = mkdtempSync(join(tmpdir(), 'gbrain-dream-brain-'));
try {
// Point GBRAIN_HOME at an empty tempdir so loadConfig sees no config,
// then force a bad Postgres URL via env so connectEngine throws on
// the attempt to reach a non-existent server. Port 9 is the discard
// protocol — guaranteed not to accept Postgres traffic.
// GBRAIN_HOME=/tmp/x yields configDir() === '/tmp/x/.gbrain' (config.ts:687-690).
mkdirSync(join(tmpHome, '.gbrain'), { recursive: true });
writeFileSync(join(tmpHome, '.gbrain', 'config.json'), JSON.stringify({
engine: 'postgres',
database_url: 'postgresql://nobody:nobody@127.0.0.1:9/nodb',
}));
const { stderr, status } = runDream(['--dir', tmpBrain, '--phase', 'lint'], {
GBRAIN_HOME: tmpHome,
});
// Filesystem-only phases still run; exit code reflects cycle outcome,
// not connect failure. The KEY contract: the WARNING text appears.
expect(stderr).toContain('[dream] WARNING: could not connect to DB');
expect(stderr).toContain('Running filesystem-only phases');
expect(stderr).toContain('DB-dependent phases');
// Sanity: process did not hang / segfault. Exit is success or non-zero
// (filesystem phases are tolerant). We assert it terminated, not the code.
expect(status).toBeGreaterThanOrEqual(0);
} finally {
rmSync(tmpHome, { recursive: true, force: true });
rmSync(tmpBrain, { recursive: true, force: true });
}
});
test('successful connect emits NO WARNING line', () => {
const tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-dream-ok-'));
const tmpBrain = mkdtempSync(join(tmpdir(), 'gbrain-dream-brain-'));
try {
// PGLite at a fresh tempdir always connects cleanly. No DATABASE_URL.
// GBRAIN_HOME=/tmp/x yields configDir() === '/tmp/x/.gbrain' (config.ts:687-690).
mkdirSync(join(tmpHome, '.gbrain'), { recursive: true });
writeFileSync(join(tmpHome, '.gbrain', 'config.json'), JSON.stringify({
engine: 'pglite',
}));
const { stderr, status } = runDream(['--dir', tmpBrain, '--phase', 'lint'], {
GBRAIN_HOME: tmpHome,
// Strip any inherited Postgres URL so PGLite is unambiguously the engine.
DATABASE_URL: '',
GBRAIN_DATABASE_URL: '',
});
expect(stderr).not.toContain('[dream] WARNING: could not connect to DB');
expect(status).toBeGreaterThanOrEqual(0);
} finally {
rmSync(tmpHome, { recursive: true, force: true });
rmSync(tmpBrain, { recursive: true, force: true });
}
});
});