/** * @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("reports an unknown profile as ENOTFOUND", async () => { stubFetch(); await assert.rejects( () => usage.profileUsage("nope"), (e) => e.code === "ENOTFOUND" ); }); });