diff --git a/bin/gstack-gbrain-sync.ts b/bin/gstack-gbrain-sync.ts index 61d9e677f7..b8e4d2848a 100644 --- a/bin/gstack-gbrain-sync.ts +++ b/bin/gstack-gbrain-sync.ts @@ -289,11 +289,14 @@ function gbrainSupportsSourcesRename(env?: NodeJS.ProcessEnv): boolean { * helper can be exercised without a real gbrain CLI. */ export function sourceLocalPath(sourceId: string, env?: NodeJS.ProcessEnv): string | null { - const list = execGbrainJson>( - ["sources", "list", "--json"], - { baseEnv: env }, - ); - if (!list) return null; + 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; + // gbrain v0.18.0+ returns {sources: [...]}; older versions returned a raw array. + // Defensive against both shapes so this works on any gbrain version. + const list = Array.isArray(raw) ? raw : raw.sources ?? []; const found = list.find((s) => s.id === sourceId); return found?.local_path ?? null; } diff --git a/test/gstack-gbrain-sync.test.ts b/test/gstack-gbrain-sync.test.ts index 0f1edec214..0fc710b3a1 100644 --- a/test/gstack-gbrain-sync.test.ts +++ b/test/gstack-gbrain-sync.test.ts @@ -812,7 +812,24 @@ describe("sourceLocalPath", () => { rmSync(bindir, { recursive: true, force: true }); }); - it("returns local_path when the source exists", () => { + // gbrain v0.18.0+ returns {sources: [...]}; this is the live contract. + it("returns local_path when the source exists (gbrain v0.18.0+ wrapped shape)", () => { + makeShim(bindir, { + "sources list --json": { + stdout: JSON.stringify({ + sources: [ + { id: "other-source", local_path: "/x" }, + { id: "target-id", local_path: "/repo/match" }, + ], + }), + }, + }); + expect(sourceLocalPath("target-id", envWithBindir(bindir))).toBe("/repo/match"); + }); + + // Pre-v0.18.0 gbrain returned a raw array. Keep back-compat so the helper + // works regardless of which gbrain the user has on PATH. + it("returns local_path when the source exists (pre-v0.18.0 raw-array shape)", () => { makeShim(bindir, { "sources list --json": { stdout: JSON.stringify([ @@ -824,7 +841,14 @@ describe("sourceLocalPath", () => { expect(sourceLocalPath("target-id", envWithBindir(bindir))).toBe("/repo/match"); }); - it("returns null when the source is missing", () => { + it("returns null when the source is missing (wrapped shape)", () => { + makeShim(bindir, { + "sources list --json": { stdout: JSON.stringify({ sources: [] }) }, + }); + expect(sourceLocalPath("missing-id", envWithBindir(bindir))).toBeNull(); + }); + + it("returns null when the source is missing (raw-array shape)", () => { makeShim(bindir, { "sources list --json": { stdout: "[]" }, });