diff --git a/src/main/rate-limits/codex-fetcher-auth-errors.test.ts b/src/main/rate-limits/codex-fetcher-auth-errors.test.ts index 43b3bc44e5d..b172551e1dd 100644 --- a/src/main/rate-limits/codex-fetcher-auth-errors.test.ts +++ b/src/main/rate-limits/codex-fetcher-auth-errors.test.ts @@ -97,6 +97,51 @@ describe('fetchCodexRateLimits auth errors', () => { expect(ptySpawnMock).not.toHaveBeenCalled() }) + it('returns the app-server chatgpt-auth-required error without spawning the PTY probe', async () => { + const rpcChild = makeRpcChild() + const authError = 'chatgpt authentication required to read rate limits' + + childSpawnMock.mockReturnValue(rpcChild) + rpcChild.stdin.write.mockImplementation((line: string) => { + const msg = JSON.parse(line) as { id?: number; method?: string } + if (msg.method === 'initialize') { + setTimeout(() => { + rpcChild.stdout.emit( + 'data', + Buffer.from(`${JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: {} })}\n`) + ) + }, 0) + } + if (msg.method === 'account/rateLimits/read') { + setTimeout(() => { + rpcChild.stdout.emit( + 'data', + Buffer.from( + `${JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + error: { code: -32600, message: authError } + })}\n` + ) + ) + }, 0) + } + }) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(1) + await vi.advanceTimersByTimeAsync(1) + + await expect(resultPromise).resolves.toMatchObject({ + provider: 'codex', + session: null, + weekly: null, + status: 'error', + error: authError + }) + expect(ptySpawnMock).not.toHaveBeenCalled() + }) + it('preserves Codex PTY auth errors when the CLI exits before status is available', async () => { const ptyHandlers: { onData?: (data: string) => void; onExit?: () => void } = {} const authError = diff --git a/src/renderer/src/components/status-bar/tooltip.test.ts b/src/renderer/src/components/status-bar/tooltip.test.ts index d9387633fa0..6daf47a8c1c 100644 --- a/src/renderer/src/components/status-bar/tooltip.test.ts +++ b/src/renderer/src/components/status-bar/tooltip.test.ts @@ -185,6 +185,20 @@ describe('provider usage error copy', () => { ) }) + it('classifies the Codex chatgpt-auth-required rate-limits read error as auth, not Limited', () => { + // Why: the message mentions "rate limits" only as the object it failed to + // read; labeling it "Limited" would wrongly imply the user hit a limit. + const p = provider({ + provider: 'codex', + error: 'chatgpt authentication required to read rate limits' + }) + + expect(getProviderUsageStatusLabel(p)).toBe('Refresh failed') + expect(getProviderUsageErrorMessage(p)).toBe( + 'Codex usage could not be refreshed. Agent sessions may still be signed in.' + ) + }) + it('keeps generic OAuth and network failures as raw refresh details', () => { const oauth = provider({ error: 'OAuth API returned 500' }) const network = provider({ error: 'Network error while refreshing OAuth usage: ECONNRESET' }) diff --git a/src/renderer/src/components/status-bar/usage-error-copy.ts b/src/renderer/src/components/status-bar/usage-error-copy.ts index 83fc1888beb..0b0c3bc2eb6 100644 --- a/src/renderer/src/components/status-bar/usage-error-copy.ts +++ b/src/renderer/src/components/status-bar/usage-error-copy.ts @@ -30,7 +30,13 @@ export function getProviderDisplayName(provider: ProviderRateLimits['provider']) } function isUsageRateLimitError(message: string | null): boolean { - return Boolean(message && /\brate[- ]?limits?\b|\brate[- ]?limited\b/i.test(message)) + // Why: Codex app-server's "chatgpt authentication required to read rate + // limits" mentions rate limits only as the thing it could not read; treat + // authentication-required failures as auth, never as the user being limited. + if (!message || /\bauthentication required\b/i.test(message)) { + return false + } + return /\brate[- ]?limits?\b|\brate[- ]?limited\b/i.test(message) } const USAGE_AUTH_ERROR_PATTERNS = [ @@ -45,6 +51,7 @@ const USAGE_AUTH_ERROR_PATTERNS = [ /\bauth (?:is missing|tokens are missing|does not expose)\b/i, /\bunauthori[sz]ed\b/i, /\bunauthenticated\b/i, + /\bauthentication required\b/i, /\bplease reauthenticate\b/i, /\bsign in\b/i, /\blogged in to another account\b/i, diff --git a/src/shared/codex-auth-errors.ts b/src/shared/codex-auth-errors.ts index 04bf9c9f71e..9f25a4b444c 100644 --- a/src/shared/codex-auth-errors.ts +++ b/src/shared/codex-auth-errors.ts @@ -7,7 +7,11 @@ const CODEX_AUTH_ERROR_PATTERNS = [ /please reauthenticate/i, /not logged in/i, /token data is not available/i, - /auth (?:is missing|tokens are missing|does not expose)/i + /auth (?:is missing|tokens are missing|does not expose)/i, + // Why: app-server rejects account/rateLimits/read with this when auth.json + // holds only an API key; without classification the fetcher falls through to + // a hidden PTY probe that can only time out (15s) on every refresh. + /chatgpt authentication required/i ] const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g')