fix(usage): show Cursor quota percentages without counting bonus spend as overage (#7778)

* test: checkpoint usage helper before manual QA fixes

* fix: show Cursor plan quotas and filter disabled usage providers

* fix: display Cursor plan and capitalize Codex plan labels

* fix: reconcile usage refreshes across account and provider changes

* fix: keep usage cards in alphabetical order

* docs: add usage fix QA evidence and provider contract

* test: replace usage hook mocks with an injected client

* fix: keep usage footer in alphabetical order (pair-review item 2)

* refactor: remove usage provider enablement filtering

---------

Co-authored-by: kb <kb@wardbitz.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Stackie Jia <jsq2627@gmail.com>
This commit is contained in:
paseo-bot[bot]
2026-10-24 06:55:07 +02:00
committed by GitHub
co-authored by kb Stackie Jia
parent f3d74b8575
commit c1b257b8f4
2 changed files with 119 additions and 1 deletions
@@ -0,0 +1,92 @@
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { fetchUsage } from "./usage.js";
it("shows the reported Pro percentage pools without treating bonus spend as an overage", async () => {
const directory = await mkdtemp(join(tmpdir(), "cursor-reported-pools-"));
const locator = join(directory, "auth.json");
try {
await writeFile(locator, JSON.stringify({ accessToken: "fixture-token" }));
const report = await fetchUsage({ store: "file", locator }, async () =>
Response.json({
billingCycleStart: "2026-09-25T07:10:08.000Z",
billingCycleEnd: "2026-10-25T07:10:08.000Z",
planUsage: {
totalSpend: 5717,
includedSpend: 2000,
bonusSpend: 3717,
limit: 2000,
autoPercentUsed: 9.157777777777778,
apiPercentUsed: 35.46666666666667,
totalPercentUsed: 11.54949494949495,
},
spendLimitUsage: { individualLimit: 100, individualRemaining: 100, limitType: "user" },
}),
);
expect(report).toMatchObject({
status: "available",
windows: [
{ id: "plan_usage", usedPct: 11.54949494949495 },
{ id: "cursor_usage", usedPct: 9.157777777777778 },
{ id: "third_party_usage", usedPct: 35.46666666666667 },
],
balances: [],
});
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it.each([
["Pro+", 42015, 7000, 34.583333333333336, 4.6818181818181825, 32.07251908396947],
["Teams", 77145, 2000, 61.8352380952381, 61.09, 61.716],
] as const)(
"preserves the reported %s pools separately from bonus-inclusive dollars",
async (_plan, totalSpend, limit, autoPercentUsed, apiPercentUsed, totalPercentUsed) => {
const report = await reportFor({
totalSpend,
limit,
autoPercentUsed,
apiPercentUsed,
totalPercentUsed,
});
expect(report).toMatchObject({
status: "available",
windows: [
{ id: "plan_usage", usedPct: totalPercentUsed, summary: true },
{ id: "cursor_usage", usedPct: autoPercentUsed },
{ id: "third_party_usage", usedPct: apiPercentUsed },
],
balances: [],
});
},
);
it("retains a zero percentage without inventing absent breakdowns", async () => {
expect(await reportFor({ totalPercentUsed: "0", autoPercentUsed: null })).toMatchObject({
status: "available",
windows: [{ id: "plan_usage", usedPct: 0, remainingPct: 100 }],
balances: [],
});
});
it("keeps dollar-only reports when the response has no percentages", async () => {
expect(await reportFor({ totalSpend: 1500, remaining: 2500, limit: 4000 })).toMatchObject({
status: "available",
windows: [],
balances: [{ id: "plan_usage", used: 15, remaining: 25, limit: 40, unit: "usd" }],
});
});
async function reportFor(planUsage: Record<string, unknown>) {
const directory = await mkdtemp(join(tmpdir(), "cursor-usage-pools-"));
const locator = join(directory, "auth.json");
try {
await writeFile(locator, JSON.stringify({ accessToken: "fixture-token" }));
return await fetchUsage({ store: "file", locator }, async () => Response.json({ planUsage }));
} finally {
await rm(directory, { recursive: true, force: true });
}
}
+27 -1
View File
@@ -9,10 +9,12 @@ import {
toneFromUsedPct,
usedPctOf,
unavailable,
windowFromUsedPct,
type UsageAccount,
type UsageReport,
type UsageBalance,
type UsageScope,
type UsageWindow,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
@@ -60,6 +62,9 @@ const CursorUsageResponseSchema = z.object({
bonusSpend: ApiNullableNumberSchema,
remaining: ApiNullableNumberSchema,
limit: ApiNullableNumberSchema,
totalPercentUsed: ApiNullableNumberSchema,
autoPercentUsed: ApiNullableNumberSchema,
apiPercentUsed: ApiNullableNumberSchema,
})
.nullish(),
billingCycleStart: CursorBillingCycleTimestampSchema,
@@ -177,8 +182,29 @@ export async function fetchUsage(
const resp = CursorUsageResponseSchema.parse(await res.json());
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
const windows: UsageWindow[] = [];
const balances: UsageBalance[] = [];
if (resp.planUsage) {
// Total spend includes bonus credits; returned percentages measure quota usage.
for (const [id, label, usedPct] of [
["plan_usage", "Total", resp.planUsage.totalPercentUsed],
["cursor_usage", "Cursor", resp.planUsage.autoPercentUsed],
["third_party_usage", "Third Party", resp.planUsage.apiPercentUsed],
] as const) {
if (usedPct === null) continue;
windows.push(
windowFromUsedPct({
id,
label,
utilizationPct: usedPct,
resetsAt: billingCycleEnd,
summary: id === "plan_usage",
tone: toneFromUsedPct(usedPct),
}),
);
}
}
if (resp.planUsage && windows.length === 0) {
const totalSpend = centsToDollars(resp.planUsage.totalSpend);
const remaining = centsToDollars(resp.planUsage.remaining);
const limit = centsToDollars(resp.planUsage.limit);
@@ -197,7 +223,7 @@ export async function fetchUsage(
return {
status: "available",
planLabel: undefined,
windows: [],
windows,
balances,
details: [],
};