Files
fusion/packages/dashboard/src/__tests__/github-discussions.test.ts
Phil Larson c1bc95c1db 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`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 08:49:28 -07:00

81 lines
4.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { DiscussionsDisabledError, GitHubClient } from "../github.js";
describe("GitHub Discussions GraphQL transport", () => {
afterEach(() => vi.unstubAllGlobals());
it("lists repository discussion categories through the authenticated GraphQL transport", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
data: { repository: { discussionCategories: { nodes: [
{ id: "DC_1", name: "Ideas", slug: "ideas" },
{ id: "DC_2", name: "General", slug: "general" },
] } } },
}), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const categories = await new GitHubClient({ token: "test", forceMode: "token" }).listDiscussionCategories("Runfusion", "Fusion");
expect(categories).toEqual([
{ id: "DC_1", name: "Ideas", slug: "ideas" },
{ id: "DC_2", name: "General", slug: "general" },
]);
expect(String(fetchMock.mock.calls[0][0])).toBe("https://api.github.com/graphql");
});
it("surfaces GraphQL scope or disabled-Discussions failures to the caller", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({
errors: [{ message: "Resource not accessible by personal access token" }],
}), { status: 200 })));
await expect(new GitHubClient({ token: "test", forceMode: "token" }).listDiscussionCategories("Runfusion", "Fusion"))
.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({
errors: [{ message: "Discussions are disabled for this repository" }],
}), { status: 200 })));
await expect(new GitHubClient({ token: "test", forceMode: "token" }).searchDiscussions("Runfusion", "Fusion", "report"))
.rejects.toBeInstanceOf(DiscussionsDisabledError);
});
it("maps disabled Discussions errors from create to the same typed signal", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({
errors: [{ message: "Discussions are not enabled for this repository" }],
}), { status: 200 })));
await expect(new GitHubClient({ token: "test", forceMode: "token" }).createDiscussion("Runfusion", "Fusion", "Title", "Body", "DC_1"))
.rejects.toBeInstanceOf(DiscussionsDisabledError);
});
it("uses the first category when the selected category is stale", 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: 1, url: "https://github.com/Runfusion/Fusion/discussions/1" } } } }), { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await new GitHubClient({ token: "test", forceMode: "token" }).createDiscussion("Runfusion", "Fusion", "Title", "Body", "stale-category");
expect(JSON.parse(String(fetchMock.mock.calls[1][1].body)).variables.categoryId).toBe("DC_1");
});
});