From 6a456ae489a32cfaec4a8e1be3f74e3d14d906a7 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:43:30 -0800 Subject: [PATCH] test(grants): a pending revoke error and confirm do not survive a 403 The existing forbidden-reload test revoked successfully, so both states were already clear before the 403 and it passed with either clear removed. This one fails the revoke first, loses access, gets it back, and checks neither the error nor the held confirm returns. Removing either clear fails it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../__tests__/V2AgentRuntimeGrants.test.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx index 77e8e55670..c506f63026 100644 --- a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx @@ -281,6 +281,46 @@ test('a revoke whose reload is forbidden leaves no grant rows or revoke controls expect(screen.queryByRole('region', { name: 'Connector grants' })).toBeNull(); }); +test('a pending revoke error and confirm do not survive losing access and getting it back', async () => { + // The revoke fails, so its error and the held confirm both stay up. Access + // is then lost (a 403 reload) and restored (a clean reload); the list comes + // back with neither. A new token recreates the api client, which reruns the + // load, the way a session change would. + let forbid = false; + axios.post.mockRejectedValue(new Error('denied')); + axios.get.mockImplementation((url) => { + if (url.includes('/agent-binding/placement')) return Promise.resolve({ data: { boundMachineId: null, requestedMachineId: null, boundMachineRevoked: false, activeMachines: [] } }); + if (url.includes('/grants') && forbid) return Promise.reject({ response: { status: 403, data: { error: 'Forbidden' } } }); + return Promise.resolve({ data: response }); + }); + const tree = (token) => ( + + +
+
+
+ ); + const { rerender } = render(tree('jwt-1')); + openGrants(); + await screen.findByText(/Work · Granted by Owner/); + fireEvent.click(screen.getByRole('button', { name: 'Revoke' })); + fireEvent.click(screen.getByRole('button', { name: 'Yes, revoke it' })); + expect(await within(grantsRegion()).findByRole('alert')).toHaveTextContent('Could not revoke the grant.'); + expect(screen.getByRole('button', { name: 'Yes, revoke it' })).toBeInTheDocument(); + + forbid = true; + rerender(tree('jwt-2')); + expect(await screen.findByText('Owner and admins only')).toBeInTheDocument(); + + forbid = false; + rerender(tree('jwt-3')); + const grantsSection = await screen.findByRole('region', { name: 'Connector grants' }); + expect(await within(grantsSection).findByText(/Work · Granted by Owner/)).toBeInTheDocument(); + expect(within(grantsSection).queryByRole('alert')).toBeNull(); + expect(within(grantsSection).queryByRole('button', { name: 'Yes, revoke it' })).toBeNull(); + expect(within(grantsSection).getByRole('button', { name: 'Revoke' })).toBeEnabled(); +}); + test('a failed grant load opens the list, and its toggle still closes it', async () => { axios.get.mockImplementation((url) => { if (url.includes('/agent-binding/placement')) return Promise.resolve({ data: { boundMachineId: null, requestedMachineId: null, boundMachineRevoked: false, activeMachines: [] } });