Files
stanp-opencode/tests/claude-quota.test.ts
T
StanPonomarev 59da616bc5
CI / verify (pull_request) Successful in 8s
reduce testing
2026-10-08 09:34:13 +02:00

172 lines
5.6 KiB
TypeScript

import { afterEach, beforeEach, expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ClaudeQuotaTracker, normalizeClaudeQuota } from "../src/claude-quota.js";
const payload = {
five_hour: { utilization: 12.5, resets_at: "2026-10-07T12:00:00+00:00" },
seven_day: { utilization: 140, resets_at: null },
seven_day_opus: null,
seven_day_sonnet: { utilization: 3, resets_at: "2026-10-10T00:00:00+00:00" },
limits: [
{ kind: "session", percent: 12.5, resets_at: "2026-10-07T12:00:00+00:00", scope: null },
{
kind: "weekly_scoped",
percent: 40,
resets_at: "2026-10-10T00:00:00+00:00",
scope: { model: { id: "claude-fable-5-1", display_name: "Fable" } },
},
{
kind: "weekly_scoped",
percent: 3,
resets_at: "2026-10-10T00:00:00+00:00",
scope: { model: { id: "claude-sonnet-5-5", display_name: "Sonnet" } },
},
],
extra_usage: { is_enabled: false },
};
let dir: string;
let path: string;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "claude-quota-"));
path = join(dir, ".credentials.json");
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
function credentials(expiresAt: number) {
return JSON.stringify({
claudeAiOauth: {
accessToken: "token-123",
refreshToken: "refresh",
expiresAt,
scopes: ["user:inference"],
subscriptionType: "Max",
rateLimitTier: "default_claude_max_20x",
},
});
}
test("normalizes Claude usage windows", () => {
expect(normalizeClaudeQuota(payload, "Max", 1)).toEqual({
plan: "max",
fetchedAt: 1,
windows: [
{
id: "claude:five_hour",
label: null,
seconds: 18_000,
used: 12.5,
resetsAt: Date.parse("2026-10-07T12:00:00Z"),
},
{
id: "claude:seven_day",
label: null,
seconds: 604_800,
used: 100,
resetsAt: Number.NaN,
},
{
id: "claude:seven_day_sonnet",
label: "Sonnet",
seconds: 604_800,
used: 3,
resetsAt: Date.parse("2026-10-10T00:00:00Z"),
},
{
id: "claude:weekly_scoped:claude-fable-5-1",
label: "Fable",
seconds: 604_800,
used: 40,
resetsAt: Date.parse("2026-10-10T00:00:00Z"),
},
],
});
expect(normalizeClaudeQuota({ five_hour: null, seven_day: null }, "pro")).toBeUndefined();
});
test("reads credentials file and sends OAuth usage request", async () => {
await writeFile(path, credentials(Date.now() + 3_600_000));
const calls: Array<{ url: string; headers: Headers }> = [];
let changed = 0;
const tracker = new ClaudeQuotaTracker(
async () => {
changed++;
},
async (input, init) => {
calls.push({ url: String(input), headers: new Headers(init?.headers) });
return Response.json(payload);
},
60_000,
path,
);
await tracker.init();
expect(calls).toHaveLength(1);
expect(calls[0]!.url).toBe("https://api.anthropic.com/api/oauth/usage");
expect(calls[0]!.headers.get("authorization")).toBe("Bearer token-123");
expect(calls[0]!.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
expect(calls[0]!.headers.get("accept")).toBe("application/json");
expect(tracker.status()?.plan).toBe("max");
expect(tracker.status()?.windows[0]?.remaining).toBe(87.5);
expect(changed).toBe(1);
await rm(path);
await tracker.refresh();
expect(tracker.status()).toBeUndefined();
expect(changed).toBe(2);
await tracker.dispose();
});
test("failed fetches retry with re-read credentials and give up after five attempts", async () => {
{
await writeFile(path, credentials(Date.now() + 3_600_000));
const tokens: Array<string | null> = [];
let changed = 0;
const tracker = new ClaudeQuotaTracker(
async () => {
changed++;
},
async (_input, init) => {
tokens.push(new Headers(init?.headers).get("authorization"));
if (tokens.length > 1) return Response.json(payload);
await writeFile(
path,
credentials(Date.now() + 3_600_000).replace("token-123", "token-456"),
);
throw new Error("network down");
},
60_000,
path,
1,
);
await tracker.init();
expect(tokens).toEqual(["Bearer token-123", "Bearer token-456"]);
expect(tracker.status()?.plan).toBe("max");
expect(changed).toBe(1);
await tracker.dispose();
}
{
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
const tracker = new ClaudeQuotaTracker(
async () => {},
async () => {
calls++;
return new Response("nope", { status: 500 });
},
60_000,
path,
1,
);
await tracker.init();
expect(calls).toBe(5);
expect(tracker.status()).toBeUndefined();
await tracker.refresh();
expect(calls).toBe(10);
await tracker.dispose();
}
});