1405 lines · 45.9 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 { ChildProcessSpawner } from "effect/unstable/process"; import * as GitLabCli from "../sourceControl/GitLabCli.ts";import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; const mockedExecute = vi.fn<GitLabCli.GitLabCli["Service"]["execute"]>(); const layer = it.layer(  GitLabPullRequestCli.layer.pipe(    Layer.provide(      Layer.mock(GitLabCli.GitLabCli)({        execute: mockedExecute,      }),    ),  ),); function output(stdout: string, stdoutTruncated = false, stdoutInvalidUtf8 = false) {  return {    exitCode: ChildProcessSpawner.ExitCode(0),    stdout,    stderr: "",    stdoutTruncated,    stderrTruncated: false,    stdoutInvalidUtf8,  };} function mergeRequests(count: number, firstNumber: number): string {  return JSON.stringify(    Array.from({ length: count }, (_, index) => ({      iid: firstNumber + index,      title: `Merge request ${firstNumber + index}`,      web_url: `https://gitlab.com/acme/web/-/merge_requests/${firstNumber + index}`,      source_branch: "feat/page",      target_branch: "main",      created_at: "2026-07-01T00:00:00Z",      updated_at: "2026-07-02T00:00:00Z",    })),  );} /** A page of `/diffs` as GitLab serves it, a full one unless the count says otherwise. */function diffPage(firstIndex: number, count = 100): string {  return JSON.stringify(    Array.from({ length: count }, (_, index) => ({      old_path: `src/${firstIndex + index}.ts`,      new_path: `src/${firstIndex + index}.ts`,      diff: "@@ -1 +1 @@\n-a\n+b\n",    })),  );} /** A page of merge request notes, which is what the flat conversation is read from. */function notes(count: number, firstId: number): string {  return JSON.stringify(    Array.from({ length: count }, (_, index) => ({      id: firstId + index,      body: `note ${firstId + index}`,      author: { username: "bilal" },      created_at: "2026-07-01T00:00:00Z",    })),  );} /** Who opened the merge request, and somebody already reviewing it. */const author = { id: 1, username: "bilal" };const reviewer = { id: 5, username: "octocat" }; /** One merge request as `/merge_requests/:iid` answers with it. */function mergeRequestJson(overrides: Record<string, unknown>): string {  return JSON.stringify({    iid: 7,    title: "Merge request 7",    web_url: "https://gitlab.com/acme/web/-/merge_requests/7",    source_branch: "feat/page",    target_branch: "main",    created_at: "2026-07-01T00:00:00Z",    updated_at: "2026-07-02T00:00:00Z",    author,    ...overrides,  });} /** The endpoint or subcommand of the nth glab invocation. */function argsOfCall(index: number): ReadonlyArray<string> {  return callAt(index).args;} /** The whole nth invocation, so a request body can be asserted alongside its path. */function callAt(index: number) {  const call = mockedExecute.mock.calls[index];  assert.isDefined(call);  return call[0];} afterEach(() => {  mockedExecute.mockReset();}); layer("GitLabPullRequestCli.layer", (it) => {  it.effect("asks GitLab for one row more than the page, to probe for a next page", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output(mergeRequests(3, 1))));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,      });       assert.strictEqual(batch.items.length, 3);      assert.isFalse(batch.truncated);      assert.strictEqual(batch.cursorAdvance, 3);      const path = argsOfCall(0)[1] ?? "";      expect(path).toContain("projects/acme%2Fweb/merge_requests");      expect(path).toContain("per_page=11");      expect(path).toContain("state=opened");    }),  );   it.effect("walks pages at a fixed size, because GitLab pages by offset", () =>    Effect.gen(function* () {      mockedExecute        .mockReturnValueOnce(Effect.succeed(output(mergeRequests(100, 1))))        .mockReturnValueOnce(Effect.succeed(output(mergeRequests(100, 101))));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 150,      });       assert.strictEqual(batch.items.length, 150);      assert.isTrue(batch.truncated);      for (const index of [0, 1]) {        expect(argsOfCall(index)[1]).toContain("per_page=100");      }      expect(argsOfCall(0)[1]).toContain("page=1");      expect(argsOfCall(1)[1]).toContain("page=2");    }),  );   it.effect("hands a search to GitLab's own search parameter", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,        query: "page",      });       // GitLab matches `search` against title and description, which is more than the row shows.      expect(argsOfCall(0)[1]).toContain("search=page");    }),  );   it.effect("carries on from the number of rows already delivered", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output(mergeRequests(3, 1))));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,        cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 10 },      });       // GitLab's timestamp filter has no tie-breaker, so an offset is what advances through a      // boundary shared by more rows than one page can hold.      const path = argsOfCall(0)[1] ?? "";      expect(path).not.toContain("updated_before=");      expect(path).toContain("order_by=updated_at");      expect(path).toContain("per_page=11");      expect(path).toContain("page=1");    }),  );   it.effect("advances beyond several pages sharing the cursor timestamp", () =>    Effect.gen(function* () {      mockedExecute        .mockReturnValueOnce(Effect.succeed(output(mergeRequests(11, 144))))        .mockReturnValueOnce(Effect.succeed(output(mergeRequests(11, 155))));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,        cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 150 },      });       expect(argsOfCall(0)[1]).toContain("per_page=11");      expect(argsOfCall(0)[1]).toContain("page=14");      expect(argsOfCall(1)[1]).toContain("page=15");      expect(batch.items.map((item) => item.number)).toEqual([        151, 152, 153, 154, 155, 156, 157, 158, 159, 160,      ]);      assert.isTrue(batch.truncated);    }),  );   it.effect("advances the cursor through malformed raw rows", () =>    Effect.gen(function* () {      // @effect-diagnostics-next-line preferSchemaOverJson:off      const rows = JSON.parse(mergeRequests(2, 1)) as ReadonlyArray<unknown>;      mockedExecute.mockReturnValueOnce(        // @effect-diagnostics-next-line preferSchemaOverJson:off        Effect.succeed(output(JSON.stringify([{ iid: "malformed" }, ...rows]))),      );      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 2,      });       expect(batch.items.map((item) => item.number)).toEqual([1, 2]);      assert.strictEqual(batch.cursorAdvance, 3);      assert.isTrue(batch.truncated);    }),  );   it.effect("URL-encodes a search, so it cannot add a parameter of its own", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,        query: '-a&per_page=1 "b"',      });       const path = argsOfCall(0)[1] ?? "";      expect(path).toContain("search=-a%26per_page%3D1%20%22b%22");      // The page size the walk fixed is still the only one in the query.      assert.strictEqual(path.match(/per_page=/g)?.length, 1);    }),  );   it.effect("asks for no search at all when the reader typed only spaces", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,        query: "   ",      });       expect(argsOfCall(0)[1]).not.toContain("search=");    }),  );   it.effect("stops walking on a short page", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output(mergeRequests(40, 1))));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 150,      });       assert.strictEqual(batch.items.length, 40);      assert.isFalse(batch.truncated);      assert.strictEqual(mockedExecute.mock.calls.length, 1);    }),  );   it.effect("stops walking when every row on a page fails to decode", () =>    Effect.gen(function* () {      // Full pages of unusable rows: nothing is collected, so the collected-count bound never      // trips and only the page bound can end the walk.      // @effect-diagnostics-next-line preferSchemaOverJson:off      const unusable = JSON.stringify(Array.from({ length: 100 }, () => ({ iid: "nope" })));      mockedExecute.mockReturnValue(Effect.succeed(output(unusable)));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       const batch = yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 150,      });       assert.strictEqual(batch.items.length, 0);      // ceil((150 + 1) / 100) pages, not one request per page forever.      assert.strictEqual(mockedExecute.mock.calls.length, 2);    }),  );   it.effect("asks GitLab for every state on the All tab", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "all",        involvement: "all",        viewer: "bilal",        limit: 10,      });       expect(argsOfCall(0)[1]).toContain("state=all");    }),  );   it.effect("filters by the reviewer when the viewer is reviewing", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/web",        state: "open",        involvement: "reviewing",        viewer: "bilal",        limit: 10,      });       expect(argsOfCall(0)[1]).toContain("reviewer_username=bilal");    }),  );   it.effect("addresses a nested group project by its encoded full path", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.listMergeRequests({        cwd: "/w",        repository: "acme/platform/web",        state: "open",        involvement: "all",        viewer: "bilal",        limit: 10,      });       expect(argsOfCall(0)[1]).toContain("projects/acme%2Fplatform%2Fweb/merge_requests");    }),  );   it.effect("merges immediately rather than leaving auto-merge armed", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.runMergeRequestAction({        cwd: "/w",        repository: "acme/web",        number: 7,        action: "merge",        mergeMethod: "squash",      });       expect(argsOfCall(0)).toEqual([        "mr",        "merge",        "7",        "--repo",        "acme/web",        "--auto-merge=false",        "--yes",        "--squash",      ]);    }),  );   it.effect("arms auto-merge with the same strategy a merge would have used", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.runMergeRequestAction({        cwd: "/w",        repository: "acme/web",        number: 7,        action: "enable-auto-merge",        mergeMethod: "squash",      });       expect(argsOfCall(0)).toEqual([        "mr",        "merge",        "7",        "--repo",        "acme/web",        "--auto-merge=true",        "--yes",        "--squash",      ]);    }),  );   it.effect("cancels an armed auto-merge through the API glab has no flag for", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("{}")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.runMergeRequestAction({        cwd: "/w",        repository: "acme/platform/web",        number: 7,        action: "disable-auto-merge",      });       expect(argsOfCall(0)).toEqual([        "api",        "projects/acme%2Fplatform%2Fweb/merge_requests/7/cancel_merge_when_pipeline_succeeds",        "--method",        "POST",      ]);    }),  );   it.effect("brings a stale branch up to date by rebasing it, the only way GitLab has", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.runMergeRequestAction({        cwd: "/w",        repository: "acme/web",        number: 7,        action: "update-branch",      });       expect(argsOfCall(0)).toEqual(["mr", "rebase", "7", "--repo", "acme/web"]);    }),  );   it.effect("moves a merge request back to draft through glab", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli;       yield* cli.runMergeRequestAction({        cwd: "/w",        repository: "acme/web",        number: 7,        action: "draft",      });       expect(argsOfCall(0)).toEqual(["mr", "update", "7", "--repo", "acme/web", "--draft"]);    }),  );   it.effect("sends a comment body over stdin, never in argv", () =>    Effect.gen(function* () {      mockedExecute.mockReturnValueOnce(Effect.succeed(output("")));      const cli = yield* GitLabPullRequestCli.GitLabPullRequestCli; 

Lines 1–500 of 1405.