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 AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts";import * as AzureDevOpsPullRequestCli from "./AzureDevOpsPullRequestCli.ts";import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; const mockedExecute = vi.fn<AzureDevOpsCli.AzureDevOpsCli["Service"]["execute"]>(); const layer = it.layer( AzureDevOpsPullRequestCli.layer.pipe( Layer.provide( Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({ execute: mockedExecute, }), ), ),); function output(stdout: string) { return { exitCode: ChildProcessSpawner.ExitCode(0), stdout, stderr: "", stdoutTruncated: false, stderrTruncated: false, };} function pullRequestRows( count: number, firstNumber: number,): ReadonlyArray<Record<string, unknown>> { return Array.from({ length: count }, (_, index) => ({ pullRequestId: firstNumber + index, title: `Pull request ${firstNumber + index}`, status: "active", sourceRefName: "refs/heads/feat/page", targetRefName: "refs/heads/main", creationDate: "2026-07-01T00:00:00Z", repository: { name: "web", project: { name: "platform" } }, url: `https://dev.azure.com/acme/_apis/git/repositories/web/pullRequests/${firstNumber + index}`, }));} function pullRequests(count: number, firstNumber: number): string { return JSON.stringify(pullRequestRows(count, firstNumber));} /** The arguments of the nth az invocation. */function argsOfCall(index: number): ReadonlyArray<string> { const call = mockedExecute.mock.calls[index]; assert.isDefined(call); return call[0].args;} afterEach(() => { mockedExecute.mockReset();}); layer("AzureDevOpsPullRequestCli.layer", (it) => { 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* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); assert.strictEqual(batch.items.length, 3); assert.isFalse(batch.truncated); expect(argsOfCall(0)).toEqual([ "repos", "pr", "list", "--detect", "true", "--repository", "web", "--status", "active", "--include-links", "--top", "11", "--only-show-errors", "--output", "json", ]); }), ); it.effect("reads an Azure pull request page larger than the VCS default output limit", () => Effect.gen(function* () { const rows = pullRequestRows(100, 1).map((row) => ({ ...row, description: "x".repeat(10_000), })); // @effect-diagnostics-next-line preferSchemaOverJson:off const response = JSON.stringify(rows); expect(Buffer.byteLength(response)).toBeGreaterThan(1_000_000); mockedExecute.mockImplementationOnce((input) => { const maxOutputBytes = "maxOutputBytes" in input && typeof input.maxOutputBytes === "number" ? input.maxOutputBytes : 1_000_000; return Effect.succeed( maxOutputBytes >= Buffer.byteLength(response) ? output(response) : { ...output(response.slice(0, maxOutputBytes)), stdoutTruncated: true, }, ); }); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "merged", involvement: "all", viewer: "bilal@acme.dev", limit: 99, }); assert.strictEqual(batch.items.length, 99); assert.isTrue(batch.truncated); }), ); it.effect("reads the page unnarrowed when asked to search, having nothing to search with", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); const provider = yield* AzureDevOpsPullRequestProvider.make; const page = yield* provider.listChangeRequests({ cwd: "/w", repository: "web", host: "dev.azure.com", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, query: "page", }); // `az repos pr list` filters by status, creator, reviewer and branch, and by no text at // all. The rows come back as they would have without a search, for the caller to narrow; // nothing of the search reaches the command, where it could only mean the wrong thing. assert.strictEqual(page.items.length, 3); expect(argsOfCall(0)).toEqual([ "repos", "pr", "list", "--detect", "true", "--repository", "web", "--status", "active", "--include-links", "--top", "11", "--only-show-errors", "--output", "json", ]); }), ); it.effect("steps over what it has already handed over, which is all Azure can be told", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, // The instant is the same cursor every other host reads; Azure has no filter for it and // takes the count instead. cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 20 }, }); const args = argsOfCall(0); expect(args).toContain("--skip"); assert.strictEqual(args[args.indexOf("--skip") + 1], "20"); expect(args).not.toContain("2026-07-02T00:00:00Z"); }), ); it.effect("reports truncation from the extra row", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(11, 1)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); assert.strictEqual(batch.items.length, 10); assert.isTrue(batch.truncated); assert.strictEqual(batch.cursorAdvance, 10); }), ); it.effect("advances by malformed raw rows and keeps reading until the page is full", () => Effect.gen(function* () { mockedExecute .mockReturnValueOnce( Effect.succeed( output( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { pullRequestId: "malformed" }, pullRequestRows(1, 1)[0], { pullRequestId: "also malformed" }, ]), ), ), ) .mockReturnValueOnce(Effect.succeed(output(pullRequests(2, 2)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 2, }); expect(batch.items.map((item) => item.number)).toEqual([1, 2]); assert.isTrue(batch.truncated); // Three raw rows from the first request and one from the second produced this page. assert.strictEqual(batch.cursorAdvance, 4); const secondArgs = argsOfCall(1); assert.strictEqual(secondArgs[secondArgs.indexOf("--skip") + 1], "3"); assert.strictEqual(secondArgs[secondArgs.indexOf("--top") + 1], "2"); }), ); it.effect("narrows to the author on the authored tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "closed", involvement: "authored", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--creator"); expect(argsOfCall(0)).toContain("bilal@acme.dev"); // Azure calls a closed pull request abandoned. expect(argsOfCall(0)).toContain("abandoned"); }), ); it.effect("asks Azure for every status on the All tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "all", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--status"); expect(argsOfCall(0)).toContain("all"); }), ); it.effect("narrows to the reviewer on the reviewing tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "reviewing", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--reviewer"); }), ); it.effect("reads the signed-in account, which az reports as a bare value", () => Effect.gen(function* () { // `--query user` unwraps the object, so the wrapper has to put it back. mockedExecute.mockReturnValueOnce( // @effect-diagnostics-next-line preferSchemaOverJson:off Effect.succeed(output(JSON.stringify({ name: "bilal@acme.dev", type: "user" }))), ); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const viewer = yield* cli.getViewer({ cwd: "/w" }); assert.strictEqual(viewer, "bilal@acme.dev"); expect(argsOfCall(0)).toEqual([ "account", "show", "--query", "user", "--only-show-errors", "--output", "json", ]); }), ); it.effect("fails when nobody is signed in", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(""))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const error = yield* Effect.flip(cli.getViewer({ cwd: "/w" })); assert.strictEqual(error._tag, "AzureDevOpsViewerUnavailableError"); }), ); it.effect("completes a pull request to merge it, squashing only when asked", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action: "merge", mergeMethod: "squash", }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", "--status", "completed", "--squash", "true", "--only-show-errors", "--output", "json", ]); }), ); it.effect("stores the squash choice with an auto-completion, as a merge now does", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action: "enable-auto-merge", mergeMethod: "squash", }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", "--auto-complete", "true", "--squash", "true", "--only-show-errors", "--output", "json", ]); }), ); it.effect.each([ { action: "enable-auto-merge", expected: ["--auto-complete", "true"] }, { action: "disable-auto-merge", expected: ["--auto-complete", "false"] }, { action: "draft", expected: ["--draft", "true"] }, { action: "ready", expected: ["--draft", "false"] }, { action: "close", expected: ["--status", "abandoned"] }, { action: "reopen", expected: ["--status", "active"] }, ] as const)("moves a pull request with $action", ({ action, expected }) => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", ...expected, "--only-show-errors", "--output", "json", ]); }), ); it.effect.each([ { name: "a title", rewrite: { title: "Add the page" }, expected: ["--title=Add the page"] }, { name: "a description", rewrite: { body: "Why the page changed" }, expected: ["--description=Why the page changed"], }, { name: "both", rewrite: { title: "Add the page", body: "Why the page changed" }, expected: ["--title=Add the page", "--description=Why the page changed"], }, ] as const)("rewrites $name, sending nothing it was not given", ({ rewrite, expected }) => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.updatePullRequest({ cwd: "/w", number: 42, ...rewrite }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", ...expected, "--only-show-errors", "--output", "json", ]); }), ); it.effect("sends a description that starts with a dash as one value, not as a flag", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.updatePullRequest({ cwd: "/w", number: 42, body: "- rewrote the page\n- kept the rest", }); // One argument, so the leading dash of an ordinary bullet list never reaches az as a flag, // and the whole text stays together where `--description` would otherwise take several. expect(argsOfCall(0)).toContain("--description=- rewrote the page\n- kept the rest"); }), ); it.effect("rewrites through the provider, which says it takes one", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}")));Lines 1–500 of 653. 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 AzureDevOpsCli from "../sourceControl/AzureDevOpsCli.ts"; import * as AzureDevOpsPullRequestCli from "./AzureDevOpsPullRequestCli.ts"; import * as AzureDevOpsPullRequestProvider from "./AzureDevOpsPullRequestProvider.ts"; const mockedExecute = vi.fn<AzureDevOpsCli.AzureDevOpsCli["Service"]["execute"]>(); const layer = it.layer( AzureDevOpsPullRequestCli.layer.pipe( Layer.provide( Layer.mock(AzureDevOpsCli.AzureDevOpsCli)({ execute: mockedExecute, }), ), ), ); function output(stdout: string) { return { exitCode: ChildProcessSpawner.ExitCode(0), stdout, stderr: "", stdoutTruncated: false, stderrTruncated: false, }; } function pullRequestRows( count: number, firstNumber: number, ): ReadonlyArray<Record<string, unknown>> { return Array.from({ length: count }, (_, index) => ({ pullRequestId: firstNumber + index, title: `Pull request ${firstNumber + index}`, status: "active", sourceRefName: "refs/heads/feat/page", targetRefName: "refs/heads/main", creationDate: "2026-07-01T00:00:00Z", repository: { name: "web", project: { name: "platform" } }, url: `https://dev.azure.com/acme/_apis/git/repositories/web/pullRequests/${firstNumber + index}`, })); } function pullRequests(count: number, firstNumber: number): string { return JSON.stringify(pullRequestRows(count, firstNumber)); } /** The arguments of the nth az invocation. */ function argsOfCall(index: number): ReadonlyArray<string> { const call = mockedExecute.mock.calls[index]; assert.isDefined(call); return call[0].args; } afterEach(() => { mockedExecute.mockReset(); }); layer("AzureDevOpsPullRequestCli.layer", (it) => { 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* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); assert.strictEqual(batch.items.length, 3); assert.isFalse(batch.truncated); expect(argsOfCall(0)).toEqual([ "repos", "pr", "list", "--detect", "true", "--repository", "web", "--status", "active", "--include-links", "--top", "11", "--only-show-errors", "--output", "json", ]); }), ); it.effect("reads an Azure pull request page larger than the VCS default output limit", () => Effect.gen(function* () { const rows = pullRequestRows(100, 1).map((row) => ({ ...row, description: "x".repeat(10_000), })); // @effect-diagnostics-next-line preferSchemaOverJson:off const response = JSON.stringify(rows); expect(Buffer.byteLength(response)).toBeGreaterThan(1_000_000); mockedExecute.mockImplementationOnce((input) => { const maxOutputBytes = "maxOutputBytes" in input && typeof input.maxOutputBytes === "number" ? input.maxOutputBytes : 1_000_000; return Effect.succeed( maxOutputBytes >= Buffer.byteLength(response) ? output(response) : { ...output(response.slice(0, maxOutputBytes)), stdoutTruncated: true, }, ); }); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "merged", involvement: "all", viewer: "bilal@acme.dev", limit: 99, }); assert.strictEqual(batch.items.length, 99); assert.isTrue(batch.truncated); }), ); it.effect("reads the page unnarrowed when asked to search, having nothing to search with", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); const provider = yield* AzureDevOpsPullRequestProvider.make; const page = yield* provider.listChangeRequests({ cwd: "/w", repository: "web", host: "dev.azure.com", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, query: "page", }); // `az repos pr list` filters by status, creator, reviewer and branch, and by no text at // all. The rows come back as they would have without a search, for the caller to narrow; // nothing of the search reaches the command, where it could only mean the wrong thing. assert.strictEqual(page.items.length, 3); expect(argsOfCall(0)).toEqual([ "repos", "pr", "list", "--detect", "true", "--repository", "web", "--status", "active", "--include-links", "--top", "11", "--only-show-errors", "--output", "json", ]); }), ); it.effect("steps over what it has already handed over, which is all Azure can be told", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, // The instant is the same cursor every other host reads; Azure has no filter for it and // takes the count instead. cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 20 }, }); const args = argsOfCall(0); expect(args).toContain("--skip"); assert.strictEqual(args[args.indexOf("--skip") + 1], "20"); expect(args).not.toContain("2026-07-02T00:00:00Z"); }), ); it.effect("reports truncation from the extra row", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(11, 1)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); assert.strictEqual(batch.items.length, 10); assert.isTrue(batch.truncated); assert.strictEqual(batch.cursorAdvance, 10); }), ); it.effect("advances by malformed raw rows and keeps reading until the page is full", () => Effect.gen(function* () { mockedExecute .mockReturnValueOnce( Effect.succeed( output( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { pullRequestId: "malformed" }, pullRequestRows(1, 1)[0], { pullRequestId: "also malformed" }, ]), ), ), ) .mockReturnValueOnce(Effect.succeed(output(pullRequests(2, 2)))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const batch = yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "all", viewer: "bilal@acme.dev", limit: 2, }); expect(batch.items.map((item) => item.number)).toEqual([1, 2]); assert.isTrue(batch.truncated); // Three raw rows from the first request and one from the second produced this page. assert.strictEqual(batch.cursorAdvance, 4); const secondArgs = argsOfCall(1); assert.strictEqual(secondArgs[secondArgs.indexOf("--skip") + 1], "3"); assert.strictEqual(secondArgs[secondArgs.indexOf("--top") + 1], "2"); }), ); it.effect("narrows to the author on the authored tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "closed", involvement: "authored", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--creator"); expect(argsOfCall(0)).toContain("bilal@acme.dev"); // Azure calls a closed pull request abandoned. expect(argsOfCall(0)).toContain("abandoned"); }), ); it.effect("asks Azure for every status on the All tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "all", involvement: "all", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--status"); expect(argsOfCall(0)).toContain("all"); }), ); it.effect("narrows to the reviewer on the reviewing tab", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "web", state: "open", involvement: "reviewing", viewer: "bilal@acme.dev", limit: 10, }); expect(argsOfCall(0)).toContain("--reviewer"); }), ); it.effect("reads the signed-in account, which az reports as a bare value", () => Effect.gen(function* () { // `--query user` unwraps the object, so the wrapper has to put it back. mockedExecute.mockReturnValueOnce( // @effect-diagnostics-next-line preferSchemaOverJson:off Effect.succeed(output(JSON.stringify({ name: "bilal@acme.dev", type: "user" }))), ); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const viewer = yield* cli.getViewer({ cwd: "/w" }); assert.strictEqual(viewer, "bilal@acme.dev"); expect(argsOfCall(0)).toEqual([ "account", "show", "--query", "user", "--only-show-errors", "--output", "json", ]); }), ); it.effect("fails when nobody is signed in", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(""))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; const error = yield* Effect.flip(cli.getViewer({ cwd: "/w" })); assert.strictEqual(error._tag, "AzureDevOpsViewerUnavailableError"); }), ); it.effect("completes a pull request to merge it, squashing only when asked", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action: "merge", mergeMethod: "squash", }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", "--status", "completed", "--squash", "true", "--only-show-errors", "--output", "json", ]); }), ); it.effect("stores the squash choice with an auto-completion, as a merge now does", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action: "enable-auto-merge", mergeMethod: "squash", }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", "--auto-complete", "true", "--squash", "true", "--only-show-errors", "--output", "json", ]); }), ); it.effect.each([ { action: "enable-auto-merge", expected: ["--auto-complete", "true"] }, { action: "disable-auto-merge", expected: ["--auto-complete", "false"] }, { action: "draft", expected: ["--draft", "true"] }, { action: "ready", expected: ["--draft", "false"] }, { action: "close", expected: ["--status", "abandoned"] }, { action: "reopen", expected: ["--status", "active"] }, ] as const)("moves a pull request with $action", ({ action, expected }) => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.runPullRequestAction({ cwd: "/w", number: 42, action }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", ...expected, "--only-show-errors", "--output", "json", ]); }), ); it.effect.each([ { name: "a title", rewrite: { title: "Add the page" }, expected: ["--title=Add the page"] }, { name: "a description", rewrite: { body: "Why the page changed" }, expected: ["--description=Why the page changed"], }, { name: "both", rewrite: { title: "Add the page", body: "Why the page changed" }, expected: ["--title=Add the page", "--description=Why the page changed"], }, ] as const)("rewrites $name, sending nothing it was not given", ({ rewrite, expected }) => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.updatePullRequest({ cwd: "/w", number: 42, ...rewrite }); expect(argsOfCall(0)).toEqual([ "repos", "pr", "update", "--detect", "true", "--id", "42", ...expected, "--only-show-errors", "--output", "json", ]); }), ); it.effect("sends a description that starts with a dash as one value, not as a flag", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}"))); const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; yield* cli.updatePullRequest({ cwd: "/w", number: 42, body: "- rewrote the page\n- kept the rest", }); // One argument, so the leading dash of an ordinary bullet list never reaches az as a flag, // and the whole text stays together where `--description` would otherwise take several. expect(argsOfCall(0)).toContain("--description=- rewrote the page\n- kept the rest"); }), ); it.effect("rewrites through the provider, which says it takes one", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("{}")));