148 lines
4.3 KiB
JavaScript
148 lines
4.3 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const credential = vi.fn();
|
|
|
|
vi.mock('./auth.js', () => ({
|
|
loadClaudeCredential: () => credential(),
|
|
}));
|
|
|
|
import { fetchQuota, isConfigured, resetClaudeQuotaCache } from './index.js';
|
|
|
|
const CREDENTIAL = {
|
|
accessToken: 'access-a',
|
|
refreshToken: 'refresh-a',
|
|
expiresAt: Date.now() + 3_600_000,
|
|
planLabel: 'max',
|
|
source: 'keychain',
|
|
};
|
|
|
|
const PAYLOAD = {
|
|
limits: [
|
|
{ kind: 'session', percent: 5, resets_at: '2026-08-14T19:10:00Z', scope: null },
|
|
{ kind: 'weekly_all', percent: 4, resets_at: '2026-08-20T15:00:00Z', scope: null },
|
|
],
|
|
};
|
|
|
|
const jsonResponse = (body) => ({
|
|
ok: true,
|
|
status: 200,
|
|
headers: new Headers(),
|
|
json: async () => body,
|
|
});
|
|
|
|
const errorResponse = (status, headers = {}) => ({
|
|
ok: false,
|
|
status,
|
|
headers: new Headers(headers),
|
|
json: async () => ({}),
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetClaudeQuotaCache();
|
|
credential.mockReturnValue(CREDENTIAL);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('Claude quota provider', () => {
|
|
it('reports not configured when no credential source has a token', async () => {
|
|
credential.mockReturnValue(null);
|
|
|
|
expect(isConfigured()).toBe(false);
|
|
const result = await fetchQuota();
|
|
expect(result.configured).toBe(false);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.usage).toBeNull();
|
|
});
|
|
|
|
it('returns windows and the plan label from the credential', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse(PAYLOAD)));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.planLabel).toBe('max');
|
|
expect(result.usage.windows['5h'].usedPercent).toBe(5);
|
|
});
|
|
|
|
it('coalesces concurrent refreshes into one Anthropic request', async () => {
|
|
let resolveResponse;
|
|
const fetchMock = vi.fn().mockReturnValue(new Promise((resolve) => {
|
|
resolveResponse = resolve;
|
|
}));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const first = fetchQuota();
|
|
const second = fetchQuota();
|
|
resolveResponse(jsonResponse(PAYLOAD));
|
|
|
|
const [firstResult, secondResult] = await Promise.all([first, second]);
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(firstResult.ok).toBe(true);
|
|
expect(secondResult.ok).toBe(true);
|
|
});
|
|
|
|
it('keeps serving the last good values while Anthropic rate limits', async () => {
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce(jsonResponse(PAYLOAD))
|
|
.mockResolvedValueOnce(errorResponse(429, { 'retry-after': '120' }));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await fetchQuota();
|
|
const rateLimited = await fetchQuota();
|
|
|
|
expect(rateLimited.ok).toBe(true);
|
|
expect(rateLimited.usage.windows['5h'].usedPercent).toBe(5);
|
|
});
|
|
|
|
it('does not call Anthropic again while the rate-limit cooldown is active', async () => {
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce(jsonResponse(PAYLOAD))
|
|
.mockResolvedValueOnce(errorResponse(429));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await fetchQuota();
|
|
await fetchQuota();
|
|
await fetchQuota();
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('never shows one account cached values after the credential changes', async () => {
|
|
const fetchMock = vi.fn()
|
|
.mockResolvedValueOnce(jsonResponse(PAYLOAD))
|
|
.mockResolvedValueOnce(errorResponse(429));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await fetchQuota();
|
|
credential.mockReturnValue({ ...CREDENTIAL, accessToken: 'access-b', refreshToken: 'refresh-b', planLabel: null });
|
|
const afterSwitch = await fetchQuota();
|
|
|
|
expect(afterSwitch.ok).toBe(false);
|
|
expect(afterSwitch.usage).toBeNull();
|
|
});
|
|
|
|
it('explains an expired session instead of reporting a bare 401', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(errorResponse(401)));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.configured).toBe(true);
|
|
expect(result.error).toContain('Claude Code');
|
|
});
|
|
|
|
it('surfaces a network failure instead of an empty successful result', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
|
|
|
|
const result = await fetchQuota();
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.usage).toBeNull();
|
|
expect(result.error).toBe('socket hang up');
|
|
});
|
|
});
|