import { assert, it, afterEach, 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 GitLabCli from "./GitLabCli.ts"; const mockedRun = vi.fn(); const layer = it.layer( GitLabCli.layer.pipe( Layer.provide( Layer.mock(VcsProcess.VcsProcess)({ run: mockedRun, }), ), ), ); function processOutput(stdout: string): VcsProcess.VcsProcessOutput { return { exitCode: ChildProcessSpawner.ExitCode(0), stdout, stderr: "", stdoutTruncated: false, stderrTruncated: false, }; } afterEach(() => { mockedRun.mockReset(); }); layer("GitLabCli.layer", (it) => { it.effect("parses merge request view output", () => Effect.gen(function* () { mockedRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ iid: 42, title: "Add MR thread creation", web_url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/42", target_branch: "main", source_branch: "feature/mr-threads", state: "opened", source_project_id: 101, target_project_id: 100, source_project: { path_with_namespace: "octocat/t3code", }, }), ), ), ); const result = yield* Effect.gen(function* () { const glab = yield* GitLabCli.GitLabCli; return yield* glab.getMergeRequest({ cwd: "/repo", reference: "42", }); }); assert.deepStrictEqual(result, { number: 42, title: "Add MR thread creation", url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/42", baseRefName: "main", headRefName: "feature/mr-threads", state: "open", isCrossRepository: true, headRepositoryNameWithOwner: "octocat/t3code", headRepositoryOwnerLogin: "octocat", }); expect(mockedRun).toHaveBeenCalledWith( expect.objectContaining({ command: "glab", cwd: "/repo", args: ["mr", "view", "42", "--output", "json"], }), ); }), ); it.effect("skips invalid entries when parsing MR lists", () => Effect.gen(function* () { mockedRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { iid: 0, title: "invalid", web_url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/0", target_branch: "main", source_branch: "feature/invalid", }, { iid: 43, title: " Valid MR ", web_url: " https://gitlab.com/pingdotgg/t3code/-/merge_requests/43 ", target_branch: " main ", source_branch: " feature/mr-list ", state: "merged", }, ]), ), ), ); const result = yield* Effect.gen(function* () { const glab = yield* GitLabCli.GitLabCli; return yield* glab.listMergeRequests({ cwd: "/repo", headSelector: "feature/mr-list", state: "all", }); }); assert.deepStrictEqual(result, [ { number: 43, title: "Valid MR", url: "https://gitlab.com/pingdotgg/t3code/-/merge_requests/43", baseRefName: "main", headRefName: "feature/mr-list", state: "merged", }, ]); expect(mockedRun).toHaveBeenCalledWith( expect.objectContaining({ command: "glab", cwd: "/repo", args: [ "mr", "list", "--source-branch", "feature/mr-list", "--all", "--per-page", "20", "--output", "json", ], }), ); }), ); it.effect("reads repository clone URLs", () => Effect.gen(function* () { mockedRun.mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ path_with_namespace: "octocat/t3code", web_url: "https://gitlab.com/octocat/t3code", http_url_to_repo: "https://gitlab.com/octocat/t3code.git", ssh_url_to_repo: "git@gitlab.com:octocat/t3code.git", }), ), ), ); const result = yield* Effect.gen(function* () { const glab = yield* GitLabCli.GitLabCli; return yield* glab.getRepositoryCloneUrls({ cwd: "/repo", repository: "octocat/t3code", }); }); assert.deepStrictEqual(result, { nameWithOwner: "octocat/t3code", url: "https://gitlab.com/octocat/t3code", sshUrl: "git@gitlab.com:octocat/t3code.git", }); }), ); it.effect("creates merge requests through the GitLab API without placing the body in argv", () => Effect.gen(function* () { mockedRun.mockReturnValueOnce(Effect.succeed(processOutput("{}"))); const glab = yield* GitLabCli.GitLabCli; yield* glab.createMergeRequest({ cwd: "/repo", baseBranch: "main", headSelector: "owner:feature/provider", title: "Provider MR", bodyFile: "/tmp/t3-mr-body.md", }); expect(mockedRun).toHaveBeenCalledWith( expect.objectContaining({ command: "glab", cwd: "/repo", args: [ "api", "--method", "POST", "projects/:fullpath/merge_requests", "--raw-field", "source_branch=feature/provider", "--raw-field", "target_branch=main", "--raw-field", "title=Provider MR", "--field", "description=@/tmp/t3-mr-body.md", ], }), ); }), ); it.effect("creates repositories under an explicit namespace", () => Effect.gen(function* () { mockedRun .mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ id: 1234 }), ), ), ) .mockReturnValueOnce( Effect.succeed( processOutput( // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ path_with_namespace: "octocat/t3code", web_url: "https://gitlab.com/octocat/t3code", http_url_to_repo: "https://gitlab.com/octocat/t3code.git", ssh_url_to_repo: "git@gitlab.com:octocat/t3code.git", }), ), ), ); const glab = yield* GitLabCli.GitLabCli; const result = yield* glab.createRepository({ cwd: "/repo", repository: "octocat/t3code", visibility: "public", }); assert.deepStrictEqual(result, { nameWithOwner: "octocat/t3code", url: "https://gitlab.com/octocat/t3code", sshUrl: "git@gitlab.com:octocat/t3code.git", }); expect(mockedRun).toHaveBeenNthCalledWith( 1, expect.objectContaining({ command: "glab", cwd: "/repo", args: ["api", "namespaces/octocat"], }), ); expect(mockedRun).toHaveBeenNthCalledWith( 2, expect.objectContaining({ command: "glab", cwd: "/repo", args: [ "api", "--method", "POST", "projects", "--raw-field", "path=t3code", "--raw-field", "name=t3code", "--raw-field", "visibility=public", "--raw-field", "namespace_id=1234", ], }), ); }), ); it.effect("does not pass unsupported force flags when checking out merge requests", () => Effect.gen(function* () { mockedRun.mockReturnValueOnce(Effect.succeed(processOutput(""))); const glab = yield* GitLabCli.GitLabCli; yield* glab.checkoutMergeRequest({ cwd: "/repo", reference: "42", force: true, }); expect(mockedRun).toHaveBeenCalledWith( expect.objectContaining({ command: "glab", cwd: "/repo", args: ["mr", "checkout", "42"], }), ); }), ); it.effect("surfaces a friendly error when the merge request is not found", () => Effect.gen(function* () { const cause = new VcsProcessExitError({ operation: "GitLabCli.execute", command: "glab", cwd: "/repo", exitCode: 1, detail: "GET 404 merge request not found", failureKind: "not-found", }); mockedRun.mockReturnValueOnce(Effect.fail(cause)); const error = yield* Effect.gen(function* () { const glab = yield* GitLabCli.GitLabCli; return yield* glab.getMergeRequest({ cwd: "/repo", reference: "4888", }); }).pipe(Effect.flip); assert.equal(error.message.includes("Merge request 4888 was not found"), true); assert.strictEqual(error._tag, "GitLabMergeRequestNotFoundError"); assert.strictEqual(error.command, "glab"); assert.strictEqual(error.cwd, "/repo"); assert.strictEqual(error.cause, cause); assert.equal(error.message.includes(cause.detail), false); }), ); it.effect("keeps non-merge-request not-found failures generic", () => Effect.gen(function* () { const cause = new VcsProcessExitError({ operation: "GitLabCli.execute", command: "glab", cwd: "/repo", exitCode: 1, detail: "GET 404 project not found", failureKind: "not-found", }); mockedRun.mockReturnValueOnce(Effect.fail(cause)); const error = yield* Effect.gen(function* () { const glab = yield* GitLabCli.GitLabCli; return yield* glab.getRepositoryCloneUrls({ cwd: "/repo", repository: "missing/project", }); }).pipe(Effect.flip); assert.strictEqual(error._tag, "GitLabCliCommandError"); assert.strictEqual(error.cause, cause); }), ); it.effect("preserves rate-limit failures as a distinct error", () => Effect.gen(function* () { const cause = new VcsProcessExitError({ operation: "GitLabCli.execute", command: "glab", cwd: "/repo", exitCode: 1, detail: "API rate limit exceeded.", failureKind: "rate-limited", }); mockedRun.mockReturnValueOnce(Effect.fail(cause)); const glab = yield* GitLabCli.GitLabCli; const error = yield* glab .execute({ cwd: "/repo", args: ["api", "projects"] }) .pipe(Effect.flip); assert.strictEqual(error._tag, "GitLabCliRateLimitError"); assert.strictEqual(error.cause, cause); }), ); });