mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
Codex review on the initial self-heal patch (b1671ee) found 3 real gaps:
- P1: the self-heal ran even under --dry-run, mutating the filesystem
during what's documented as a preview-only command. Gated the whole
self-heal (both discoverGitRoot and the headCommit read) on
!opts.dryRun, same as the existing !opts.sourceId ownership check.
- P2: if `git init` succeeded but the process died before the baseline
commit landed, the next run's discoverGitRoot would succeed (`.git`
exists) and skip recovery entirely, permanently wedging on "No
commits in repo" forever. Added the same self-heal at the
`git rev-parse HEAD` catch site, sharing a new createSyncBaselineCommit
helper with the discoverGitRoot catch.
- P2: the baseline commit inherited the operator's global
commit.gpgSign, which can block headless cron/launchd runs on an
unavailable signing agent/pinentry. Added --no-gpg-sign.
Two new tests cover dry-run no-mutation and unborn-HEAD recovery.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCVEvUVm15bFuKqskWgfNS
149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
/**
|
|
* #2964 — sync phase self-heals a never-git-initialized default brain dir.
|
|
*
|
|
* A legacy `sync.repo_path`-anchored default brain (no `sources` row, no
|
|
* `--source`) can reach `performSync` pointed at a directory that was never
|
|
* `git init`-ed (predates git-backed sync, or was rsync'd from another
|
|
* machine without its `.git`). Before this fix, `discoverGitRoot` threw
|
|
* unconditionally and the dream cycle's sync phase failed every night with
|
|
* no self-recovery, even though `doctor`'s sync checks reported "ok" (for
|
|
* an unrelated reason — they only look at the `sources` table, which has
|
|
* no rows for this kind of brain).
|
|
*
|
|
* gbrain owns this directory outright, so the fix self-heals by
|
|
* `git init`-ing it and capturing the current on-disk state as the sync
|
|
* baseline — but ONLY for the default/no-`sourceId` path. A registered
|
|
* local source (`sources add --path`, no `--url`) is the user's own
|
|
* external directory and must keep failing loudly rather than being
|
|
* silently git-initialized without consent.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, rmSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
|
|
function mdPage(title: string, body = 'Content.'): string {
|
|
return `---\ntype: note\ntitle: ${title}\n---\n\n${body}`;
|
|
}
|
|
|
|
describe('#2964: sync auto-inits a never-git-initialized default brain dir', () => {
|
|
let engine: PGLiteEngine;
|
|
let dir: string;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
}, 60_000);
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
dir = mkdtempSync(join(tmpdir(), 'gbrain-2964-'));
|
|
writeFileSync(join(dir, 'page1.md'), mdPage('Page 1'));
|
|
writeFileSync(join(dir, 'page2.md'), mdPage('Page 2'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('default (no sourceId) sync on a non-git dir auto-inits git and imports files', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
expect(existsSync(join(dir, '.git'))).toBe(false);
|
|
|
|
const result = await performSync(engine, {
|
|
repoPath: dir,
|
|
noPull: true,
|
|
noEmbed: true,
|
|
full: true,
|
|
});
|
|
|
|
expect(result.status).toBe('first_sync');
|
|
expect(result.added).toBe(2);
|
|
expect(existsSync(join(dir, '.git'))).toBe(true);
|
|
expect(await engine.getPage('page1')).not.toBeNull();
|
|
expect(await engine.getPage('page2')).not.toBeNull();
|
|
});
|
|
|
|
test('a second sync after auto-init sees no changes (baseline commit captured current on-disk state)', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
const first = await performSync(engine, {
|
|
repoPath: dir,
|
|
noPull: true,
|
|
noEmbed: true,
|
|
full: true,
|
|
});
|
|
expect(first.added).toBe(2);
|
|
|
|
// No new files, no explicit `full` — a real incremental sync against the
|
|
// auto-init baseline. Before this fix there was no baseline to diff
|
|
// against (sync errored outright); a naive fix that skipped the initial
|
|
// commit would make this call re-report both files as "added" again.
|
|
const second = await performSync(engine, {
|
|
repoPath: dir,
|
|
noPull: true,
|
|
noEmbed: true,
|
|
});
|
|
expect(second.status).not.toBe('first_sync');
|
|
expect(second.added).toBe(0);
|
|
expect(second.modified).toBe(0);
|
|
});
|
|
|
|
test('a registered local source (sourceId set, no remote_url) on a non-git dir still throws — not auto-inited', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
await expect(
|
|
performSync(engine, {
|
|
repoPath: dir,
|
|
sourceId: 'default',
|
|
noPull: true,
|
|
noEmbed: true,
|
|
full: true,
|
|
}),
|
|
).rejects.toThrow(/git repository/i);
|
|
expect(existsSync(join(dir, '.git'))).toBe(false);
|
|
});
|
|
|
|
test('--dry-run on a non-git dir throws without writing anything to disk', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
await expect(
|
|
performSync(engine, {
|
|
repoPath: dir,
|
|
dryRun: true,
|
|
noPull: true,
|
|
noEmbed: true,
|
|
full: true,
|
|
}),
|
|
).rejects.toThrow(/git repository/i);
|
|
// The whole point of --dry-run is "preview only" — it must never git-init
|
|
// or commit on our behalf, even though we could self-heal.
|
|
expect(existsSync(join(dir, '.git'))).toBe(false);
|
|
});
|
|
|
|
test('unborn-HEAD recovery: a bare `git init` with zero commits (interrupted prior self-heal) still completes', async () => {
|
|
const { performSync } = await import('../src/commands/sync.ts');
|
|
const { execSync } = await import('child_process');
|
|
// Simulate a self-heal that ran `git init` but died before the baseline
|
|
// commit landed (process killed, disk full, etc.) — `.git` exists so
|
|
// discoverGitRoot succeeds, but `git rev-parse HEAD` still fails.
|
|
execSync('git init -q', { cwd: dir });
|
|
|
|
const result = await performSync(engine, {
|
|
repoPath: dir,
|
|
noPull: true,
|
|
noEmbed: true,
|
|
full: true,
|
|
});
|
|
|
|
expect(result.status).toBe('first_sync');
|
|
expect(result.added).toBe(2);
|
|
expect(execSync('git rev-parse HEAD', { cwd: dir }).toString().trim()).not.toBe('');
|
|
});
|
|
});
|