import * as Result from "effect/Result";import { describe, expect, it } from "vite-plus/test"; import { buildReviewSubmissionJson, buildReviewerRequestJson, decodePullRequestActivityJson, decodePullRequestDetailJson, decodePullRequestFilesJson, decodePullRequestListJson, decodeRepositoryAccessJson, decodeReviewerCandidatesJson, decodeReviewThreadCommentsJson, decodeReviewThreadsJson, decodeViewerPermissionsJson, reviewThreadConversation,} 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("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 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("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" }] }), ), ); expect(result.reviewers).toEqual([ { login: "julius", name: null, avatarUrl: "https://avatars/j.png" }, ]); }); it("carries a resolved thread into the conversation, which was still said", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_a", isResolved: false, path: "apps/server/src/ws.ts", comments: { nodes: [{ id: "t1", body: "fix this", createdAt: "2026-07-01T00:00:00Z" }], }, }, { id: "PRRT_b", isResolved: true, path: "apps/web/src/main.tsx", comments: { nodes: [{ id: "t2", body: "done", createdAt: "2026-07-01T00:00:00Z" }] }, }, ]), ), ); const comments = reviewThreadConversation(result.threads.map((entry) => entry.thread)); expect(comments.map((comment) => comment.id)).toEqual(["t1", "t2"]); expect(comments[0]).toMatchObject({ id: "t1", kind: "review-comment", path: "apps/server/src/ws.ts", }); }); it("carries every reply, not only the remark each thread opened with", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_c", isResolved: false, path: "apps/server/src/ws.ts", comments: { nodes: [ { id: "t1", body: "fix this", createdAt: "2026-07-01T00:00:00Z" }, { id: "t2", body: "fixed", createdAt: "2026-07-01T01:00:00Z" }, ], }, }, ]), ), ); const comments = reviewThreadConversation(result.threads.map((entry) => entry.thread)); expect(comments.map((comment) => comment.id)).toEqual(["t1", "t2"]); }); it("hands back the cursor the next page of threads carries on from", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson( [ { id: "PRRT_d", path: "apps/server/src/ws.ts", isResolved: false, comments: { nodes: [{ id: "t1", createdAt: "2026-07-01T00:00:00Z" }] }, }, ], 80, { hasNextPage: true, endCursor: "Y3Vyc29yOjE" }, ), ), ); expect(result.nextCursor).toBe("Y3Vyc29yOjE"); }); it("keeps GitHub's own count of a thread whose comments were not all read", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_e", path: "apps/server/src/ws.ts", isResolved: false, comments: { totalCount: 140, pageInfo: { hasNextPage: true, endCursor: "Y3Vyc29yOjI" }, nodes: [{ id: "t1", createdAt: "2026-07-01T00:00:00Z" }], }, }, ]), ), ); expect(result.threads[0]).toMatchObject({ commentCount: 140, nextCommentCursor: "Y3Vyc29yOjI", }); }); it("ends a thread's walk on the last page, which still names a cursor", () => { const decoded = expectSuccess( decodeReviewThreadCommentsJson( JSON.stringify({ data: { node: { comments: { pageInfo: { hasNextPage: false, endCursor: "Y3Vyc29yOjk" }, nodes: [{ id: "t9", body: "last", createdAt: "2026-07-01T00:00:00Z" }], }, }, }, }), ), ); expect(decoded.comments.map((comment) => comment.id)).toEqual(["t9"]); expect(decoded.nextCursor).toBeNull(); });}); describe("repository access decoding", () => { const repositoryJson = (viewerPermission?: string | null) => JSON.stringify({ mergeCommitAllowed: true, squashMergeAllowed: false, rebaseMergeAllowed: true, ...(viewerPermission === undefined ? {} : { viewerPermission }), }); it("reads the three settings gh reports", () => { expect( expectSuccess(decodeRepositoryAccessJson(repositoryJson("ADMIN"))).mergeCapabilities, ).toEqual({ merge: true, squash: false, rebase: true }); }); it("fails rather than defaulting open when a setting is missing", () => { const decoded = decodeRepositoryAccessJson(JSON.stringify({ mergeCommitAllowed: true })); expect(Result.isSuccess(decoded)).toBe(false); }); it("counts the roles that can push as write, and the ones that cannot as read", () => { for (const permission of ["ADMIN", "MAINTAIN", "WRITE"]) { expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson(permission))).canWrite).toBe( true, ); } for (const permission of ["TRIAGE", "READ", "NONE"]) { expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson(permission))).canWrite).toBe( false, ); } }); it("withholds write where gh names no permission, which is not a standing it gave", () => { // The one place an unknown answer is not granted: a Merge button a reader cannot use wastes // the press, where a missing one still leaves the pull request open on its host. expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson())).canWrite).toBe(false);Lines 1–500 of 959. Load more
import * as Result from "effect/Result"; import { describe, expect, it } from "vite-plus/test"; import { buildReviewSubmissionJson, buildReviewerRequestJson, decodePullRequestActivityJson, decodePullRequestDetailJson, decodePullRequestFilesJson, decodePullRequestListJson, decodeRepositoryAccessJson, decodeReviewerCandidatesJson, decodeReviewThreadCommentsJson, decodeReviewThreadsJson, decodeViewerPermissionsJson, reviewThreadConversation, } 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("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 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("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" }] }), ), ); expect(result.reviewers).toEqual([ { login: "julius", name: null, avatarUrl: "https://avatars/j.png" }, ]); }); it("carries a resolved thread into the conversation, which was still said", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_a", isResolved: false, path: "apps/server/src/ws.ts", comments: { nodes: [{ id: "t1", body: "fix this", createdAt: "2026-07-01T00:00:00Z" }], }, }, { id: "PRRT_b", isResolved: true, path: "apps/web/src/main.tsx", comments: { nodes: [{ id: "t2", body: "done", createdAt: "2026-07-01T00:00:00Z" }] }, }, ]), ), ); const comments = reviewThreadConversation(result.threads.map((entry) => entry.thread)); expect(comments.map((comment) => comment.id)).toEqual(["t1", "t2"]); expect(comments[0]).toMatchObject({ id: "t1", kind: "review-comment", path: "apps/server/src/ws.ts", }); }); it("carries every reply, not only the remark each thread opened with", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_c", isResolved: false, path: "apps/server/src/ws.ts", comments: { nodes: [ { id: "t1", body: "fix this", createdAt: "2026-07-01T00:00:00Z" }, { id: "t2", body: "fixed", createdAt: "2026-07-01T01:00:00Z" }, ], }, }, ]), ), ); const comments = reviewThreadConversation(result.threads.map((entry) => entry.thread)); expect(comments.map((comment) => comment.id)).toEqual(["t1", "t2"]); }); it("hands back the cursor the next page of threads carries on from", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson( [ { id: "PRRT_d", path: "apps/server/src/ws.ts", isResolved: false, comments: { nodes: [{ id: "t1", createdAt: "2026-07-01T00:00:00Z" }] }, }, ], 80, { hasNextPage: true, endCursor: "Y3Vyc29yOjE" }, ), ), ); expect(result.nextCursor).toBe("Y3Vyc29yOjE"); }); it("keeps GitHub's own count of a thread whose comments were not all read", () => { const result = expectSuccess( decodeReviewThreadsJson( threadsJson([ { id: "PRRT_e", path: "apps/server/src/ws.ts", isResolved: false, comments: { totalCount: 140, pageInfo: { hasNextPage: true, endCursor: "Y3Vyc29yOjI" }, nodes: [{ id: "t1", createdAt: "2026-07-01T00:00:00Z" }], }, }, ]), ), ); expect(result.threads[0]).toMatchObject({ commentCount: 140, nextCommentCursor: "Y3Vyc29yOjI", }); }); it("ends a thread's walk on the last page, which still names a cursor", () => { const decoded = expectSuccess( decodeReviewThreadCommentsJson( JSON.stringify({ data: { node: { comments: { pageInfo: { hasNextPage: false, endCursor: "Y3Vyc29yOjk" }, nodes: [{ id: "t9", body: "last", createdAt: "2026-07-01T00:00:00Z" }], }, }, }, }), ), ); expect(decoded.comments.map((comment) => comment.id)).toEqual(["t9"]); expect(decoded.nextCursor).toBeNull(); }); }); describe("repository access decoding", () => { const repositoryJson = (viewerPermission?: string | null) => JSON.stringify({ mergeCommitAllowed: true, squashMergeAllowed: false, rebaseMergeAllowed: true, ...(viewerPermission === undefined ? {} : { viewerPermission }), }); it("reads the three settings gh reports", () => { expect( expectSuccess(decodeRepositoryAccessJson(repositoryJson("ADMIN"))).mergeCapabilities, ).toEqual({ merge: true, squash: false, rebase: true }); }); it("fails rather than defaulting open when a setting is missing", () => { const decoded = decodeRepositoryAccessJson(JSON.stringify({ mergeCommitAllowed: true })); expect(Result.isSuccess(decoded)).toBe(false); }); it("counts the roles that can push as write, and the ones that cannot as read", () => { for (const permission of ["ADMIN", "MAINTAIN", "WRITE"]) { expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson(permission))).canWrite).toBe( true, ); } for (const permission of ["TRIAGE", "READ", "NONE"]) { expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson(permission))).canWrite).toBe( false, ); } }); it("withholds write where gh names no permission, which is not a standing it gave", () => { // The one place an unknown answer is not granted: a Merge button a reader cannot use wastes // the press, where a missing one still leaves the pull request open on its host. expect(expectSuccess(decodeRepositoryAccessJson(repositoryJson())).canWrite).toBe(false);