diff --git a/docs/mobile-web-parity.md b/docs/mobile-web-parity.md index 360ad91b..56447ecb 100644 --- a/docs/mobile-web-parity.md +++ b/docs/mobile-web-parity.md @@ -57,7 +57,7 @@ issues and PRs. Keep it open until its full acceptance boundary is verified. | MW-04 | P1 | Member invitations, role/expiry changes, and removal for groups/projects. | Queued | Issue needed | | MW-05 | P0 | Issue/work-item editing, metadata, types, and validated state transitions. | In progress | [#329](https://github.com/Theorvane/labfox/issues/329), [PR #330](https://github.com/Theorvane/labfox/pull/330) shipped title and description editing; [#339](https://github.com/Theorvane/labfox/issues/339), [PR #340](https://github.com/Theorvane/labfox/pull/340) shipped due-date editing; [#341](https://github.com/Theorvane/labfox/issues/341), [PR #342](https://github.com/Theorvane/labfox/pull/342) shipped issue-label editing; [#343](https://github.com/Theorvane/labfox/issues/343), [PR #344](https://github.com/Theorvane/labfox/pull/344) shipped assignee editing; [#345](https://github.com/Theorvane/labfox/issues/345), [PR #346](https://github.com/Theorvane/labfox/pull/346) shipped milestone editing; [#347](https://github.com/Theorvane/labfox/issues/347), [PR #348](https://github.com/Theorvane/labfox/pull/348) shipped confidentiality; [#349](https://github.com/Theorvane/labfox/issues/349), [PR #350](https://github.com/Theorvane/labfox/pull/350) shipped discussion locking. Type-conversion compatibility and remaining work-item actions need separate issues. | | MW-06 | P1 | Boards and iterations: discover mobile-web behavior, then list/detail/mutations. | Queued | Issue needed | -| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. | +| MW-07 | P0 | Advanced MR review: audit inline discussions/suggestions and finish missing review/approval flows. | In progress | [#552](https://github.com/Theorvane/labfox/issues/552), [PR #553](https://github.com/Theorvane/labfox/pull/553) prevent overlapping approval, merge, and other MR commands per project/IID, including while the repository resolves; failures release the reservation for explicit retry. [#554](https://github.com/Theorvane/labfox/issues/554), [PR #555](https://github.com/Theorvane/labfox/pull/555) add repository-session observation, pre-dispatch cancellation and late-result/error isolation, success-only current-session refresh/analytics/inbox effects, cancelled to-do outcomes without current-account toasts, and generation-owned duplicate reservations. Already dispatched server writes cannot be undone. [#556](https://github.com/Theorvane/labfox/issues/556), [PR #557](https://github.com/Theorvane/labfox/pull/557) add localized approval-read loading/error states, disabled approval toggles until successful reads, read-only retry, retained-data refresh protection, reactive new-session reads with stale-result isolation, and current-state toggle selection while preserving unavailable endpoint compatibility and independent merge controls. [#558](https://github.com/Theorvane/labfox/issues/558), [PR #559](https://github.com/Theorvane/labfox/pull/559) derives approval ownership from the authenticated account user ID and documented `approved_by` membership, ignores conflicting legacy current-user flags, and refreshes membership on account replacement. Regression coverage checks real toggle dispatch on mobile/desktop in both themes. [#560](https://github.com/Theorvane/labfox/issues/560), [PR #561](https://github.com/Theorvane/labfox/pull/561) validates successful approval responses before model parsing: explicit approver lists, complete user wrappers, positive integral user IDs, and nonnegative integral counts when present. Malformed reads become sanitized domain errors; status mapping precedes payload parsing. End-to-end widget tests verify blocked approval toggles and read-only recovery in both widths/themes, while unavailable endpoint compatibility remains intact. [#562](https://github.com/Theorvane/labfox/issues/562), [PR #563](https://github.com/Theorvane/labfox/pull/563) adds an account-bound, one-page MR discussion reader with generated grouped-note models and nullable resolution metadata. It preserves next-page headers without assuming totals or fetching ahead, rejects incomplete groups and nonadvancing cursors, and keeps HTTP failures distinct from sanitized parsing errors. Building on the API/models foundation, [#564](https://github.com/Theorvane/labfox/issues/564), [PR #565](https://github.com/Theorvane/labfox/pull/565) connects grouped MR replies to the detail-screen comments flow and aggregates resolution across all resolvable notes without claiming unknown states are resolved. The shared top-level composer refreshes discussion page one after posting; explicit load-more and read/page retries preserve groups and cursors. Account-bound reads/posts ignore late results, drafts reset when the session or resource changes, and resizing preserves the current draft. Five locales and narrow/compact/wide light/dark layouts are covered. [#566](https://github.com/Theorvane/labfox/issues/566), [PR #567](https://github.com/Theorvane/labfox/pull/567) adds the MR discussion reply API foundation: encoded thread/project paths, explicit IID routing, exact nonempty Markdown bodies, validated generated note responses, sanitized typed errors and no automatic authentication replay for the write. [#568](https://github.com/Theorvane/labfox/issues/568), [PR #569](https://github.com/Theorvane/labfox/pull/569) connects a localized inline reply composer to the selected discussion. One active reply preserves the top-level draft; cancel and write reservations prevent accidental overlaps. Replies preserve Markdown, retain failed drafts for explicit retry, refresh authoritative discussion page one and ignore old-session completions. Account/MR replacement resets the selection, resizing and pagination preserve current drafts, and a removed target restores top-level submission. Five locales and three widths in both themes are covered. [#570](https://github.com/Theorvane/labfox/issues/570), [PR #571](https://github.com/Theorvane/labfox/pull/571) adds known-state Resolve/Reopen controls and a strict account-bound mutation: encoded identity, explicit resolution, no redirects/authentication replay, and updated thread/state confirmation. Resolution shares comment/reply write reservations, preserves the top-level draft, retains localized permission/failure feedback for explicit retry, and refreshes discussions and MR detail only on current-session success. Unknown/nonresolvable states offer no toggle, and an active reply draft prevents resolution. [#572](https://github.com/Theorvane/labfox/issues/572), [PR #573](https://github.com/Theorvane/labfox/pull/573) preserves generated diff-note position metadata: original version SHAs, renamed paths, old/new text lines, multiline range endpoints and image/file types. Discussion reads, replies and resolution responses reject malformed or fractional coordinates before parsing; absent legacy fields remain unknown. [#574](https://github.com/Theorvane/labfox/issues/574), [PR #575](https://github.com/Theorvane/labfox/pull/575) adds paginated diff-version reads and an explicitly selected original unified-diff snapshot. Generated version/file models retain SHA triplets, renamed paths, raw text and nullable omission flags; absent files differ from empty snapshots. Identity/payload validation, status-first typed failures and no eager pagination or current-diff fallback are covered. [#576](https://github.com/Theorvane/labfox/issues/576) / [PR #577](https://github.com/Theorvane/labfox/pull/577) adds an expandable original-diff context beside positioned notes. It matches complete SHA triplets, both file paths and exact old/new single-line text coordinates; explicit older-page reads and read-only retries preserve drafts. The matched original hunk/line has an accessible non-color marker. Unsupported, omitted, truncated or ambiguous contexts remain unavailable without a current-diff fallback; account/resource replacement resets panels and stale reads cannot dispatch follow-up snapshots or affect the new session. Five locales, three widths and both themes are covered. Multiline context, positioned thread creation and suggestion application remain separate slices. [#578](https://github.com/Theorvane/labfox/issues/578), [PR #580](https://github.com/Theorvane/labfox/pull/580) adds the single-line positioned discussion creation API foundation: exact original SHA/path/line anchors, Markdown preservation, pre-dispatch argument validation, no redirects or authentication replay, created-thread confirmation and sanitized typed failures. The line selector/composer, multiline creation and suggestions remain separate. [#579](https://github.com/Theorvane/labfox/issues/579), [PR #581](https://github.com/Theorvane/labfox/pull/581) connects the latest authoritative diff-version snapshot to localized single-line selection and discussion creation. Exact original coordinates, indexed eligibility, shared write reservations, retained drafts, session/resource isolation and visible authoritative discussion inspection before uncertain-write retry are covered. The existing MR Changes route restores from project/IID; commit diffs stay read-only. Multiline context/creation and suggestion application remain separate. [#582](https://github.com/Theorvane/labfox/issues/582) / [PR #583](https://github.com/Theorvane/labfox/pull/583) adds typed immutable suggestion metadata to MR discussion responses, preserving original/replacement code, inclusive ranges, applied status and both documented applicability spellings. Missing states remain unknown; positive integral identities/coordinates, range order, response-wide uniqueness, content/flag types and alias agreement are validated before generated parsing. Suggestion presentation/application and multiline context/creation remain separate. [#584](https://github.com/Theorvane/labfox/issues/584) / [PR #585](https://github.com/Theorvane/labfox/pull/585) adds read-only suggestion previews beside user notes with exact selectable original/replacement code, nullable application/applicability states, localized ranges and empty/unavailable content, bounded code panes, retained drafts across resize/theme, and account/resource/metadata isolation. Suggestion application and batching remain separate. [#586](https://github.com/Theorvane/labfox/issues/586) / [PR #587](https://github.com/Theorvane/labfox/pull/587) adds the single-suggestion application API foundation with a positive global identity, exact optional commit messages, strict HTTP 200 and matching applied=true confirmation, shared metadata validation, typed sanitized failures, and no redirect/authentication replay. Application UI/session reservations and batch application remain separate. [#588](https://github.com/Theorvane/labfox/issues/588) / [PR #589](https://github.com/Theorvane/labfox/pull/589) adds explicit single-suggestion confirmation with literal code and optional commit messages, a fresh exact-discussion preflight, shared reservations including pagination, current-view/account isolation, confirmed-success detail/discussion/review-snapshot refreshes, and draft-preserving read-only recovery before uncertain-write retry. Five locales, three widths, both themes, keyboard insets and large text are covered. Batch application and multiline context/creation remain separate. [#590](https://github.com/Theorvane/labfox/issues/590) / [PR #591](https://github.com/Theorvane/labfox/pull/591) adds the batch suggestion API foundation: one account-bound request with immutable positive unique global IDs, exact optional messages, strict complete matching applied-state confirmation, preserved server order/nullable metadata, status-first sanitized failures, and no redirects, authentication replay or single-write fallback. Batch selection/confirmation and fresh-state recovery UI, plus multiline context/creation, remain separate. | | MW-08 | P1 | CI/CD configuration: pipeline editor, variables, triggers, schedule editing, and execution history. | In progress | [#423](https://github.com/Theorvane/labfox/issues/423), [PR #424](https://github.com/Theorvane/labfox/pull/424) cover timing editing; [#425](https://github.com/Theorvane/labfox/issues/425), [PR #426](https://github.com/Theorvane/labfox/pull/426) cover deletion; [#427](https://github.com/Theorvane/labfox/issues/427), [PR #428](https://github.com/Theorvane/labfox/pull/428) cover creation; [#429](https://github.com/Theorvane/labfox/issues/429), [PR #430](https://github.com/Theorvane/labfox/pull/430) cover ownership transfer; [#431](https://github.com/Theorvane/labfox/issues/431), [PR #432](https://github.com/Theorvane/labfox/pull/432) cover ref/active editing. These slices await review or maintainer merge. [#433](https://github.com/Theorvane/labfox/issues/433), [PR #434](https://github.com/Theorvane/labfox/pull/434) cover schedule-specific execution history in review: newest-first header pagination, retained rows/cursor on failed next pages, retry, run-now/refresh reload with inline errors, stale-request protection, and identifier-only pipeline navigation. History filters, variables/inputs, pipeline configuration editing, and triggers remain separate slices. | | MW-09 | P1 | Wiki creation/editing/deletion and history with conflict handling. | In progress | [#351](https://github.com/Theorvane/labfox/issues/351), [PR #352](https://github.com/Theorvane/labfox/pull/352) shipped Markdown page creation; [#353](https://github.com/Theorvane/labfox/issues/353), [PR #354](https://github.com/Theorvane/labfox/pull/354) shipped editing with best-effort stale-draft detection; [#373](https://github.com/Theorvane/labfox/issues/373), [PR #374](https://github.com/Theorvane/labfox/pull/374) shipped deletion with confirmation and best-effort stale-page detection; [#375](https://github.com/Theorvane/labfox/issues/375), [PR #376](https://github.com/Theorvane/labfox/pull/376) shipped Markdown template selection. [#377](https://github.com/Theorvane/labfox/issues/377), [PR #378](https://github.com/Theorvane/labfox/pull/378) cover template listing and creation in review. Page history and atomic conflict protection remain. The documented wiki REST API can retrieve a page by version SHA but does not list page history. | | MW-10 | P1 | Snippet creation/editing/deletion, files, and visibility. | In progress | [#355](https://github.com/Theorvane/labfox/issues/355), [PR #356](https://github.com/Theorvane/labfox/pull/356) shipped single-file project snippet creation; [#357](https://github.com/Theorvane/labfox/issues/357), [PR #358](https://github.com/Theorvane/labfox/pull/358) shipped deletion with confirmation; [#359](https://github.com/Theorvane/labfox/issues/359), [PR #360](https://github.com/Theorvane/labfox/pull/360) shipped title and description editing; [#361](https://github.com/Theorvane/labfox/issues/361), [PR #362](https://github.com/Theorvane/labfox/pull/362) shipped single-file content editing; [#363](https://github.com/Theorvane/labfox/issues/363), [PR #364](https://github.com/Theorvane/labfox/pull/364) shipped private/public visibility editing; [#365](https://github.com/Theorvane/labfox/issues/365), [PR #366](https://github.com/Theorvane/labfox/pull/366) shipped multi-file content editing; [#367](https://github.com/Theorvane/labfox/issues/367), [PR #368](https://github.com/Theorvane/labfox/pull/368) shipped adding a file; [#369](https://github.com/Theorvane/labfox/issues/369), [PR #370](https://github.com/Theorvane/labfox/pull/370) shipped deleting a file; [#371](https://github.com/Theorvane/labfox/issues/371), [PR #372](https://github.com/Theorvane/labfox/pull/372) shipped moving and renaming a file. Broader visibility management remains. | diff --git a/docs/mr-suggestion-application-api.md b/docs/mr-suggestion-application-api.md index f30d9a07..de3e0b2c 100644 --- a/docs/mr-suggestion-application-api.md +++ b/docs/mr-suggestion-application-api.md @@ -10,6 +10,6 @@ Only HTTP 200 with a strictly validated matching suggestion ID and `applied: tru The shared suggestion validator retains the existing MR read/reply/resolution/positioned-creation boundaries: positive integral IDs and supplied coordinates, ordered complete ranges, string content, boolean status, consistent legacy/documented applicability aliases, and response-wide identity uniqueness. It runs before generated numeric parsing so fractional values cannot be truncated. The apply response uses the same validator and generated DTO; malformed or unconfirmed results become `Invalid suggestion application response.` without source text, IDs or raw parser details. -An error or unconfirmed response can follow an accepted repository write. A caller must reload authoritative discussions and inspect the current suggestion state before offering an explicitly confirmed manual retry. The API does not implement that UI, shared discussion reservation, account/resource completion handling, custom-message composer or batch application. Those are separate slices; the existing preview remains read-only. No live repository change or physical-device validation is claimed. +An error or unconfirmed response can follow an accepted repository write. A caller must reload authoritative discussions and inspect the current suggestion state before offering an explicitly confirmed manual retry. The API itself does not own UI confirmation or completion effects. The app now implements single-suggestion confirmation, shared discussion reservations, account/resource isolation and read-only recovery as documented in [the application UI](mr-suggestion-application-ui.md). The [batch API](mr-suggestion-batch-api.md) is a separate foundation; batch selection and confirmation UI remain follow-up work. No live repository change or physical-device validation is claimed. Tests use documented synthetic payloads and dummy credentials. Reference: [Suggest Changes API](https://docs.gitlab.com/api/suggestions/). diff --git a/docs/mr-suggestion-application-ui.md b/docs/mr-suggestion-application-ui.md index 5039c457..a98f262d 100644 --- a/docs/mr-suggestion-application-ui.md +++ b/docs/mr-suggestion-application-ui.md @@ -23,4 +23,4 @@ These are synthetic Flutter widget-test captures with dummy metadata and SDK fon | Mobile, 390 × 844 | [Capture](images/mr-suggestion-apply/apply-390-light.png) | [Capture](images/mr-suggestion-apply/apply-390-dark.png) | | Desktop, 1200 × 900 | [Capture](images/mr-suggestion-apply/apply-1200-light.png) | [Capture](images/mr-suggestion-apply/apply-1200-dark.png) | -Batch suggestion application and multiline discussion context/creation remain separate parity slices. +The [batch application API](mr-suggestion-batch-api.md) is available as a separate foundation. Batch selection/confirmation UI and multiline discussion context/creation remain separate parity slices. diff --git a/docs/mr-suggestion-batch-api.md b/docs/mr-suggestion-batch-api.md new file mode 100644 index 00000000..79097a23 --- /dev/null +++ b/docs/mr-suggestion-batch-api.md @@ -0,0 +1,17 @@ +# Confirmed batch suggestion application API + +`GitLabClient.suggestions.applyBatch(suggestionIds, commitMessage: ...)` uses authenticated `PUT /suggestions/batch_apply` with an `ids` array of global suggestion identities. It sends one batch request, with no fallback to separate suggestion writes. The account-bound instance URL and existing authentication retain self-hosted subpaths and PAT/OAuth support. + +The input must be nonempty, positive, and unique. The client rejects invalid inputs before dispatch rather than silently removing duplicates or invalid identities. A snapshot taken before the first asynchronous boundary protects both the outgoing request and confirmation against caller list mutation. The requested order is preserved in the body. A null message is omitted; present strings, including empty or whitespace-only messages, remain exact. GitLab decides their validity and the caller's permissions. + +Only HTTP 200 with a valid array confirms the batch. Every requested identity must appear exactly once with `applied=true`; missing, extra, duplicate, fractional, unknown or unapplied identities reject the entire returned confirmation. No partial result is exposed. The returned list is immutable and preserves server order, which need not match request order. Original/replacement code, inclusive line ranges and both applicability spellings use the existing strict validator before generated parsing. Optional metadata remains nullable, empty strings remain empty, and conflicting flags or reversed ranges are rejected. + +The write disables redirects and automatic OAuth refresh/replay. Status-first mapping preserves authentication, permission, not-found, rate-limit and other HTTP status information; transport failures are sanitized domain errors. Malformed or incomplete confirmation becomes `Invalid suggestion batch application response.` without source code, identities or raw decoding details. + +This confirms returned suggestion states, not a commit count, branch SHA, permission claim or client-side atomicity guarantee. An error or incomplete response can follow an accepted repository write. Callers must freshly inspect every selected discussion and require renewed confirmation before an explicit manual retry. There is no automatic application retry, single-write fallback, rollback or inferred success for part of a batch. + +This slice provides the API only. The existing app still offers single-suggestion confirmation/application. Batch selection, MR grouping, multi-patch confirmation, shared UI reservations and complete fresh-state recovery belong to a separate UI slice. No model, dependency or generated file changes, live GitLab writes or physical-device validation are included. + +Test-first coverage includes missing-method failure and 130 failing behavior contracts against a minimal unsafe implementation, followed by all 149 batch contracts and 254 existing single-application/metadata cases passing together. Tests cover exact routing/authentication/messages, invalid inputs, reordered and sparse confirmed payloads, every member's strict metadata boundary, incomplete identities, caller mutation, immutable results, typed failures and no OAuth replay. + +Reference: [GitLab batch suggestion application](https://docs.gitlab.com/api/suggestions/#apply-multiple-suggestions). diff --git a/packages/gitlab_api/lib/src/suggestions/suggestions_api.dart b/packages/gitlab_api/lib/src/suggestions/suggestions_api.dart index 132e9ef7..323f5bf8 100644 --- a/packages/gitlab_api/lib/src/suggestions/suggestions_api.dart +++ b/packages/gitlab_api/lib/src/suggestions/suggestions_api.dart @@ -42,6 +42,78 @@ class SuggestionsApi { } } + /// Applies one batch without falling back to separate suggestion writes. + /// + /// Every requested identity must appear once with applied=true. Response + /// order is preserved; no commit count or atomicity guarantee is inferred. + /// An uncertain result requires fresh discussion inspection before retrying. + Future> applyBatch( + List suggestionIds, { + String? commitMessage, + }) async { + // Copy before the first await so caller changes cannot alter the write or + // the identities whose returned state must be confirmed. + final ids = List.unmodifiable(suggestionIds); + if (ids.isEmpty || + ids.any((id) => id < 1) || + ids.toSet().length != ids.length) { + throw ArgumentError( + 'Nonempty unique positive suggestion IDs are required.', + 'suggestionIds', + ); + } + try { + final response = await _dio.put( + '/suggestions/batch_apply', + data: {'ids': ids, 'commit_message': ?commitMessage}, + options: Options( + followRedirects: false, + extra: {'labfox_no_auth_retry': true}, + ), + ); + if (response.statusCode != 200) { + throw mapStatus( + response.statusCode, + response.headers.map, + context: 'applying code suggestions', + ); + } + return _confirmedBatch(response.data, ids.toSet()); + } on DioException catch (error) { + throw mapError(error, context: 'applying code suggestions'); + } + } + + static List _confirmedBatch( + Object? payload, + Set expectedIds, + ) { + try { + if (payload is! List || payload.length != expectedIds.length) { + throw const FormatException(); + } + validateSuggestionPayloads(payload); + final suggestions = []; + for (final entry in payload) { + final suggestion = Suggestion.fromJson(entry as Map); + if (!expectedIds.contains(suggestion.id) || + suggestion.applied != true) { + throw const FormatException(); + } + suggestions.add(suggestion); + } + return List.unmodifiable(suggestions); + } on FormatException { + throw const GitLabServerException( + 'Invalid suggestion batch application response.', + ); + } on TypeError { + throw const GitLabServerException( + 'Invalid suggestion batch application response.', + ); + } + } + static Suggestion _confirmed(Object? payload, int suggestionId) { try { if (payload is! Map) throw const FormatException(); diff --git a/packages/gitlab_api/test/suggestions_batch_apply_api_test.dart b/packages/gitlab_api/test/suggestions_batch_apply_api_test.dart new file mode 100644 index 00000000..b399b371 --- /dev/null +++ b/packages/gitlab_api/test/suggestions_batch_apply_api_test.dart @@ -0,0 +1,481 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:gitlab_api/gitlab_api.dart'; +import 'package:test/test.dart'; + +const _patch = { + 'id': 5, + 'from_line': 10, + 'to_line': 12, + 'from_content': ' original();\n', + 'to_content': ' reviewed();\n', + 'applicable': false, + 'applied': true, +}; +List> _payload() => [ + _patch, + {..._patch, 'id': 6}, +]; +void main() { + for (final base in [ + 'https://gitlab.example.com', + 'https://git.example.com/gitlab/', + ]) { + for (final bearer in [false, true]) { + for (final message in [null, '', ' Exact\nMessage ']) { + test( + 'batch routing auth and exact message $base $bearer $message', + () async { + late RequestOptions request; + var writes = 0; + final client = _client( + (o) { + request = o; + writes++; + return (status: 200, body: _payload()); + }, + baseUrl: base, + bearer: bearer, + ); + addTearDown(client.close); + final result = await client.suggestions.applyBatch([ + 5, + 6, + ], commitMessage: message); + expect(request.method, 'PUT'); + expect(request.path, '/suggestions/batch_apply'); + expect( + request.uri.toString(), + Uri.parse(base).path.startsWith('/gitlab') + ? 'https://git.example.com/gitlab/api/v4/suggestions/batch_apply' + : 'https://gitlab.example.com/api/v4/suggestions/batch_apply', + ); + expect(request.queryParameters, isEmpty); + expect( + request.data, + message == null + ? { + 'ids': [5, 6], + } + : { + 'ids': [5, 6], + 'commit_message': message, + }, + ); + expect( + request.headers[bearer ? 'Authorization' : 'PRIVATE-TOKEN'], + bearer ? 'Bearer glpat-xxxxxxxxxxxx' : 'glpat-xxxxxxxxxxxx', + ); + expect(request.followRedirects, false); + expect(request.extra['labfox_no_auth_retry'], true); + expect(writes, 1); + expect(result.map((s) => s.id), [5, 6]); + expect(result.every((s) => s.applied == true), true); + expect(result.first.fromContent, _patch['from_content']); + expect(result.last.toContent, _patch['to_content']); + expect(result.first.patchApplicable, false); + expect(() => result.clear(), throwsUnsupportedError); + }, + ); + } + } + } + for (final ids in >[ + [], + [0], + [-1], + [5, 0], + [5, -1], + [5, 5], + [6, 5, 6], + [5, 6, 0], + ]) { + test('invalid batch IDs reject before dispatch $ids', () async { + var writes = 0; + final client = _client((_) { + writes++; + return (status: 200, body: _payload()); + }); + addTearDown(client.close); + await expectLater( + client.suggestions.applyBatch(ids), + throwsArgumentError, + ); + expect(writes, 0); + }); + } + for (final ids in [ + [5, 6], + [6, 5], + ]) { + test( + 'accepts reordered complete response and preserves server order $ids', + () async { + final client = _client( + (_) => (status: 200, body: _payload().reversed.toList()), + ); + addTearDown(client.close); + final result = await client.suggestions.applyBatch(ids); + expect(result.map((s) => s.id), [6, 5]); + expect(ids, ids.first == 5 ? [5, 6] : [6, 5]); + }, + ); + } + test( + 'one suggestion uses the batch endpoint without inventing minimum count', + () async { + late RequestOptions request; + final client = _client((o) { + request = o; + return (status: 200, body: [_patch]); + }); + addTearDown(client.close); + final result = await client.suggestions.applyBatch([5]); + expect(result.single.id, 5); + expect(request.path, '/suggestions/batch_apply'); + expect(request.data, { + 'ids': [5], + }); + }, + ); + for (final mutation in [0, 1, 2]) { + test( + 'snapshots caller IDs before asynchronous dispatch mutation=$mutation', + () async { + final ids = [5, 6]; + late RequestOptions request; + final client = _client((o) { + request = o; + return (status: 200, body: _payload()); + }); + addTearDown(client.close); + final future = client.suggestions.applyBatch(ids); + switch (mutation) { + case 0: + ids.clear(); + case 1: + ids[0] = 99; + case 2: + ids.add(7); + } + final result = await future; + expect(request.data, { + 'ids': [5, 6], + }); + expect(result.map((s) => s.id), [5, 6]); + }, + ); + } + final valid = >{ + 'sparse': {'id': 5, 'applied': true}, + 'null metadata': { + 'id': 5, + 'applied': true, + 'from_line': null, + 'to_line': null, + 'from_content': null, + 'to_content': null, + 'appliable': null, + 'applicable': null, + }, + 'legacy alias': {..._patch, 'applicable': null, 'appliable': false}, + 'matching aliases': {..._patch, 'appliable': false}, + 'empty content': {..._patch, 'from_content': '', 'to_content': ''}, + 'future fields': { + ..._patch, + 'future': {'value': true}, + }, + 'applied independent of applicability': {..._patch, 'applicable': true}, + 'partial range': {..._patch, 'to_line': null}, + }; + for (final entry in valid.entries) { + test('preserves confirmed batch nullable metadata ${entry.key}', () async { + final client = _client( + (_) => ( + status: 200, + body: [ + entry.value, + {'id': 6, 'applied': true}, + ], + ), + ); + addTearDown(client.close); + final result = await client.suggestions.applyBatch([5, 6]); + final s = result.first; + expect(s.fromLine, entry.value['from_line']); + expect(s.toLine, entry.value['to_line']); + expect(s.fromContent, entry.value['from_content']); + expect(s.toContent, entry.value['to_content']); + expect(s.applied, true); + expect(s.applicable, entry.value['applicable']); + expect(s.appliable, entry.value['appliable']); + expect(result.last.fromContent, isNull); + }); + } + final invalidShapes = { + 'null': null, + 'scalar': 'private-content-marker', + 'object': _patch, + 'empty': [], + 'missing one': [_patch], + 'unrequested extra': [ + ..._payload(), + {..._patch, 'id': 7}, + ], + 'duplicate': [_patch, _patch], + 'unexpected ID': [ + _patch, + {..._patch, 'id': 7}, + ], + 'nested array': [ + _patch, + [ + {..._patch, 'id': 6}, + ], + ], + 'null member': [_patch, null], + 'scalar member': [_patch, 'private-content-marker'], + 'missing identity': [ + _patch, + {'applied': true}, + ], + }; + for (final entry in invalidShapes.entries) { + test('rejects incomplete or ambiguous batch shape ${entry.key}', () async { + var writes = 0; + final client = _client((_) { + writes++; + return (status: 200, body: entry.value); + }); + addTearDown(client.close); + await expectLater( + client.suggestions.applyBatch([5, 6]), + throwsA( + isA().having( + (e) => e.message, + 'sanitized', + 'Invalid suggestion batch application response.', + ), + ), + ); + expect(writes, 1); + }); + } + final mutations = >{ + for (final v in [null, 0, -1, 5.5, '5', true, 7]) 'id $v': {'id': v}, + for (final v in [null, false, 'true', 1]) 'applied $v': {'applied': v}, + for (final key in ['from_line', 'to_line']) + for (final v in [0, -1, 10.5, '10', true]) '$key $v': {key: v}, + 'reversed range': {'from_line': 13}, + for (final key in ['from_content', 'to_content']) + for (final v in [ + 1, + true, + ['code'], + {'content': 'code'}, + ]) + '$key $v': {key: v}, + for (final key in ['applicable', 'appliable']) + for (final v in [1, 'false', [], {}]) '$key $v': {key: v}, + 'conflicting aliases': {'appliable': true}, + }; + for (final index in [0, 1]) { + for (final entry in mutations.entries) { + test( + 'validates every returned batch member $index ${entry.key}', + () async { + final payload = _payload(); + payload[index] = {...payload[index], ...entry.value}; + var writes = 0; + final client = _client((_) { + writes++; + return (status: 200, body: payload); + }); + addTearDown(client.close); + await expectLater( + client.suggestions.applyBatch([5, 6]), + throwsA( + isA().having( + (e) => e.message, + 'sanitized', + 'Invalid suggestion batch application response.', + ), + ), + ); + expect(writes, 1); + }, + ); + } + } + for (final status in [ + 201, + 202, + 204, + 301, + 302, + 303, + 307, + 308, + 400, + 401, + 403, + 404, + 409, + 422, + 429, + 500, + 502, + 503, + ]) { + test( + 'maps batch status $status before payload decoding without replay', + () async { + var writes = 0; + final client = _client( + (_) { + writes++; + return ( + status: status, + body: {'message': 'private-content-marker'}, + ); + }, + headers: { + 'retry-after': ['17'], + 'location': ['https://other.example.com/write'], + }, + ); + addTearDown(client.close); + final matcher = switch (status) { + 401 => isA(), + 403 => isA(), + 404 => isA(), + 429 => isA().having( + (e) => e.retryAfter, + 'retryAfter', + const Duration(seconds: 17), + ), + _ => isA(), + }; + await expectLater( + client.suggestions.applyBatch([5, 6]), + throwsA( + allOf( + matcher, + isA() + .having((e) => e.statusCode, 'status', status) + .having( + (e) => e.message, + 'sanitized', + isNot(contains('private-content-marker')), + ), + ), + ), + ); + expect(writes, 1); + }, + ); + } + for (final type in [ + DioExceptionType.connectionError, + DioExceptionType.connectionTimeout, + DioExceptionType.sendTimeout, + DioExceptionType.receiveTimeout, + DioExceptionType.badCertificate, + DioExceptionType.cancel, + ]) { + test('batch transport $type is typed and never replayed', () async { + var writes = 0; + final client = _client((o) { + writes++; + throw DioException( + requestOptions: o, + type: type, + error: 'private-content-marker', + ); + }); + addTearDown(client.close); + await expectLater( + client.suggestions.applyBatch([5, 6]), + throwsA( + isA().having( + (e) => e.message, + 'sanitized', + isNot(contains('private-content-marker')), + ), + ), + ); + expect(writes, 1); + }); + } + test( + 'OAuth batch failure never refreshes or falls back to single writes', + () async { + var writes = 0, refreshes = 0; + final paths = []; + final client = _client( + (o) { + writes++; + paths.add(o.path); + return (status: 401, body: {}); + }, + bearer: true, + onUnauthorized: () async { + refreshes++; + return 'dummy-refreshed-token'; + }, + ); + addTearDown(client.close); + await expectLater( + client.suggestions.applyBatch([5, 6]), + throwsA(isA()), + ); + expect(writes, 1); + expect(refreshes, 0); + expect(paths, ['/suggestions/batch_apply']); + }, + ); +} + +GitLabClient _client( + ({int status, Object? body}) Function(RequestOptions) handler, { + String baseUrl = 'https://gitlab.example.com', + bool bearer = false, + Map> headers = const {}, + Future Function()? onUnauthorized, +}) { + final dio = Dio()..httpClientAdapter = _Adapter(handler, headers); + return GitLabClient( + baseUrl: baseUrl, + token: 'glpat-xxxxxxxxxxxx', + bearer: bearer, + onUnauthorized: onUnauthorized, + dio: dio, + ); +} + +class _Adapter implements HttpClientAdapter { + _Adapter(this.handler, this.headers); + final ({int status, Object? body}) Function(RequestOptions) handler; + final Map> headers; + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + final r = handler(options); + return ResponseBody.fromString( + r.body == null ? '' : json.encode(r.body), + r.status, + headers: { + Headers.contentTypeHeader: [Headers.jsonContentType], + ...headers, + }, + ); + } + + @override + void close({bool force = false}) {} +}