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>
66 lines
3.2 KiB
TypeScript
66 lines
3.2 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: 'RESOLVER.md', expected: 'metafile', note: 'top-level master routing config (closes #345)' },
|
|
{ path: 'brain/RESOLVER.md', expected: 'metafile', note: 'RESOLVER.md anywhere is metafile (closes #345)' },
|
|
{ path: 'people/alice.txt', expected: 'strategy', note: '.txt rejected by markdown strategy' },
|
|
{ path: 'ops/scratch/note.md', expected: null, note: 'ops/ is ordinary content, not pruned (#2404)' },
|
|
{ path: 'vendor/pkg/note.md', expected: 'pruned-dir', note: 'vendor/ 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 structural metafiles', () => {
|
|
expect([...SYNC_SKIP_FILES]).toEqual(['schema.md', 'index.md', 'log.md', 'README.md', 'RESOLVER.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);
|
|
}
|
|
});
|
|
});
|