diff --git a/src/main/rate-limits/cursor-usage-mapping.test.ts b/src/main/rate-limits/cursor-usage-mapping.test.ts index 1e7da2188fc..52ecee8ca26 100644 --- a/src/main/rate-limits/cursor-usage-mapping.test.ts +++ b/src/main/rate-limits/cursor-usage-mapping.test.ts @@ -114,6 +114,42 @@ describe('mapCursorUsageSummary', () => { expect(mapped.monthly).toBeNull() }) + it('maps a team-limited member allowance without publishing team-wide on-demand', () => { + const mapped = mapCursorUsageSummary( + parseCursorUsageSummary({ + ...CYCLE, + membershipType: 'enterprise', + limitType: 'team', + isUnlimited: false, + individualUsage: { + overall: { enabled: true, used: 75_000, limit: 500_000, remaining: 425_000 } + }, + teamUsage: { + onDemand: { enabled: true, used: 500_000, limit: 2_500_000, remaining: 2_000_000 } + } + }) + ) + expect(mapped.planType).toBe('enterprise') + expect(mapped.monthly?.usedPercent).toBe(15) + expect(mapped.buckets).toEqual([]) + }) + + it('uses the overall allowance only when the plan pool is absent or disabled', () => { + const overall = { enabled: true, used: 90, limit: 100 } + expect( + mapCursorUsageSummary({ + ...CYCLE, + individualUsage: { plan: { enabled: true, totalPercentUsed: 30 }, overall } + }).monthly?.usedPercent + ).toBe(30) + expect( + mapCursorUsageSummary({ + ...CYCLE, + individualUsage: { plan: { enabled: false, totalPercentUsed: 0 }, overall } + }).monthly?.usedPercent + ).toBe(90) + }) + it('reports an unlimited plan without inventing a percentage', () => { const mapped = mapCursorUsageSummary({ ...CYCLE, isUnlimited: true, membershipType: 'ultra' }) expect(mapped.isUnlimited).toBe(true) diff --git a/src/main/rate-limits/cursor-usage-mapping.ts b/src/main/rate-limits/cursor-usage-mapping.ts index f2051807d83..e8fc476ccb2 100644 --- a/src/main/rate-limits/cursor-usage-mapping.ts +++ b/src/main/rate-limits/cursor-usage-mapping.ts @@ -27,7 +27,12 @@ const cursorUsageSummarySchema = z isUnlimited: z.unknown(), // The dashboard sends null for absent pools; optional-only schemas would reject the entire valid response. individualUsage: z - .object({ plan: cursorPoolSchema.nullish(), onDemand: cursorPoolSchema.nullish() }) + .object({ + plan: cursorPoolSchema.nullish(), + // Team-limited accounts report the member's included allowance as `overall`. + overall: cursorPoolSchema.nullish(), + onDemand: cursorPoolSchema.nullish() + }) .partial() .nullish() }) @@ -104,6 +109,14 @@ function windowMinutesFor(startMs: number | null, endMs: number | null): number return Math.max(1, Math.round((endMs - startMs) / 60_000)) } +// A disabled primary pool must not hide the fallback pool Cursor actually meters. +function preferEnabledPool( + primary: CursorPool | null | undefined, + fallback: CursorPool | null | undefined +): CursorPool | null | undefined { + return primary?.enabled === false && fallback?.enabled === true ? fallback : (primary ?? fallback) +} + // On-demand's raw allowance avoids rounding in its reported percentage. function poolPercent( pool: CursorPool | null | undefined, @@ -132,7 +145,7 @@ export function mapCursorUsageSummary(summary: CursorUsageSummary): CursorUsageM resetDescription: description }) - const plan = summary.individualUsage?.plan + const plan = preferEnabledPool(summary.individualUsage?.plan, summary.individualUsage?.overall) // Team-owned zero pools must not imply healthy quota or suppress legacy fallback. const planEnabled = plan?.enabled !== false const buckets: RateLimitBucket[] = [] @@ -145,6 +158,7 @@ export function mapCursorUsageSummary(summary: CursorUsageSummary): CursorUsageM buckets.push({ name: CURSOR_OTHER_MODELS_BUCKET_NAME, ...toWindow(otherModels) }) } + // teamUsage.onDemand is team-wide spend, not this member's allowance, so it stays unmapped. const onDemand = summary.individualUsage?.onDemand const onDemandPercent = onDemand?.enabled === true ? poolPercent(onDemand, 'totalPercentUsed') : null