import * as Result from "effect/Result";import { describe, expect, it } from "vite-plus/test"; import { buildReviewSubmissionJson, buildReviewerRequestJson, decodeBaseComparisonJson, decodePullRequestActivityJson, decodePullRequestDetailJson, decodePullRequestFilesJson, decodePullRequestListJson, decodePullRequestNodeIdJson, decodePullRequestSearchJson, decodeRepositoryAccessJson, decodeReviewerCandidatesJson, decodeReviewThreadCommentsJson, decodeReviewThreadsJson, decodeViewerPermissionsJson, reviewThreadConversation, REVIEW_THREADS_GRAPHQL_QUERY,} from "./gitHubPullRequestJson.ts"; function listJson(entries: ReadonlyArray<Record<string, unknown>>): string { return JSON.stringify( entries.map((entry) => ({ number: 1, title: "Add the pull requests page", url: "https://github.com/pingdotgg/t3code/pull/1", headRefName: "feat/page", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-02T00:00:00Z", ...entry, })), );} function expectSuccess<A>(result: Result.Result<A, unknown>): A { expect(Result.isSuccess(result)).toBe(true); if (!Result.isSuccess(result)) throw new Error("expected a successful decode"); return result.success;} describe("pull request list decoding", () => { it("treats a merge timestamp as merged even when the state still says closed", () => { const [entry] = expectSuccess( decodePullRequestListJson(listJson([{ state: "CLOSED", mergedAt: "2026-07-03T00:00:00Z" }])), ).items; expect(entry?.state).toBe("merged"); }); it("normalizes mergeability and defaults unknown values", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([{ mergeable: "CONFLICTING" }, { mergeable: "SOMETHING_NEW" }, {}]), ), ); expect(batch.items.map((entry) => entry.mergeability)).toEqual([ "conflicting", "unknown", "unknown", ]); }); it("keeps user review requests and drops team ones, which are not logins", () => { const [entry] = expectSuccess( decodePullRequestListJson( listJson([{ reviewRequests: [{ login: "octocat" }, { slug: "web-platform" }] }]), ), ).items; expect(entry?.reviewRequestLogins).toEqual(["octocat"]); }); it("normalizes the review decision and reports nothing for one GitHub does not summarize", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([ { reviewDecision: "APPROVED" }, { reviewDecision: "CHANGES_REQUESTED" }, { reviewDecision: "REVIEW_REQUIRED" }, { reviewDecision: null }, ]), ), ); expect(batch.items.map((entry) => entry.reviewDecision)).toEqual([ "approved", "changes-requested", "review-required", null, ]); }); it("rolls the head commit's checks up to the one word a row has space for", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([ // A failure outranks a run still going, and a completed run has to be read through its // conclusion rather than its status. { statusCheckRollup: [ { name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }, { name: "build", status: "IN_PROGRESS" }, { name: "test", status: "COMPLETED", conclusion: "FAILURE" }, ], }, { statusCheckRollup: [ { name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }, { name: "build", status: "QUEUED" }, ], }, { statusCheckRollup: [{ name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }] }, // A commit status reports one `state` and no `status` at all. { statusCheckRollup: [{ context: "ci/legacy", state: "ERROR" }] }, // Neither a pass, a failure nor a wait is no verdict rather than a green tick. { statusCheckRollup: [{ name: "lint", status: "COMPLETED", conclusion: "SKIPPED" }] }, { statusCheckRollup: [] }, {}, ]), ), ); expect(batch.items.map((entry) => entry.checksState)).toEqual([ "failing", "pending", "passing", "failing", null, null, null, ]); }); it("skips malformed entries but still counts them, so paging does not stop early", () => { const raw = `[${listJson([{}]).slice(1, -1)},{"number":"not-a-number"}]`; const batch = expectSuccess(decodePullRequestListJson(raw)); expect(batch.items).toHaveLength(1); expect(batch.rawCount).toBe(2); });}); describe("pull request search decoding", () => { function searchJson(rollupStates: ReadonlyArray<string | null>): string { return JSON.stringify({ data: { search: { pageInfo: { hasNextPage: false }, nodes: rollupStates.map((state, index) => ({ number: index + 1, title: "Add the pull requests page", url: "https://github.com/pingdotgg/t3code/pull/1", headRefName: "feat/page", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-02T00:00:00Z", repository: { nameWithOwner: "pingdotgg/t3code" }, commits: { nodes: [{ commit: { statusCheckRollup: state === null ? null : { state } } }], }, })), }, }, }); } it("maps the rollup enum the search answers with onto the same three words", () => { // The search asks GitHub for the verdict rather than the checks behind it, so this path sees // one enum where the listing sees an array. const batch = expectSuccess( decodePullRequestSearchJson( searchJson(["SUCCESS", "FAILURE", "ERROR", "PENDING", "EXPECTED", null]), ), ); expect(batch.items.map((entry) => entry.checksState)).toEqual([ "passing", "failing", "failing", "pending", "pending", null, ]); });}); describe("pull request detail decoding", () => { const detailJson = JSON.stringify({ number: 7, title: "Detail", url: "https://github.com/pingdotgg/t3code/pull/7", headRefName: "feat/detail", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-05T00:00:00Z", body: "Body", statusCheckRollup: [ { __typename: "CheckRun", name: "build", status: "IN_PROGRESS" }, { __typename: "CheckRun", name: "test", status: "COMPLETED", conclusion: "FAILURE" }, { __typename: "StatusContext", context: "ci/legacy", state: "SUCCESS" }, ], comments: [{ id: "c1", body: "second", createdAt: "2026-07-04T00:00:00Z" }], reviews: [ { id: "r1", body: "first", state: "CHANGES_REQUESTED", submittedAt: "2026-07-03T00:00:00Z" }, { id: "r2", body: " ", state: "APPROVED", submittedAt: "2026-07-06T00:00:00Z" }, ], commits: [ { oid: "abc1234", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", authors: [ { login: "octocat", name: "Octo Cat", email: "octo@example.com" }, { name: "Pair Author", email: "pair@example.com" }, ], }, ], }); it("maps check-run status and commit-status state onto one vocabulary", () => { const detail = expectSuccess(decodePullRequestDetailJson(detailJson)); expect(detail.checks.map((check) => [check.name, check.status])).toEqual([ ["build", "pending"], ["test", "failure"], ["ci/legacy", "success"], ]); }); it("reads an auto-merge request as armed, its null as off and its absence as neither", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const armed = (entry: Record<string, unknown>) => expectSuccess(decodePullRequestDetailJson(JSON.stringify({ ...raw, ...entry }))) .autoMergeEnabled; expect( armed({ autoMergeRequest: { enabledBy: { login: "octocat" }, mergeMethod: "SQUASH" } }), ).toBe(true); expect(armed({ autoMergeRequest: null })).toBe(false); // `gh` not answering for the field at all is not GitHub saying the merge is unarmed. expect(armed({})).toBeUndefined(); }); it("shows a re-running check once, as the run that is happening now", () => { // What `statusCheckRollup` reports while a workflow is being re-run: the same check twice, // the finished run and the one that replaced it, with no id to tell them apart. const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestDetailJson( JSON.stringify({ ...raw, statusCheckRollup: [ { __typename: "CheckRun", name: "Prepare PR size config", workflowName: "PR Size", status: "COMPLETED", conclusion: "SUCCESS", startedAt: "2026-08-11T16:06:20Z", completedAt: "2026-08-11T16:06:25Z", }, { __typename: "CheckRun", name: "Prepare PR size config", workflowName: "PR Size", status: "IN_PROGRESS", conclusion: "", startedAt: "2026-08-11T17:01:04Z", completedAt: "0001-01-01T00:00:00Z", }, ], }), ), ); expect(detail.checks.map((check) => [check.name, check.status])).toEqual([ ["Prepare PR size config", "pending"], ]); expect(detail.checksState).toBe("pending"); }); it("merges reviews with comments in time order and keeps a bodyless approval", () => { const detail = expectSuccess(decodePullRequestActivityJson(detailJson)); // r2 approved without writing anything, which is still the event worth seeing. expect(detail.comments.map((comment) => comment.id)).toEqual(["r1", "c1", "r2"]); expect(detail.comments.at(-1)?.reviewState).toBe("APPROVED"); }); it("keeps every attributed commit author, including an unlinked signature", () => { const detail = expectSuccess(decodePullRequestActivityJson(detailJson)); expect(detail.commits[0]?.authors).toEqual([ { login: "octocat", name: "Octo Cat", avatarUrl: null }, { login: "Pair Author", name: "Pair Author", avatarUrl: null }, ]); }); it("drops the bodyless review GitHub opens to hold line comments", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [ // What a reviewer leaving inline comments produces: a container with a state but // nothing to read. Its comments come from the review threads instead. { id: "r4", body: "", state: "COMMENTED", submittedAt: "2026-07-07T00:00:00Z" }, { id: "r5", body: "Looks good.", state: "COMMENTED", submittedAt: "2026-07-08T00:00:00Z", }, ], }), ), ); expect(detail.comments.map((comment) => comment.id)).toEqual(["c1", "r5"]); }); it.each(["APPROVED", "CHANGES_REQUESTED", "DISMISSED"])( "keeps a bodyless %s review, which is the event itself", (state) => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [{ id: "r6", body: "", state, submittedAt: "2026-07-07T00:00:00Z" }], }), ), ); expect(detail.comments.map((comment) => comment.id)).toContain("r6"); }, ); it("drops a review that carries neither a body nor a state", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [{ id: "r3", body: " ", submittedAt: "2026-07-07T00:00:00Z" }], }), ), ); expect(detail.comments.map((comment) => comment.id)).toEqual(["c1"]); });}); describe("review thread decoding", () => { const threadsJson = ( nodes: ReadonlyArray<Record<string, unknown>>, totalCount = nodes.length, pageInfo: Record<string, unknown> = { hasNextPage: false, endCursor: null }, ): string => JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount, pageInfo, nodes } } } }, }); /** The same query carries the review roster, so it is built alongside the threads. */ const reviewJson = (input: { readonly requested?: ReadonlyArray<unknown>; readonly reviewed?: ReadonlyArray<unknown>; }): string => JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, reviewRequests: { nodes: (input.requested ?? []).map((r) => ({ requestedReviewer: r })), }, latestReviews: { nodes: (input.reviewed ?? []).map((a) => ({ author: a })) }, }, }, }, }); it("keeps a reviewer who has already reviewed, app or person, with their avatar", () => { const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{ login: "julius", name: "Julius", avatarUrl: "https://avatars/j.png" }], // An app that has reviewed is no longer an outstanding request, which is why asking // only for requests reported nobody on a pull request a bot had reviewed. reviewed: [{ login: "macroscopeapp", avatarUrl: "https://avatars/in/900172.png" }], }), ), ); expect(result.reviewers).toEqual([ { login: "julius", name: "Julius", avatarUrl: "https://avatars/j.png" }, { login: "macroscopeapp", name: null, avatarUrl: "https://avatars/in/900172.png" }, ]); }); it("carries per-commit line counts from the pull-request connection", () => { const result = expectSuccess( decodeReviewThreadsJson( JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, commits: { nodes: [ { commit: { oid: "abc123", additions: 18, deletions: 7 } }, { commit: { oid: "def456", additions: 3, deletions: 0 } }, ], }, }, }, }, }), ), ); expect([...result.commitStats]).toEqual([ ["abc123", { additions: 18, deletions: 7 }], ["def456", { additions: 3, deletions: 0 }], ]); }); it("decodes the newest commits off the same connection, oldest to newest", () => { const result = expectSuccess( decodeReviewThreadsJson( JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, commits: { nodes: [ { commit: { oid: "abc123", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", additions: 18, deletions: 7, authors: { nodes: [{ name: "Julius", user: { login: "julius" } }] }, }, }, { commit: { oid: "def456", messageHeadline: "Fix the flaky test", committedDate: "2026-07-06T00:00:00Z", }, }, ], }, }, }, }, }), ), ); expect(result.commits).toEqual([ { oid: "abc123", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", authors: [{ login: "julius", name: "Julius", avatarUrl: null }], }, { oid: "def456", messageHeadline: "Fix the flaky test", committedDate: "2026-07-06T00:00:00Z", authors: [], }, ]); }); it("lists someone who was asked and then answered only once", () => { const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{ login: "julius", avatarUrl: "https://avatars/j.png" }], reviewed: [{ login: "julius", avatarUrl: "https://avatars/j.png" }], }), ), ); expect(result.reviewers).toHaveLength(1); }); it("skips a team request, which names nobody to show", () => { const result = expectSuccess(decodeReviewThreadsJson(reviewJson({ requested: [null] }))); expect(result.reviewers).toEqual([]); }); it("keeps the conversation when a request is from a team, which has no login", () => { // GraphQL answers with an empty object for a union member the query has no fragment for. // Failing on it would take the whole response down, comments included. const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{}, { login: "julius", avatarUrl: "https://avatars/j.png" }] }), ), );Lines 1–500 of 1363. Load more
import * as Result from "effect/Result"; import { describe, expect, it } from "vite-plus/test"; import { buildReviewSubmissionJson, buildReviewerRequestJson, decodeBaseComparisonJson, decodePullRequestActivityJson, decodePullRequestDetailJson, decodePullRequestFilesJson, decodePullRequestListJson, decodePullRequestNodeIdJson, decodePullRequestSearchJson, decodeRepositoryAccessJson, decodeReviewerCandidatesJson, decodeReviewThreadCommentsJson, decodeReviewThreadsJson, decodeViewerPermissionsJson, reviewThreadConversation, REVIEW_THREADS_GRAPHQL_QUERY, } from "./gitHubPullRequestJson.ts"; function listJson(entries: ReadonlyArray<Record<string, unknown>>): string { return JSON.stringify( entries.map((entry) => ({ number: 1, title: "Add the pull requests page", url: "https://github.com/pingdotgg/t3code/pull/1", headRefName: "feat/page", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-02T00:00:00Z", ...entry, })), ); } function expectSuccess<A>(result: Result.Result<A, unknown>): A { expect(Result.isSuccess(result)).toBe(true); if (!Result.isSuccess(result)) throw new Error("expected a successful decode"); return result.success; } describe("pull request list decoding", () => { it("treats a merge timestamp as merged even when the state still says closed", () => { const [entry] = expectSuccess( decodePullRequestListJson(listJson([{ state: "CLOSED", mergedAt: "2026-07-03T00:00:00Z" }])), ).items; expect(entry?.state).toBe("merged"); }); it("normalizes mergeability and defaults unknown values", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([{ mergeable: "CONFLICTING" }, { mergeable: "SOMETHING_NEW" }, {}]), ), ); expect(batch.items.map((entry) => entry.mergeability)).toEqual([ "conflicting", "unknown", "unknown", ]); }); it("keeps user review requests and drops team ones, which are not logins", () => { const [entry] = expectSuccess( decodePullRequestListJson( listJson([{ reviewRequests: [{ login: "octocat" }, { slug: "web-platform" }] }]), ), ).items; expect(entry?.reviewRequestLogins).toEqual(["octocat"]); }); it("normalizes the review decision and reports nothing for one GitHub does not summarize", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([ { reviewDecision: "APPROVED" }, { reviewDecision: "CHANGES_REQUESTED" }, { reviewDecision: "REVIEW_REQUIRED" }, { reviewDecision: null }, ]), ), ); expect(batch.items.map((entry) => entry.reviewDecision)).toEqual([ "approved", "changes-requested", "review-required", null, ]); }); it("rolls the head commit's checks up to the one word a row has space for", () => { const batch = expectSuccess( decodePullRequestListJson( listJson([ // A failure outranks a run still going, and a completed run has to be read through its // conclusion rather than its status. { statusCheckRollup: [ { name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }, { name: "build", status: "IN_PROGRESS" }, { name: "test", status: "COMPLETED", conclusion: "FAILURE" }, ], }, { statusCheckRollup: [ { name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }, { name: "build", status: "QUEUED" }, ], }, { statusCheckRollup: [{ name: "lint", status: "COMPLETED", conclusion: "SUCCESS" }] }, // A commit status reports one `state` and no `status` at all. { statusCheckRollup: [{ context: "ci/legacy", state: "ERROR" }] }, // Neither a pass, a failure nor a wait is no verdict rather than a green tick. { statusCheckRollup: [{ name: "lint", status: "COMPLETED", conclusion: "SKIPPED" }] }, { statusCheckRollup: [] }, {}, ]), ), ); expect(batch.items.map((entry) => entry.checksState)).toEqual([ "failing", "pending", "passing", "failing", null, null, null, ]); }); it("skips malformed entries but still counts them, so paging does not stop early", () => { const raw = `[${listJson([{}]).slice(1, -1)},{"number":"not-a-number"}]`; const batch = expectSuccess(decodePullRequestListJson(raw)); expect(batch.items).toHaveLength(1); expect(batch.rawCount).toBe(2); }); }); describe("pull request search decoding", () => { function searchJson(rollupStates: ReadonlyArray<string | null>): string { return JSON.stringify({ data: { search: { pageInfo: { hasNextPage: false }, nodes: rollupStates.map((state, index) => ({ number: index + 1, title: "Add the pull requests page", url: "https://github.com/pingdotgg/t3code/pull/1", headRefName: "feat/page", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-02T00:00:00Z", repository: { nameWithOwner: "pingdotgg/t3code" }, commits: { nodes: [{ commit: { statusCheckRollup: state === null ? null : { state } } }], }, })), }, }, }); } it("maps the rollup enum the search answers with onto the same three words", () => { // The search asks GitHub for the verdict rather than the checks behind it, so this path sees // one enum where the listing sees an array. const batch = expectSuccess( decodePullRequestSearchJson( searchJson(["SUCCESS", "FAILURE", "ERROR", "PENDING", "EXPECTED", null]), ), ); expect(batch.items.map((entry) => entry.checksState)).toEqual([ "passing", "failing", "failing", "pending", "pending", null, ]); }); }); describe("pull request detail decoding", () => { const detailJson = JSON.stringify({ number: 7, title: "Detail", url: "https://github.com/pingdotgg/t3code/pull/7", headRefName: "feat/detail", baseRefName: "main", createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-05T00:00:00Z", body: "Body", statusCheckRollup: [ { __typename: "CheckRun", name: "build", status: "IN_PROGRESS" }, { __typename: "CheckRun", name: "test", status: "COMPLETED", conclusion: "FAILURE" }, { __typename: "StatusContext", context: "ci/legacy", state: "SUCCESS" }, ], comments: [{ id: "c1", body: "second", createdAt: "2026-07-04T00:00:00Z" }], reviews: [ { id: "r1", body: "first", state: "CHANGES_REQUESTED", submittedAt: "2026-07-03T00:00:00Z" }, { id: "r2", body: " ", state: "APPROVED", submittedAt: "2026-07-06T00:00:00Z" }, ], commits: [ { oid: "abc1234", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", authors: [ { login: "octocat", name: "Octo Cat", email: "octo@example.com" }, { name: "Pair Author", email: "pair@example.com" }, ], }, ], }); it("maps check-run status and commit-status state onto one vocabulary", () => { const detail = expectSuccess(decodePullRequestDetailJson(detailJson)); expect(detail.checks.map((check) => [check.name, check.status])).toEqual([ ["build", "pending"], ["test", "failure"], ["ci/legacy", "success"], ]); }); it("reads an auto-merge request as armed, its null as off and its absence as neither", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const armed = (entry: Record<string, unknown>) => expectSuccess(decodePullRequestDetailJson(JSON.stringify({ ...raw, ...entry }))) .autoMergeEnabled; expect( armed({ autoMergeRequest: { enabledBy: { login: "octocat" }, mergeMethod: "SQUASH" } }), ).toBe(true); expect(armed({ autoMergeRequest: null })).toBe(false); // `gh` not answering for the field at all is not GitHub saying the merge is unarmed. expect(armed({})).toBeUndefined(); }); it("shows a re-running check once, as the run that is happening now", () => { // What `statusCheckRollup` reports while a workflow is being re-run: the same check twice, // the finished run and the one that replaced it, with no id to tell them apart. const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestDetailJson( JSON.stringify({ ...raw, statusCheckRollup: [ { __typename: "CheckRun", name: "Prepare PR size config", workflowName: "PR Size", status: "COMPLETED", conclusion: "SUCCESS", startedAt: "2026-08-11T16:06:20Z", completedAt: "2026-08-11T16:06:25Z", }, { __typename: "CheckRun", name: "Prepare PR size config", workflowName: "PR Size", status: "IN_PROGRESS", conclusion: "", startedAt: "2026-08-11T17:01:04Z", completedAt: "0001-01-01T00:00:00Z", }, ], }), ), ); expect(detail.checks.map((check) => [check.name, check.status])).toEqual([ ["Prepare PR size config", "pending"], ]); expect(detail.checksState).toBe("pending"); }); it("merges reviews with comments in time order and keeps a bodyless approval", () => { const detail = expectSuccess(decodePullRequestActivityJson(detailJson)); // r2 approved without writing anything, which is still the event worth seeing. expect(detail.comments.map((comment) => comment.id)).toEqual(["r1", "c1", "r2"]); expect(detail.comments.at(-1)?.reviewState).toBe("APPROVED"); }); it("keeps every attributed commit author, including an unlinked signature", () => { const detail = expectSuccess(decodePullRequestActivityJson(detailJson)); expect(detail.commits[0]?.authors).toEqual([ { login: "octocat", name: "Octo Cat", avatarUrl: null }, { login: "Pair Author", name: "Pair Author", avatarUrl: null }, ]); }); it("drops the bodyless review GitHub opens to hold line comments", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [ // What a reviewer leaving inline comments produces: a container with a state but // nothing to read. Its comments come from the review threads instead. { id: "r4", body: "", state: "COMMENTED", submittedAt: "2026-07-07T00:00:00Z" }, { id: "r5", body: "Looks good.", state: "COMMENTED", submittedAt: "2026-07-08T00:00:00Z", }, ], }), ), ); expect(detail.comments.map((comment) => comment.id)).toEqual(["c1", "r5"]); }); it.each(["APPROVED", "CHANGES_REQUESTED", "DISMISSED"])( "keeps a bodyless %s review, which is the event itself", (state) => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [{ id: "r6", body: "", state, submittedAt: "2026-07-07T00:00:00Z" }], }), ), ); expect(detail.comments.map((comment) => comment.id)).toContain("r6"); }, ); it("drops a review that carries neither a body nor a state", () => { const raw = JSON.parse(detailJson) as Record<string, unknown>; const detail = expectSuccess( decodePullRequestActivityJson( JSON.stringify({ ...raw, reviews: [{ id: "r3", body: " ", submittedAt: "2026-07-07T00:00:00Z" }], }), ), ); expect(detail.comments.map((comment) => comment.id)).toEqual(["c1"]); }); }); describe("review thread decoding", () => { const threadsJson = ( nodes: ReadonlyArray<Record<string, unknown>>, totalCount = nodes.length, pageInfo: Record<string, unknown> = { hasNextPage: false, endCursor: null }, ): string => JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount, pageInfo, nodes } } } }, }); /** The same query carries the review roster, so it is built alongside the threads. */ const reviewJson = (input: { readonly requested?: ReadonlyArray<unknown>; readonly reviewed?: ReadonlyArray<unknown>; }): string => JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, reviewRequests: { nodes: (input.requested ?? []).map((r) => ({ requestedReviewer: r })), }, latestReviews: { nodes: (input.reviewed ?? []).map((a) => ({ author: a })) }, }, }, }, }); it("keeps a reviewer who has already reviewed, app or person, with their avatar", () => { const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{ login: "julius", name: "Julius", avatarUrl: "https://avatars/j.png" }], // An app that has reviewed is no longer an outstanding request, which is why asking // only for requests reported nobody on a pull request a bot had reviewed. reviewed: [{ login: "macroscopeapp", avatarUrl: "https://avatars/in/900172.png" }], }), ), ); expect(result.reviewers).toEqual([ { login: "julius", name: "Julius", avatarUrl: "https://avatars/j.png" }, { login: "macroscopeapp", name: null, avatarUrl: "https://avatars/in/900172.png" }, ]); }); it("carries per-commit line counts from the pull-request connection", () => { const result = expectSuccess( decodeReviewThreadsJson( JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, commits: { nodes: [ { commit: { oid: "abc123", additions: 18, deletions: 7 } }, { commit: { oid: "def456", additions: 3, deletions: 0 } }, ], }, }, }, }, }), ), ); expect([...result.commitStats]).toEqual([ ["abc123", { additions: 18, deletions: 7 }], ["def456", { additions: 3, deletions: 0 }], ]); }); it("decodes the newest commits off the same connection, oldest to newest", () => { const result = expectSuccess( decodeReviewThreadsJson( JSON.stringify({ data: { repository: { pullRequest: { reviewThreads: { totalCount: 0, nodes: [] }, commits: { nodes: [ { commit: { oid: "abc123", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", additions: 18, deletions: 7, authors: { nodes: [{ name: "Julius", user: { login: "julius" } }] }, }, }, { commit: { oid: "def456", messageHeadline: "Fix the flaky test", committedDate: "2026-07-06T00:00:00Z", }, }, ], }, }, }, }, }), ), ); expect(result.commits).toEqual([ { oid: "abc123", messageHeadline: "Ship the timeline", committedDate: "2026-07-05T00:00:00Z", authors: [{ login: "julius", name: "Julius", avatarUrl: null }], }, { oid: "def456", messageHeadline: "Fix the flaky test", committedDate: "2026-07-06T00:00:00Z", authors: [], }, ]); }); it("lists someone who was asked and then answered only once", () => { const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{ login: "julius", avatarUrl: "https://avatars/j.png" }], reviewed: [{ login: "julius", avatarUrl: "https://avatars/j.png" }], }), ), ); expect(result.reviewers).toHaveLength(1); }); it("skips a team request, which names nobody to show", () => { const result = expectSuccess(decodeReviewThreadsJson(reviewJson({ requested: [null] }))); expect(result.reviewers).toEqual([]); }); it("keeps the conversation when a request is from a team, which has no login", () => { // GraphQL answers with an empty object for a union member the query has no fragment for. // Failing on it would take the whole response down, comments included. const result = expectSuccess( decodeReviewThreadsJson( reviewJson({ requested: [{}, { login: "julius", avatarUrl: "https://avatars/j.png" }] }), ), );