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