Skip to content

feat(web-ui): PROOF9-gated merge button on PR Status panel#583

Merged
frankbria merged 3 commits intomainfrom
feature/issue-571-proof9-gated-merge-button
Apr 14, 2026
Merged

feat(web-ui): PROOF9-gated merge button on PR Status panel#583
frankbria merged 3 commits intomainfrom
feature/issue-571-proof9-gated-merge-button

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Apr 14, 2026

Summary

Implements #571: [Phase 4A] PR Status: PROOF9-gated merge button

  • Extend PRStatusPanel with a PROOF9-gated Merge button
  • Button disabled until both gates pass: all CI checks green + all PROOF9 requirements satisfied/waived
  • Polls proofApi.getStatus (15 s) alongside existing PR status polling (30 s)
  • Inline blocking messages: open PROOF9 requirement titles with links to /proof/{id}, CI status text
  • On merge success: confirmation banner, polling stops
  • On merge failure: error banner, button re-enabled
  • Tooltip on disabled button summarises what's blocking

Changes

File What
web-ui/src/types/index.ts Add MergeResponse, MergePRRequest types
web-ui/src/lib/api.ts Add prApi.merge()POST /api/v2/pr/{n}/merge
web-ui/src/components/review/PRStatusPanel.tsx PROOF9 SWR hook, gate logic, merge button, banners
web-ui/__tests__/components/review/PRStatusPanel.test.tsx 9 Jest tests covering all acceptance criteria

Acceptance Criteria

  • Merge button disabled when CI failing or open PROOF9 requirements exist
  • Blocking reason shown inline with PROOF9 link when applicable
  • Merge calls correct API endpoint and shows success state
  • npm test pass (756/756)

Test Plan

  • Unit tests written (TDD: RED → GREEN)
  • All 756 tests passing
  • npm run build succeeds
  • Lint clean on changed files
  • Internal code review passed

Implementation Notes

Closes #571

Summary by CodeRabbit

  • New Features

    • Merge button in PR review panel with smart enable/disable based on CI and PROOF9 status.
    • Shows blocking PROOF9 requirements as links with a “View all” link; renders CI and PROOF9 blocking messages when applicable.
    • Merge UX improvements: disabled/merging/in‑progress state, success banner, and clear error messages with retry behavior.
    • Tooltip explains why merge is disabled.
  • Tests

    • Added comprehensive tests covering CI and PROOF9 gating, merge flows, and messaging.

Add a merge button to PRStatusPanel that enforces two gates before
enabling: all PROOF9 requirements must be satisfied/waived and all CI
checks must pass. Polls proof status via proofApi.getStatus alongside
the existing PR status polling. Shows inline blocking messages with
links to open requirements when PROOF9 is blocking, and CI status text
when checks are pending or failing. On successful merge, displays a
confirmation banner and stops all polling.

- Add MergeResponse / MergePRRequest types
- Add prApi.merge() calling POST /api/v2/pr/{n}/merge
- Extend PRStatusPanel with proof SWR hook, gate logic, merge button
- 9 Jest tests covering all acceptance criteria
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

PRStatusPanel now enforces a PROOF9 + CI merge gate: it fetches PROOF9 status, computes merge eligibility, calls a new PR merge API, and surfaces merge/in-flight/success/failure states and inline blocking reasons in the UI.

Changes

Cohort / File(s) Summary
Component Implementation
web-ui/src/components/review/PRStatusPanel.tsx
Added local merge state (isMerging, merged, mergeError), PROOF9 status fetch, combined CI+PROOF9 canMerge logic, handleMerge calling prApi.merge, polling stop on local merge, blocking messages/UI, and tooltip-wrapped Merge button.
API & Types
web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Added prApi.merge(workspacePath, prNumber, request?) POST to /api/v2/pr/{prNumber}/merge and new types MergePRRequest and MergeResponse.
Tests
web-ui/__tests__/components/review/PRStatusPanel.test.tsx
New Jest + React Testing Library suite mocking prApi, proofApi, useSWR, and workspace path; verifies PROOF9/CI gating, blocking messages/links, merge in-flight/success/failure flows, and UI state transitions.

Sequence Diagram

