import { afterEach, describe, expect, it, vi } from "bun:test";
import type { UsageFetchContext } from "../src/usage";
import { claudeUsageProvider } from "../src/usage/claude";
const VALID_PAYLOAD = {
five_hour: { utilization: 42, resets_at: new Date(Date.now() + 5 * 60_000).toISOString() },
};
function jsonResponse(status: number, body: unknown, headers: Record<string, string> = {}): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json", ...headers },
});
}
function makeContext(fetchImpl: typeof fetch, retryWait?: UsageFetchContext["retryWait"]): UsageFetchContext {
return { fetch: fetchImpl, retryWait };
}
function baseParams() {
return {
provider: "anthropic" as const,
credential: {
type: "oauth" as const,
accessToken: "oat-test",
accountId: "org_test",
email: "user@example.com",
expiresAt: Date.now() + 60_000,
},
};
}
describe("claudeUsageProvider retry contract", () => {
afterEach(() => {
vi.restoreAllMocks();
});
const instantRetryWait: UsageFetchContext["retryWait"] = async () => {};
it("retries on 429 and succeeds on a later attempt", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
if (attempt < 3) return jsonResponse(429, { error: "rate_limited" });
return jsonResponse(200, VALID_PAYLOAD);
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock, instantRetryWait));
expect(report).not.toBeNull();
expect(attempt).toBe(3);
expect(report?.limits[0]?.amount.used).toBe(42);
});
it("retries on 503 then succeeds", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
if (attempt === 1) return jsonResponse(503, { error: "unavailable" });
return jsonResponse(200, VALID_PAYLOAD);
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock, instantRetryWait));
expect(report).not.toBeNull();
expect(attempt).toBe(2);
});
it("does NOT retry on 401 — permanent for this credential", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
return jsonResponse(401, { error: "unauthorized" });
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock));
expect(report).toBeNull();
expect(attempt).toBe(1);
});
it("does NOT retry on 404 — permanent for this credential", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
return jsonResponse(404, { error: "not_found" });
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock));
expect(report).toBeNull();
expect(attempt).toBe(1);
});
it("returns null after MAX_RETRIES of consecutive 429s", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
return jsonResponse(429, { error: "rate_limited" });
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock, instantRetryWait));
expect(report).toBeNull();
expect(attempt).toBe(3);
});
it("honours Retry-After when retrying a 429", async () => {
let attempt = 0;
const retryWait = vi.fn(async (_delayMs: number, _signal?: AbortSignal) => {});
const fetchMock = (async () => {
attempt += 1;
if (attempt === 1) {
return jsonResponse(429, { error: "rate_limited" }, { "retry-after": "1" });
}
return jsonResponse(200, VALID_PAYLOAD);
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock, retryWait));
expect(report).not.toBeNull();
expect(attempt).toBe(2);
expect(retryWait).toHaveBeenCalledTimes(1);
expect(retryWait.mock.calls[0]?.[0]).toBe(1000);
});
it("aborts the retry sleep when the signal fires mid-backoff", async () => {
let attempt = 0;
const fetchMock = (async (_url: string | URL, init?: RequestInit) => {
attempt += 1;
if (init?.signal?.aborted) throw new Error("AbortError");
if (attempt === 1) {
return jsonResponse(429, { error: "rate_limited" }, { "retry-after": "60" });
}
return jsonResponse(200, VALID_PAYLOAD);
}) as unknown as typeof fetch;
const controller = new AbortController();
const retryWait = vi.fn(async (delayMs: number, signal?: AbortSignal) => {
expect(delayMs).toBe(60_000);
if (signal?.aborted) throw new Error("AbortError");
const { promise, reject } = Promise.withResolvers<void>();
const onAbort = () => reject(new Error("AbortError"));
signal?.addEventListener("abort", onAbort, { once: true });
queueMicrotask(() => controller.abort());
try {
await promise;
} finally {
signal?.removeEventListener("abort", onAbort);
}
});
const report = await claudeUsageProvider.fetchUsage(
{ ...baseParams(), signal: controller.signal },
makeContext(fetchMock, retryWait),
);
expect(report).toBeNull();
expect(retryWait).toHaveBeenCalledTimes(1);
expect(attempt).toBe(1);
});
it("falls back to lastPayload when retries exhausted with stale-but-valid data", async () => {
let attempt = 0;
const fetchMock = (async () => {
attempt += 1;
if (attempt === 1) {
return jsonResponse(200, {});
}
return jsonResponse(429, { error: "rate_limited" });
}) as unknown as typeof fetch;
const report = await claudeUsageProvider.fetchUsage(baseParams(), makeContext(fetchMock, instantRetryWait));
expect(report).toBeNull();
expect(attempt).toBe(3);
});
});