---
id: 12631
title: Real MergeRequestService merge-request operations + tests
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
  - build
assignees:
  - neo-opus-vega
createdAt: '2026-06-06T12:07:10Z'
updatedAt: '2026-06-06T20:50:33Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12631'
author: neo-opus-vega
commentsCount: 0
parentIssue: 11404
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy:
  - '[x] 12624 GitLabClient + real IssueService issue operations + tests'
blocking: []
closedAt: '2026-06-06T20:50:33Z'
---
# Real MergeRequestService merge-request operations + tests

## Context

#12624 shipped the `GitLabClient` (REST + GraphQL + PAT auth + pagination) and the real `IssueService` issue operations (PR #12625, **APPROVED**, in the merge queue). This ticket is the **merge-request twin** — the next #11404 sub per @neo-gpt's `[epic-resolution]` pointer (*"#11404 KEEP_OPEN after #12625 — next GitLab sub should be non-micro MR/sync batch"*). It brings GitLab's PR-equivalent (Merge Requests) to parity with the issue surface that just landed.

**Live latest-open sweep:** checked the latest 20 open issues at 2026-06-06; no equivalent MR-ops sub found (#12624 is issues-only; #12456 is the AiConfig-SSOT cleanup; no `MergeRequestService` / `merge_request` ticket exists). KB unavailable for the semantic pass — not treated as evidence of absence; the live tracker sweep is the authoritative duplicate gate here.

## The Problem

Epic #11404 scopes the GitLab Workflow MCP server to **issues + merge requests** parity. Issues are done (#12624). The MR surface today is **scaffold-only**: `ai/mcp/server/gitlab-workflow/openapi.yaml` declares `list_merge_requests` / `get_merge_request` as scaffold entries with no backing service. Without real MR operations, a client-project deployment cannot coordinate the MR lifecycle (review routing, comments, labels, assignees/reviewers) through Neo's Agent-OS substrate — the exact gap the epic exists to close, now half-filled.

## The Architectural Reality

Mirror the proven `IssueService` shape under `ai/services/gitlab-workflow/`:

- **`MergeRequestService.mjs`** (new) — `extends Neo.core.Base`, `@singleton`; the MR twin of `IssueService.mjs`. Real ops via the existing `GitLabClient` (no new client). Private helpers mirror IssueService: `#apiErrorResponse` / `#mutationErrorResponse` / `#userErrors` / GID + label/assignee resolvers.
- **`queries/mrQueries.mjs` + `queries/mrMutations.mjs`** (new) — GraphQL for `project(fullPath){ mergeRequest(iid){…} mergeRequests{…} }`, `createNote`, `mergeRequestSetLabels`, `mergeRequestSetAssignees`, `mergeRequestSetReviewers`. Sibling to the issue queries shipped in #12624.
- **`ai/services.mjs`** — add `import _GL_MergeRequestService`, wrap via `makeSafe(_GL_MergeRequestService, gitlabSpec)` against the existing `gitlabSpec`, export. **ADR 0019**: read resolved `AiConfig` leaves at the use site; never re-implement / alias / mutate the shared singleton.
- **`openapi.yaml`** — replace the 2 scaffold MR entries with real op descriptions + response schemas (`MergeRequestResponse` / `MergeRequestCommentResponse` / `MergeRequestLabelResponse` / `MergeRequestAssigneeResponse`), mirroring the 4 issue response schemas added in #12624.

**Reuse, do not rebuild:** `GitLabClient` (`ai/services/gitlab-workflow/api/GitLabClient.mjs`) is provider-agnostic transport — the MR service consumes it directly (AC6). `shared/contentPath.mjs` is out of scope here (that's the Syncer sub-task).

## The Fix

- New: `ai/services/gitlab-workflow/MergeRequestService.mjs`, `queries/mrQueries.mjs`, `queries/mrMutations.mjs`
- Edit: `ai/services.mjs` (+import / makeSafe / export), `ai/mcp/server/gitlab-workflow/openapi.yaml` (real MR op descriptions + response schemas)
- Tests: `test/playwright/unit/ai/services/gitlab-workflow/MergeRequestService.spec.mjs` (mock-HTTP pattern; auth, GraphQL shape, userErrors, label/assignee resolution) — parity with the IssueService spec

## Contract Ledger Matrix

| Target Surface | Source of Authority | Proposed Behavior | Fallback / Edge Case | Docs | Evidence |
|---|---|---|---|---|---|
| `get_merge_request` / `list_merge_requests` MCP tools | #11404 epic; `IssueService` precedent (#12624); GitLab GraphQL `mergeRequest`/`mergeRequests` | Real reads via `GitLabClient` GraphQL; structured `{items,count}` for list | API error → structured `#apiErrorResponse` (no throw across the MCP boundary) | `openapi.yaml` real schemas | spec covers a mocked read round-trip |
| `manage_mr_comment` (createNote) | GitLab `createNote(noteableId: MR-GID)`; IssueService `manageIssueComment` precedent | Create MR note; resolve MR GID from iid first | userErrors surfaced via `#mutationErrorResponse` | `openapi.yaml` | spec asserts userErrors path |
| `manage_mr_labels` / `manage_mr_assignees` (+ reviewers) | GitLab `mergeRequestSetLabels` / `…SetAssignees` / `…SetReviewers`; IssueService precedent | Add/remove via set-mutations; resolve label/user IDs | partial-resolution → structured error, no silent drop | `openapi.yaml` | spec covers add + remove |

Row-level Surface-Anchor V-B-A: each GraphQL field above is verified against the GitLab GraphQL schema + the IssueService twin already merged-pending in #12625; `create_merge_request` is **deferred** (MR creation needs source/target branch + diff semantics beyond this batch — see Out of Scope) unless review says otherwise.

## Decision Record impact

- `aligned-with` **ADR 0019** (AiConfig reactive Provider SSOT — `makeSafe` wiring reads resolved leaves; no singleton mutation).
- `aligned-with` **ADR 0004** (§1.3 provider-agnostic regeneratable-cache substrate — GitLab-as-source-of-truth generalizes verbatim).

## Acceptance Criteria

- [ ] AC1: `MergeRequestService.mjs` implements real read + comment + label + assignee/reviewer operations via `GitLabClient` (no scaffold responses).
- [ ] AC2: `queries/mrQueries.mjs` + `queries/mrMutations.mjs` carry the GraphQL with GID resolution mirroring the issue queries.
- [ ] AC3: `ai/services.mjs` wraps the service via `makeSafe(..., gitlabSpec)` + exports it; reads resolved AiConfig leaves at use site (ADR 0019), no singleton mutation.
- [ ] AC4: `openapi.yaml` MR ops carry real descriptions + response schemas (no scaffold labels on the implemented ops).
- [ ] AC5: Test coverage parity with `IssueService.spec.mjs` (auth, GraphQL shape, userErrors, label/assignee resolution) — mock-HTTP, no live instance.
- [ ] AC6: Consumes the existing `GitLabClient` directly — **no** duplicate client implementation.
- [ ] AC7: JSDoc is **best-knowledge, explicitly NOT integration-validated against a live GitLab instance** (the #12625 overclaim lesson — truthful provenance, no "validated against live GitLab").

## Out of Scope

- **`MergeRequestSyncer` + `LocalFileService`** — epic Sub-task 3, a separate sub (the file-IO + `resources/content/gitlab/merge-requests/` substrate). This ticket is the service/tool surface only.
- **`create_merge_request`** — MR creation (source/target branch + diff semantics) is heavier than the read/comment/label/assignee batch; deferred to a follow-up unless cross-family review pulls it in.
- **GitLab Discussions / Pipelines / OIDC** — per epic Out of Scope.

## Avoided Traps

- **Re-implementing `GitLabClient`** — rejected; the #12624 client is provider-agnostic transport, consumed directly (AC6). A second client is migration debt.
- **Claiming live-integration validation** — rejected; #12625's reviewer (@neo-gpt) caught exactly this overclaim. JSDoc states best-knowledge-not-yet-integration-validated (AC7).
- **Mutating the shared AiConfig singleton** — rejected (ADR 0019 / the #12335 live-DB-bleed class); read resolved leaves at use site only.

## Related

- **Epic parent:** #11404 (GitLab Workflow MCP Server parity).
- **Issue twin + dependency:** #12624 / PR #12625 (the `GitLabClient` this reuses; APPROVED, unmerged). **Implementation note:** branch stacks on the #12624 branch to consume `GitLabClient`, OR lands after #12625 merges. This is *stackable*, not a hold — work proceeds on the stacked branch.
- **ADRs:** 0019 (AiConfig SSOT), 0004 (provider-agnostic substrate).

Authored by Claude Opus 4.8 (Claude Code).
Origin Session ID: a54e89a3-4259-4b41-9e26-561f665de744

Retrieval Hint: "GitLab MergeRequestService merge-request operations GraphQL mergeRequestSetLabels makeSafe gitlabSpec #11404 twin of #12624"

## Timeline

- 2026-06-06T12:07:10Z @neo-opus-vega assigned to @neo-opus-vega
- 2026-06-06T12:07:11Z @neo-opus-vega added the `enhancement` label
- 2026-06-06T12:07:11Z @neo-opus-vega added the `ai` label
- 2026-06-06T12:07:11Z @neo-opus-vega added the `architecture` label
- 2026-06-06T12:07:11Z @neo-opus-vega added the `build` label
- 2026-06-06T12:07:31Z @neo-opus-vega added parent issue #11404
- 2026-06-06T12:07:35Z @neo-opus-vega marked this issue as being blocked by #12624
- 2026-06-06T20:29:51Z @neo-opus-vega cross-referenced by PR #12653
- 2026-06-06T20:50:33Z @tobiu referenced in commit `01bc4fb` - "feat(ai): real GitLab MergeRequestService — MR ops + tests (#12631) (#12653)

Replace the list-only MR scaffold with real GitLab GraphQL behavior (list/get/comment/labels/assignees/reviewers) via the shared GitLabClient, mirroring the merged IssueService twin. Reuse the noteable-generic note mutations + project-label query; add mrQueries/mrMutations; wire services.mjs + toolService + openapi. 18 mocked unit specs."
- 2026-06-06T20:50:33Z @tobiu closed this issue

