From f56a1add91859a84a45fb4f3f1652c78e499af15 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:15:47 +0000 Subject: [PATCH] fix(usage): complete enterprise spending percentages Derive missing remaining percentages so CLI spending output matches the Usage card. Preserve explicitly reported percentages and cover missing currency and absent rolling windows. Co-authored-by: Matt Cowger Co-authored-by: JasprW --- .../claude-usage-source/server/usage.test.ts | 41 ++++++++++--------- .../codex-usage-source/server/usage.test.ts | 18 ++++++++ plugins/codex-usage-source/server/usage.ts | 4 +- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/plugins/claude-usage-source/server/usage.test.ts b/plugins/claude-usage-source/server/usage.test.ts index f8490b20ed..877535e76c 100644 --- a/plugins/claude-usage-source/server/usage.test.ts +++ b/plugins/claude-usage-source/server/usage.test.ts @@ -162,7 +162,7 @@ describe("claude usage source", () => { expect.objectContaining({ signal: expect.any(AbortSignal) }), ); }); - it("reports USD extra usage monthly limit and used credits in dollars", async () => { + it.each([false, true])("reports USD extra usage with rolling windows %j", async (hasWindows) => { writeClaudeCredentials(claudeHome, "at_valid"); fetchApi = mockFetch( new Map([ @@ -170,7 +170,7 @@ describe("claude usage source", () => { "https://api.anthropic.com/api/oauth/usage", () => jsonResponse({ - ...makeClaudeResponse(), + ...(hasWindows ? makeClaudeResponse() : {}), extra_usage: { is_enabled: true, monthly_limit: 5000, @@ -198,25 +198,28 @@ describe("claude usage source", () => { ); }); - it("does not report extra usage balances without explicit USD currency", async () => { - writeClaudeCredentials(claudeHome, "at_valid"); - fetchApi = mockFetch( - new Map([ - [ - "https://api.anthropic.com/api/oauth/usage", - () => - jsonResponse({ - ...makeClaudeResponse(), - extra_usage: { monthly_limit: 5000, used_credits: 1250, currency: "EUR" }, - }), - ], - ]), - ); + it.each(["EUR", null, undefined])( + "does not report extra usage balances for currency %j", + async (currency) => { + writeClaudeCredentials(claudeHome, "at_valid"); + fetchApi = mockFetch( + new Map([ + [ + "https://api.anthropic.com/api/oauth/usage", + () => + jsonResponse({ + ...makeClaudeResponse(), + extra_usage: { monthly_limit: 5000, used_credits: 1250, currency }, + }), + ], + ]), + ); - const claude = findProvider(await service().listUsage(), "claude"); + const claude = findProvider(await service().listUsage(), "claude"); - expect(claude.balances).toEqual([]); - }); + expect(claude.balances).toEqual([]); + }, + ); it("accepts a null Claude resets_at when a window has no scheduled reset", async () => { writeClaudeCredentials(claudeHome, "at_valid"); diff --git a/plugins/codex-usage-source/server/usage.test.ts b/plugins/codex-usage-source/server/usage.test.ts index 18247c9f71..7de5b868cd 100644 --- a/plugins/codex-usage-source/server/usage.test.ts +++ b/plugins/codex-usage-source/server/usage.test.ts @@ -142,6 +142,24 @@ test("reports enterprise individual spend limit percentage and reset", async () }); }); +test.each([ + [{ used_percent: 5 }, 5, 95], + [{ remaining_percent: 95 }, 5, 95], + [{ used_percent: 5, remaining_percent: 90 }, 5, 90], + [{ used_percent: 110 }, 110, 0], +])( + "completes individual spending percentages for %j", + async (individual_limit, usedPct, remainingPct) => { + const report = await fetchUsage(authInput(fixtureHome), async () => + Response.json({ spend_control: { individual_limit } }), + ); + expect(report).toMatchObject({ + status: "available", + windows: [expect.objectContaining({ id: "individual_limit", usedPct, remainingPct })], + }); + }, +); + test("omits individual spend limit without a percentage", async () => { const report = await fetchUsage(authInput(fixtureHome), async () => Response.json({ spend_control: { individual_limit: { reset_at: 1795996800 } } }), diff --git a/plugins/codex-usage-source/server/usage.ts b/plugins/codex-usage-source/server/usage.ts index 135948f4a0..2994f9f07b 100644 --- a/plugins/codex-usage-source/server/usage.ts +++ b/plugins/codex-usage-source/server/usage.ts @@ -243,14 +243,14 @@ export async function fetchUsage( const usedPct = limit.used_percent ?? (limit.remaining_percent != null ? 100 - limit.remaining_percent : undefined); - if (usedPct === undefined && limit.remaining_percent == null) return []; + if (usedPct === undefined) return []; return [ { id: "individual_limit", label: "Individual spend limit", summary: true, usedPct, - remainingPct: limit.remaining_percent, + remainingPct: limit.remaining_percent ?? Math.max(0, 100 - usedPct), resetsAt: limit.reset_at != null ? new Date(limit.reset_at * 1000).toISOString() : null, tone: toneFromUsedPct(usedPct),