3475 lines · 113.0 KB · TypeScript
RawDownload
import { afterEach, assert, expect, it, vi } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema";import * as TestClock from "effect/testing/TestClock";import { ChildProcessSpawner } from "effect/unstable/process"; import * as GitHubCli from "../sourceControl/GitHubCli.ts";import * as GitHubGraphQlBudget from "../sourceControl/githubGraphQlBudget.ts";import * as GitHubPullRequestCli from "./GitHubPullRequestCli.ts";import { BASE_COMPARISON_GRAPHQL_QUERY } from "./gitHubPullRequestJson.ts"; const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); const mockedExecute = vi.fn<GitHubCli.GitHubCli["Service"]["execute"]>();const mockedStackMemberships = vi.fn<GitHubCli.GitHubCli["Service"]["execute"]>(() =>  Effect.succeed(output('{"data":{}}')),);const mockedGetPullRequest = vi.fn<GitHubCli.GitHubCli["Service"]["getPullRequest"]>(); const layer = it.layer(  GitHubPullRequestCli.layer.pipe(    Layer.provide(      Layer.mock(GitHubCli.GitHubCli)({        execute: (input) =>          input.args.some((arg) => arg.includes("query PullRequestStackMemberships"))            ? mockedStackMemberships(input)            : mockedExecute(input),        getPullRequest: mockedGetPullRequest,      }),    ),    Layer.provide(GitHubGraphQlBudget.layer),  ),); function output(stdout: string, stdoutTruncated = false, stdoutInvalidUtf8 = false) {  return {    exitCode: ChildProcessSpawner.ExitCode(0),    stdout,    stderr: "",    stdoutTruncated,    stderrTruncated: false,    stdoutInvalidUtf8,  };} function pullRequests(  count: number,  firstNumber: number,  overrides: (number: number) => Readonly<Record<string, unknown>> = () => ({}),): string {  return JSON.stringify(    Array.from({ length: count }, (_, index) => ({      number: firstNumber + index,      title: `Pull request ${firstNumber + index}`,      url: `https://github.com/acme/web/pull/${firstNumber + index}`,      headRefName: "feat/page",      baseRefName: "main",      createdAt: "2026-07-01T00:00:00Z",      updatedAt: "2026-07-02T00:00:00Z",      ...overrides(firstNumber + index),    })),  );} function pullRequestFiles(count: number, firstIndex: number): string {  return JSON.stringify(    Array.from({ length: count }, (_, index) => ({      filename: `src/file${firstIndex + index}.ts`,      status: "modified",      patch: "@@ -1 +1 @@\n-old\n+new",    })),  );} /** One thread's comments as the GraphQL read returns them, cursor and all. */function threadComments(  ids: ReadonlyArray<string>,  endCursor: string | null,  totalCount = ids.length,) {  return {    totalCount,    pageInfo: { hasNextPage: endCursor !== null, endCursor },    nodes: ids.map((id) => ({ id, body: id, createdAt: "2026-07-01T00:00:00Z" })),  };} function thread(id: string, ...commentIds: ReadonlyArray<string>) {  return {    id,    path: "src/a.ts",    line: 1,    diffSide: "RIGHT",    isResolved: false,    isOutdated: false,    comments: threadComments(commentIds, null),  };} function reviewThreadsPage(  nodes: ReadonlyArray<Record<string, unknown>>,  endCursor: string | null,): string {  return JSON.stringify({    data: {      repository: {        pullRequest: {          reviewThreads: {            totalCount: nodes.length,            pageInfo: { hasNextPage: endCursor !== null, endCursor },            nodes,          },        },      },    },  });} function threadCommentsPage(  ids: ReadonlyArray<string>,  endCursor: string | null,  totalCount: number,  pullRequestId = "PR_7",): string {  return JSON.stringify({    data: {      repository: { pullRequest: { id: "PR_7" } },      node: {        pullRequest: { id: pullRequestId },        comments: threadComments(ids, endCursor, totalCount),      },    },  });} /** What `gh pr diff` answers on a pull request GitHub will not serve a diff for. */const diffRefused = new GitHubCli.GitHubCliCommandError({  command: "gh",  cwd: "/w",  cause: new Error("HTTP 406: the diff exceeded the maximum number of files (300)"),}); /** The whole invocation the nth call made, so both argv and stdin can be asserted. */function callAt(index: number) {  const call = mockedExecute.mock.calls[index];  assert.isDefined(call);  return call[0];} /** The one argument `--search` carries, which is where every listing filter ends up. */function searchOfCall(index: number): string | undefined {  const args = callAt(index).args;  const flag = args.indexOf("--search");  // Absent is its own answer: a read that carries no `--search` at all is what the fallback is.  return flag === -1 ? undefined : args[flag + 1];} /** One row as a search answers it, which is the listing's row one connection deeper. */function searchItem(number: number, repository: string, updatedAt: string) {  return {    number,    title: `Pull request ${number}`,    url: `https://github.com/${repository}/pull/${number}`,    author: { login: "octocat", avatarUrl: "https://avatars/octocat" },    headRefName: "feat/page",    baseRefName: "main",    state: "OPEN",    isDraft: false,    mergeable: "MERGEABLE",    createdAt: "2026-07-01T00:00:00Z",    updatedAt,    repository: { nameWithOwner: repository },    reviewRequests: { nodes: [{ requestedReviewer: { login: "hubot" } }] },    labels: { nodes: [{ name: "bug", color: "ff0000" }] },  };} function searchPage(nodes: ReadonlyArray<unknown>, hasNextPage = false) {  return output(JSON.stringify({ data: { search: { pageInfo: { hasNextPage }, nodes } } }));} /** The search a batched read sent, which travels in the request body rather than in argv. */function searchQueryOfCall(index: number): string | undefined {  const body = JSON.parse(callAt(index).stdin ?? "{}") as { variables?: { q?: string } };  return body.variables?.q;} afterEach(() => {  mockedExecute.mockReset();  mockedStackMemberships.mockReset();  mockedGetPullRequest.mockReset();}); layer("GitHubPullRequestCli.layer", (it) => {  it.effect("reads linked pull request status with the overview fields in one request", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(        Effect.succeed(          output(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              number: 7,              title: "Reuse the summary",              url: "https://github.com/acme/web/pull/7",              author: { login: "octocat", name: "Octo Cat" },              baseRefName: "main",              headRefName: "feat/summary",              state: "OPEN",              isDraft: false,              mergeable: "MERGEABLE",              reviewDecision: "APPROVED",              additions: 12,              deletions: 3,              changedFiles: 2,              createdAt: "2026-08-20T00:00:00.000Z",              updatedAt: "2026-08-24T12:34:56.000Z",              reviewRequests: [],              labels: [],              statusCheckRollup: [                { __typename: "CheckRun", status: "COMPLETED", conclusion: "SUCCESS", name: "ci" },              ],              body: "",            }),          ),        ),      );      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const summary = yield* cli.getPullRequestSummary({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      });       assert.deepStrictEqual(        {          number: summary.number,          state: summary.state,          headBranch: summary.headBranch,          isDraft: summary.isDraft,          author: summary.author?.login,          additions: summary.additions,          deletions: summary.deletions,          changedFiles: summary.changedFiles,          reviewDecision: summary.reviewDecision,          checksState: summary.checksState,          mergeability: summary.mergeability,        },        {          number: 7,          state: "open",          headBranch: "feat/summary",          isDraft: false,          author: "octocat",          additions: 12,          deletions: 3,          changedFiles: 2,          reviewDecision: "approved",          checksState: "passing",          mergeability: "mergeable",        },      );      expect(mockedExecute).toHaveBeenCalledOnce();      expect(mockedExecute.mock.calls[0]?.[0]?.args).toEqual([        "pr",        "view",        "7",        "--repo",        "github.com/acme/web",        "--json",        expect.stringContaining("statusCheckRollup"),      ]);      expect(mockedGetPullRequest).not.toHaveBeenCalled();    }),  );   it.effect("reads the stack a pull request is in through the stacks preview, on its host", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(        Effect.succeed(          output(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify([              {                id: 42,                number: 3,                url: "https://api.github.com/repos/acme/web/stacks/3",                base: { ref: "main" },                pull_requests: [                  {                    number: 6,                    head: { ref: "feat/one" },                    state: "closed",                    merged_at: "2026-09-02",                  },                  { number: 7, head: { ref: "feat/two" }, state: "open", merged_at: null },                ],              },            ]),          ),        ),      );      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const stack = yield* cli.getPullRequestStack({        cwd: "/w",        repository: "acme/web",        host: "ghe.example.com",        number: 7,      });       assert.deepStrictEqual(stack, {        id: "42",        number: 3,        url: "https://api.github.com/repos/acme/web/stacks/3",        base: "main",        layers: [          { number: 6, headBranch: "feat/one", state: "merged" },          { number: 7, headBranch: "feat/two", state: "open" },        ],      });      assert.deepStrictEqual(callAt(0).args, [        "api",        "--hostname",        "ghe.example.com",        "repos/acme/web/stacks?pull_request=7",      ]);    }),  );   it.effect("fetches layer titles only when the caller asks for stack details", () =>    Effect.gen(function* () {      const minimal = {        url: "https://api.github.com/repos/acme/web/stacks/3",        number: 3,        base: { ref: "main" },        pull_requests: [          { number: 7, head: { ref: "feat/two", sha: "abc123" }, state: "open", merged_at: null },        ],      };      // @effect-diagnostics-next-line preferSchemaOverJson:off      mockedExecute.mockReturnValueOnce(Effect.succeed(output(JSON.stringify([minimal]))));      mockedExecute.mockReturnValueOnce(        Effect.succeed(          output(            // @effect-diagnostics-next-line preferSchemaOverJson:off            JSON.stringify({              ...minimal,              pull_requests: [{ ...minimal.pull_requests[0], title: "Second layer", draft: false }],            }),          ),        ),      );      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;      const stack = yield* cli.getPullRequestStack({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,        includeDetails: true,      });      expect(stack?.layers[0]).toMatchObject({        title: "Second layer",        headSha: "abc123",        isDraft: false,      });      expect(callAt(1).args).toEqual([        "api",        "--hostname",        "github.com",        "repos/acme/web/stacks/3",      ]);    }),  );   it.effect("reads an empty stacks listing as not stacked", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const stack = yield* cli.getPullRequestStack({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      });       assert.isNull(stack);    }),  );   it.effect("reads a host that refuses the stacks preview as not stacked", () =>    Effect.gen(function* () {      // The CLI classifies a missing preview endpoint as not found.      mockedExecute.mockReturnValueOnce(        Effect.fail(          new GitHubCli.GitHubPullRequestNotFoundError({            command: "gh",            cwd: "/w",            cause: new Error("HTTP 404: Not Found (https://api.github.com/repos/acme/web/stacks)"),          }),        ),      );      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const stack = yield* cli.getPullRequestStack({        cwd: "/w",        repository: "acme/web",        host: "github.com",        number: 7,      });       assert.isNull(stack);    }),  );   it.effect("does not read a signed-out gh as an unstacked pull request", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(        Effect.fail(          new GitHubCli.GitHubCliAuthenticationError({            command: "gh",            cwd: "/w",            cause: new Error("gh auth login"),          }),        ),      );      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const error = yield* Effect.flip(        cli.getPullRequestStack({          cwd: "/w",          repository: "acme/web",          host: "github.com",          number: 7,        }),      );       assert.strictEqual(error._tag, "GitHubCliAuthenticationError");    }),  );   it.effect("preserves transient stack failures instead of reporting no stack", () =>    Effect.gen(function* () {      const failure = new GitHubCli.GitHubCliCommandError({        command: "gh",        cwd: "/w",        cause: new Error("HTTP 503"),      });      mockedExecute.mockReturnValueOnce(Effect.fail(failure));      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;      const error = yield* Effect.flip(        cli.getPullRequestStack({          cwd: "/w",          repository: "acme/web",          host: "github.com",          number: 7,        }),      );      assert.strictEqual(error, failure);    }),  );   it.effect("reports a stacks answer it cannot read against the stack read", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output('[{"id":42}]')));      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const error = yield* Effect.flip(        cli.getPullRequestStack({          cwd: "/w",          repository: "acme/web",          host: "github.com",          number: 7,        }),      );       assert.strictEqual(error._tag, "GitHubPullRequestReadError");      if (error._tag !== "GitHubPullRequestReadError") return;      assert.strictEqual(error.operation, "getPullRequestStack");    }),  );   it.effect("asks for one row more than the page, to probe for a next page", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1))));      const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;       const batch = yield* cli.listPullRequests({        cwd: "/w",        repository: "acme/web",        host: "github.com",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,      }); 

Lines 1–500 of 3475.