## 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 -->
81 lines
4.2 KiB
TypeScript
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");
|
|
});
|
|
|
|
});
|