/** * @file cc-usage.test.js * @description Tests for per-profile quota lookup. * * Two properties carry the whole feature. A profile's refreshed OAuth tokens * must be written back — the exchange retires the refresh token it was given, * so dropping the result leaves an account the user has to log into again. And * a still-valid token must NOT be refreshed, because every refresh rewrites * credentials on disk and the point of a read is to read. * * `fetch` is stubbed throughout: nothing here talks to Anthropic. * * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, beforeEach, afterEach, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "cc-usage-")); process.env.CLAUDE_HOME = TMP; const profiles = require("../lib/cc-profiles"); const usage = require("../lib/cc-usage"); const realFetch = global.fetch; let calls = []; after(() => { global.fetch = realFetch; try { fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 5 }); } catch { /* best-effort temp cleanup */ } }); function writeProfileFile(name, credentials) { const dir = profiles.profilesDir(); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync( path.join(dir, `${name}.json`), JSON.stringify({ name, label: "Label kept", savedAt: 1, account: { emailAddress: "a@b.c" }, credentials, }) ); } /** Answer the token endpoint and the usage endpoint, recording both. */ function stubFetch({ usageStatus = 200, usageBody } = {}) { global.fetch = async (url, init) => { calls.push({ url: String(url), init }); if (String(url).includes("/v1/oauth/token")) { return { ok: true, status: 200, text: async () => JSON.stringify({ access_token: "fresh-access", refresh_token: "rotated-refresh", expires_in: 28800, }), }; } return { ok: usageStatus === 200, status: usageStatus, json: async () => usageBody || { five_hour: { utilization: 48.0, resets_at: "2026-09-17T13:00:00Z" }, seven_day: { utilization: 66.0, resets_at: "2026-09-21T08:00:00Z" }, seven_day_opus: null, }, }; }; } beforeEach(() => { calls = []; fs.rmSync(profiles.profilesDir(), { recursive: true, force: true }); }); afterEach(() => { global.fetch = realFetch; }); describe("profileUsage", () => { it("reports the two windows, and only those", async () => { writeProfileFile("live", { accessToken: "still-good", refreshToken: "r", expiresAt: Date.now() + 3_600_000, }); stubFetch(); const result = await usage.profileUsage("live"); assert.deepEqual(result.fiveHour, { utilization: 48, resetsAt: "2026-09-17T13:00:00Z" }); assert.deepEqual(result.sevenDay, { utilization: 66, resetsAt: "2026-09-21T08:00:00Z" }); // Nothing token-shaped may be in what a route is about to serialise. assert.equal(JSON.stringify(result).includes("still-good"), false); }); it("does not refresh a token that is still valid", async () => { writeProfileFile("live", { accessToken: "still-good", refreshToken: "r", expiresAt: Date.now() + 3_600_000, }); stubFetch(); const result = await usage.profileUsage("live"); assert.equal(result.refreshed, false); assert.equal( calls.some((c) => c.url.includes("/v1/oauth/token")), false, "a read must not rewrite credentials on disk" ); assert.match(calls[0].init.headers.Authorization, /still-good/); }); it("refreshes an expired token and WRITES THE ROTATED PAIR BACK", async () => { // The exchange retires the refresh token it was handed. A refresh whose // result is not persisted leaves the profile holding a dead credential — // the account could never be activated again without a fresh login. writeProfileFile("stale", { accessToken: "expired", refreshToken: "original-refresh", expiresAt: Date.now() - 1000, subscriptionType: "team", }); stubFetch(); const result = await usage.profileUsage("stale"); assert.equal(result.refreshed, true); const stored = profiles.readProfile("stale"); assert.equal(stored.credentials.accessToken, "fresh-access"); assert.equal(stored.credentials.refreshToken, "rotated-refresh"); assert.ok(stored.credentials.expiresAt > Date.now()); // Everything else about the profile survives the rewrite. assert.equal(stored.label, "Label kept"); assert.equal(stored.credentials.subscriptionType, "team"); assert.equal(stored.account.emailAddress, "a@b.c"); }); it("retries once through a refresh when a token that looked valid is refused", async () => { writeProfileFile("revoked", { accessToken: "revoked-but-unexpired", refreshToken: "original-refresh", expiresAt: Date.now() + 3_600_000, }); let first = true; global.fetch = async (url) => { calls.push({ url: String(url) }); if (String(url).includes("/v1/oauth/token")) { return { ok: true, status: 200, text: async () => JSON.stringify({ access_token: "fresh", refresh_token: "rot", expires_in: 100 }), }; } if (first) { first = false; return { ok: false, status: 401, json: async () => ({}) }; } return { ok: true, status: 200, json: async () => ({ five_hour: { utilization: 1, resets_at: null }, seven_day: null }), }; }; const result = await usage.profileUsage("revoked"); assert.equal(result.refreshed, true); assert.deepEqual(result.fiveHour, { utilization: 1, resetsAt: null }); }); it("refuses a profile with no refresh token instead of half-answering", async () => { writeProfileFile("tokenless", { accessToken: "expired", expiresAt: Date.now() - 1000 }); stubFetch(); await assert.rejects( () => usage.profileUsage("tokenless"), (e) => e.code === "ENOREFRESH" ); }); it("uses the CLI's own token for the account that is logged in, not the saved copy", async () => { // The saved copy of the LIVE account is the one set of credentials certain // to fail: the CLI has refreshed since, and that retired the refresh token // sitting in the profile file. Refreshing it here would also rotate the // token the user's terminal is using. const account = { accountUuid: "uuid-live", emailAddress: "a@b.c" }; fs.mkdirSync(profiles.profilesDir(), { recursive: true }); fs.writeFileSync( path.join(profiles.profilesDir(), "mine.json"), JSON.stringify({ name: "mine", account, credentials: { accessToken: "retired", refreshToken: "retired-r", expiresAt: 1 }, }) ); fs.writeFileSync( path.join(TMP, ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "live-token", refreshToken: "live-r", expiresAt: Date.now() + 3_600_000, }, }) ); fs.writeFileSync(path.join(TMP, ".claude.json"), JSON.stringify({ oauthAccount: account })); stubFetch(); const result = await usage.profileUsage("mine"); assert.equal(result.refreshed, false); assert.match(calls[0].init.headers.Authorization, /live-token/); assert.equal( calls.some((c) => c.url.includes("/v1/oauth/token")), false, "refreshing the live account would log the user's own terminal out" ); fs.rmSync(path.join(TMP, ".credentials.json")); fs.rmSync(path.join(TMP, ".claude.json")); }); it("names a spent refresh token instead of echoing the upstream 400", async () => { writeProfileFile("superseded", { accessToken: "expired", refreshToken: "already-rotated", expiresAt: Date.now() - 1000, }); global.fetch = async (url) => { calls.push({ url: String(url) }); return { ok: false, status: 400, text: async () => '{"error": "invalid_grant", "error_description": "not found"}', }; }; await assert.rejects( () => usage.profileUsage("superseded"), (e) => e.code === "ESTALE" && /save the profile again/.test(e.message) ); }); it("reports an unknown profile as ENOTFOUND", async () => { stubFetch(); await assert.rejects( () => usage.profileUsage("nope"), (e) => e.code === "ENOTFOUND" ); }); });