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
This commit is contained in:
程序员阿江(Relakkes) 2026-05-19 00:05:59 +08:00
parent d2ff9d0c00
commit f497a08453
2 changed files with 36 additions and 1 deletions

View File

@ -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(<ChatGPTOfficialLogin />)
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()
})
})

View File

@ -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