Skip to content

fix(gbrain-sync): sourceLocalPath handles wrapped {sources:[...]} shape from gbrain v0.30+#1622

Open
Evode-Manirahari wants to merge 1 commit into
garrytan:mainfrom
Evode-Manirahari:fix/sources-list-shape-compat
Open

fix(gbrain-sync): sourceLocalPath handles wrapped {sources:[...]} shape from gbrain v0.30+#1622
Evode-Manirahari wants to merge 1 commit into
garrytan:mainfrom
Evode-Manirahari:fix/sources-list-shape-compat

Conversation

@Evode-Manirahari
Copy link
Copy Markdown

Problem

After upgrading to gstack v1.40.0.0 + gbrain v0.30.0.0 or newer (current released gbrain is v0.37.1.0), the first /sync-gbrain after upgrade dies before any stage runs:

$ /sync-gbrain
[gbrain-sync] mode=incremental engine=pglite
gstack-gbrain-sync fatal: list.find is not a function.
(In 'list.find((s) => s.id === sourceId)', 'list.find' is undefined)

Root cause

gbrain sources list --json started wrapping its output as {"sources":[...]} in gbrain v0.30+. The v1.40 hostname-fold migration helper sourceLocalPath() (added in #1547, bin/gstack-gbrain-sync.ts:291) typed the response as a bare array and called .find() on it directly:

// before
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(...);
if (!list) return null;
const found = list.find((s) => s.id === sourceId);  // crash: list is an object, not an array

The sibling helper in lib/gbrain-sources.ts (statusForSourceId, lines 72-85) already unwraps both shapes correctly — sourceLocalPath was the lone holdout because it was added separately in the v1.40 migration work.

Actual output on a current gbrain:

$ gbrain sources list --json
{
  "sources": [
    { "id": "default", "name": "default", ... },
    { "id": "gstack-code-myrepo-b6de5213", "local_path": "/Users/me/myrepo", ... }
  ]
}

Fix

Accept both wire shapes (defense against the same regression in either direction):

const raw = execGbrainJson<
  | Array<{ id: string; local_path?: string }>
  | { sources?: Array<{ id: string; local_path?: string }> }
>(["sources", "list", "--json"], { baseEnv: env });
if (!raw) return null;
const list = Array.isArray(raw) ? raw : raw.sources || [];
const found = list.find((s) => s.id === sourceId);

Tests

Three new cases in the existing sourceLocalPath describe block in test/gstack-gbrain-sync.test.ts:

  1. returns local_path when gbrain wraps the list as {sources: [...]} (v0.30+ shape) — the regression.
  2. returns null when the wrapped {sources: [...]} shape has no matching id.
  3. treats {sources: undefined} the same as an empty list — defensive case for partial responses.

The existing 3 bare-array tests still pass, locking in backward compat with pre-v0.30 gbrain shims.

Run: bun test test/gstack-gbrain-sync.test.ts → 38 pass, 0 fail (130 expect() calls).

Repro on a fresh machine

# Upgrade gstack to v1.40 (in CHANGELOG, v1.40.0.0 — the hostname-fold migration release)
# Have a working gbrain >= v0.30
cd <any-git-repo>
/sync-gbrain
# → "gstack-gbrain-sync fatal: list.find is not a function"

Found via

Hit this while syncing a freshly-shipped repo against gbrain v0.37.1.0 from a fresh gstack v1.40.0.0 install. Patched locally first; this PR brings the fix upstream.

gbrain v0.30+ wraps `gbrain sources list --json` output as
`{"sources":[...]}` instead of returning a bare array. The new v1.40
hostname-fold migration helper `sourceLocalPath()` (added in garrytan#1547)
typed the response as a bare array and crashed every first-sync-after-
upgrade with `list.find is not a function`:

  $ /sync-gbrain
  gstack-gbrain-sync fatal: list.find is not a function.
  (In 'list.find((s) => s.id === sourceId)', 'list.find' is undefined)

The sibling helper in lib/gbrain-sources.ts (`statusForSourceId`) already
unwraps the shape correctly — this PR brings sourceLocalPath in line.

Repro: on any machine running gstack v1.40.0.0 + gbrain v0.30.0.0 or
newer (current released gbrain is v0.37.1.0), run /sync-gbrain in any
git repo. The orchestrator dies before any stage runs.

Fix: accept both shapes via `Array.isArray(raw) ? raw : raw.sources || []`.

Tests: 3 new cases in the `sourceLocalPath` describe block pin the
wrapped shape, the wrapped-but-no-match shape, and the
`{sources: undefined}` defensive case. The existing 3 bare-array tests
still cover the legacy path. 38/38 in test/gstack-gbrain-sync.test.ts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant