diff --git a/bin/gstack-gbrain-source-wireup b/bin/gstack-gbrain-source-wireup index a364db921..7947fd587 100755 --- a/bin/gstack-gbrain-source-wireup +++ b/bin/gstack-gbrain-source-wireup @@ -332,8 +332,21 @@ do_wireup() { exit 0 fi + # #2662: `sync --repo ` resolves against the brain's DEFAULT source and + # can silently repoint that source's local_path anchor at our worktree while + # the source registered above gets nothing. Target the registered source by + # id. `--source` support is probed first (the documented floor is gbrain >= + # 0.18.0 and nothing proves the flag exists there): an older gbrain keeps the + # wrong-but-working --repo call with an upgrade warning, never a hard failure. local sync_out sync_redacted - sync_out=$(gbrain sync --repo "$WORKTREE" 2>&1) || { + local -a sync_cmd + if gbrain sync --help 2>/dev/null | grep -q -- '--source'; then + sync_cmd=(gbrain sync --source "$id") + else + warn "this gbrain's sync lacks --source; falling back to 'sync --repo' (upgrade gbrain so the sync targets source $id directly — #2662)" + sync_cmd=(gbrain sync --repo "$WORKTREE") + fi + sync_out=$("${sync_cmd[@]}" 2>&1) || { # Redact any postgres:// URLs from the error message in case gbrain logged # a connection error containing the full DSN with password. The user sees # "***REDACTED***" instead of credentials in their stderr or any log. diff --git a/test/gstack-gbrain-source-wireup.test.ts b/test/gstack-gbrain-source-wireup.test.ts index e40d3b6cc..71e2d8b17 100644 --- a/test/gstack-gbrain-source-wireup.test.ts +++ b/test/gstack-gbrain-source-wireup.test.ts @@ -42,10 +42,12 @@ let gbrainStateFile: string; function makeFakeGbrain(opts: { version?: string | null; // null = "binary missing" (don't write the file) syncFails?: boolean; + syncHelpNoSource?: boolean; // simulate an older gbrain whose sync lacks --source }) { const version = opts.version ?? '0.18.2'; if (version === null) return; // simulate missing binary by NOT writing one const syncFails = opts.syncFails ?? false; + const syncHelpNoSource = opts.syncHelpNoSource ?? false; // Stub gbrain reads/writes state from a JSON file. Fields: // sources: [{id, local_path, federated}] @@ -109,6 +111,13 @@ json.dump(state, open('$STATE','w'), indent=2) fi # sync --repo

→ records, optionally fails +# sync --help → advertise flags (the wireup probes this before choosing the +# sync form; the default fake mirrors a current gbrain, which HAS --source) +if [ "$1" = "sync" ] && [ "$2" = "--help" ]; then + echo "Usage: gbrain sync [--repo ]${syncHelpNoSource ? '' : ' [--source ]'}" + exit 0 +fi + if [ "$1" = "sync" ]; then ${syncFails ? 'echo "sync failed: connection error" >&2; exit 1' : 'echo "1 page imported"; exit 0'} fi @@ -193,6 +202,31 @@ describe('gstack-gbrain-source-wireup — wireup mode', () => { expect(state.sources[0].federated).toBe(true); }); + test('the real sync targets the REGISTERED source, never --repo (#2662)', () => { + // `sync --repo ` resolves against the brain's DEFAULT source and can + // silently repoint its local_path anchor at our worktree. This case runs + // WITHOUT GSTACK_BRAIN_NO_SYNC — the skip-mode cases never reach the sync, + // so asserting the sync argv there would be vacuous. + setupGstackRepo('git@github.com:user/gstack-brain-user.git'); + makeFakeGbrain({}); + const r = run([]); + expect(r.status).toBe(0); + const calls = gbrainCalls(); + expect(calls.some((c) => c.startsWith('gbrain sync --source gstack-brain-user'))).toBe(true); + expect(calls.some((c) => c.includes('sync --repo'))).toBe(false); + }); + + test('older gbrain without sync --source: falls back to --repo with an upgrade warning', () => { + setupGstackRepo('git@github.com:user/gstack-brain-user.git'); + makeFakeGbrain({ syncHelpNoSource: true }); + const r = run([]); + expect(r.status).toBe(0); + const calls = gbrainCalls(); + expect(calls.some((c) => c.startsWith('gbrain sync --repo'))).toBe(true); + expect(calls.some((c) => c.includes('sync --source '))).toBe(false); + expect(r.stderr).toContain('#2662'); + }); + test('idempotent re-run after success: no new sources add call', () => { setupGstackRepo('git@github.com:user/gstack-brain-user.git'); makeFakeGbrain({});