25 KiB
status, system, requirements, created, owners
| status | system | requirements | created | owners | ||
|---|---|---|---|---|---|---|
| draft | integrations |
|
2026-08-11 |
|
GitLab MR Status Chip System Design Part 4
Purpose and boundaries
This design preserves the technical source detail for REQ-INTEGRATIONS-GITLAB-MR-STATUS-CHIP-001 during migration.
Requirement mapping
| Requirement | Design section |
|---|---|
REQ-INTEGRATIONS-GITLAB-MR-STATUS-CHIP-001 |
Migrated source detail |
Migrated source detail
Scenarios
Rendering and absence:
-
GIVEN a task with no linked MRs, WHEN its session view renders, THEN no element with testid
mr-status-chipexists. -
GIVEN a task whose only linked MR has
state: "merged", WHEN its session view renders, THEN no element with testidmr-status-chipexists. -
GIVEN a task with two linked MRs, both terminal (one
merged, oneclosed), WHEN its session view renders, THEN no element with testidmr-status-chipexists. -
GIVEN a task with one open MR and one merged MR, WHEN its session view renders, THEN
mr-status-chipis present withdata-mr-count="1"anddata-mr-iidequal to the open MR's iid. -
GIVEN a task with one open MR and no linked PR, WHEN the chat status bar renders, THEN
mr-status-chipis present insidechat-status-bar. -
GIVEN a task with one open MR, WHEN the passthrough toolbar renders, THEN
mr-status-chipis present insidepassthrough-status-row. -
GIVEN a task linked to both an open GitHub PR and an open GitLab MR, WHEN the chat status bar renders, THEN both
pr-status-chipandmr-status-chipare present insidechat-status-bar, andpr-status-chipprecedesmr-status-chipin DOM order.This is the only ordering assertion. The Azure DevOps chip is not part of any assertion, and the requirement that
MRStatusChipbe rendered beforeAzureDevOpsTaskPullRequestChip(see Selection and ordering) stands as a source-order requirement on the two mount points, verified by reading the diff rather than by a test. That is a deliberate trade, stated honestly rather than dressed up as a technical limit: the Azure chip has nodata-testid, and although its hardcodedaria-labelwould make a role-based locator possible, this spec declines to build an AC on a string owned by a component this card may not modify. See Selection and ordering for the full reasoning. A stated code-review check that everyone can see is better than an AC whose failure mode is an unrelated team renaming their label.
Status derivation:
- GIVEN an open MR with
pipeline_state: "failure"anddraft: true, WHEN the chip renders, THENdata-statusisfailed. - GIVEN an open MR with
draft: trueandpipeline_state: "pending", WHEN the chip renders, THENdata-statusisdraft. - GIVEN an open MR with
approval_state: "approved"andpipeline_state: "success", WHEN the chip renders, THENdata-statusisreadyand the glyph carries the emerald colour class. - GIVEN an open MR with
approval_state: "pending"andpipeline_state: "success", WHEN the chip renders, THENdata-statusisawaiting_approval. - GIVEN an open MR with
approval_state: ""andpipeline_state: "pending", WHEN the chip renders, THENdata-statusisrunning. - GIVEN an open MR with
approval_state: "pending"andpipeline_state: "pending", WHEN the chip renders, THENdata-statusisawaiting_approval, notrunning, because row 6 carries no pipeline condition. - GIVEN an open MR with every status field empty, WHEN the chip renders,
THEN
data-statusisneutral. - GIVEN an open MR with
unresolved_discussions: 7and otherwisereadyfields, WHEN the chip renders, THENdata-statusis stillready, because unresolved discussions do not feed chip status. - GIVEN any
TaskMR, WHENgetMRStatusColoris called before and after the refactor, THEN it returns the identical class string. A table-driven test SHALL cover every branch of the priority table.
Aggregation and selection:
- GIVEN a task with two open MRs, one
runningand onefailed, WHEN the chip renders, THENdata-statusisfailed,data-mr-countis2, anddata-mr-iidis the failed MR's iid. - GIVEN a task with two open MRs that are both
failed, with iids 12 and 7, WHEN the chip renders, THENdata-mr-iidis7. - GIVEN a task with two open MRs that are both
failed, sharing iid 7 across projectsgroup/aandgroup/b, WHEN the chip renders, THEN the selected MR is the one ingroup/a. - GIVEN a task with three open MRs whose backend response order is reversed
between two renders, WHEN the chip renders each time, THEN
data-mr-iidis identical both times. - GIVEN a task with two open MRs at rank 0 with different statuses, one
draftwith iid 9 and oneneutralwith iid 3, WHEN the chip renders, THENdata-mr-iidis3anddata-statusisneutral(the status of the MR the tiebreak selected), for either input array order. - GIVEN any list of MRs, WHEN
aggregateMRChipStatus(mrs)is called, THEN its result equalsmrChipStatus(selectChipMR(mrs)), orneutralwhenselectChipMR(mrs)is null. A property test SHALL assert this over shuffled input orders. - GIVEN any two
MRChipStatusvaluesaandb, THENMR_CHIP_STATUS_RANK[a] < MR_CHIP_STATUS_RANK[b]if and only ifSTATUS_RANK[colour(a)] < STATUS_RANK[colour(b)], asserted by a table-driven test over all 8 statuses. - GIVEN any list of MRs, WHEN
aggregateMRStatusColoris called before and after this change, THEN it returns the identical class string. This includes all-terminal lists, where its existing first-in-input-order tie behaviour is preserved:[merged, closed]returnstext-purple-500and[closed, merged]returnstext-muted-foreground, both before and after.
Disclosure:
- GIVEN a fine-pointer viewport and a task with one open MR, WHEN the
user hovers
mr-status-chip, THENmr-topbar-popover-innerbecomes visible and its bounding boxyis greater than or equal to 0. - GIVEN that popover is open, WHEN the pointer crosses the gap between trigger and content, THEN the popover stays open.
- GIVEN that popover is open, WHEN the user clicks the chip itself, THEN the popover stays open and no navigation occurs.
- GIVEN a coarse-pointer viewport and a task with one open MR, WHEN the
user taps
mr-status-chip, THENmr-status-chip-drawerbecomes visible and containsmr-topbar-popover-inner. - GIVEN the drawer is open, WHEN the user activates
mr-status-chip-drawer-close, THEN the drawer closes and focus returns to the chip trigger. - GIVEN a coarse-pointer viewport, WHEN the chip renders, THEN no hover popover is mounted.
- GIVEN a fine-pointer viewport, WHEN the user moves keyboard focus onto the chip trigger, THEN the popover opens and focus stays on the trigger.
- GIVEN the popover was opened by keyboard focus, WHEN focus moves off
the trigger without any pointer entering the trigger or the content, THEN
the popover closes, because
onBluris wired toonTriggerLeaveand the scheduled close finds the pointer over neither region. - GIVEN the popover was opened by keyboard focus and the pointer is then moved onto the popover content, WHEN focus moves off the trigger, THEN the popover stays open, because the scheduled close re-checks both hover regions at fire time and the pointer is over the content.
- GIVEN a fine-pointer viewport and a task with one open MR, WHEN the user clicks the chip trigger while its popover is closed, THEN no navigation occurs and no panel opens; the popover opens only when the 150ms hover-open delay elapses with the pointer still over the trigger.
- GIVEN a fine-pointer viewport and a task with one open MR, WHEN the viewport width is any value from 320px to 1920px, THEN exactly one disclosure branch renders and the chip is never blank. No width between breakpoints leaves it rendered by neither branch.
Popover content and actions:
-
GIVEN an open MR with 6 of 10 pipeline jobs passing, WHEN its chip popover opens, THEN the pass-rate row reads
6/10 (60%). -
GIVEN an open MR whose pipeline has not started (
pipeline_jobs_total: 0and no fetched jobs), WHEN its chip popover opens, THENmr-pipeline-emptyis rendered. -
GIVEN an open MR and a workspace with GitLab authenticated whose status request has already resolved, WHEN its chip popover opens, THEN
mr-popover-link-anotheris rendered. (The pre-resolution window is a separate AC below; the qualifier is what keeps the two from disagreeing.) -
GIVEN an open MR and a workspace with GitLab not configured, WHEN its chip popover opens, THEN
mr-popover-link-anotheris not rendered. -
GIVEN an open MR and a workspace with GitLab authenticated, WHEN the chip mounts and its popover is opened before the mount-time GitLab status request resolves, THEN
mr-popover-link-anotheris absent at first and appears once the status resolves, without the chip unmounting or remounting and without any error being shown. -
GIVEN a task with exactly one open MR, WHEN the user unlinks it from the chip popover and the request succeeds, THEN the association is removed from the store and
mr-status-chipunmounts. -
GIVEN the unlink request fails, WHEN the user unlinks from the chip popover, THEN an error toast is shown and
mr-status-chipremains. -
GIVEN an MR that satisfies
isMRReadyToMerge, WHEN its chip popover opens, THENmr-merge-buttonis rendered anddata-mr-ready-to-mergeon the trigger is"true". -
GIVEN a task with two open MRs, WHEN the multi chip renders, THEN
data-mr-ready-to-mergeis present on the trigger and equalsisMRReadyToMerge(acted-on MR). -
GIVEN a fine-pointer viewport and an open chip popover, WHEN the user activates
mr-popover-link-another, THEN the popover closes and the task-MR link dialog opens. -
GIVEN a coarse-pointer viewport and an open chip drawer, WHEN the user activates
mr-popover-link-another, THENmr-status-chip-draweris no longer visible and the task-MR link dialog is open, so the two dialogs are never nested. -
GIVEN the link dialog opened from the chip, WHEN the user links a second open MR that outranks the current selection, THEN the dialog closes, the new association is in the store, and the trigger's
data-mr-countanddata-mr-iidupdate to include and name it. -
GIVEN two independent fixtures of the same task and association — one where the user unlinks from the chip popover, one where the user unlinks from
MRTopbarButton— WHEN the request succeeds in each, THEN both remove the same association from the store and neither toasts. -
GIVEN those same two independent fixtures, WHEN the request fails in each, THEN both show the error toast built from
gitlab:failedToUnlinkMergeRequestandgitlab:theMergeRequestIsStillLinked, and both leave the association in the store.These are two separate fixtures, not a sequence: the second unlink of one association would fail for a different reason (the row is already gone) and would test nothing about parity. Parity holds by construction because both surfaces call the one extracted
useUnlinkTaskMRand no second copy of that closure exists; these ACs pin that the extraction actually happened.
Automation badges:
-
GIVEN task MR automation with
auto_fix_enabled: falseandauto_merge_enabled: false, WHEN the chip renders, THEN neithermr-status-auto-fix-chipnormr-status-auto-merge-chipis rendered. -
GIVEN
auto_fix_enabled: true,auto_fix_max_rounds: 5, and a lifecycle state for the selected MR withauto_fix_round_count: 2, WHEN the chip renders, THENmr-status-auto-fix-chipreads2/5. -
GIVEN
auto_fix_enabled: true,auto_fix_max_rounds: 5, and no lifecycle state at all for the selected MR (findMRAutomationStateForMRreturnsundefined), WHEN the chip renders, THENmr-status-auto-fix-chipIS rendered and reads0/5, and carriesdata-auto-fix-exhausted="false".This is the ordinary steady state of an enabled MR that has not needed a fix yet, so it is the badge's most common appearance and it is pinned rather than left to inference. The value follows from
autoFixRoundForState, which is total: givenundefinedit returns{current: 0, max, exhausted: false}. Hiding the badge until round >= 1 is specifically NOT the behaviour — the badge communicates that auto-fix is on, which is true at round 0. -
GIVEN
auto_fix_enabled: trueand a lifecycle state withauto_fix_exhausted_atset, WHEN the chip renders, THENmr-status-auto-fix-chipcarriesdata-auto-fix-exhausted="true". -
GIVEN
auto_fix_enabled: trueandauto_fix_max_roundsabsent or not a finite number, WHEN the chip renders, THEN the badge's denominator is10. -
GIVEN two open MRs where one is exhausted at round 3 of 5 and the other is at round 4 of 5, WHEN the multi chip renders, THEN the badge shows the exhausted MR's
3/5. -
GIVEN two open MRs whose auto-fix rounds are fully tied — neither exhausted, both at round 2 of 5 — with iids 12 and 7, WHEN the multi chip renders, THEN the badge is the one belonging to iid
7, resolved by the samemr_iid/project_path/idorder thatselectChipMRuses. This pins the tiebreak leg of the round-aggregation rule, which the exhausted-beats- non-exhausted and higher-current-wins legs above do not reach. -
GIVEN
auto_merge_enabled: true, WHEN the chip renders, THENmr-status-auto-merge-chipis rendered. -
GIVEN automation options have not loaded, WHEN the chip renders, THEN the chip still renders its status glyph and no badges.
Idempotency and re-render:
-
GIVEN unchanged store state, WHEN the chip re-renders any number of times, THEN its rendered output and
data-*attributes are identical. -
GIVEN the task's MR automation options are already in the store, WHEN the chip mounts, THEN no request is issued.
-
GIVEN the task's MR automation options are absent from the store, WHEN a single chip mounts, THEN exactly one options request is issued for that task.
-
GIVEN the task's MR automation options are absent from the store, WHEN two chips for the same task mount in the same React commit, THEN each may issue one request, and once both settle the store holds exactly one options object for that task and both chips render identical badges. Cross-mount request dedupe is explicitly NOT asserted; see Sync and freshness.
-
GIVEN the chip is mounted, WHEN no user action occurs, THEN it issues no further request: it neither polls nor re-fetches on an interval.
-
GIVEN the chip's popover is open on a task with two open MRs, WHEN a store update makes the other MR the higher-ranked one, THEN the popover keeps showing the MR it opened with, the trigger's
data-mr-iidstill names that MR,data-selection-frozenis"true", and the trigger'sdata-statusupdates to the new live selection's status. -
GIVEN that same popover, WHEN the user closes it, THEN in the render after close
data-selection-frozenis"false"anddata-mr-iidnames the new live selection. -
GIVEN the chip's popover is open on an MR whose
pipeline_stateis"success", withdetailed_merge_status: "mergeable",unresolved_discussions: 0,draft: false— soisMRReadyToMergeis true and the trigger'sdata-mr-ready-to-mergeis"true"— WHEN a store update sets that same MR'spipeline_stateto"failure", THENdata-mr-ready-to-mergebecomes"false"andmr-merge-buttonis no longer rendered in the open popover.The transition is chosen so the assertion cannot pass vacuously. A
pending->failuretransition would leaveisMRReadyToMergefalse on both sides (it requirespipeline_state === "success"), so it proves nothing about liveness;success->failureis the smallest change that actually flips the observed value. -
GIVEN that same popover is open on that MR, WHEN the same store update lands, THEN the trigger's
data-statuschanges fromreadytofailed, confirming the body and the trigger read the same live row. -
GIVEN the chip's popover is open, WHEN the MR it is showing is unlinked from another surface, THEN the popover closes.
-
GIVEN the chip's popover is open, WHEN the MR it is showing transitions to
mergedwhile another open MR remains, THEN the popover closes and the trigger re-selects the remaining open MR. -
GIVEN two chips are mounted at once, WHEN the user opens one, THEN the other stays closed.
-
GIVEN two chips are mounted at once for the same task, WHEN the user opens the chat-status-bar chip by keyboard focus and then opens the passthrough-status-row chip by hover, THEN both disclosures are open at the same time, each rendering
mr-topbar-popover-inner, and neither closes the other. Simultaneous open is permitted; no cross-chip coordinator exists. -
GIVEN those two disclosures are both open, THEN each chip issues its own MR feedback read — two requests for the one task. Cross-chip feedback dedupe is explicitly NOT asserted; see Sync and freshness.
-
GIVEN the chip's popover is open on a task with two open MRs, so
data-selection-frozenis"true", WHEN a store update makes the other MR the badge-selected one (it becomes exhausted while the frozen MR is not), THENmr-status-auto-fix-chipupdates to the newly badge-selected MR's round and itsdata-auto-fix-exhaustedbecomes"true", whiledata-mr-iidstill names the frozen MR. The badges do not freeze.
Internationalization:
- GIVEN the pseudo-locale is active, WHEN the chip and its drawer
render, THEN every string this feature adds is pseudo-localized,
including the trigger's
aria-label, the drawer title, and the drawer's screen-reader description. - GIVEN the pseudo-locale is active, WHEN the chip's popover renders
MRCIPopover, THEN GitLab-sourced domain data inside it is NOT pseudo-localized: MR titles, author and reviewer names,project_path, branch names, and pipeline job and stage names render verbatim. Perapps/web/CLAUDE.mdthese are user/domain data, never UI copy, and this feature does not change howMRCIPopoverrenders them. The i18n assertion is scoped to chip-owned copy for exactly this reason.
Out of scope
Each of these is a deliberate exclusion, not an oversight. Each is its own card.
- MR conflict / merge-blocked as a distinct chip status. GitHub's chip has
conflict(mergeable_state: "dirty") andbehindstates. GitLab exposes the equivalent throughdetailed_merge_status/merge_status, but introducing it as a chip status would add a branch togetMRStatusColorand therefore change the kanban card icon and the topbar trigger colour for every conflicted MR in the product. That is a change to a different surface than this card's, somrChipStatusdeliberately produces no status that alters an existing colour. - Re-tuning the GitLab MR status colour priority. For example, making a
green pipeline with no approval requirement read green rather than falling to
neutral. The chip inherits today's priority table verbatim. - A GitLab multi-MR tabbed popover. GitHub has
MultiPRCIPopoverwith segmented per-PR tabs, backingPRStatusChipMultiHoverCardandPRStatusChipMultiDrawer. GitLab has no such component, andmr-topbar-button.tsxalready carries this exact exclusion. The multi-MR chip therefore shows the aggregate status plus a count on the trigger, and exactly one MR (the selected MR) inside its popover. - Reaching a terminal MR from the chip. Because there is no multi-MR
surface, the chip renders only for open MRs, and a merged or closed
association is not reachable from it. Unlinking a terminal association stays
with the topbar dropdown and the MR detail panel. This is a deliberate
divergence from
PRStatusChip, which keeps terminal PRs in its multi-PR surface for exactly that reason. - GitLab equivalents of
PRMergedBanner/PRClosedBanner. GitHub's chip row shows an archive-prompt banner when a task's PR merges or closes. No GitLab counterpart is added here. - Opening the MR detail panel from the chip popover.
MRCIPopover'sonOpenDetailPanelprop is optional and the chip omits it, so the popover title renders as static text. The dockview-settle logic that makes that open reliable is owned by the topbar button, which remains the entry point. - Giving GitLab MR hydration a single provider-level owner. It currently has four independent call sites, each with its own per-instance "already fetched" ref, so which surfaces a session has visited decides whether a given task's MRs are in the store. Consolidating them is its own card. Until it lands, this spec pins no hydration outcome and no AC depends on one. See Sync and freshness.
- A shared GitLab MR feedback cache or background sync. No store slice, no warmer, no polling is added.
- Making
useGitLabStatuscache across mounts, and stopping it blanking the shared status. Its effect refetches on every mount and clears the store tonullfirst, so each new consumer costs a request and transiently makesuseGitLabAvailable()readfalseeverywhere. The chip inherits this rather than introducing it —MRTopbarButtonalready calls the same helper on the same route — and cannot fix it locally, because the hook serves navigation, the kanban board, the settings sidebar and the integrations menu. Fixing it means a card that can test all of those callers. See Sync and freshness. - A cross-chip disclosure coordinator. Two chips mounted for one task may
both have an open disclosure, and each then issues its own MR feedback read.
Serialising that would require new shared "currently open chip" state that
neither mount point nor
MRTopbarButtonhas today, and a rule for which chip loses. The duplicate is an idempotentGETinto a per-instance reducer. See Sync and freshness. - A cross-mount in-flight guard for
useTaskMRAutomationOptions. Its lazy fetch gates on render-captured state, so two chips mounted in one React commit can each issue aGET. Fixing it means changing a hookMRAutomationControlsalso uses from the topbar, so it needs a card that can test both callers. The duplicate is an idempotentGETthat converges to one stored object. See Sync and freshness. - Restoring focus when the chip unmounts underneath its own drawer.
Unlinking the task's last open MR from inside the drawer closes the drawer and
unmounts the trigger in the same update, so there is nothing to return focus
to and it falls to
document.body. Choosing and moving focus to a surviving landmark is a shared problem withPRStatusChiprather than one this chip introduces, and fixing it well means deciding a focus target for the whole status row. See Accessibility. - Making
MRCIPopover's controls Tab-reachable from a hover popover. Radix portals the content with no tab bridge from the trigger, so link, unlink and merge are pointer- or drawer-only on a fine-pointer device. This gap is pre-existing and shared withPRStatusChipandMRTopbarButton; the chip inherits it rather than creating it, and fixing it would change a component the topbar also renders. See Accessibility. - Changing
aggregateMRStatusColor's array-order tie behaviour. Its first-in-input-order tie is observable on the multi-MR kanban badge today. This card freezes it rather than fixing it, because changing it is re-tuning the colour priority, already excluded above. See State machine. - Enriched My-GitLab list badges, per-job checks UI in the detail panel, mergeability explanation or conflict-fix CTA, merge-method picker, GitLab webhooks, poller rate limiting, and poller-side watch reconciliation.
- Depending on the parent card's
resolvePushRepofix. This feature is independent in code. Its tests seed MR associations directly through the existing link endpoint and the GitLab mock provider, never through autolink.