mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 10:18:14 +00:00
fix(cliproxy): fetch Claude OAuth quota from usage endpoint
This commit is contained in:
1 parent
42d6427c60
commit
9e880825e7
3 files changed
+119
-110
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Claude Quota Response Normalization Helpers
|
* Claude Quota Response Normalization Helpers
|
||||||
*
|
*
|
||||||
* Parses Anthropic policy limits payload into normalized windows and core usage summary.
|
* Parses Anthropic policy-limits or OAuth-usage payloads into normalized windows and core usage summary.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ClaudeCoreUsageSummary, ClaudeQuotaWindow } from './quota-types';
|
import type { ClaudeCoreUsageSummary, ClaudeQuotaWindow } from './quota-types';
|
||||||
@@ -185,8 +185,19 @@ function normalizeRestriction(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse raw policy limits response into normalized windows.
|
* Parse raw policy limits response into normalized windows.
|
||||||
* Supports both array and object-map `restrictions` shapes.
|
* Supports both policy-limits `restrictions` payloads and OAuth usage payloads
|
||||||
|
* keyed by window name (`five_hour`, `seven_day`, `seven_day_sonnet`, ...).
|
||||||
*/
|
*/
|
||||||
|
const CLAUDE_USAGE_WINDOW_KEYS = new Set([
|
||||||
|
'five_hour',
|
||||||
|
'seven_day',
|
||||||
|
'seven_day_opus',
|
||||||
|
'seven_day_sonnet',
|
||||||
|
'seven_day_oauth_apps',
|
||||||
|
'seven_day_cowork',
|
||||||
|
'iguana_necktie',
|
||||||
|
]);
|
||||||
|
|
||||||
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
|
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
|
||||||
const rawRestrictions = payload['restrictions'];
|
const rawRestrictions = payload['restrictions'];
|
||||||
const windows: ClaudeQuotaWindow[] = [];
|
const windows: ClaudeQuotaWindow[] = [];
|
||||||
@@ -206,9 +217,19 @@ export function buildClaudeQuotaWindows(payload: Record<string, unknown>): Claud
|
|||||||
if (window) windows.push(window);
|
if (window) windows.push(window);
|
||||||
}
|
}
|
||||||
} else if (toObject(payload)) {
|
} else if (toObject(payload)) {
|
||||||
|
for (const [key, value] of Object.entries(payload)) {
|
||||||
|
if (!CLAUDE_USAGE_WINDOW_KEYS.has(key)) continue;
|
||||||
|
const raw = toObject(value);
|
||||||
|
if (!raw) continue;
|
||||||
|
const window = normalizeRestriction(raw, key);
|
||||||
|
if (window) windows.push(window);
|
||||||
|
}
|
||||||
|
|
||||||
// Some responses may contain a single restriction object directly.
|
// Some responses may contain a single restriction object directly.
|
||||||
const direct = normalizeRestriction(payload);
|
if (windows.length === 0) {
|
||||||
if (direct) windows.push(direct);
|
const direct = normalizeRestriction(payload);
|
||||||
|
if (direct) windows.push(direct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Quota Fetcher for Claude (Anthropic) Accounts
|
* Quota Fetcher for Claude (Anthropic) Accounts
|
||||||
*
|
*
|
||||||
* Fetches policy limits from Claude API and normalizes 5h + weekly windows.
|
* Fetches OAuth usage windows from Claude API and normalizes 5h + weekly windows.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
@@ -17,11 +17,10 @@ import {
|
|||||||
|
|
||||||
export { buildClaudeQuotaWindows, buildClaudeCoreUsageSummary };
|
export { buildClaudeQuotaWindows, buildClaudeCoreUsageSummary };
|
||||||
|
|
||||||
export const CLAUDE_POLICY_LIMITS_URL = 'https://api.anthropic.com/api/claude_code/policy_limits';
|
export const CLAUDE_OAUTH_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
||||||
const CLAUDE_QUOTA_TIMEOUT_MS = 10000;
|
const CLAUDE_QUOTA_TIMEOUT_MS = 10000;
|
||||||
const CLAUDE_QUOTA_MAX_ATTEMPTS = 2;
|
const CLAUDE_QUOTA_MAX_ATTEMPTS = 2;
|
||||||
const CLAUDE_USER_AGENT = 'ccs-cli/claude-quota';
|
const CLAUDE_OAUTH_BETA_HEADER = 'oauth-2025-04-20';
|
||||||
const CLAUDE_OAUTH_UNSUPPORTED_MESSAGE = 'oauth authentication is currently not supported';
|
|
||||||
|
|
||||||
interface ClaudeAuthData {
|
interface ClaudeAuthData {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -193,16 +192,6 @@ function buildEmptyResult(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPolicyUnavailableResult(accountId: string): ClaudeQuotaResult {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
windows: [],
|
|
||||||
coreUsage: { fiveHour: null, weekly: null },
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
accountId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch quota for a single Claude account.
|
* Fetch quota for a single Claude account.
|
||||||
*/
|
*/
|
||||||
@@ -230,46 +219,43 @@ export async function fetchClaudeQuota(
|
|||||||
const timeoutId = setTimeout(() => controller.abort(), CLAUDE_QUOTA_TIMEOUT_MS);
|
const timeoutId = setTimeout(() => controller.abort(), CLAUDE_QUOTA_TIMEOUT_MS);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(CLAUDE_POLICY_LIMITS_URL, {
|
const response = await fetch(CLAUDE_OAUTH_USAGE_URL, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${authData.accessToken}`,
|
Authorization: `Bearer ${authData.accessToken}`,
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'User-Agent': CLAUDE_USER_AGENT,
|
'Content-Type': 'application/json',
|
||||||
|
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
console.error(`[i] Claude policy limits status: ${response.status} (attempt ${attempt})`);
|
console.error(`[i] Claude OAuth usage status: ${response.status} (attempt ${attempt})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
const errorMessage = await readResponseErrorMessage(response);
|
const errorMessage = await readResponseErrorMessage(response);
|
||||||
if (errorMessage && errorMessage.toLowerCase().includes(CLAUDE_OAUTH_UNSUPPORTED_MESSAGE)) {
|
return buildEmptyResult(
|
||||||
if (verbose) {
|
errorMessage || 'Authentication required for Claude OAuth usage',
|
||||||
console.error(
|
accountId,
|
||||||
'[i] Claude policy limits endpoint does not support OAuth tokens; treating quota as unavailable'
|
true
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return buildPolicyUnavailableResult(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildEmptyResult('Authentication required for policy limits', accountId, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
// Some accounts may not expose policy limits; treat as unavailable but successful.
|
return buildEmptyResult('Claude OAuth usage endpoint not found', accountId);
|
||||||
return buildPolicyUnavailableResult(accountId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
return buildEmptyResult('Not authorized for policy limits', accountId);
|
return buildEmptyResult('Not authorized for Claude OAuth usage', accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
lastError = `Policy limits API error: ${response.status}`;
|
lastError =
|
||||||
|
(await readResponseErrorMessage(response)) ||
|
||||||
|
`Claude OAuth usage API error: ${response.status}`;
|
||||||
if (
|
if (
|
||||||
attempt < CLAUDE_QUOTA_MAX_ATTEMPTS &&
|
attempt < CLAUDE_QUOTA_MAX_ATTEMPTS &&
|
||||||
(response.status === 429 || response.status >= 500)
|
(response.status === 429 || response.status >= 500)
|
||||||
@@ -283,11 +269,11 @@ export async function fetchClaudeQuota(
|
|||||||
try {
|
try {
|
||||||
payload = await response.json();
|
payload = await response.json();
|
||||||
} catch {
|
} catch {
|
||||||
return buildEmptyResult('Invalid policy limits format', accountId);
|
return buildEmptyResult('Invalid Claude OAuth usage format', accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toObject(payload)) {
|
if (!toObject(payload)) {
|
||||||
return buildEmptyResult('Invalid policy limits format', accountId);
|
return buildEmptyResult('Invalid Claude OAuth usage format', accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const windows = buildClaudeQuotaWindows(payload as Record<string, unknown>);
|
const windows = buildClaudeQuotaWindows(payload as Record<string, unknown>);
|
||||||
@@ -304,7 +290,7 @@ export async function fetchClaudeQuota(
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
lastError =
|
lastError =
|
||||||
error instanceof Error && error.name === 'AbortError'
|
error instanceof Error && error.name === 'AbortError'
|
||||||
? 'Policy limits request timeout'
|
? 'Claude OAuth usage request timeout'
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unknown error';
|
: 'Unknown error';
|
||||||
@@ -313,7 +299,7 @@ export async function fetchClaudeQuota(
|
|||||||
const errorDetails =
|
const errorDetails =
|
||||||
error instanceof Error ? (error.stack ?? error.message) : JSON.stringify(error);
|
error instanceof Error ? (error.stack ?? error.message) : JSON.stringify(error);
|
||||||
console.error(
|
console.error(
|
||||||
`[!] Claude policy limits failed (attempt ${attempt}): ${lastError}${errorDetails ? `\n${errorDetails}` : ''}`
|
`[!] Claude OAuth usage failed (attempt ${attempt}): ${lastError}${errorDetails ? `\n${errorDetails}` : ''}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Claude Quota Fetcher Unit Tests
|
* Claude Quota Fetcher Unit Tests
|
||||||
*
|
*
|
||||||
* Covers policy limits parsing and auth/token edge cases.
|
* Covers Claude quota parsing and auth/token edge cases.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
@@ -157,6 +157,31 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
expect(windows[0].rateLimitType).toBe('five_hour');
|
expect(windows[0].rateLimitType).toBe('five_hour');
|
||||||
expect(windows[0].remainingPercent).toBe(40);
|
expect(windows[0].remainingPercent).toBe(40);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses OAuth usage payload keyed by window name', () => {
|
||||||
|
const windows = buildClaudeQuotaWindows({
|
||||||
|
five_hour: {
|
||||||
|
utilization: 39,
|
||||||
|
resets_at: '2026-02-28T10:00:00Z',
|
||||||
|
},
|
||||||
|
seven_day_sonnet: {
|
||||||
|
utilization: 9,
|
||||||
|
resets_at: '2026-03-06T10:00:00Z',
|
||||||
|
},
|
||||||
|
extra_usage: {
|
||||||
|
is_enabled: true,
|
||||||
|
monthly_limit: 5000,
|
||||||
|
used_credits: 1200,
|
||||||
|
utilization: 0.24,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(2);
|
||||||
|
expect(windows[0].rateLimitType).toBe('five_hour');
|
||||||
|
expect(windows[0].remainingPercent).toBe(61);
|
||||||
|
expect(windows[1].rateLimitType).toBe('seven_day_sonnet');
|
||||||
|
expect(windows[1].remainingPercent).toBe(91);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildClaudeCoreUsageSummary', () => {
|
describe('buildClaudeCoreUsageSummary', () => {
|
||||||
@@ -285,7 +310,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchClaudeQuota', () => {
|
describe('fetchClaudeQuota', () => {
|
||||||
it('fetches and normalizes policy limits response', async () => {
|
it('fetches and normalizes Claude OAuth usage response', async () => {
|
||||||
createClaudeAccount('claude-main@example.com', {
|
createClaudeAccount('claude-main@example.com', {
|
||||||
access_token: 'claude-token',
|
access_token: 'claude-token',
|
||||||
expired: '2099-01-01T00:00:00.000Z',
|
expired: '2099-01-01T00:00:00.000Z',
|
||||||
@@ -293,30 +318,26 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
global.fetch = mock((url: string, options?: RequestInit) => {
|
global.fetch = mock((url: string, options?: RequestInit) => {
|
||||||
expect(url).toBe('https://api.anthropic.com/api/claude_code/policy_limits');
|
expect(url).toBe('https://api.anthropic.com/api/oauth/usage');
|
||||||
expect(options?.method).toBe('GET');
|
expect(options?.method).toBe('GET');
|
||||||
expect(options?.headers).toMatchObject({
|
expect(options?.headers).toMatchObject({
|
||||||
Authorization: 'Bearer claude-token',
|
Authorization: 'Bearer claude-token',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'anthropic-beta': 'oauth-2025-04-20',
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
restrictions: [
|
five_hour: {
|
||||||
{
|
utilization: 39,
|
||||||
rateLimitType: 'five_hour',
|
resets_at: '2026-03-01T01:00:00Z',
|
||||||
utilization: 0.5,
|
},
|
||||||
resetsAt: '2026-03-01T01:00:00Z',
|
seven_day: {
|
||||||
status: 'allowed',
|
utilization: 75,
|
||||||
},
|
resets_at: '2026-03-07T01:00:00Z',
|
||||||
{
|
},
|
||||||
rateLimitType: 'seven_day',
|
|
||||||
utilization: 0.75,
|
|
||||||
resetsAt: '2026-03-07T01:00:00Z',
|
|
||||||
status: 'allowed_warning',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -331,7 +352,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.accountId).toBe('claude-main@example.com');
|
expect(result.accountId).toBe('claude-main@example.com');
|
||||||
expect(result.windows).toHaveLength(2);
|
expect(result.windows).toHaveLength(2);
|
||||||
expect(result.coreUsage?.fiveHour?.remainingPercent).toBe(50);
|
expect(result.coreUsage?.fiveHour?.remainingPercent).toBe(61);
|
||||||
expect(result.coreUsage?.weekly?.remainingPercent).toBe(25);
|
expect(result.coreUsage?.weekly?.remainingPercent).toBe(25);
|
||||||
|
|
||||||
const all = await fetchAllClaudeQuotas();
|
const all = await fetchAllClaudeQuotas();
|
||||||
@@ -340,7 +361,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
expect(all[0].quota.success).toBe(true);
|
expect(all[0].quota.success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns needsReauth on 401 responses', async () => {
|
it('returns needsReauth on empty 401 OAuth usage responses', async () => {
|
||||||
createClaudeAccount(
|
createClaudeAccount(
|
||||||
'claude-auth@example.com',
|
'claude-auth@example.com',
|
||||||
{
|
{
|
||||||
@@ -360,9 +381,9 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
expect(result.error).toContain('Authentication');
|
expect(result.error).toContain('Authentication');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats OAuth-unsupported 401 as policy-limits unavailable', async () => {
|
it('surfaces nested OAuth usage 401 messages', async () => {
|
||||||
createClaudeAccount(
|
createClaudeAccount(
|
||||||
'claude-oauth-unsupported@example.com',
|
'claude-oauth-nested-message@example.com',
|
||||||
{
|
{
|
||||||
access_token: 'oauth-token',
|
access_token: 'oauth-token',
|
||||||
expired: '2099-01-01T00:00:00.000Z',
|
expired: '2099-01-01T00:00:00.000Z',
|
||||||
@@ -378,7 +399,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
error: {
|
error: {
|
||||||
type: 'authentication_error',
|
type: 'authentication_error',
|
||||||
message: 'OAuth authentication is currently not supported.',
|
message: 'OAuth session expired.',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||||
@@ -386,16 +407,14 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
)
|
)
|
||||||
) as typeof fetch;
|
) as typeof fetch;
|
||||||
|
|
||||||
const result = await fetchClaudeQuota('claude-oauth-unsupported@example.com');
|
const result = await fetchClaudeQuota('claude-oauth-nested-message@example.com');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(false);
|
||||||
expect(result.needsReauth).toBeUndefined();
|
expect(result.needsReauth).toBe(true);
|
||||||
expect(result.windows).toHaveLength(0);
|
expect(result.error).toContain('OAuth session expired.');
|
||||||
expect(result.coreUsage?.fiveHour).toBeNull();
|
|
||||||
expect(result.coreUsage?.weekly).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats root-level OAuth-unsupported 401 message as policy-limits unavailable', async () => {
|
it('surfaces root-level OAuth usage 401 messages', async () => {
|
||||||
createClaudeAccount('claude-oauth-root-message@example.com', {
|
createClaudeAccount('claude-oauth-root-message@example.com', {
|
||||||
access_token: 'oauth-token',
|
access_token: 'oauth-token',
|
||||||
expired: '2099-01-01T00:00:00.000Z',
|
expired: '2099-01-01T00:00:00.000Z',
|
||||||
@@ -406,7 +425,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
message: 'OAuth authentication is currently not supported.',
|
message: 'OAuth session expired.',
|
||||||
}),
|
}),
|
||||||
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||||
)
|
)
|
||||||
@@ -415,14 +434,12 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
|
|
||||||
const result = await fetchClaudeQuota('claude-oauth-root-message@example.com');
|
const result = await fetchClaudeQuota('claude-oauth-root-message@example.com');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(false);
|
||||||
expect(result.needsReauth).toBeUndefined();
|
expect(result.needsReauth).toBe(true);
|
||||||
expect(result.windows).toHaveLength(0);
|
expect(result.error).toContain('OAuth session expired.');
|
||||||
expect(result.coreUsage?.fiveHour).toBeNull();
|
|
||||||
expect(result.coreUsage?.weekly).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats plain-text OAuth-unsupported 401 as policy-limits unavailable', async () => {
|
it('surfaces plain-text OAuth usage 401 messages', async () => {
|
||||||
createClaudeAccount('claude-oauth-plaintext@example.com', {
|
createClaudeAccount('claude-oauth-plaintext@example.com', {
|
||||||
access_token: 'oauth-token',
|
access_token: 'oauth-token',
|
||||||
expired: '2099-01-01T00:00:00.000Z',
|
expired: '2099-01-01T00:00:00.000Z',
|
||||||
@@ -430,18 +447,14 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
global.fetch = mock(() =>
|
global.fetch = mock(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(new Response('OAuth session expired.', { status: 401 }))
|
||||||
new Response('OAuth authentication is currently not supported.', { status: 401 })
|
|
||||||
)
|
|
||||||
) as typeof fetch;
|
) as typeof fetch;
|
||||||
|
|
||||||
const result = await fetchClaudeQuota('claude-oauth-plaintext@example.com');
|
const result = await fetchClaudeQuota('claude-oauth-plaintext@example.com');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(false);
|
||||||
expect(result.needsReauth).toBeUndefined();
|
expect(result.needsReauth).toBe(true);
|
||||||
expect(result.windows).toHaveLength(0);
|
expect(result.error).toContain('OAuth session expired.');
|
||||||
expect(result.coreUsage?.fiveHour).toBeNull();
|
|
||||||
expect(result.coreUsage?.weekly).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps non-matching 401 payloads in the reauth path', async () => {
|
it('keeps non-matching 401 payloads in the reauth path', async () => {
|
||||||
@@ -469,11 +482,11 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.needsReauth).toBe(true);
|
expect(result.needsReauth).toBe(true);
|
||||||
expect(result.error).toContain('Authentication');
|
expect(result.error).toContain('Token revoked.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats 404 policy limits responses as unavailable but successful', async () => {
|
it('treats 404 OAuth usage responses as failures', async () => {
|
||||||
createClaudeAccount('claude-policy-limits-404@example.com', {
|
createClaudeAccount('claude-usage-404@example.com', {
|
||||||
access_token: 'oauth-token',
|
access_token: 'oauth-token',
|
||||||
expired: '2099-01-01T00:00:00.000Z',
|
expired: '2099-01-01T00:00:00.000Z',
|
||||||
type: 'claude',
|
type: 'claude',
|
||||||
@@ -481,13 +494,10 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
|
|
||||||
global.fetch = mock(() => Promise.resolve(new Response('', { status: 404 }))) as typeof fetch;
|
global.fetch = mock(() => Promise.resolve(new Response('', { status: 404 }))) as typeof fetch;
|
||||||
|
|
||||||
const result = await fetchClaudeQuota('claude-policy-limits-404@example.com');
|
const result = await fetchClaudeQuota('claude-usage-404@example.com');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(false);
|
||||||
expect(result.needsReauth).toBeUndefined();
|
expect(result.error).toContain('not found');
|
||||||
expect(result.windows).toHaveLength(0);
|
|
||||||
expect(result.coreUsage?.fiveHour).toBeNull();
|
|
||||||
expect(result.coreUsage?.weekly).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails fast when auth file has no token', async () => {
|
it('fails fast when auth file has no token', async () => {
|
||||||
@@ -515,7 +525,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
|
|
||||||
global.fetch = mock(() =>
|
global.fetch = mock(() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
new Response(JSON.stringify({ restrictions: [] }), {
|
new Response(JSON.stringify({}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
@@ -545,14 +555,10 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
restrictions: [
|
five_hour: {
|
||||||
{
|
utilization: 40,
|
||||||
rateLimitType: 'five_hour',
|
resets_at: '2026-03-01T01:00:00Z',
|
||||||
utilization: 0.4,
|
},
|
||||||
resetsAt: '2026-03-01T01:00:00Z',
|
|
||||||
status: 'allowed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
)
|
)
|
||||||
@@ -584,14 +590,10 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Response(
|
new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
restrictions: [
|
seven_day: {
|
||||||
{
|
utilization: 30,
|
||||||
rateLimitType: 'seven_day',
|
resets_at: '2026-03-07T01:00:00Z',
|
||||||
utilization: 0.3,
|
},
|
||||||
resetsAt: '2026-03-07T01:00:00Z',
|
|
||||||
status: 'allowed',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||||
)
|
)
|
||||||
@@ -654,7 +656,7 @@ describe('Claude Quota Fetcher', () => {
|
|||||||
Authorization: 'Bearer valid-anthropic-token',
|
Authorization: 'Bearer valid-anthropic-token',
|
||||||
});
|
});
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
new Response(JSON.stringify({ restrictions: [] }), {
|
new Response(JSON.stringify({}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user