959 lines · 30.6 KB · TypeScript
RawDownload
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.