From c1bc95c1db051575b1be39b28f346fb0687b68e3 Mon Sep 17 00:00:00 2001 From: Phil Larson Date: Mon, 20 Jul 2026 08:49:28 -0700 Subject: [PATCH] fix(dashboard): honor forced GraphQL transport (#2353) ## Summary - honor `forceMode` for GraphQL queries and mutations - keep token-backed Discussion operations on the selected token transport even when `gh` is authenticated - add regression coverage for forced-token category lookup, errors, and Discussion creation ## Test plan - `CI=true pnpm --filter @fusion/dashboard exec vitest run src/__tests__/github-discussions.test.ts src/__tests__/github-forced-mode.test.ts src/__tests__/github-pr-threads.test.ts` - `CI=true pnpm build` ## Summary by CodeRabbit * **Bug Fixes** * GitHub Discussions GraphQL operations now consistently honor the selected transport/authentication mode. * Forced token-based transport correctly applies to discussion queries and mutations (including discussion creation). * Missing required authentication is now reported instead of falling back to an unauthenticated path. * **Tests** * Added a test covering discussion creation using forced token-based GraphQL transport. * **Release** * Shipped as a patch release of the Fusion library. --- .changeset/fix-github-discussions-force-mode.md | 7 +++++++ .../src/__tests__/github-discussions.test.ts | 17 +++++++++++++++++ packages/dashboard/src/github.ts | 8 ++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-github-discussions-force-mode.md diff --git a/.changeset/fix-github-discussions-force-mode.md b/.changeset/fix-github-discussions-force-mode.md new file mode 100644 index 0000000000..42bcb8e7df --- /dev/null +++ b/.changeset/fix-github-discussions-force-mode.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Honor forced GitHub transport selection for GraphQL discussion queries and mutations. +category: fix +dev: Keep Discussion GraphQL operations on the explicitly selected token or gh CLI transport. diff --git a/packages/dashboard/src/__tests__/github-discussions.test.ts b/packages/dashboard/src/__tests__/github-discussions.test.ts index 42bb0e7c30..7c6a33eca5 100644 --- a/packages/dashboard/src/__tests__/github-discussions.test.ts +++ b/packages/dashboard/src/__tests__/github-discussions.test.ts @@ -31,6 +31,23 @@ describe("GitHub Discussions GraphQL transport", () => { .rejects.toThrow("Resource not accessible by personal access token"); }); + it("creates a discussion through the forced token GraphQL transport", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ + data: { repository: { id: "R_1", discussionCategories: { nodes: [{ id: "DC_1" }] } } }, + }), { status: 200 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + data: { createDiscussion: { discussion: { id: "D_1", number: 42, url: "https://github.com/Runfusion/Fusion/discussions/42" } } }, + }), { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + + const discussion = await new GitHubClient({ token: "test", forceMode: "token" }) + .createDiscussion("Runfusion", "Fusion", "Title", "Body", "DC_1"); + + expect(discussion).toEqual({ id: "D_1", number: 42, htmlUrl: "https://github.com/Runfusion/Fusion/discussions/42" }); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls.every(([url]) => String(url) === "https://api.github.com/graphql")).toBe(true); + }); it("maps disabled Discussions errors from search to a typed signal", async () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ diff --git a/packages/dashboard/src/github.ts b/packages/dashboard/src/github.ts index b9ca98dfa3..6bb0e6d10f 100644 --- a/packages/dashboard/src/github.ts +++ b/packages/dashboard/src/github.ts @@ -2457,7 +2457,8 @@ export class GitHubClient { /** Run a read-only GraphQL query (gh CLI when available, else token/REST). */ private async runGraphqlQuery(query: string, variables: Record): Promise { - if (this.hasGhAuth()) { + if (this.forceMode === "gh-cli" || (this.forceMode === undefined && this.hasGhAuth())) { + if (this.forceMode === "gh-cli") this.requireGh(); const args = ["api", "graphql", "-f", `query=${query}`]; for (const [key, value] of Object.entries(variables)) { if (value === null) continue; @@ -2469,6 +2470,7 @@ export class GitHubClient { if (payload.errors?.length) throw new Error(payload.errors[0].message); return payload.data; } + if (this.forceMode === "token") this.requireToken(); if (this.token) { const response = await fetch(`${this.baseUrl}/graphql`, { method: "POST", @@ -2513,7 +2515,8 @@ export class GitHubClient { } private async runGraphqlMutation(query: string, variables: Record): Promise { - if (this.hasGhAuth()) { + if (this.forceMode === "gh-cli" || (this.forceMode === undefined && this.hasGhAuth())) { + if (this.forceMode === "gh-cli") this.requireGh(); const args = ["api", "graphql", "-f", `query=${query}`]; for (const [key, value] of Object.entries(variables)) { args.push("-F", `${key}=${value}`); @@ -2523,6 +2526,7 @@ export class GitHubClient { if (payload.errors?.length) throw new Error(payload.errors[0].message); return; } + if (this.forceMode === "token") this.requireToken(); if (this.token) { const response = await fetch(`${this.baseUrl}/graphql`, { method: "POST",