mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 17:45:16 +00:00
fix(import): make checkpoints staging-first — canonical dir identity + self-describing metadata
Port of #1731 (diazMelgarejo) onto current master. gbrain import wrote ~/.gbrain/import-checkpoint.json with the caller's raw dir argument, so a checkpoint left behind by an interrupted run (e.g. SIGTERM) could carry "." or a symlinked spelling — an identity that resolves to whatever CWD the next consumer happens to run from. Downstream tooling that treated the checkpoint dir as an owned staging boundary could then act on the wrong directory. - runImport captures the import target ONCE via resolveImportTargetDir (resolve + realpathSync) and threads that canonical value through collection, checkpoint load/save, and resume filtering - checkpoints are self-describing (schema_version: 1, owner: "gbrain", kind: "import"); loadCheckpoint tolerates absent metadata (legacy path-based files) but rejects present-and-wrong metadata and any relative dir - checkpoint contract documented in docs/guides/live-sync.md (llms bundle regenerated) - test/import-resume.test.ts fixture now realpaths its tmpdir so planted checkpoints match the canonicalized dir (macOS /var -> /private/var) Fixes #1728 Co-authored-by: Lawrence Melgarejo <Lawrence@cyre.me> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Lawrence Melgarejo
Claude Fable 5
parent
3aeb622dc7
commit
e2edbc64ac
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { writeFileSync, readFileSync, existsSync, mkdtempSync, rmSync } from 'fs';
|
||||
import { writeFileSync, readFileSync, existsSync, mkdtempSync, rmSync, mkdirSync, symlinkSync, realpathSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
saveCheckpoint,
|
||||
resumeFilter,
|
||||
clearCheckpoint,
|
||||
resolveImportTargetDir,
|
||||
type ImportCheckpoint,
|
||||
} from '../src/core/import-checkpoint.ts';
|
||||
|
||||
@@ -52,6 +53,9 @@ describe('loadCheckpoint', () => {
|
||||
|
||||
test('returns null when dir mismatches the current run', () => {
|
||||
const cp: ImportCheckpoint = {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/other/brain',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-05-14T00:00:00Z',
|
||||
@@ -86,6 +90,30 @@ describe('loadCheckpoint', () => {
|
||||
expect(stderrCaptured).not.toContain('Older checkpoint format');
|
||||
});
|
||||
|
||||
test('returns null when dir is relative (#1728 — CWD-dependent identity)', () => {
|
||||
writeFileSync(cpPath, JSON.stringify({
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '.',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-01-01T00:00:00Z',
|
||||
}));
|
||||
expect(loadCheckpoint(cpPath, '.')).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when self-describing metadata is wrong', () => {
|
||||
writeFileSync(cpPath, JSON.stringify({
|
||||
schema_version: 99,
|
||||
owner: 'some-tool',
|
||||
kind: 'other',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-01-01T00:00:00Z',
|
||||
}));
|
||||
expect(loadCheckpoint(cpPath, '/tmp/example-brain')).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when completedPaths contains non-strings', () => {
|
||||
writeFileSync(cpPath, JSON.stringify({
|
||||
dir: '/tmp/example-brain',
|
||||
@@ -97,6 +125,9 @@ describe('loadCheckpoint', () => {
|
||||
|
||||
test('returns the checkpoint for valid v0.33.2 payload', () => {
|
||||
const cp: ImportCheckpoint = {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['meetings/2026-05-13.md', 'concepts/foo.md'],
|
||||
timestamp: '2026-05-14T12:34:56Z',
|
||||
@@ -107,12 +138,31 @@ describe('loadCheckpoint', () => {
|
||||
expect(loaded?.dir).toBe('/tmp/example-brain');
|
||||
expect(loaded?.completedPaths).toEqual(['meetings/2026-05-13.md', 'concepts/foo.md']);
|
||||
expect(loaded?.timestamp).toBe('2026-05-14T12:34:56Z');
|
||||
expect(loaded?.schema_version).toBe(1);
|
||||
expect(loaded?.owner).toBe('gbrain');
|
||||
expect(loaded?.kind).toBe('import');
|
||||
});
|
||||
|
||||
test('returns legacy path-based checkpoint without metadata as v1 in memory', () => {
|
||||
writeFileSync(cpPath, JSON.stringify({
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-05-14T12:34:56Z',
|
||||
}));
|
||||
const loaded = loadCheckpoint(cpPath, '/tmp/example-brain');
|
||||
expect(loaded?.schema_version).toBe(1);
|
||||
expect(loaded?.owner).toBe('gbrain');
|
||||
expect(loaded?.kind).toBe('import');
|
||||
expect(loaded?.dir).toBe('/tmp/example-brain');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveCheckpoint', () => {
|
||||
test('round-trips through loadCheckpoint', () => {
|
||||
const cp: ImportCheckpoint = {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['a.md', 'b.md', 'c.md'],
|
||||
timestamp: '2026-05-14T00:00:00Z',
|
||||
@@ -125,16 +175,25 @@ describe('saveCheckpoint', () => {
|
||||
|
||||
test('serializes completedPaths sorted (deterministic output)', () => {
|
||||
saveCheckpoint(cpPath, {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['z.md', 'a.md', 'm.md'],
|
||||
timestamp: '2026-05-14T00:00:00Z',
|
||||
});
|
||||
const onDisk = JSON.parse(readFileSync(cpPath, 'utf-8'));
|
||||
expect(onDisk.schema_version).toBe(1);
|
||||
expect(onDisk.owner).toBe('gbrain');
|
||||
expect(onDisk.kind).toBe('import');
|
||||
expect(onDisk.completedPaths).toEqual(['a.md', 'm.md', 'z.md']);
|
||||
});
|
||||
|
||||
test('atomic-ish write — no stray .tmp file after success', () => {
|
||||
saveCheckpoint(cpPath, {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-05-14T00:00:00Z',
|
||||
@@ -148,6 +207,9 @@ describe('saveCheckpoint', () => {
|
||||
const badPath = join(workDir, 'does-not-exist', 'cp.json');
|
||||
expect(() =>
|
||||
saveCheckpoint(badPath, {
|
||||
schema_version: 1,
|
||||
owner: 'gbrain',
|
||||
kind: 'import',
|
||||
dir: '/tmp/example-brain',
|
||||
completedPaths: ['a.md'],
|
||||
timestamp: '2026-05-14T00:00:00Z',
|
||||
@@ -157,6 +219,32 @@ describe('saveCheckpoint', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveImportTargetDir', () => {
|
||||
test('captures a relative import target as an absolute real path', () => {
|
||||
const target = join(workDir, 'staging');
|
||||
mkdirSync(target);
|
||||
const cwd = process.cwd();
|
||||
try {
|
||||
process.chdir(workDir);
|
||||
expect(resolveImportTargetDir('staging')).toBe(realpathSync(target));
|
||||
} finally {
|
||||
process.chdir(cwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('collapses symlink spelling to the real import target', () => {
|
||||
const target = join(workDir, 'real-staging');
|
||||
const link = join(workDir, 'linked-staging');
|
||||
mkdirSync(target);
|
||||
symlinkSync(target, link);
|
||||
expect(resolveImportTargetDir(link)).toBe(realpathSync(target));
|
||||
});
|
||||
|
||||
test('throws when the target does not exist', () => {
|
||||
expect(() => resolveImportTargetDir(join(workDir, 'nope'))).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resumeFilter', () => {
|
||||
test('empty completed set returns all files unchanged', () => {
|
||||
const all = ['a.md', 'b.md', 'c.md'];
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* `afterAll`) per CLAUDE.md test-isolation rules R3 + R4.
|
||||
*/
|
||||
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from 'fs';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, mkdirSync, realpathSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
@@ -52,7 +52,9 @@ beforeEach(async () => {
|
||||
gbrainHomeDir = join(workspace, '.gbrain');
|
||||
mkdirSync(gbrainHomeDir, { recursive: true });
|
||||
cpPath = join(gbrainHomeDir, 'import-checkpoint.json');
|
||||
brainDir = mkdtempSync(join(tmpdir(), 'gbrain-import-resume-brain-'));
|
||||
// #1728: realpath so planted checkpoints match runImport's canonicalized
|
||||
// dir (macOS tmpdir is a /var → /private/var symlink).
|
||||
brainDir = realpathSync(mkdtempSync(join(tmpdir(), 'gbrain-import-resume-brain-')));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user