mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-03 20:13:02 +00:00
test(cliproxy): cover Claude quota probe 429s
This commit is contained in:
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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user