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. Load more
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;