849 lines · 26.9 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import type { PullRequestReaction } from "@t3tools/contracts"; import * as GitHubCli from "../sourceControl/GitHubCli.ts";import * as GitHubPullRequestCli from "./GitHubPullRequestCli.ts";import { gitHubViewerPermissions, loginAvatarUrl, make } from "./GitHubPullRequestProvider.ts";import type { GitHubReviewThreadComments } from "./gitHubPullRequestJson.ts"; it.effect("uses one narrow read for a linked pull request summary", () =>  Effect.gen(function* () {    let summaryReads = 0;    const provider = yield* make.pipe(      Effect.provide(        Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({          getPullRequestSummary: () =>            Effect.sync(() => {              summaryReads += 1;              return {                number: 7,                title: "Summary",                url: "https://github.com/acme/web/pull/7",                headBranch: "feat/summary",                baseBranch: "main",                state: "open" as const,                updatedAt: "2026-08-24T12:34:56.000Z",                author: { login: "octocat", name: null, avatarUrl: null },              };            }),        }),      ),    );     const readSummary = provider.getChangeRequestSummary;    if (readSummary === undefined) return yield* Effect.die("summary read was not implemented");    const summary = yield* readSummary({      cwd: "/w",      repository: "acme/web",      host: "github.com",      number: 7,    });     expect(summary.state).toBe("open");    expect(summaryReads).toBe(1);    // The author's avatar comes from the login-shaped URL, not a second request.    expect(summary.author?.avatarUrl).toBe("https://github.com/octocat.png?size=80");  }),); it.effect("declares host-native stacks and passes the one the CLI reads through", () =>  Effect.gen(function* () {    const stack = {      id: "42",      number: 3,      url: "https://github.com/acme/web/stacks/3",      base: "main",      layers: [        { number: 6, headBranch: "feat/one", state: "merged" as const },        { number: 7, headBranch: "feat/two", state: "open" as const },      ],    };    const provider = yield* make.pipe(      Effect.provide(        Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({          getPullRequestStack: (input) => Effect.succeed(input.number === 7 ? stack : null),        }),      ),    );     expect(provider.capabilities.stacks).toBe(true);    const readStack = provider.getChangeRequestStack;    if (readStack === undefined) return yield* Effect.die("stack read was not implemented");    const ref = { cwd: "/w", repository: "acme/web", host: "github.com" };    expect(yield* readStack({ ...ref, number: 7 })).toEqual(stack);    expect(yield* readStack({ ...ref, number: 8 })).toBeNull();  }),); it.effect("reports a failed stack read against its own operation", () =>  Effect.gen(function* () {    const provider = yield* make.pipe(      Effect.provide(        Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({          getPullRequestStack: () =>            Effect.fail(              new GitHubPullRequestCli.GitHubPullRequestReadError({                command: "gh",                cwd: "/w",                operation: "getPullRequestStack",                cause: new Error("unreadable"),              }),            ),        }),      ),    );     const readStack = provider.getChangeRequestStack;    if (readStack === undefined) return yield* Effect.die("stack read was not implemented");    const error = yield* Effect.flip(      readStack({ cwd: "/w", repository: "acme/web", host: "github.com", number: 7 }),    );     expect(error.operation).toBe("getChangeRequestStack");    expect(error.reason).toBe("failed");  }),); describe("gitHubViewerPermissions", () => {  it("offers everything to a viewer who can write to the repository", () => {    expect(      gitHubViewerPermissions({        canWrite: true,        canTriage: true,        canUpdate: true,        didAuthor: false,      }),    ).toEqual({      // Arming a merge for later is the merge, so it travels with it.      actions: [        "merge",        "enable-auto-merge",        "disable-auto-merge",        "revert",        "approve-workflows",        "ready",        "draft",        "close",        "reopen",      ],      comment: true,      resolve: true,      stackRebase: true,      verdicts: ["comment", "approve", "request-changes"],      requestReviewers: true,      labels: true,    });  });   it("leaves a passer-by on a repository they can only read nothing but the review", () => {    // Every open-source pull request somebody else opened: GitHub says no to all five actions    // and to resolving, and yes to commenting and to every verdict.    expect(      gitHubViewerPermissions({        canWrite: false,        canTriage: false,        canUpdate: false,        didAuthor: false,      }),    ).toEqual({      actions: [],      comment: true,      resolve: false,      verdicts: ["comment", "approve", "request-changes"],      // Asking somebody else to review is the one thing read access never stretches to.      requestReviewers: false,      labels: false,    });  });   it("lets a triager label without letting them merge or ask for a review", () => {    const permissions = gitHubViewerPermissions({      canWrite: false,      canTriage: true,      canUpdate: false,      didAuthor: false,    });    expect(permissions.labels).toBe(true);    expect(permissions.requestReviewers).toBe(false);    expect(permissions.actions).toEqual([]);  });   it("keeps an author's own pull request theirs to close, with read access and no more", () => {    expect(      gitHubViewerPermissions({        canWrite: false,        canTriage: false,        canUpdate: true,        didAuthor: true,      }),    ).toEqual({      // Merging is the one thing writing is needed for, now or later; the rest an author may do.      actions: ["ready", "draft", "close", "reopen"],      comment: true,      resolve: true,      // GitHub refuses an author's approval of their own change, so the page does not offer one.      verdicts: ["comment"],      requestReviewers: false,      labels: false,    });  });   it.effect("uses the small viewer-access read for core permissions", () =>    Effect.gen(function* () {      const provider = yield* make;      const detail = yield* provider.getChangeRequest({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      });       expect(detail.viewerPermissions).toEqual({        actions: ["ready", "draft", "close", "reopen"],        comment: true,        resolve: false,        verdicts: ["comment", "approve", "request-changes"],        requestReviewers: false,        labels: false,      });      expect(detail.workflowApprovalsRequired).toBeUndefined();      expect(detail.checks).toContainEqual({        name: "Workflow approval status",        status: "action-required",        description: "GitHub could not determine whether workflows are awaiting approval.",        url: null,      });    }).pipe(      Effect.provide(        Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({          getPullRequestDetail: () =>            Effect.succeed({              authorId: null,              number: 7,              title: "Pull request 7",              url: "https://github.com/acme/web/pull/7",              author: null,              isCrossRepository: true,              headRepositoryOwner: null,              headBranch: "feat/page",              baseBranch: "main",              state: "open",              isDraft: false,              mergeability: "mergeable",              reviewDecision: null,              additions: 1,              deletions: 1,              createdAt: "2026-07-01T00:00:00Z",              updatedAt: "2026-07-02T00:00:00Z",              reviewRequestLogins: [],              hasTeamReviewRequest: false,              checksState: null,              labels: [],              body: "",              changedFiles: 1,              mergedAt: null,              closedAt: null,              checks: [],              comments: [],              commits: [],            }),          getRepositoryAccess: () =>            Effect.succeed({              canWrite: false,              mergeCapabilities: { merge: true, squash: true, rebase: true },            }),          getViewerAccess: () =>            Effect.succeed({              canWrite: false,              canTriage: false,              canUpdate: true,              didAuthor: false,            }),        }),      ),    ),  );   it.effect("keeps fork workflows awaiting approval out of the passing state", () =>    Effect.gen(function* () {      const provider = yield* make;      const detail = yield* provider.getChangeRequest({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      });       expect(detail.workflowApprovalsRequired).toBe(1);      expect(detail.checks).toEqual([        {          name: "manual gate",          status: "action-required",          description: null,          url: "https://example.com/manual-gate",        },        {          name: "build",          status: "success",          description: null,          url: null,        },        {          name: "contributor tests",          status: "action-required",          description: "A maintainer must approve this workflow before it can run.",          url: "https://github.com/acme/web/actions/runs/123",        },      ]);    }).pipe(      Effect.provide(        Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({          getPullRequestDetail: () =>            Effect.succeed({              authorId: null,              number: 7,              title: "Pull request 7",              url: "https://github.com/acme/web/pull/7",              author: null,              isCrossRepository: true,              headRepositoryOwner: "octocat",              headSha: "abc123",              headBranch: "feat/page",              baseBranch: "main",              state: "open",              isDraft: false,              mergeability: "mergeable",              reviewDecision: null,              additions: 1,              deletions: 1,              createdAt: "2026-07-01T00:00:00Z",              updatedAt: "2026-07-02T00:00:00Z",              reviewRequestLogins: [],              hasTeamReviewRequest: false,              checksState: "passing",              labels: [],              body: "",              changedFiles: 1,              mergedAt: null,              closedAt: null,              checks: [                {                  name: "manual gate",                  status: "action-required",                  description: null,                  url: "https://example.com/manual-gate",                },                { name: "build", status: "success", description: null, url: null },              ],              comments: [],              commits: [],            }),          listWorkflowRunsRequiringApproval: () =>            Effect.succeed([              {                id: 123,                name: "contributor tests",                url: "https://github.com/acme/web/actions/runs/123",              },            ]),          getPullRequestBaseComparison: () =>            Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),          getRepositoryAccess: () =>            Effect.succeed({              canWrite: true,              mergeCapabilities: { merge: true, squash: true, rebase: true },            }),          getViewerAccess: () =>            Effect.succeed({ canWrite: true, canTriage: true, canUpdate: true, didAuthor: false }),        }),      ),    ),  );}); const openDetail = {  authorId: null,  number: 7,  title: "Pull request 7",  url: "https://github.com/acme/web/pull/7",  author: null,  isCrossRepository: true,  headRepositoryOwner: "acme",  headSha: "abc123",  headBranch: "feat/page",  baseBranch: "main",  state: "open" as const,  isDraft: false,  mergeability: "mergeable" as const,  reviewDecision: null,  additions: 1,  deletions: 1,  createdAt: "2026-07-01T00:00:00Z",  updatedAt: "2026-07-02T00:00:00Z",  reviewRequestLogins: [],  hasTeamReviewRequest: false,  checksState: null,  labels: [],  body: "",  changedFiles: 1,  mergedAt: null,  closedAt: null,  checks: [],  comments: [],  commits: [],}; it.effect("does not classify same-repository gates as fork workflow approvals", () =>  Effect.gen(function* () {    const provider = yield* make;    const detail = yield* provider.getChangeRequest({      cwd: "/w",      repository: "acme/web",      host: "github.com",      number: 7,    });     expect(detail.workflowApprovalsRequired).toBe(0);    expect(detail.checks).toEqual([]);  }).pipe(    Effect.provide(      Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({        getPullRequestDetail: () => Effect.succeed({ ...openDetail, isCrossRepository: false }),        getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),        listWorkflowRunsRequiringApproval: () =>          Effect.die("same-repository pull requests must not probe fork workflow approvals"),        getRepositoryAccess: () =>          Effect.succeed({            canWrite: true,            mergeCapabilities: { merge: true, squash: true, rebase: true },          }),        getViewerAccess: () =>          Effect.succeed({ canWrite: true, canTriage: true, canUpdate: true, didAuthor: false }),      }),    ),  ),); it.effect("keeps an unsafe workflow approval scope visible as unknown", () =>  Effect.gen(function* () {    const provider = yield* make;    const detail = yield* provider.getChangeRequest({      cwd: "/w",      repository: "acme/web",      host: "github.com",      number: 7,    });     expect(detail.workflowApprovalsRequired).toBeUndefined();    expect(detail.checks).toEqual([      {        name: "Workflow approval status",        status: "action-required",        description: "GitHub could not determine whether workflows are awaiting approval.",        url: null,      },    ]);  }).pipe(    Effect.provide(      Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({        getPullRequestDetail: () => Effect.succeed(openDetail),        getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),        listWorkflowRunsRequiringApproval: () =>          Effect.fail(            new GitHubPullRequestCli.GitHubWorkflowApprovalRefusedError({              command: "gh",              cwd: "/w",              number: 7,              reason: "head-not-unique",              observedCount: 2,              limit: 1_000,            }),          ),        getRepositoryAccess: () =>          Effect.succeed({            canWrite: true,            mergeCapabilities: { merge: true, squash: true, rebase: true },          }),        getViewerAccess: () =>          Effect.succeed({ canWrite: true, canTriage: true, canUpdate: true, didAuthor: false }),      }),    ),  ),); it.effect("propagates workflow discovery rate limits", () =>  Effect.gen(function* () {    const provider = yield* make;    const error = yield* provider      .getChangeRequest({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      })      .pipe(Effect.flip);     expect(error.operation).toBe("getChangeRequest");    expect(error.reason).toBe("rate-limited");  }).pipe(    Effect.provide(      Layer.mock(GitHubPullRequestCli.GitHubPullRequestCli)({        getPullRequestDetail: () => Effect.succeed(openDetail),        getPullRequestBaseComparison: () => Effect.succeed({ behindBy: 0, viewerCanUpdate: true }),        listWorkflowRunsRequiringApproval: () =>          Effect.fail(            new GitHubCli.GitHubCliRateLimitError({              command: "gh",              cwd: "/w",              cause: new Error("rate limited"),

Lines 1–500 of 849.