From f497a08453b5ef1195492b27bcae16f9c3000076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 19 May 2026 00:05:59 +0800 Subject: [PATCH] Clear stale ChatGPT OAuth fallback links Manual authorization links now behave as transient recovery state: a fresh login clears older URLs, a successful copy clears the exposed URL before polling, and logged-in status defensively drops any stale manual link. Constraint: OAuth authorization URLs should not survive beyond the active recovery action. Rejected: Keep the copy button after a successful copy | that leaves an expired authorization URL visible after later auth state changes. Confidence: high Scope-risk: narrow Tested: cd desktop && bun run test -- src/components/settings/ChatGPTOfficialLogin.test.tsx src/stores/hahaOpenAIOAuthStore.test.ts Tested: cd desktop && bun run test -- src/components/settings/ChatGPTOfficialLogin.test.tsx src/stores/hahaOpenAIOAuthStore.test.ts src/__tests__/generalSettings.test.tsx --testNamePattern "ChatGPT|OpenAI OAuth|Providers tab|ChatGPTOfficialLogin|hahaOpenAIOAuthStore" Tested: cd desktop && bun run lint Tested: git diff --check --- .../settings/ChatGPTOfficialLogin.test.tsx | 29 ++++++++++++++++++- .../settings/ChatGPTOfficialLogin.tsx | 8 +++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/desktop/src/components/settings/ChatGPTOfficialLogin.test.tsx b/desktop/src/components/settings/ChatGPTOfficialLogin.test.tsx index 48f7b93b..d568f080 100644 --- a/desktop/src/components/settings/ChatGPTOfficialLogin.test.tsx +++ b/desktop/src/components/settings/ChatGPTOfficialLogin.test.tsx @@ -90,7 +90,34 @@ describe('ChatGPTOfficialLogin', () => { }) expect(copyTextToClipboardMock).toHaveBeenCalledWith(authorizeUrl) + expect(useHahaOpenAIOAuthStore.getState().error).toBeNull() expect(useHahaOpenAIOAuthStore.getState().isPolling).toBe(true) - consoleErrorSpy.mockRestore() + expect(screen.queryByText(/Unable to open browser/)).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Copy authorization link' })).not.toBeInTheDocument() + }) + + it('keeps the authorization link available when copy fails', async () => { + const authorizeUrl = 'https://chatgpt.com/oauth/authorize?state=openai-state' + statusMock.mockResolvedValue({ loggedIn: false }) + startMock.mockResolvedValue({ authorizeUrl, state: 'openai-state' }) + shellOpenMock.mockRejectedValue(new Error('shell unavailable')) + copyTextToClipboardMock.mockResolvedValue(false) + vi.spyOn(console, 'error').mockImplementation(() => {}) + + render() + + await screen.findByRole('button', { name: 'Sign in with ChatGPT' }) + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Sign in with ChatGPT' })) + }) + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Copy authorization link' })) + }) + + expect(copyTextToClipboardMock).toHaveBeenCalledWith(authorizeUrl) + expect(useHahaOpenAIOAuthStore.getState().isPolling).toBe(false) + expect(screen.getByText(/Unable to copy authorization link/)).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Copy authorization link' })).toBeInTheDocument() }) }) diff --git a/desktop/src/components/settings/ChatGPTOfficialLogin.tsx b/desktop/src/components/settings/ChatGPTOfficialLogin.tsx index c973254f..087596b3 100644 --- a/desktop/src/components/settings/ChatGPTOfficialLogin.tsx +++ b/desktop/src/components/settings/ChatGPTOfficialLogin.tsx @@ -26,7 +26,14 @@ export function ChatGPTOfficialLogin() { return () => stopPolling() }, [fetchStatus, stopPolling]) + useEffect(() => { + if (status?.loggedIn) { + setManualAuthorizeUrl(null) + } + }, [status?.loggedIn]) + const handleLogin = async () => { + setManualAuthorizeUrl(null) try { const { authorizeUrl } = await login() setManualAuthorizeUrl(authorizeUrl) @@ -49,6 +56,7 @@ export function ChatGPTOfficialLogin() { if (!manualAuthorizeUrl) return const copied = await copyTextToClipboard(manualAuthorizeUrl) if (copied) { + setManualAuthorizeUrl(null) useHahaOpenAIOAuthStore.setState({ error: null }) startPolling() return