Files
gbrain/test/sort-newest-first.test.ts
T
3325b405bb v0.34.2.0 fix(import): path-based checkpoint resume — kills parallel-drop + failed-file-skip + sort-flip bugs (#988)
* feat(sync): sort files newest-first for faster salience on recent content

Problem: sync processes files in git-diff order (alphabetical), so
meetings/2020-* embeds before meetings/2026-*. After a burst of writes,
new pages can be invisible to search for hours while older pages process first.

Fix: sort addsAndMods descending in both incremental sync and full import.
Brain paths are date-prefixed by convention, so lexicographic descending
naturally prioritizes recent content.

This ensures the most relevant pages become searchable first.

* feat(import): path-based checkpoint resume + sort-newest-first helper

Replace gbrain import's positional `processedIndex` checkpoint with a
path-set checkpoint via `src/core/import-checkpoint.ts`. A file is only
"done" when its processFile returns success — failed files never enter
the set, parallel workers can't lose slow files, and sort-order changes
don't drop the newest N files on resume.

Three bug classes fixed:
- Parallel import + slow worker = silent file drop on crash-resume
- Failed file = checkpoint advanced past it, never retried until manual clear
- Sort-order flip (v0.33.x) = cross-version resume drops newest N files

Old positional checkpoints are detected on first resume and discarded
with a stderr log line. Re-walking is cheap because content_hash
short-circuits unchanged files.

Also extracts the descending-lex sort into src/core/sort-newest-first.ts
so import.ts and sync.ts share a single source of truth.

Tests:
- test/sort-newest-first.test.ts (5 hermetic cases)
- test/import-checkpoint.test.ts (18 unit cases over the helpers)
- test/import-resume.test.ts (refactored — GBRAIN_HOME isolation,
  drives runImport against PGLite, 5 integration cases including
  SLUG_MISMATCH retry regression)

Includes the original sort-newest-first contribution from
@garrytan-agents's PR #964 (commit 8dbcf6a5).

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update project documentation for v0.34.2.0

Add CLAUDE.md Key Files entries for the path-based import checkpoint
work: new entries for src/core/import-checkpoint.ts and
src/core/sort-newest-first.ts, plus a dedicated src/commands/import.ts
entry covering the v0.34.2.0 refactor. Update src/commands/sync.ts
entry to reference sortNewestFirst. Regenerate llms-full.txt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(tests): swap banned /data/brain placeholder for /tmp/example-brain

scripts/check-privacy.sh banlist includes /data/brain/ (legacy private
OpenClaw fork layout). New test files must not use it — CI privacy
guard caught this on PR #988's first push.

No behavior change. test/import-checkpoint.test.ts is unit-level with
no fs access; the dir string is just an identity marker for the
loadCheckpoint dir-mismatch guard.

---------

Co-authored-by: garrytan-agents <garrytan-agents@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 20:51:11 -07:00

49 lines
1.5 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { sortNewestFirst } from '../src/core/sort-newest-first.ts';
describe('sortNewestFirst', () => {
test('date-prefixed paths get sorted newest-first', () => {
const input = [
'meetings/2020-01-01-old.md',
'meetings/2026-05-13-recent.md',
'meetings/2024-03-15-middle.md',
];
const out = sortNewestFirst([...input]);
expect(out[0]).toBe('meetings/2026-05-13-recent.md');
expect(out[1]).toBe('meetings/2024-03-15-middle.md');
expect(out[2]).toBe('meetings/2020-01-01-old.md');
});
test('mixed prefixes produce deterministic descending order', () => {
const input = [
'concepts/a.md',
'meetings/2026-05-13.md',
'people/zoe.md',
'daily/2026-05-12.md',
];
const out = sortNewestFirst([...input]);
// Pure lex descending — pin the exact order so the contract is locked.
expect(out).toEqual([
'people/zoe.md',
'meetings/2026-05-13.md',
'daily/2026-05-12.md',
'concepts/a.md',
]);
});
test('empty input returns empty', () => {
expect(sortNewestFirst([])).toEqual([]);
});
test('single element returns single element', () => {
expect(sortNewestFirst(['only.md'])).toEqual(['only.md']);
});
test('mutates in place AND returns the same reference', () => {
const arr = ['a.md', 'c.md', 'b.md'];
const ret = sortNewestFirst(arr);
expect(ret).toBe(arr); // same reference
expect(arr).toEqual(['c.md', 'b.md', 'a.md']); // caller's view reflects the sort
});
});