diff --git a/web-ui/__tests__/components/review/PRStatusPanel.test.tsx b/web-ui/__tests__/components/review/PRStatusPanel.test.tsx index 6e2210b6..0428a9a5 100644 --- a/web-ui/__tests__/components/review/PRStatusPanel.test.tsx +++ b/web-ui/__tests__/components/review/PRStatusPanel.test.tsx @@ -34,6 +34,10 @@ const failingCIChecks = [ { name: 'tests', status: 'completed', conclusion: 'failure' }, ]; +const pendingCIChecks = [ + { name: 'tests', status: 'in_progress', conclusion: null }, +]; + const basePRStatus = { ci_checks: successfulCIChecks, review_status: 'approved', @@ -79,11 +83,20 @@ const proofStatusWithOpenReqs = { // ── Helpers ─────────────────────────────────────────────────────────────────── -const setupSWRMock = (prStatus: object, proofStatus: object) => { +const setupSWRMock = ( + prStatus: object | undefined, + proofStatus: object | undefined, + options?: { proofLoading?: boolean; proofError?: unknown } +) => { 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: options?.proofError, + isLoading: options?.proofLoading ?? false, + mutate: jest.fn(), + } as any; } return { data: prStatus, error: undefined, isLoading: false, mutate: jest.fn() } as any; }); @@ -185,4 +198,32 @@ describe('PRStatusPanel — PROOF9-gated merge button', () => { expect(screen.getByText(/ci checks failing/i)).toBeInTheDocument(); expect(screen.getByText('Fix critical bug')).toBeInTheDocument(); }); + + it('shows "Waiting for CI checks" when CI checks are pending', () => { + setupSWRMock({ ...basePRStatus, ci_checks: pendingCIChecks }, cleanProofStatus); + render(); + expect(screen.getByText(/waiting for ci checks/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^merge$/i })).toBeDisabled(); + }); + + it('shows PROOF9 loading skeleton instead of "All clear" while proof data loads', () => { + setupSWRMock(basePRStatus, undefined, { proofLoading: true }); + render(); + expect(screen.queryByText(/all clear/i)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^merge$/i })).toBeDisabled(); + }); + + it('shows PROOF9 error message when proof API fails', () => { + setupSWRMock(basePRStatus, undefined, { proofError: new Error('Network error') }); + render(); + expect(screen.getByText(/unable to load proof9 status/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^merge$/i })).toBeDisabled(); + }); + + it('shows success banner when PR was merged externally', () => { + setupSWRMock({ ...basePRStatus, merge_state: 'merged' }, cleanProofStatus); + render(); + expect(screen.getByText(/merged successfully/i)).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /merge/i })).not.toBeInTheDocument(); + }); }); diff --git a/web-ui/src/components/review/PRStatusPanel.tsx b/web-ui/src/components/review/PRStatusPanel.tsx index 6e43669d..158499b8 100644 --- a/web-ui/src/components/review/PRStatusPanel.tsx +++ b/web-ui/src/components/review/PRStatusPanel.tsx @@ -97,7 +97,7 @@ export function PRStatusPanel({ prNumber, workspacePath }: PRStatusPanelProps) { } ); - const { data: proofData } = useSWR( + const { data: proofData, error: proofError, isLoading: proofLoading } = useSWR( proofKey, () => proofApi.getStatus(workspacePath), { refreshInterval: merged ? 0 : 15_000 } @@ -121,6 +121,7 @@ export function PRStatusPanel({ prNumber, workspacePath }: PRStatusPanelProps) { ); const ciPassing = !ciFailing && !ciPending; + const alreadyMerged = merged || data?.merge_state === 'merged'; const canMerge = !!data && !!proofData && openRequirements.length === 0 && ciPassing; // ── Merge handler ───────────────────────────────────────────────────────── @@ -204,7 +205,13 @@ export function PRStatusPanel({ prNumber, workspacePath }: PRStatusPanelProps) { {/* PROOF9 gate section */}
PROOF9 - {openRequirements.length === 0 ? ( + {proofLoading ? ( +
+ ) : proofError && !proofData ? ( +

+ Unable to load PROOF9 status — merge blocked until resolved. +

+ ) : openRequirements.length === 0 ? (

All clear @@ -233,7 +240,7 @@ export function PRStatusPanel({ prNumber, workspacePath }: PRStatusPanelProps) { )} {/* Blocking messages */} - {data && (ciFailing || ciPending) && !merged && ( + {data && (ciFailing || ciPending) && !alreadyMerged && (

{ciFailing ? 'CI checks failing' : 'Waiting for CI checks'}

@@ -247,7 +254,7 @@ export function PRStatusPanel({ prNumber, workspacePath }: PRStatusPanelProps) { )} {/* Success banner or Merge button */} - {merged ? ( + {alreadyMerged ? (
PR #{prNumber} merged successfully