import { assert, describe, expect, it, vi } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as GitLabPullRequestCli from "./GitLabPullRequestCli.ts"; import { gitLabViewerPermissions, make } from "./GitLabPullRequestProvider.ts"; describe("gitLabViewerPermissions", () => { it("offers everything to a viewer GitLab says can merge", () => { expect(gitLabViewerPermissions({ viewerCanMerge: true })).toEqual({ // Arming a merge for later and taking the arming back answer to the same `can_merge`. actions: [ "merge", "ready", "draft", "close", "reopen", "update-branch", "enable-auto-merge", "disable-auto-merge", ], comment: true, resolve: true, verdicts: ["comment", "approve"], // GitLab says nothing about who may set a reviewer, and an unreported permission is granted. requestReviewers: true, // Rebase and nothing else: GitLab cannot merge a target branch into a source branch, so // offering the choice would be offering something no request could carry out. updateMethods: ["rebase"], }); }); it("keeps merge, now and later, from a viewer GitLab says cannot", () => { // `user.can_merge` already accounts for the role, the approval rules and a protected target // branch, so it is the one answer here that does not have to be inferred. expect(gitLabViewerPermissions({ viewerCanMerge: false })).toEqual({ actions: ["ready", "draft", "close", "reopen"], comment: true, resolve: true, verdicts: ["comment", "approve"], requestReviewers: true, }); }); it("names no way of updating a branch it will not let this viewer update", () => { // The action and the strategy behind it go together: a button offered with nothing to press // it with, or a strategy left standing next to a withheld button, is a half-refusal. expect(gitLabViewerPermissions({ viewerCanMerge: false }).updateMethods).toBeUndefined(); }); it("treats an author with read access as any other reader, which is all GitLab says", () => { // Its REST API names no relationship between the viewer and the merge request beyond // `can_merge`, so the four an author keeps stay offered to everyone rather than being taken // from the one person entitled to them. expect(gitLabViewerPermissions({ viewerCanMerge: false }).actions).toEqual([ "ready", "draft", "close", "reopen", ]); }); }); describe("getChangeRequest base freshness", () => { const detail = { number: 7, title: "Merge request 7", url: "https://gitlab.com/acme/web/-/merge_requests/7", author: null, headBranch: "feat/page", baseBranch: "main", state: "open" as const, isDraft: false, mergeability: "mergeable" as const, additions: 0, deletions: 0, createdAt: "2026-07-01T00:00:00Z", updatedAt: "2026-07-02T00:00:00Z", reviewRequestLogins: [], labels: [], body: "", changedFiles: 1, mergedAt: null, closedAt: null, reviewers: [], checks: [], viewerCanMerge: true, reviewerIds: [], }; const readWith = (divergence: { readonly divergedCommits?: number }) => Effect.gen(function* () { const provider = yield* make; return yield* provider.getChangeRequest({ cwd: "/w", repository: "acme/web", host: "gitlab.com", number: 7, }); }).pipe( Effect.provide( Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({ getMergeRequestDetail: () => Effect.succeed({ ...detail, ...divergence }), getProjectMergeCapabilities: () => Effect.succeed({ merge: true, squash: true, rebase: true }), }), ), ); it.effect("reads a counted divergence as a branch that has fallen behind", () => Effect.gen(function* () { const changeRequest = yield* readWith({ divergedCommits: 3 }); expect(changeRequest.baseComparison).toBe("behind"); expect(changeRequest.behindBy).toBe(3); }), ); it.effect("reads a divergence of none as a branch that is current", () => Effect.gen(function* () { const changeRequest = yield* readWith({ divergedCommits: 0 }); expect(changeRequest.baseComparison).toBe("up-to-date"); expect(changeRequest.behindBy).toBe(0); }), ); it.effect("says nothing at all where GitLab counted nothing", () => Effect.gen(function* () { // An install too old to answer has to leave the page silent rather than let it claim the // branch is current, which is the one wrong thing this banner could say. const changeRequest = yield* readWith({}); expect(changeRequest.baseComparison).toBe("unknown"); expect(changeRequest.behindBy).toBeUndefined(); }), ); }); describe("rewriting what has already been said", () => { const updateMergeRequest = vi.fn(() => Effect.void); const updateNote = vi.fn(() => Effect.void); const providerWith = make.pipe( Effect.provide( Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({ updateMergeRequest, updateNote }), ), ); it.effect("sends only the half of the merge request the reader rewrote", () => Effect.gen(function* () { const provider = yield* providerWith; assert.isDefined(provider.updateChangeRequest); yield* provider.updateChangeRequest({ cwd: "/w", repository: "acme/web", host: "gitlab.com", number: 7, body: "What this changes.", }); // GitLab calls it the description, and the title stays out of the request entirely. expect(updateMergeRequest).toHaveBeenCalledWith({ cwd: "/w", repository: "acme/web", number: 7, description: "What this changes.", }); }), ); it.effect("rewrites a positioned comment through the same note as any other", () => Effect.gen(function* () { const provider = yield* providerWith; assert.isDefined(provider.updateComment); yield* provider.updateComment({ cwd: "/w", repository: "acme/web", host: "gitlab.com", number: 7, commentId: "42", kind: "review-comment", body: "Reworded.", }); expect(updateNote).toHaveBeenCalledWith({ cwd: "/w", repository: "acme/web", number: 7, noteId: "42", body: "Reworded.", }); }), ); });