Skip to content

Commit 3e906c1

Browse files
garrytanclaude
authored andcommitted
fix(gbrain-sync): sourceLocalPath handles wrapped {sources:[...]} shape from gbrain v0.20+
gbrain v0.20+ changed `gbrain sources list --json` to return {sources: [...]} instead of a flat array. sourceLocalPath crashed upstream with `list.find is not a function` on every /sync-gbrain invocation against modern gbrain. Accept both shapes for forward/backward compat, matching probeSource/sourcePageCount in lib/gbrain-sources.ts. Contributed by @jakehann11 via garrytan#1571. Closes garrytan#1567. Supersedes garrytan#1564 (@tonyjzhou, same fix, different shape — credit retained). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a4f68f5 commit 3e906c1

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

bin/gstack-gbrain-sync.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,20 @@ function gbrainSupportsSourcesRename(env?: NodeJS.ProcessEnv): boolean {
287287
* `env` is the environment passed to the spawned `gbrain` process; defaults
288288
* to `process.env`. Tests inject a PATH that points at a gbrain shim so the
289289
* helper can be exercised without a real gbrain CLI.
290+
*
291+
* Shape note: `gbrain sources list --json` returns `{sources: [...]}` (v0.20+);
292+
* older versions returned a flat array. Accept both for forward/backward compat
293+
* (mirrors `probeSource`/`sourcePageCount` in lib/gbrain-sources.ts).
290294
*/
291295
export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null {
292-
const list = execGbrainJson<Array<{ id: string; local_path?: string }>>(
296+
const raw = execGbrainJson<unknown>(
293297
["sources", "list", "--json"],
294298
{ baseEnv: env },
295299
);
296-
if (!list) return null;
300+
if (!raw) return null;
301+
const list: Array<{ id?: string; local_path?: string }> = Array.isArray(raw)
302+
? (raw as Array<{ id?: string; local_path?: string }>)
303+
: ((raw as { sources?: Array<{ id?: string; local_path?: string }> }).sources ?? []);
297304
const found = list.find((s) => s.id === sourceId);
298305
return found?.local_path ?? null;
299306
}

test/gstack-gbrain-sync.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,4 +837,29 @@ describe("sourceLocalPath", () => {
837837
});
838838
expect(sourceLocalPath("any-id", envWithBindir(bindir))).toBeNull();
839839
});
840+
841+
// gbrain v0.20+ wraps the response as `{sources: [...]}`. Older versions
842+
// returned a flat array. sourceLocalPath was returning null (or crashing
843+
// with `list.find is not a function` upstream) because it only handled
844+
// the flat-array shape. Pin both shapes here.
845+
it("handles {sources: [...]} wrapped shape (gbrain v0.20+)", () => {
846+
makeShim(bindir, {
847+
"sources list --json": {
848+
stdout: JSON.stringify({
849+
sources: [
850+
{ id: "other-source", local_path: "/x" },
851+
{ id: "target-id", local_path: "/repo/match" },
852+
],
853+
}),
854+
},
855+
});
856+
expect(sourceLocalPath("target-id", envWithBindir(bindir))).toBe("/repo/match");
857+
});
858+
859+
it("returns null when the source is missing in the wrapped shape", () => {
860+
makeShim(bindir, {
861+
"sources list --json": { stdout: JSON.stringify({ sources: [] }) },
862+
});
863+
expect(sourceLocalPath("missing-id", envWithBindir(bindir))).toBeNull();
864+
});
840865
});

0 commit comments

Comments
 (0)