sequenceDiagram
    participant User
    participant PRStatusPanel as PR Status Panel
    participant prApi
    participant proofApi
    participant Server

    User->>PRStatusPanel: Open PR page
    PRStatusPanel->>prApi: getStatus(prNumber)
    PRStatusPanel->>proofApi: getStatus(workspacePath)
    proofApi-->>PRStatusPanel: proof requirements
    prApi-->>PRStatusPanel: CI check data
    PRStatusPanel->>PRStatusPanel: Compute canMerge (CI passed && no open PROOF9)

    alt Gates Cleared
        User->>PRStatusPanel: Click Merge
        PRStatusPanel->>PRStatusPanel: Set isMerging = true (button disabled, label "merging")
        PRStatusPanel->>prApi: merge(workspacePath, prNumber, { method })
        prApi->>Server: POST /api/v2/pr/merge
        alt Merge Success
            Server-->>prApi: MergeResponse (merged: true)
            prApi-->>PRStatusPanel: success
            PRStatusPanel->>PRStatusPanel: Set merged = true, show success banner, stop polling
        else Merge Failure
            Server-->>prApi: Error (detail)
            prApi-->>PRStatusPanel: error
            PRStatusPanel->>PRStatusPanel: Set mergeError, show error banner, re-enable button
        end
    else Gates Blocked
        PRStatusPanel->>User: Disable Merge button, show tooltip and inline blocking messages (PROOF9 list, CI status)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hops and checks, I scout the trail,

PROOF9 cleared and CI without fail,
The merge button twinkles, ready to go,
I press—then watch the banners glow,
A rabbit's cheer for ship and flow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main feature: adding a PROOF9-gated merge button to the PR Status panel.
Linked Issues check ✅ Passed All acceptance criteria from #571 are met: merge button disabled when CI failing or PROOF9 requirements open, blocking reasons shown inline with PROOF9 links, merge calls correct API endpoint, and test suite passes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing #571: type definitions for merge operations, API method for merge endpoint, PRStatusPanel component updates with PROOF9 gating, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-571-proof9-gated-merge-button

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Review — feat(web-ui): PROOF9-gated merge button on PR Status panel

Overall this is a clean, well-scoped implementation that follows the existing patterns in the codebase. The gate logic is correct and the test coverage hits all nine acceptance criteria. A couple of issues worth addressing before merge:


Bug: error message never surfaces from axios rejections

web-ui/src/components/review/PRStatusPanel.tsxhandleMerge

} catch (err: unknown) {
  const apiErr = err as { detail?: string };
  setMergeError(apiErr?.detail ?? 'Merge failed. Please try again.');
}

When prApi.merge rejects via axios, the thrown object is an AxiosError. The error detail lives at err.response.data.detail, not err.detail. The current cast will always produce undefined, so users always see the generic fallback message regardless of what the server says.

The test sidesteps this by rejecting with a plain object { detail: '...' }, which is why it passes.

Fix:

} catch (err: unknown) {
  const axiosDetail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
  const plainDetail = (err as { detail?: string })?.detail;
  setMergeError(axiosDetail ?? plainDetail ?? 'Merge failed. Please try again.');
}

And update the test to use an axios-shaped rejection so it stays aligned with real behavior.


Bug: merge button enabled while PR/proof data is still loading

canMerge is computed from data?.ci_checks ?? [] and proofData?.requirements ?? []. When both SWR hooks haven't resolved yet, those default to empty arrays, so ciFailing = false, ciPending = false, openRequirements = [], and canMerge = true. The merge button renders outside the {data && ...} guard, so it appears enabled before we know the actual gate state.

The simplest fix is to gate canMerge on data availability:

const canMerge =
  !!data && !!proofData &&
  openRequirements.length === 0 && ciPassing;

Minor: TooltipContent can render empty

