mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): snapshot pending before reload so expired-auth cancel actually fires (#1972)
reloadConnectorStatuses commits its prune setState (and runs the connectorAuthorizationPendingRef mirror effect) between the two awaits in onFocus, so cancelStaleAuthorizations was reading a post-prune ref and never identified anyone as stuck. Capture the snapshot inside onFocus before reloadConnectorStatuses runs, and pass it explicitly so the expired-auth daemon cancel actually fires. Refs #1909 #1354
This commit is contained in:
parent
d36ceacf3a
commit
0d2c87242f
2 changed files with 90 additions and 4 deletions
|
|
@ -582,13 +582,13 @@ export function ConnectorsBrowser({
|
|||
}, [connectorAuthorizationPending]);
|
||||
|
||||
const cancelStaleAuthorizations = useCallback(async (
|
||||
pendingBeforeReload: ConnectorAuthorizationPendingState,
|
||||
statuses: ConnectorStatusResponse['statuses'],
|
||||
nowMs = Date.now(),
|
||||
) => {
|
||||
const pending = connectorAuthorizationPendingRef.current;
|
||||
const stuck = Object.keys(pending).filter((connectorId) => {
|
||||
const stuck = Object.keys(pendingBeforeReload).filter((connectorId) => {
|
||||
if (statuses[connectorId]?.status === 'connected') return false;
|
||||
const expiresAt = pending[connectorId]?.expiresAt;
|
||||
const expiresAt = pendingBeforeReload[connectorId]?.expiresAt;
|
||||
if (!expiresAt) return false;
|
||||
const expiresAtMs = Date.parse(expiresAt);
|
||||
return Number.isFinite(expiresAtMs) && expiresAtMs <= nowMs;
|
||||
|
|
@ -705,8 +705,9 @@ export function ConnectorsBrowser({
|
|||
// card recovers to its default state instead of staying stuck loading.
|
||||
useEffect(() => {
|
||||
async function onFocus() {
|
||||
const pendingBeforeReload = connectorAuthorizationPendingRef.current;
|
||||
const statuses = await reloadConnectorStatuses();
|
||||
await cancelStaleAuthorizations(statuses);
|
||||
await cancelStaleAuthorizations(pendingBeforeReload, statuses);
|
||||
}
|
||||
window.addEventListener('focus', onFocus);
|
||||
return () => window.removeEventListener('focus', onFocus);
|
||||
|
|
|
|||
|
|
@ -552,6 +552,91 @@ describe('ConnectorsBrowser', () => {
|
|||
).toHaveProperty('github');
|
||||
});
|
||||
|
||||
it('auto-cancels stuck pending authorization on focus once the daemon authorization window has expired', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
const startMs = new Date('2026-05-17T10:00:00.000Z').getTime();
|
||||
vi.setSystemTime(startMs);
|
||||
|
||||
const availableConnector: ConnectorDetail = {
|
||||
...configuredComposioConnector,
|
||||
status: 'available',
|
||||
auth: { provider: 'composio', configured: true },
|
||||
};
|
||||
vi.mocked(fetchConnectors).mockResolvedValue([availableConnector]);
|
||||
vi.mocked(fetchConnectorDiscovery).mockResolvedValue([availableConnector]);
|
||||
vi.mocked(fetchConnectorStatuses).mockResolvedValue({
|
||||
github: { status: 'available' },
|
||||
});
|
||||
vi.mocked(connectConnector).mockResolvedValue({
|
||||
connector: availableConnector,
|
||||
auth: {
|
||||
kind: 'redirect_required',
|
||||
redirectUrl: 'https://example.com/oauth',
|
||||
expiresAt: new Date(startMs + 5 * 60 * 1000).toISOString(),
|
||||
},
|
||||
});
|
||||
vi.mocked(cancelConnectorAuthorization).mockResolvedValue(availableConnector);
|
||||
|
||||
render(<ConnectorsBrowser composioConfigured />);
|
||||
|
||||
await screen.findByText('GitHub');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
|
||||
await screen.findByRole('button', { name: 'Cancel' });
|
||||
|
||||
vi.setSystemTime(startMs + 10 * 60 * 1000);
|
||||
|
||||
fireEvent(window, new Event('focus'));
|
||||
|
||||
await waitFor(() => expect(cancelConnectorAuthorization).toHaveBeenCalledWith('github'));
|
||||
await screen.findByRole('button', { name: 'Connect' });
|
||||
expect(
|
||||
JSON.parse(window.sessionStorage.getItem('od-connectors-authorization-pending') ?? '{}'),
|
||||
).not.toHaveProperty('github');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('marks the auto-cancel as failed when the daemon /authorization/cancel returns null after the authorization expires', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] });
|
||||
const startMs = new Date('2026-05-17T10:00:00.000Z').getTime();
|
||||
vi.setSystemTime(startMs);
|
||||
|
||||
const availableConnector: ConnectorDetail = {
|
||||
...configuredComposioConnector,
|
||||
status: 'available',
|
||||
auth: { provider: 'composio', configured: true },
|
||||
};
|
||||
vi.mocked(fetchConnectors).mockResolvedValue([availableConnector]);
|
||||
vi.mocked(fetchConnectorDiscovery).mockResolvedValue([availableConnector]);
|
||||
vi.mocked(fetchConnectorStatuses).mockResolvedValue({
|
||||
github: { status: 'available' },
|
||||
});
|
||||
vi.mocked(connectConnector).mockResolvedValue({
|
||||
connector: availableConnector,
|
||||
auth: {
|
||||
kind: 'redirect_required',
|
||||
redirectUrl: 'https://example.com/oauth',
|
||||
expiresAt: new Date(startMs + 5 * 60 * 1000).toISOString(),
|
||||
},
|
||||
});
|
||||
vi.mocked(cancelConnectorAuthorization).mockResolvedValue(null);
|
||||
|
||||
render(<ConnectorsBrowser composioConfigured />);
|
||||
|
||||
await screen.findByText('GitHub');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Connect' }));
|
||||
await screen.findByRole('button', { name: 'Cancel' });
|
||||
|
||||
vi.setSystemTime(startMs + 10 * 60 * 1000);
|
||||
|
||||
fireEvent(window, new Event('focus'));
|
||||
|
||||
await waitFor(() => expect(cancelConnectorAuthorization).toHaveBeenCalledWith('github'));
|
||||
expect(await screen.findByText("Couldn't cancel authorization. Try again.")).toBeTruthy();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not auto-cancel pending authorization on focus when the daemon already reports the connector as connected', async () => {
|
||||
const availableConnector: ConnectorDetail = {
|
||||
...configuredComposioConnector,
|
||||
|
|
|
|||
Loading…
Reference in a new issue