mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* 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>
97 lines
4.2 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|