test(cliproxy): cover Claude quota probe 429s

This commit is contained in:
Tam Nhu Tran committed 2026-07-29 13:22:21 -04:00
1 parent bdcfc20c0f
commit 92fb66c47b
2 files changed
+90 -4

No files matched your search

@@ -622,7 +622,7 @@ describe('Claude Quota Fetcher', () => {
expect(result.coreUsage?.fiveHour?.remainingPercent).toBe(60);
});
it('does NOT inner-retry on 429; returns retryable single-attempt result honoring Retry-After', async () => {
it('classifies 429 as usage-probe unavailability without implying inference is limited', async () => {
// Safety intent: 429 must NOT trigger an immediate, delay-free inner retry.
// The outer 10-min cache + circuit breaker honor Retry-After and bound total
// volume, so a single attempt is made and the retryable signal is surfaced.
@@ -636,18 +636,66 @@ describe('Claude Quota Fetcher', () => {
global.fetch = mock(() => {
attempt += 1;
return Promise.resolve(
new Response('', { status: 429, headers: { 'Retry-After': '120' } })
new Response(JSON.stringify({ message: 'Rate limited. Please try again later.' }), {
status: 429,
headers: { 'Retry-After': '0', 'Content-Type': 'application/json' },
})
);
}) as typeof fetch;
const result = await fetchClaudeQuota('claude-429@example.com');
expect(result.success).toBe(false);
// Single attempt — no inner retry burned on the 429.
expect(attempt).toBe(1);
expect(result.error).toBe('Claude usage status temporarily unavailable');
expect(result.error).not.toContain('Rate limited');
expect(result.errorCode).toBe('usage_probe_unavailable');
expect(result.actionHint).toContain('Inference may still be available');
expect(result.httpStatus).toBe(429);
expect(result.retryable).toBe(true);
expect(result.errorDetail).toBe('retry-after:120');
expect(result.errorDetail).toBe('retry-after:0');
expect(result.needsReauth).toBe(false);
});
it('keeps 403 OAuth usage responses on the authorization-failure path', async () => {
createClaudeAccount('claude-usage-403@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
global.fetch = mock(() => Promise.resolve(new Response('', { status: 403 }))) as typeof fetch;
const result = await fetchClaudeQuota('claude-usage-403@example.com');
expect(result.success).toBe(false);
expect(result.error).toBe('Not authorized for Claude OAuth usage');
expect(result.errorCode).toBeUndefined();
expect(result.httpStatus).toBeUndefined();
expect(result.retryable).toBeUndefined();
});
it('keeps terminal 5xx responses on the generic retryable server-error path', async () => {
createClaudeAccount('claude-usage-503@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
let attempt = 0;
global.fetch = mock(() => {
attempt += 1;
return Promise.resolve(new Response('Service unavailable', { status: 503 }));
}) as typeof fetch;
const result = await fetchClaudeQuota('claude-usage-503@example.com');
expect(attempt).toBe(2);
expect(result.success).toBe(false);
expect(result.error).toBe('Service unavailable');
expect(result.errorCode).toBeUndefined();
expect(result.httpStatus).toBe(503);
expect(result.retryable).toBe(true);
});
it('clears the request timeout before retrying a retryable HTTP error', async () => {
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'bun:test';
import { displayClaudeQuotaSection } from '../../../src/commands/cliproxy/quota-subcommand/sections/claude';
async function loadQuotaCommandTestExports() {
const moduleId = Date.now() + Math.random();
@@ -9,6 +10,43 @@ async function loadQuotaCommandTestExports() {
}
describe('cliproxy quota subcommand failure formatting', () => {
it('renders Claude usage-probe 429 as a warning with an inference-safe hint', () => {
const output: string[] = [];
const originalLog = console.log;
console.log = (...args: unknown[]) => output.push(args.map(String).join(' '));
try {
displayClaudeQuotaSection([
{
account: 'healthy@example.com',
quota: {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: 1,
accountId: 'healthy@example.com',
error: 'Claude usage status temporarily unavailable',
errorCode: 'usage_probe_unavailable',
actionHint:
'Inference may still be available. Retry the Claude quota status check later.',
httpStatus: 429,
errorDetail: 'retry-after:0',
retryable: true,
},
},
]);
} finally {
console.log = originalLog;
}
expect(output.join('\n')).toContain('[!] healthy@example.com');
expect(output.join('\n')).not.toContain('[X] healthy@example.com');
expect(output.join('\n')).toContain('Claude usage status temporarily unavailable');
expect(output.join('\n')).toContain('Inference may still be available');
expect(output.join('\n')).toContain('HTTP 429 | Code: usage_probe_unavailable | Retryable');
expect(output.join('\n')).toContain('Detail: retry-after:0');
});
it('builds Gemini failure lines with the remediation hint, code, and detail', async () => {
const { getQuotaFailureDisplayEntries } = await loadQuotaCommandTestExports();