When !canMerge but none of openRequirements.length > 0, ciFailing, or ciPending are true (shouldn't happen in practice, but possible during the loading state fix above), the tooltip renders with no text. Adding a fallback string keeps it safe:

<TooltipContent>
  {openRequirements.length > 0 && 'Resolve all open PROOF9 requirements. '}
  {ciFailing && 'Fix failing CI checks. '}
  {ciPending && 'Wait for CI checks to complete.'}
  {/* fallback if loading */}
  {!openRequirements.length && !ciFailing && !ciPending && 'Loading status…'}
</TooltipContent>

Nit: transition-all on the button

className="w-full transition-all" is fine but transitions-all can interfere with layout shifts unexpectedly. Consider scoping it to transition-colors if it's just for the disabled/enabled colour change.


What's solid

  • Gate logic is straightforward and correct (openRequirements.length === 0 && ciPassing).
  • Polling stops cleanly on merge (merged ? 0 : 15_000 / refreshInterval callback).
  • Inline PROOF9 links directly to /proof/{req.id} — good UX.
  • The <span> wrapper around the disabled button so the tooltip fires is the right pattern.
  • Types (MergePRRequest, MergeResponse) align exactly with the backend Pydantic models in pr_v2.py.
  • Test structure is clean; fixtures and the setupSWRMock helper are reusable.

The two bugs above are the blockers; the rest can be addressed at discretion.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
web-ui/src/components/review/PRStatusPanel.tsx (1)

128-140: Consider adding optimistic UI update or mutating SWR cache after successful merge.

After a successful merge, the component sets merged = true locally but doesn't update the SWR cache. While the polling will eventually catch up, calling mutate on the PR status SWR key would immediately reflect the merged state if the user navigates away and back before the next poll.

♻️ Example: Mutate SWR cache on merge success
-  const { data, error } = useSWR<PRStatusResponse>(
+  const { data, error, mutate } = useSWR<PRStatusResponse>(
     swrKey,
     () => prApi.getStatus(workspacePath, prNumber),
     ...
   );

   const handleMerge = async () => {
     setIsMerging(true);
     setMergeError(null);
     try {
       await prApi.merge(workspacePath, prNumber, { method: 'squash' });
       setMerged(true);
+      // Optimistically update cache to reflect merged state
+      mutate((prev) => prev ? { ...prev, merge_state: 'merged' } : prev, false);
     } catch (err: unknown) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/review/PRStatusPanel.tsx` around lines 128 - 140, The
handleMerge function updates local state (setMerged) after prApi.merge but
doesn't update the SWR cache; after a successful merge (inside the try after
await prApi.merge(...)), call the SWR mutate for the PR status key used
elsewhere (the same key passed to useSWR) to update the cached PR state to
merged (or revalidate) so the UI and other components reflect the merge
immediately; ensure you import/use the correct mutate function or SWR hook and
keep existing setIsMerging/setMergeError behavior.
web-ui/__tests__/components/review/PRStatusPanel.test.tsx (2)

154-170: Consider awaiting the final state after resolving the merge promise.

After calling resolveMerge(...) at line 169, the test ends without awaiting the subsequent state update. This could lead to React state updates occurring after the test completes, potentially causing console warnings about unmounted component updates in CI.

♻️ Proposed fix to await final state
     resolveMerge({ sha: 'abc', merged: true, message: 'ok' });
+
+    // Wait for final state to settle
+    await waitFor(() => {
+      expect(screen.getByText(/merged successfully/i)).toBeInTheDocument();
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/__tests__/components/review/PRStatusPanel.test.tsx` around lines 154 -
170, The test resolves the in-flight merge promise via resolveMerge(...) but
doesn't wait for the component to update, risking state updates after test
completion; after calling resolveMerge({ sha: 'abc', merged: true, message: 'ok'
}) you should await the UI settling (use waitFor) to assert the post-merge state
(e.g., that the merge button text returns to "merge" or the button is enabled)
so the test waits for PRStatusPanel to finish updating after
mockMerge/mergePromise resolves.

82-90: Type safety improvement: consider typing the SWR mock return structure.

The mock returns as any which bypasses type checking. While functional for tests, this could mask issues if the SWR hook's return signature changes.

♻️ Proposed typed mock return
 const setupSWRMock = (prStatus: object, proofStatus: object) => {
   mockUseSWR.mockImplementation((key: unknown) => {
     const keyStr = typeof key === 'string' ? key : '';
     if (keyStr.includes('/api/v2/proof/status')) {
-      return { data: proofStatus, error: undefined, isLoading: false, mutate: jest.fn() } as any;
+      return { data: proofStatus, error: undefined, isLoading: false, mutate: jest.fn() } as ReturnType<typeof useSWR>;
     }
-    return { data: prStatus, error: undefined, isLoading: false, mutate: jest.fn() } as any;
+    return { data: prStatus, error: undefined, isLoading: false, mutate: jest.fn() } as ReturnType<typeof useSWR>;
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/__tests__/components/review/PRStatusPanel.test.tsx` around lines 82 -
90, The setupSWRMock currently returns objects cast with "as any", losing type
safety; update setupSWRMock and mockUseSWR to return a properly typed SWR
response (e.g., use SWRResponse<T, E> from 'swr' or ReturnType<typeof useSWR>)
instead of any, parameterize prStatus and proofStatus with appropriate generics,
and cast each returned object to that typed SWRResponse (for the proof status
branch and the default branch) so the mocked shape matches the real useSWR
return type; update imports to include SWRResponse or use ReturnType<typeof
useSWR> to ensure compilation if the hook signature changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web-ui/__tests__/components/review/PRStatusPanel.test.tsx`:
- Around line 154-170: The test resolves the in-flight merge promise via
resolveMerge(...) but doesn't wait for the component to update, risking state
updates after test completion; after calling resolveMerge({ sha: 'abc', merged:
true, message: 'ok' }) you should await the UI settling (use waitFor) to assert
the post-merge state (e.g., that the merge button text returns to "merge" or the
button is enabled) so the test waits for PRStatusPanel to finish updating after
mockMerge/mergePromise resolves.
- Around line 82-90: The setupSWRMock currently returns objects cast with "as
any", losing type safety; update setupSWRMock and mockUseSWR to return a
properly typed SWR response (e.g., use SWRResponse<T, E> from 'swr' or
ReturnType<typeof useSWR>) instead of any, parameterize prStatus and proofStatus
with appropriate generics, and cast each returned object to that typed
SWRResponse (for the proof status branch and the default branch) so the mocked
shape matches the real useSWR return type; update imports to include SWRResponse
or use ReturnType<typeof useSWR> to ensure compilation if the hook signature
changes.

In `@web-ui/src/components/review/PRStatusPanel.tsx`:
- Around line 128-140: The handleMerge function updates local state (setMerged)
after prApi.merge but doesn't update the SWR cache; after a successful merge
(inside the try after await prApi.merge(...)), call the SWR mutate for the PR
status key used elsewhere (the same key passed to useSWR) to update the cached
PR state to merged (or revalidate) so the UI and other components reflect the
merge immediately; ensure you import/use the correct mutate function or SWR hook
and keep existing setIsMerging/setMergeError behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4318ba46-ffc4-4454-9ebc-585afe8f92c1

📥 Commits

Reviewing files that changed from the base of the PR and between 27b15b6 and 0ebd53b.

📒 Files selected for processing (4)
  • web-ui/__tests__/components/review/PRStatusPanel.test.tsx
  • web-ui/src/components/review/PRStatusPanel.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Test User added 2 commits April 14, 2026 09:06
- Mutate SWR cache on merge success for optimistic UI update
- Await final state in in-flight test to prevent act() warning
Prevent merge button from appearing enabled during initial load by
requiring both PR status and proof status data to be present before
canMerge can be true.
@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Follow-up review

Thanks for the iteration. Checking against my previous two blocker items:


✅ Fixed — loading-state gate

canMerge = !!data && !!proofData && openRequirements.length === 0 && ciPassing

Both data guards are in place. The merge button stays disabled while either SWR hook hasn't resolved yet. Good.

Also noted: mutatePRStatus is now called on success to immediately update the SWR cache — that was the coderabbitai suggestion and it's the right call.


❌ Still open — axios error message never surfaces

The catch block is unchanged:

} catch (err: unknown) {
  const apiErr = err as { detail?: string };
  setMergeError(apiErr?.detail ?? 'Merge failed. Please try again.');
}

When axios rejects, the thrown object is an AxiosError. err.detail is undefined on an AxiosError; the real detail is at err.response.data.detail. The cast succeeds, but apiErr.detail will always be undefined, so users always see the generic fallback string regardless of what the server returned.

The test still masks this because it rejects with a plain { detail: '...' } object rather than an axios-shaped error:

mockMerge.mockRejectedValueOnce({ detail: 'Cannot merge: conflicts detected' });

Fix needed in the component:

} catch (err: unknown) {
  const axiosDetail = (err as { response?: { data?: { detail?: string } } })
    ?.response?.data?.detail;
  const plainDetail = (err as { detail?: string })?.detail;
  setMergeError(axiosDetail ?? plainDetail ?? 'Merge failed. Please try again.');
}

And in the test, use an axios-shaped rejection so it stays honest:

mockMerge.mockRejectedValueOnce({
  response: { data: { detail: 'Cannot merge: conflicts detected' } },
});

Minor — tooltip still renders empty during loading

!canMerge is now true during loading (correct), but the tooltip is also shown:

{!canMerge && (
  <TooltipContent>
    {openRequirements.length > 0 && '...'}
    {ciFailing && '...'}
    {ciPending && '...'}
  </TooltipContent>
)}

While data is loading: openRequirements = [], ciFailing = false, ciPending = false — so the tooltip renders with no visible text. Either gate the tooltip on data being available or add a fallback string (suggested in my previous review).


Everything else looks good — the gate logic, SWR polling stop, inline PROOF9 links, and banner UX are solid. The axios error handling is the one remaining blocker before merge.

@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Review: feat(web-ui): PROOF9-gated merge button on PR Status panel

This is a solid, well-scoped implementation of #571. The gate logic is clear, the test coverage is good, and the backend endpoint was already in place. A few items worth addressing before merge:


Bug: PROOF9 section shows "All clear" while proofData is still loading

In PRStatusPanel.tsx, openRequirements is derived as:

const openRequirements = (proofData?.requirements ?? []).filter(r => r.status === 'open');

When proofData is undefined (initial load), this evaluates to [], so the PROOF9 gate section renders the green "All clear" state. Meanwhile canMerge correctly guards on !!proofData, leaving the merge button disabled — but with no visible reason (the PROOF9 section is telling the user everything is fine). This is a misleading intermediate state.

Suggested fix — surface a loading state for the PROOF9 section:

const { data: proofData, isLoading: proofLoading } = useSWR<ProofStatusResponse>(...);

// In render:
{proofLoading ? (
  <div className="h-4 animate-pulse rounded bg-muted" />
) : openRequirements.length === 0 ? (
  <p>...All clear...</p>
) : (
  <div>...open reqs...</div>
)}

Issue: No error handling for proofData SWR fetch failure

const { data: proofData } = useSWR<ProofStatusResponse>(...);

error is never destructured. If the proof API returns a 4xx/5xx or the network is down, proofData stays undefined, canMerge is false, and the user sees a disabled merge button with an "All clear" PROOF9 section and no explanation of why they can't merge.

Recommend destructuring error: proofError and showing a brief error message in the PROOF9 section when proofError is set (similar to how the PR status section handles error).


Minor: Externally merged PR keeps the merge button visible

If the PR was merged via GitHub directly (not through this panel), data.merge_state will be 'merged' but merged state remains false, so the merge button still renders. The user would get an API error on click (which shows the error banner), so it doesn't break anything — but it would be cleaner to treat data?.merge_state === 'merged' the same as the local merged flag for the success banner check.


Missing test: ciPending path

The test suite covers CI failing and CI+PROOF9 both blocking, but there's no test for the "Waiting for CI checks" message that appears when ciPending is true (checks with status: 'in_progress' or 'queued'). Straightforward to add alongside the existing failingCIChecks fixture.


Nit: refreshInterval for PR status already handles merged via the state variable

The polling stop logic is correct (the merged local state causes refreshInterval to return 0, and the mutatePRStatus call is a nice optimistic update). No change needed — just confirming this reads as intended.


Summary: The gate logic, API integration, and test structure are all correct. The main ask is fixing the misleading "All clear" state during PROOF9 load (#1) and surfacing a proof error state (#2). The externally-merged edge case (#3) and the missing ciPending test (#4) are lower priority.

@frankbria frankbria merged commit ba8f708 into main Apr 14, 2026
10 of 11 checks passed
@frankbria frankbria deleted the feature/issue-571-proof9-gated-merge-button branch April 14, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 4A] PR Status: PROOF9-gated merge button

1 participant