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:
Sinabina
2026-07-17 14:09:17 -07:00
co-authored by Lawrence Melgarejo Claude Fable 5
parent 3aeb622dc7
commit e2edbc64ac
6 changed files with 166 additions and 6 deletions
+89 -1
View File
@@ -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'];
+4 -2
View File
@@ -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(() => {