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>
63 lines
2.9 KiB
TypeScript
63 lines
2.9 KiB
TypeScript
/**
|
|
* v0.41.13 (#1433) — isSyncable / unsyncableReason classifier shape.
|
|
*
|
|
* The two public APIs route through the same internal `classifySync`
|
|
* helper so they cannot drift. These tests pin the contract that
|
|
* `isSyncable` returns true iff `unsyncableReason` returns null, AND
|
|
* pin the canonical SYNC_SKIP_FILES list (since the cleanup-loop guard
|
|
* at commands/sync.ts:772 keys on `unsyncableReason === 'metafile'`).
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import {
|
|
isSyncable,
|
|
unsyncableReason,
|
|
SYNC_SKIP_FILES,
|
|
type SyncableReason,
|
|
} from '../src/core/sync.ts';
|
|
|
|
describe('#1433 — isSyncable / unsyncableReason are duals of one classifier', () => {
|
|
const cases: Array<{ path: string; expected: SyncableReason | null; note: string }> = [
|
|
{ path: 'people/alice.md', expected: null, note: 'normal markdown page' },
|
|
{ path: 'docs/guide.mdx', expected: null, note: 'mdx accepted by markdown strategy' },
|
|
{ path: 'learning-and-strategy/log.md', expected: 'metafile', note: 'log.md anywhere is metafile' },
|
|
{ path: 'wiki/schema.md', expected: 'metafile', note: 'schema.md anywhere is metafile' },
|
|
{ path: 'index.md', expected: 'metafile', note: 'top-level index.md' },
|
|
{ path: 'README.md', expected: 'metafile', note: 'top-level README' },
|
|
{ path: 'docs/README.md', expected: 'metafile', note: 'nested README' },
|
|
{ path: 'people/alice.txt', expected: 'strategy', note: '.txt rejected by markdown strategy' },
|
|
{ path: 'ops/scratch/note.md', expected: 'pruned-dir', note: 'ops/ is pruned' },
|
|
{ path: '.git/notes.md', expected: 'pruned-dir', note: 'hidden dir pruned' },
|
|
{ path: 'node_modules/foo/README.md', expected: 'pruned-dir', note: 'node_modules pruned' },
|
|
];
|
|
|
|
for (const c of cases) {
|
|
test(`${c.path} → ${c.expected ?? 'syncable'} (${c.note})`, () => {
|
|
const reason = unsyncableReason(c.path);
|
|
const sync = isSyncable(c.path);
|
|
expect(reason).toBe(c.expected);
|
|
expect(sync).toBe(c.expected === null);
|
|
});
|
|
}
|
|
|
|
test('include glob: path not matching include returns include-glob-miss', () => {
|
|
expect(unsyncableReason('docs/guide.md', { include: ['people/**'] })).toBe('include-glob-miss');
|
|
expect(isSyncable('docs/guide.md', { include: ['people/**'] })).toBe(false);
|
|
});
|
|
|
|
test('exclude glob: matching path returns exclude-glob-hit', () => {
|
|
expect(unsyncableReason('drafts/wip.md', { exclude: ['drafts/**'] })).toBe('exclude-glob-hit');
|
|
expect(isSyncable('drafts/wip.md', { exclude: ['drafts/**'] })).toBe(false);
|
|
});
|
|
|
|
test('SYNC_SKIP_FILES export contains the canonical four basenames', () => {
|
|
expect([...SYNC_SKIP_FILES]).toEqual(['schema.md', 'index.md', 'log.md', 'README.md']);
|
|
});
|
|
|
|
test('isSyncable(p) === (unsyncableReason(p) === null) — duality holds for all canonical cases', () => {
|
|
for (const c of cases) {
|
|
expect(isSyncable(c.path)).toBe(unsyncableReason(c.path) === null);
|
|
}
|
|
});
|
|
});
|