From a00c0a224a2c702d410c32b1d013af5b77056335 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:45:21 -0700 Subject: [PATCH] test(github): cover the assignee-403 fallback and its unrelated-error guard (#4999) ensurePullRequestAssignee already catches GitHub's "Assigning agents is not supported with GitHub App installation tokens" 403 and degrades to applied:false (shipped in #4167, deployed) -- 198 Sentry events, all predating that fix, zero since. What was missing was a regression test for the catch itself: the branch sat at 0% coverage, so a future refactor could silently break the fallback (or start swallowing unrelated errors) with nothing to catch it. --- test/unit/github-assignees.test.ts | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/unit/github-assignees.test.ts b/test/unit/github-assignees.test.ts index 27f2346f35..be126b0337 100644 --- a/test/unit/github-assignees.test.ts +++ b/test/unit/github-assignees.test.ts @@ -75,6 +75,48 @@ describe("GitHub PR assignees (#3182)", () => { expect(result).toEqual({ applied: false }); }); + it("REGRESSION (#4999): GitHub's 'Assigning agents is not supported' 403 degrades to applied:false instead of throwing", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/4") && !url.includes("/assignees")) return Response.json({ assignees: [] }); + // The exact GitHub REST error for assigning a non-collaborator "agent" login via an App installation + // token (GITTENSORY-1G, 198 Sentry events) -- this operation can never succeed with this auth model. + if (url.includes("/issues/4/assignees") && method === "POST") { + return Response.json( + { + message: + "Assigning agents is not supported with GitHub App installation tokens. Use a user token (personal access token or OAuth token) instead. - https://docs.github.com/rest/issues/assignees#add-assignees-to-an-issue", + }, + { status: 403 }, + ); + } + return new Response("unexpected", { status: 500 }); + }); + + const result = await ensurePullRequestAssignee(createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem() }), 123, "JSONbored/gittensory", 4, "some-agent-login"); + + expect(result).toEqual({ applied: false }); + }); + + it("REGRESSION (#4999): an UNRELATED 403 (e.g. a plain permissions rejection) still propagates instead of being silently swallowed", async () => { + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); + if (url.includes("/issues/4") && !url.includes("/assignees")) return Response.json({ assignees: [] }); + if (url.includes("/issues/4/assignees") && method === "POST") { + return Response.json({ message: "Resource not accessible by integration" }, { status: 403 }); + } + return new Response("unexpected", { status: 500 }); + }); + + await expect( + ensurePullRequestAssignee(createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem() }), 123, "JSONbored/gittensory", 4, "alice"), + ).rejects.toThrow(/Resource not accessible by integration/); + }); + it("treats a response with no assignees field at all as an empty list, on both the GET and the POST", async () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { const url = input.toString();