import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";
import {
anthropicUsageFeed,
codexUsageFeed,
collectUsage,
createUsageState,
type FetchLike,
httpRequest,
registerUsageFeed,
runUsageFeed,
type UsageCredential,
unregisterUsageFeed,
usageProviders,
} from "../src/index.js";
const fixtures = resolve(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"native",
"usage",
"tests",
"fixtures",
);
const fixture = (name: string): unknown => JSON.parse(readFileSync(join(fixtures, name), "utf8"));
const codexCredential: UsageCredential = { kind: "oauth", access: "access-token", email: "codex@example.com" };
const anthropicCredential: UsageCredential = { kind: "oauth", access: "access-token" };
/** A host that answers with queued bodies instead of the network. */
function scriptedFetch(responses: { status: number; body: string }[]): FetchLike {
const queue = [...responses];
return async () => {
const next = queue.shift();
if (!next) throw new Error("scripted fetch ran out of responses");
return new Response(next.body, { status: next.status });
};
}
describe("usage feeds", () => {
it("registers the shipped providers under one lookup", () => {
const providers = usageProviders().map((feed) => feed.provider);
expect(providers).toContain("openai-codex");
expect(providers).toContain("anthropic");
});
it("asks for one request, then reports from the response", async () => {
const state = createUsageState(codexCredential, 1_789_514_000_000);
const first = codexUsageFeed.advance(state, []);
expect("requests" in first && first.requests).toHaveLength(1);
if (!("requests" in first)) throw new Error("expected requests");
const request = httpRequest(first.requests[0]!);
expect(request.url).toBe("https://chatgpt.com/backend-api/wham/usage");
expect(request.headers?.Authorization).toBe("Bearer access-token");
const step = codexUsageFeed.advance({ ...state, responses: [] }, [
{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) },
]);
if (!("report" in step)) throw new Error("expected a report");
expect(step.report.planType).toBe("prolite");
expect(step.report.limits).toHaveLength(3);
expect(step.report.limits[0]).toMatchObject({ label: "Primary", usedPercent: 100, status: "exhausted" });
});
it("sends the account header only when the account is known", () => {
const withoutAccount = codexUsageFeed.advance(createUsageState(codexCredential, 0), []);
expect("requests" in withoutAccount && httpRequest(withoutAccount.requests[0]!).headers).not.toHaveProperty(
"ChatGPT-Account-Id",
);
const withAccount = codexUsageFeed.advance(createUsageState({ ...codexCredential, accountId: "acct_1" }, 0), []);
expect("requests" in withAccount && httpRequest(withAccount.requests[0]!).headers?.["ChatGPT-Account-Id"]).toBe(
"acct_1",
);
});
it("runs the host loop against a scripted host", async () => {
const report = await runUsageFeed(codexUsageFeed, codexCredential, {
nowMs: 1_789_514_000_000,
fetchImpl: scriptedFetch([{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) }]),
});
expect(report.provider).toBe("openai-codex");
expect(report.providerName).toBe("ChatGPT Codex");
expect(report.error).toBeNull();
});
it("reports a failed fetch as data, not as an exception", async () => {
const report = await runUsageFeed(codexUsageFeed, codexCredential, {
fetchImpl: scriptedFetch([{ status: 500, body: "nope" }]),
});
expect(report.error).toBe("HTTP 500");
expect(report.limits).toEqual([]);
});
it("stops after the round limit when a feed never finishes", async () => {
registerUsageFeed({
provider: "looper",
providerName: "Looper",
credentialKinds: ["apiKey"],
advance: () => ({ requests: [{ kind: "http", method: "GET", url: "https://example.invalid/usage" }] }),
});
try {
const report = await runUsageFeed(
{
provider: "looper",
providerName: "Looper",
credentialKinds: ["apiKey"],
advance: () => ({ requests: [{ kind: "http", method: "GET", url: "https://example.invalid/usage" }] }),
},
{ kind: "apiKey", apiKey: "k" },
{ fetchImpl: scriptedFetch(Array.from({ length: 4 }, () => ({ status: 200, body: "{}" }))) },
);
expect(report.error).toMatch(/did not finish/);
} finally {
unregisterUsageFeed("looper");
}
});
});
describe("anthropic feed", () => {
const responses = [
{ status: 200, body: JSON.stringify(fixture("anthropic-usage.json")) },
{ status: 200, body: JSON.stringify(fixture("anthropic-profile.json")) },
];
it("uses the normalised limits array instead of the named windows", () => {
const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), []);
if (!("requests" in step)) throw new Error("expected requests");
expect(step.requests.map((request) => httpRequest(request).url)).toEqual([
"https://api.anthropic.com/api/oauth/usage",
"https://api.anthropic.com/api/oauth/profile",
]);
expect(httpRequest(step.requests[0]!).headers?.["anthropic-beta"]).toBe("oauth-2025-04-20");
});
it("maps percent, ISO reset times and severity onto the contract", () => {
const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), responses);
if (!("report" in step)) throw new Error("expected a report");
expect(step.report.limits.map((limit) => limit.label)).toEqual(["Session", "Weekly"]);
const weekly = step.report.limits[1]!;
expect(weekly).toMatchObject({ usedPercent: 94, windowLabel: "7d window", status: "warning" });
expect(weekly.resetsAt).toBe(Date.parse("2026-09-16T08:00:00.037593+00:00"));
expect(step.report.planType).toBe("team_standard");
expect(step.report.email).toBe("user@example.com");
});
it("keeps a window with no percentage as unknown rather than zero", () => {
expect(codexUsageFeed).toBeDefined();
const step = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
{
status: 200,
body: JSON.stringify({
limits: [{ kind: "session", group: "session", percent: null, severity: "normal" }],
}),
},
{ status: 200, body: "{}" },
]);
if (!("report" in step)) throw new Error("expected a report");
expect(step.report.limits[0]).toEqual({ label: "Session", windowLabel: "5h window", status: "unknown" });
});
it("surfaces extra usage when it is on and notes when it is off", () => {
const on = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
{
status: 200,
body: JSON.stringify({
limits: [],
extra_usage: { is_enabled: true, monthly_limit: 100, used_credits: 25 },
}),
},
{ status: 200, body: "{}" },
]);
if (!("report" in on)) throw new Error("expected a report");
expect(on.report.limits[0]).toMatchObject({ label: "Extra usage", usedPercent: 25 });
const off = anthropicUsageFeed.advance(createUsageState(anthropicCredential, 0), [
{
status: 200,
body: JSON.stringify({ limits: [], extra_usage: { is_enabled: false, credits_ever_enabled: true } }),
},
{ status: 200, body: "{}" },
]);
if (!("report" in off)) throw new Error("expected a report");
expect(off.report.notes).toEqual(["Extra usage is off"]);
});
});
describe("collectUsage", () => {
it("names a provider with no feed instead of dropping it", async () => {
const result = await collectUsage([{ provider: "google", credential: { kind: "apiKey", apiKey: "key" } }], {
fetchImpl: vi.fn() as unknown as FetchLike,
});
expect(result.reports).toEqual([]);
expect(result.skipped).toEqual([{ provider: "google", reason: "no usage feed" }]);
});
it("says which credential a provider needs when the host cannot supply it", async () => {
const result = await collectUsage([{ provider: "alibaba", credential: { kind: "apiKey", apiKey: "sk-sp-x" } }], {
fetchImpl: vi.fn() as unknown as FetchLike,
});
expect(result.reports).toEqual([]);
expect(result.skipped).toEqual([{ provider: "alibaba", reason: "needs a cli or session credential" }]);
});
it("explains a credential kind the feed cannot use", async () => {
const result = await collectUsage([{ provider: "openai-codex", credential: { kind: "apiKey", apiKey: "k" } }]);
expect(result.skipped[0]).toEqual({ provider: "openai-codex", reason: "needs a oauth credential" });
});
it("collects a report per credential", async () => {
const result = await collectUsage(
[
{ provider: "openai-codex", credential: codexCredential },
{ provider: "anthropic", credential: anthropicCredential },
],
{
nowMs: 1_789_514_000_000,
fetchImpl: scriptedFetch([
{ status: 200, body: JSON.stringify(fixture("codex-usage-live.json")) },
{ status: 200, body: JSON.stringify(fixture("anthropic-usage.json")) },
{ status: 200, body: JSON.stringify(fixture("anthropic-profile.json")) },
]),
},
);
expect(result.skipped).toEqual([]);
expect(result.reports.map((report) => report.provider)).toEqual(["openai-codex", "anthropic"]);
});
});