import { afterEach, assert, expect, it, vi } from "@effect/vitest";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";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 mockedExecute = vi.fn<GitHubCli.GitHubCli["Service"]["execute"]>();const mockedGetPullRequest = vi.fn<GitHubCli.GitHubCli["Service"]["getPullRequest"]>(); const layer = it.layer( GitHubPullRequestCli.layer.pipe( Layer.provide( Layer.mock(GitHubCli.GitHubCli)({ execute: mockedExecute, 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(); mockedGetPullRequest.mockReset();}); layer("GitHubPullRequestCli.layer", (it) => { it.effect("reads linked pull request status through one narrow request", () => Effect.gen(function* () { mockedGetPullRequest.mockReturnValueOnce( Effect.succeed({ number: 7, title: "Reuse the summary", url: "https://github.com/acme/web/pull/7", baseRefName: "main", headRefName: "feat/summary", state: "open", updatedAt: "2026-08-24T12:34:56.000Z", }), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const summary = yield* cli.getPullRequestSummary({ cwd: "/w", repository: "acme/web", host: "github.com", number: 7, }); assert.deepStrictEqual(summary, { number: 7, title: "Reuse the summary", url: "https://github.com/acme/web/pull/7", headBranch: "feat/summary", baseBranch: "main", state: "open", updatedAt: "2026-08-24T12:34:56.000Z", }); expect(mockedGetPullRequest).toHaveBeenCalledOnce(); expect(mockedGetPullRequest).toHaveBeenCalledWith({ cwd: "/w", reference: "https://github.com/acme/web/pull/7", }); expect(mockedExecute).not.toHaveBeenCalled(); }), ); 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, }); assert.strictEqual(batch.items.length, 3); assert.isFalse(batch.truncated); const args = callAt(0).args; expect(args).toContain("--repo"); expect(args).toContain("github.com/acme/web"); expect(args).toContain("--state"); expect(args).toContain("open"); expect(args).toContain("--limit"); expect(args).toContain("11"); }), ); it.effect("reports truncation from the extra row, counted before decoding", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(11, 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, }); assert.strictEqual(batch.items.length, 10); assert.isTrue(batch.truncated); }), ); it.effect("excludes merged pull requests from the Closed tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "closed", involvement: "all", viewer: "bilal", limit: 10, }); // `--state closed` includes merged pull requests, so the tab narrows through search. expect(searchOfCall(0)).toBe("is:unmerged sort:updated-desc"); }), ); it.effect("narrows to the author on the authored tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "open", involvement: "authored", viewer: "bilal", limit: 10, }); const args = callAt(0).args; expect(args).toContain("--author"); expect(args).toContain("bilal"); }), ); it.effect("narrows through search on the reviewing tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "open", involvement: "reviewing", viewer: "bilal", limit: 10, }); expect(searchOfCall(0)).toBe("review-requested:bilal sort:updated-desc"); }), ); it.effect("carries every repository and every qualifier into one search", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "pingdotgg/t3code"], state: "closed", involvement: "reviewing", viewer: "bilal", limit: 10, query: "pull requests page", cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 10 }, }); // One request for both repositories, carrying everything the per-repository read expresses // as a flag: the tab, the involvement, the reader's words, where to carry on from, and the // order the page reads in. assert.strictEqual(mockedExecute.mock.calls.length, 1); assert.strictEqual( searchQueryOfCall(0), 'is:pr is:closed is:unmerged review-requested:bilal "pull requests page" ' + "updated:<=2026-07-02T00:00:00Z sort:updated-desc repo:acme/web repo:pingdotgg/t3code", ); }), ); it.effect("narrows a search to the author, and to merged on the merged tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "merged", involvement: "authored", viewer: "bilal", limit: 10, }); assert.strictEqual( searchQueryOfCall(0), "is:pr is:merged author:bilal sort:updated-desc repo:acme/web", ); }), ); it.effect("keeps a searched-for qualifier inside the phrase, and out of argv", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "open", involvement: "all", viewer: "bilal", limit: 10, query: 'x" is:merged repo:evil/repo', }); // Quoted and escaped, so the words a reader typed narrow the listing rather than widening // it — and the whole document travels over stdin rather than in a visible argv. assert.strictEqual( searchQueryOfCall(0), 'is:pr is:open "x\\" is:merged repo:evil/repo" sort:updated-desc repo:acme/web', ); expect(callAt(0).args).not.toContain("-f"); }), ); it.effect("refuses to search for a repository GitHub cannot address", () => Effect.gen(function* () { const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const failure = yield* Effect.flip( cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "acme/web is:merged"], state: "open", involvement: "all", viewer: "bilal", limit: 10, }), ); // Nothing is sent: a name that could end its own qualifier is refused rather than escaped. assert.strictEqual(failure._tag, "GitHubRepositorySelectorError"); assert.strictEqual(mockedExecute.mock.calls.length, 0); }), ); it.effect("files each searched row under the repository it came from", () => Effect.gen(function* () { mockedExecute.mockReturnValue( Effect.succeed( searchPage([ searchItem(7, "acme/web", "2026-07-03T00:00:00Z"), searchItem(9, "pingdotgg/t3code", "2026-07-02T00:00:00Z"), // Not a pull request, which `is:pr` excludes and a decode skips rather than fails on. {}, ]), ), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const batch = yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "pingdotgg/t3code"], state: "open", involvement: "all", viewer: "bilal", limit: 10, }); assert.deepStrictEqual( batch.items.map((item) => [item.repository, item.number, item.author?.avatarUrl]), [ ["acme/web", 7, "https://avatars/octocat"], ["pingdotgg/t3code", 9, "https://avatars/octocat"], ], ); // The listing leaves the line counts to a read of their own. assert.deepStrictEqual( batch.items.map((item) => [item.additions, item.deletions]), [ [0, 0], [0, 0], ], ); assert.isFalse(batch.truncated); }), ); it.effect("reports truncation from the extra row, and from a page GitHub says has more", () => Effect.gen(function* () { mockedExecute .mockReturnValueOnce( Effect.succeed( searchPage([ searchItem(1, "acme/web", "2026-07-03T00:00:00Z"), searchItem(2, "acme/web", "2026-07-02T00:00:00Z"), searchItem(3, "acme/web", "2026-07-01T00:00:00Z"), ]), ), ) .mockReturnValueOnce( Effect.succeed(searchPage([searchItem(1, "acme/web", "2026-07-03T00:00:00Z")], true)), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const read = () => cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "open", involvement: "all", viewer: "bilal", limit: 2, });Lines 1–500 of 3094. 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 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 mockedExecute = vi.fn<GitHubCli.GitHubCli["Service"]["execute"]>(); const mockedGetPullRequest = vi.fn<GitHubCli.GitHubCli["Service"]["getPullRequest"]>(); const layer = it.layer( GitHubPullRequestCli.layer.pipe( Layer.provide( Layer.mock(GitHubCli.GitHubCli)({ execute: mockedExecute, 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(); mockedGetPullRequest.mockReset(); }); layer("GitHubPullRequestCli.layer", (it) => { it.effect("reads linked pull request status through one narrow request", () => Effect.gen(function* () { mockedGetPullRequest.mockReturnValueOnce( Effect.succeed({ number: 7, title: "Reuse the summary", url: "https://github.com/acme/web/pull/7", baseRefName: "main", headRefName: "feat/summary", state: "open", updatedAt: "2026-08-24T12:34:56.000Z", }), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const summary = yield* cli.getPullRequestSummary({ cwd: "/w", repository: "acme/web", host: "github.com", number: 7, }); assert.deepStrictEqual(summary, { number: 7, title: "Reuse the summary", url: "https://github.com/acme/web/pull/7", headBranch: "feat/summary", baseBranch: "main", state: "open", updatedAt: "2026-08-24T12:34:56.000Z", }); expect(mockedGetPullRequest).toHaveBeenCalledOnce(); expect(mockedGetPullRequest).toHaveBeenCalledWith({ cwd: "/w", reference: "https://github.com/acme/web/pull/7", }); expect(mockedExecute).not.toHaveBeenCalled(); }), ); 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, }); assert.strictEqual(batch.items.length, 3); assert.isFalse(batch.truncated); const args = callAt(0).args; expect(args).toContain("--repo"); expect(args).toContain("github.com/acme/web"); expect(args).toContain("--state"); expect(args).toContain("open"); expect(args).toContain("--limit"); expect(args).toContain("11"); }), ); it.effect("reports truncation from the extra row, counted before decoding", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(11, 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, }); assert.strictEqual(batch.items.length, 10); assert.isTrue(batch.truncated); }), ); it.effect("excludes merged pull requests from the Closed tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "closed", involvement: "all", viewer: "bilal", limit: 10, }); // `--state closed` includes merged pull requests, so the tab narrows through search. expect(searchOfCall(0)).toBe("is:unmerged sort:updated-desc"); }), ); it.effect("narrows to the author on the authored tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "open", involvement: "authored", viewer: "bilal", limit: 10, }); const args = callAt(0).args; expect(args).toContain("--author"); expect(args).toContain("bilal"); }), ); it.effect("narrows through search on the reviewing tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(output("[]"))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.listPullRequests({ cwd: "/w", repository: "acme/web", host: "github.com", state: "open", involvement: "reviewing", viewer: "bilal", limit: 10, }); expect(searchOfCall(0)).toBe("review-requested:bilal sort:updated-desc"); }), ); it.effect("carries every repository and every qualifier into one search", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "pingdotgg/t3code"], state: "closed", involvement: "reviewing", viewer: "bilal", limit: 10, query: "pull requests page", cursor: { updatedBefore: "2026-07-02T00:00:00Z", delivered: 10 }, }); // One request for both repositories, carrying everything the per-repository read expresses // as a flag: the tab, the involvement, the reader's words, where to carry on from, and the // order the page reads in. assert.strictEqual(mockedExecute.mock.calls.length, 1); assert.strictEqual( searchQueryOfCall(0), 'is:pr is:closed is:unmerged review-requested:bilal "pull requests page" ' + "updated:<=2026-07-02T00:00:00Z sort:updated-desc repo:acme/web repo:pingdotgg/t3code", ); }), ); it.effect("narrows a search to the author, and to merged on the merged tab", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "merged", involvement: "authored", viewer: "bilal", limit: 10, }); assert.strictEqual( searchQueryOfCall(0), "is:pr is:merged author:bilal sort:updated-desc repo:acme/web", ); }), ); it.effect("keeps a searched-for qualifier inside the phrase, and out of argv", () => Effect.gen(function* () { mockedExecute.mockReturnValue(Effect.succeed(searchPage([]))); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "open", involvement: "all", viewer: "bilal", limit: 10, query: 'x" is:merged repo:evil/repo', }); // Quoted and escaped, so the words a reader typed narrow the listing rather than widening // it — and the whole document travels over stdin rather than in a visible argv. assert.strictEqual( searchQueryOfCall(0), 'is:pr is:open "x\\" is:merged repo:evil/repo" sort:updated-desc repo:acme/web', ); expect(callAt(0).args).not.toContain("-f"); }), ); it.effect("refuses to search for a repository GitHub cannot address", () => Effect.gen(function* () { const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const failure = yield* Effect.flip( cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "acme/web is:merged"], state: "open", involvement: "all", viewer: "bilal", limit: 10, }), ); // Nothing is sent: a name that could end its own qualifier is refused rather than escaped. assert.strictEqual(failure._tag, "GitHubRepositorySelectorError"); assert.strictEqual(mockedExecute.mock.calls.length, 0); }), ); it.effect("files each searched row under the repository it came from", () => Effect.gen(function* () { mockedExecute.mockReturnValue( Effect.succeed( searchPage([ searchItem(7, "acme/web", "2026-07-03T00:00:00Z"), searchItem(9, "pingdotgg/t3code", "2026-07-02T00:00:00Z"), // Not a pull request, which `is:pr` excludes and a decode skips rather than fails on. {}, ]), ), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const batch = yield* cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web", "pingdotgg/t3code"], state: "open", involvement: "all", viewer: "bilal", limit: 10, }); assert.deepStrictEqual( batch.items.map((item) => [item.repository, item.number, item.author?.avatarUrl]), [ ["acme/web", 7, "https://avatars/octocat"], ["pingdotgg/t3code", 9, "https://avatars/octocat"], ], ); // The listing leaves the line counts to a read of their own. assert.deepStrictEqual( batch.items.map((item) => [item.additions, item.deletions]), [ [0, 0], [0, 0], ], ); assert.isFalse(batch.truncated); }), ); it.effect("reports truncation from the extra row, and from a page GitHub says has more", () => Effect.gen(function* () { mockedExecute .mockReturnValueOnce( Effect.succeed( searchPage([ searchItem(1, "acme/web", "2026-07-03T00:00:00Z"), searchItem(2, "acme/web", "2026-07-02T00:00:00Z"), searchItem(3, "acme/web", "2026-07-01T00:00:00Z"), ]), ), ) .mockReturnValueOnce( Effect.succeed(searchPage([searchItem(1, "acme/web", "2026-07-03T00:00:00Z")], true)), ); const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli; const read = () => cli.searchPullRequests({ cwd: "/w", host: "github.com", repositories: ["acme/web"], state: "open", involvement: "all", viewer: "bilal", limit: 2, });