mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 18:22:38 +00:00
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:
co-authored by
kb
Stackie Jia
parent
f3d74b8575
commit
c1b257b8f4
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user