diff --git a/src/main/rate-limits/claude-fetcher.test.ts b/src/main/rate-limits/claude-fetcher.test.ts index 52ae0dcb3d4..b3c5dc54c20 100644 --- a/src/main/rate-limits/claude-fetcher.test.ts +++ b/src/main/rate-limits/claude-fetcher.test.ts @@ -634,6 +634,34 @@ describe('fetchClaudeRateLimits', () => { expect(fetchViaPty).toHaveBeenCalledWith({ authPreparation }) }) + it('marks CLI plan usage shell results as usage unavailable', async () => { + const authPreparation: ClaudeRuntimeAuthPreparation = { + configDir: '/Users/test/.claude', + envPatch: {}, + stripAuthEnv: false, + provenance: 'system' + } + vi.mocked(fetchViaPty).mockResolvedValueOnce({ + provider: 'claude', + session: null, + weekly: null, + updatedAt: 1, + error: 'Claude plan usage is unavailable for this Claude CLI session.', + status: 'error' + }) + + await expect(fetchClaudeRateLimits({ authPreparation })).resolves.toMatchObject({ + provider: 'claude', + status: 'error', + error: 'Claude plan usage is unavailable for this Claude CLI session.', + usageMetadata: { + source: 'cli', + attemptedSources: ['cli'], + failureKind: 'usage-unavailable' + } + }) + }) + it('surfaces Keychain read failures as structured usage metadata when CLI fallback is disabled', async () => { vi.mocked(readActiveClaudeKeychainCredentials).mockRejectedValueOnce( new Error('security timed out after 3000ms') diff --git a/src/main/rate-limits/claude-fetcher.ts b/src/main/rate-limits/claude-fetcher.ts index 0d4e7a2be2d..2c8db5194cf 100644 --- a/src/main/rate-limits/claude-fetcher.ts +++ b/src/main/rate-limits/claude-fetcher.ts @@ -443,6 +443,21 @@ function metadataForAttempt(input: { } } +function classifyClaudeCliUsageFailure( + limits: ProviderRateLimits +): UsageRateLimitFailureKind | undefined { + if (!limits.error) { + return undefined + } + if (/rate limited/i.test(limits.error)) { + return 'rate-limited' + } + if (/plan usage is unavailable|usage is unavailable/i.test(limits.error)) { + return 'usage-unavailable' + } + return 'cli-unavailable' +} + async function fetchClaudeUsageViaCli(input: { authPreparation?: ClaudeRuntimeAuthPreparation oauthCredentials: OAuthCredentialReadResult @@ -456,7 +471,8 @@ async function fetchClaudeUsageViaCli(input: { attemptedSources: input.attempts.attemptedSources, oauthCredentials: input.oauthCredentials, authPreparation: input.authPreparation, - source: 'cli' + source: 'cli', + failureKind: classifyClaudeCliUsageFailure(limits) }) ) } diff --git a/src/renderer/src/components/status-bar/tooltip.test.ts b/src/renderer/src/components/status-bar/tooltip.test.ts index c2bef85db6b..356ecd7bd49 100644 --- a/src/renderer/src/components/status-bar/tooltip.test.ts +++ b/src/renderer/src/components/status-bar/tooltip.test.ts @@ -212,6 +212,16 @@ describe('provider usage error copy', () => { expect(getProviderUsageStatusLabel(p)).toBe('Sign-in unavailable') expect(getProviderUsageErrorMessage(p)).toBe('Claude sign-in credentials could not be read.') }) + + it('uses structured unavailable copy for Claude CLI usage shell failures', () => { + const p = provider({ + error: 'Claude plan usage is unavailable for this Claude CLI session.', + usageMetadata: { failureKind: 'usage-unavailable' } + }) + + expect(getProviderUsageStatusLabel(p)).toBe('Usage unavailable') + expect(getProviderUsageErrorMessage(p)).toBe('Claude usage is unavailable right now.') + }) }) describe('getWindowSections', () => {