import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { ChildProcessSpawner } from "effect/unstable/process"; import { VcsProcessExitError } from "@t3tools/contracts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as GitHubCli from "./GitHubCli.ts"; const processOutput = (stdout: string): VcsProcess.VcsProcessOutput => ({ exitCode: ChildProcessSpawner.ExitCode(0), stdout, stderr: "", stdoutTruncated: false, stderrTruncated: false, }); const mockRun = vi.fn(); const layer = GitHubCli.layer.pipe( Layer.provide( Layer.mock(VcsProcess.VcsProcess)({ run: mockRun, }), ), ); afterEach(() => { mockRun.mockReset(); }); describe("GitHubCli.layer", () => { it.effect("parses pull request view output", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ number: 42, title: "Add PR thread creation", url: "https://github.com/pingdotgg/codething-mvp/pull/42", baseRefName: "main", headRefName: "feature/pr-threads", state: "OPEN", mergedAt: null, isCrossRepository: true, headRepository: { nameWithOwner: "octocat/codething-mvp", }, headRepositoryOwner: { login: "octocat", }, }), ), ), ); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.getPullRequest({ cwd: "/repo", reference: "#42", }); assert.deepStrictEqual(result, { number: 42, title: "Add PR thread creation", url: "https://github.com/pingdotgg/codething-mvp/pull/42", baseRefName: "main", headRefName: "feature/pr-threads", state: "open", isCrossRepository: true, headRepositoryNameWithOwner: "octocat/codething-mvp", headRepositoryOwnerLogin: "octocat", }); expect(mockRun).toHaveBeenCalledWith({ operation: "GitHubCli.execute", command: "gh", args: [ "pr", "view", "#42", "--json", "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", ], cwd: "/repo", timeoutMs: 30_000, }); }).pipe(Effect.provide(layer)), ); it.effect("trims pull request fields decoded from gh json", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ number: 42, title: " Add PR thread creation \n", url: " https://github.com/pingdotgg/codething-mvp/pull/42 ", baseRefName: " main ", headRefName: "\tfeature/pr-threads\t", state: "OPEN", mergedAt: null, isCrossRepository: true, headRepository: { nameWithOwner: " octocat/codething-mvp ", }, headRepositoryOwner: { login: " octocat ", }, }), ), ), ); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.getPullRequest({ cwd: "/repo", reference: "#42", }); assert.deepStrictEqual(result, { number: 42, title: "Add PR thread creation", url: "https://github.com/pingdotgg/codething-mvp/pull/42", baseRefName: "main", headRefName: "feature/pr-threads", state: "open", isCrossRepository: true, headRepositoryNameWithOwner: "octocat/codething-mvp", headRepositoryOwnerLogin: "octocat", }); }).pipe(Effect.provide(layer)), ); it.effect("skips invalid entries when parsing pr lists", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { number: 0, title: "invalid", url: "https://github.com/pingdotgg/codething-mvp/pull/0", baseRefName: "main", headRefName: "feature/invalid", }, { number: 43, title: " Valid PR ", url: " https://github.com/pingdotgg/codething-mvp/pull/43 ", baseRefName: " main ", headRefName: " feature/pr-list ", headRepository: { nameWithOwner: " ", }, headRepositoryOwner: { login: " ", }, }, ]), ), ), ); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.listOpenPullRequests({ cwd: "/repo", headSelector: "feature/pr-list", }); assert.deepStrictEqual(result, [ { number: 43, title: "Valid PR", url: "https://github.com/pingdotgg/codething-mvp/pull/43", baseRefName: "main", headRefName: "feature/pr-list", state: "open", }, ]); }).pipe(Effect.provide(layer)), ); it.effect("reads repository clone URLs", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ nameWithOwner: "octocat/codething-mvp", url: "https://github.com/octocat/codething-mvp", sshUrl: "git@github.com:octocat/codething-mvp.git", }), ), ), ); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.getRepositoryCloneUrls({ cwd: "/repo", repository: "octocat/codething-mvp", }); assert.deepStrictEqual(result, { nameWithOwner: "octocat/codething-mvp", url: "https://github.com/octocat/codething-mvp", sshUrl: "git@github.com:octocat/codething-mvp.git", }); }).pipe(Effect.provide(layer)), ); it.effect("creates repositories and parses clone URLs from create output", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.succeed( processOutput( "✓ Created repository octocat/codething-mvp on github.com\nhttps://github.com/octocat/codething-mvp\n", ), ), ); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.createRepository({ cwd: "/repo", repository: "octocat/codething-mvp", visibility: "private", }); assert.deepStrictEqual(result, { nameWithOwner: "octocat/codething-mvp", url: "https://github.com/octocat/codething-mvp", sshUrl: "git@github.com:octocat/codething-mvp.git", }); expect(mockRun).toHaveBeenCalledTimes(1); expect(mockRun).toHaveBeenNthCalledWith(1, { operation: "GitHubCli.execute", command: "gh", args: ["repo", "create", "octocat/codething-mvp", "--private"], cwd: "/repo", timeoutMs: 30_000, }); }).pipe(Effect.provide(layer)), ); it.effect("falls back to constructed URLs when create output omits a URL", () => Effect.gen(function* () { mockRun.mockReturnValueOnce(Effect.succeed(processOutput(""))); const gh = yield* GitHubCli.GitHubCli; const result = yield* gh.createRepository({ cwd: "/repo", repository: "octocat/codething-mvp", visibility: "private", }); assert.deepStrictEqual(result, { nameWithOwner: "octocat/codething-mvp", url: "https://github.com/octocat/codething-mvp", sshUrl: "git@github.com:octocat/codething-mvp.git", }); }).pipe(Effect.provide(layer)), ); it.effect("surfaces a friendly error when the pull request is not found", () => Effect.gen(function* () { mockRun.mockReturnValueOnce( Effect.fail( new VcsProcessExitError({ operation: "GitHubCli.execute", command: "gh pr view", cwd: "/repo", exitCode: 1, detail: "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)", }), ), ); const gh = yield* GitHubCli.GitHubCli; const error = yield* gh .getPullRequest({ cwd: "/repo", reference: "4888", }) .pipe(Effect.flip); assert.equal(error.message.includes("Pull request not found"), true); }).pipe(Effect.provide(layer)), ); });