mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
Three verified-open defects, one family: sync silently destroying or diverging on content it should preserve. #2404 (P0) — 'ops' was hardcoded in PRUNE_DIR_NAMES (a v0.2.0-era carve-out), so any path with an ops segment was 'pruned-dir': committed ops/*.md never imported, and modified ops/* files hit the unsyncableModified delete loop (whose #1433 guard only spared 'metafile'), silently deleting put-created pages like the bundled daily-task-manager's canonical ops/tasks on every sync. Fix: remove 'ops' from the prune list (ordinary user content; the vendor/generated entries stay), and harden the delete loop to also skip 'pruned-dir' — a page under a pruned dir can only exist via a deliberate put_page. #2426 (P0) — write-through content stayed DB-only and was deleted by sync --full. All three compounding bugs fixed: 1. writePageThrough now best-effort commits the artifact (path-limited git commit) on durability-hardened repos, so the post-commit hook can push it; result carries committed?: boolean. 2. scripts/brain-commit-push.sh stages+commits BEFORE any pull — the old fetch+pull-rebase-first order aborted on any dirty tree, so the helper could never commit a MODIFIED page; brain_push's rebase-on-reject already handles an advanced remote. 3. The full-sync delete-reconcile partitions stale pages by git history (listEverCommittedPaths): never-committed source_paths are DB-only write-through — pages are KEPT and re-exported to the working tree instead of soft-deleted. Builds on the #2828 mass-delete valve (covers the below-valve cases). #2607 — the sync --full git ls-files fast path bypassed pruneDir, so a full pass imported (and resurrected soft-deleted) pages under dot-dirs and vendored trees that incremental sync excludes. Fix: isCollectibleForWalker applies the same segment-level pruneDir gate as classifySync, so full and incremental enumeration agree. One regression test per defect (all verified failing against master src): test/sync-ops-pages.serial.test.ts, test/write-through-commit.serial.test.ts, the #2426 helper-order test in test/brain-durability-hook.serial.test.ts, test/sync-reconcile-db-only.serial.test.ts, test/import-git-fastpath-prune.test.ts. Fixes #2404 Fixes #2426 Fixes #2607 Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
127 lines
5.2 KiB
TypeScript
127 lines
5.2 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { isSyncable, isCodeFilePath, slugifyCodePath, pathToSlug } from '../src/core/sync.ts';
|
|
|
|
describe('isCodeFilePath', () => {
|
|
test('recognizes common code extensions', () => {
|
|
expect(isCodeFilePath('src/foo.ts')).toBe(true);
|
|
expect(isCodeFilePath('app/foo.tsx')).toBe(true);
|
|
expect(isCodeFilePath('lib/bar.js')).toBe(true);
|
|
expect(isCodeFilePath('Foo.jsx')).toBe(true);
|
|
expect(isCodeFilePath('bundle.mjs')).toBe(true);
|
|
expect(isCodeFilePath('legacy.cjs')).toBe(true);
|
|
expect(isCodeFilePath('script.py')).toBe(true);
|
|
expect(isCodeFilePath('class.rb')).toBe(true);
|
|
expect(isCodeFilePath('main.go')).toBe(true);
|
|
});
|
|
|
|
test('rejects non-code extensions', () => {
|
|
expect(isCodeFilePath('notes.md')).toBe(false);
|
|
expect(isCodeFilePath('photo.jpg')).toBe(false);
|
|
expect(isCodeFilePath('README')).toBe(false);
|
|
// v0.20.0 Cathedral II Layer 2 widens the classifier to include
|
|
// config formats (.json, .yaml, .toml) and web formats (.css, .html,
|
|
// .vue) because the chunker supports them — they were dropped by the
|
|
// 9-extension v0.19.0 allowlist. `.json` is NO LONGER rejected.
|
|
expect(isCodeFilePath('image.svg')).toBe(false);
|
|
expect(isCodeFilePath('archive.zip')).toBe(false);
|
|
});
|
|
|
|
test('is case-insensitive', () => {
|
|
expect(isCodeFilePath('Foo.TS')).toBe(true);
|
|
expect(isCodeFilePath('Main.GO')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('isSyncable with strategy', () => {
|
|
test('default strategy (markdown) behaves as before — only .md/.mdx', () => {
|
|
expect(isSyncable('people/alice.md')).toBe(true);
|
|
expect(isSyncable('docs/guide.mdx')).toBe(true);
|
|
expect(isSyncable('src/foo.ts')).toBe(false);
|
|
});
|
|
|
|
test('strategy=code allows ts/tsx/js/py/rb/go, rejects markdown', () => {
|
|
expect(isSyncable('src/foo.ts', { strategy: 'code' })).toBe(true);
|
|
expect(isSyncable('src/foo.py', { strategy: 'code' })).toBe(true);
|
|
expect(isSyncable('src/foo.go', { strategy: 'code' })).toBe(true);
|
|
expect(isSyncable('notes.md', { strategy: 'code' })).toBe(false);
|
|
});
|
|
|
|
test('strategy=auto accepts both markdown and code', () => {
|
|
expect(isSyncable('notes.md', { strategy: 'auto' })).toBe(true);
|
|
expect(isSyncable('src/foo.ts', { strategy: 'auto' })).toBe(true);
|
|
});
|
|
|
|
test('existing skip rules apply across all strategies', () => {
|
|
// Hidden directories are always skipped
|
|
expect(isSyncable('.git/config.js', { strategy: 'code' })).toBe(false);
|
|
// README.md is skipped under markdown
|
|
expect(isSyncable('README.md', { strategy: 'markdown' })).toBe(false);
|
|
// ops/ is ordinary content — NOT skipped (#2404)
|
|
expect(isSyncable('ops/migrate.py', { strategy: 'code' })).toBe(true);
|
|
// vendored trees always skipped
|
|
expect(isSyncable('vendor/pkg/migrate.py', { strategy: 'code' })).toBe(false);
|
|
// .raw/ sidecar always skipped
|
|
expect(isSyncable('dir/.raw/code.ts', { strategy: 'code' })).toBe(false);
|
|
});
|
|
|
|
test('include globs whitelist specific patterns', () => {
|
|
expect(isSyncable('src/foo.ts', { strategy: 'code', include: ['src/**/*.ts'] })).toBe(true);
|
|
expect(isSyncable('lib/bar.ts', { strategy: 'code', include: ['src/**/*.ts'] })).toBe(false);
|
|
expect(isSyncable('src/foo.py', { strategy: 'code', include: ['src/**/*.ts'] })).toBe(false);
|
|
});
|
|
|
|
test('exclude globs blacklist specific patterns', () => {
|
|
expect(isSyncable('src/foo.ts', { strategy: 'code', exclude: ['**/*.test.ts'] })).toBe(true);
|
|
expect(isSyncable('test/foo.test.ts', { strategy: 'code', exclude: ['**/*.test.ts'] })).toBe(false);
|
|
});
|
|
|
|
test('include + exclude compose (include first, then exclude)', () => {
|
|
expect(
|
|
isSyncable('src/foo.ts', {
|
|
strategy: 'code',
|
|
include: ['src/**/*.ts'],
|
|
exclude: ['**/*.test.ts'],
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isSyncable('src/foo.test.ts', {
|
|
strategy: 'code',
|
|
include: ['src/**/*.ts'],
|
|
exclude: ['**/*.test.ts'],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('slugifyCodePath', () => {
|
|
test('flattens path with hyphens and replaces dots', () => {
|
|
expect(slugifyCodePath('src/core/chunkers/code.ts')).toBe('src-core-chunkers-code-ts');
|
|
expect(slugifyCodePath('app/models/user.rb')).toBe('app-models-user-rb');
|
|
expect(slugifyCodePath('lib/foo/bar/baz.go')).toBe('lib-foo-bar-baz-go');
|
|
});
|
|
|
|
test('drops leading ./', () => {
|
|
expect(slugifyCodePath('./src/foo.ts')).toBe('src-foo-ts');
|
|
});
|
|
|
|
test('lowercases', () => {
|
|
expect(slugifyCodePath('Src/Foo.TS')).toBe('src-foo-ts');
|
|
});
|
|
});
|
|
|
|
describe('pathToSlug with pageKind', () => {
|
|
test('pageKind=markdown (default) uses slugifyPath', () => {
|
|
expect(pathToSlug('people/alice-smith.md')).toBe('people/alice-smith');
|
|
expect(pathToSlug('docs/guide.md', undefined, { pageKind: 'markdown' })).toBe('docs/guide');
|
|
});
|
|
|
|
test('pageKind=code uses slugifyCodePath (flattened)', () => {
|
|
expect(pathToSlug('src/core/sync.ts', undefined, { pageKind: 'code' })).toBe('src-core-sync-ts');
|
|
});
|
|
|
|
test('repoPrefix is prepended (markdown and code)', () => {
|
|
expect(pathToSlug('guides/foo.md', 'gbrain')).toBe('gbrain/guides/foo');
|
|
expect(pathToSlug('src/foo.ts', 'gbrain', { pageKind: 'code' })).toBe('gbrain/src-foo-ts');
|
|
});
|
|
});
|