import Foundation import Testing @testable import CodexBarCore @Suite(.serialized) struct CursorEnterpriseUsageTests { @Test func `legacy provider cost snapshot decodes without personal spend`() throws { let json = """ { "used": 12.5, "limit": 100, "currencyCode": "USD", "period": "Monthly", "resetsAt": null, "nextRegenAmount": null, "updatedAt": 0 } """ let snapshot = try JSONDecoder().decode(ProviderCostSnapshot.self, from: Data(json.utf8)) #expect(snapshot.used == 12.5) #expect(snapshot.personalUsed == nil) } @Test func `parses enterprise overall and pooled usage summary`() throws { // Live Cursor Enterprise payload (sanitized). The Pro/Hobby `plan` block is absent; // instead Cursor reports `individualUsage.overall` (personal cap) and `teamUsage.pooled` // (shared team pool). Both blocks use cents like the existing `plan` block. let json = """ { "billingCycleStart": "2026-04-01T00:00:00.000Z", "billingCycleEnd": "2026-05-01T00:00:00.000Z", "membershipType": "enterprise", "limitType": "team", "isUnlimited": false, "individualUsage": { "overall": { "enabled": true, "used": 7384, "limit": 10000, "remaining": 2616 } }, "teamUsage": { "onDemand": { "enabled": true, "used": 0, "limit": null, "remaining": null }, "pooled": { "enabled": true, "used": 12725135, "limit": 28122000, "remaining": 15396865 } } } """ let data = try #require(json.data(using: .utf8)) let summary = try JSONDecoder().decode(CursorUsageSummary.self, from: data) #expect(summary.membershipType == "enterprise") #expect(summary.limitType == "team") #expect(summary.individualUsage?.plan == nil) #expect(summary.individualUsage?.overall?.used == 7384) #expect(summary.individualUsage?.overall?.limit == 10000) #expect(summary.individualUsage?.overall?.remaining == 2616) #expect(summary.teamUsage?.pooled?.used == 12_725_135) #expect(summary.teamUsage?.pooled?.limit == 28_122_000) } @Test func `enterprise overall drives headline percent and dollars`() throws { // Regression: Cursor Enterprise/Team accounts ship `individualUsage.overall` instead of // `individualUsage.plan`. Without a model for `overall`, the parser used to report 0% // (i.e. the menu showed "100% remaining"). The personal cap must take precedence over // any team pool, and USD figures must reflect the same source. let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)) .parseUsageSummary( CursorUsageSummary( billingCycleStart: "2026-04-01T00:00:00.000Z", billingCycleEnd: "2026-05-01T00:00:00.000Z", membershipType: "enterprise", limitType: "team", isUnlimited: false, autoModelSelectedDisplayMessage: nil, namedModelSelectedDisplayMessage: nil, individualUsage: CursorIndividualUsage( plan: nil, onDemand: nil, overall: CursorOverallUsage(enabled: true, used: 7384, limit: 10000, remaining: 2616)), teamUsage: CursorTeamUsage( onDemand: CursorOnDemandUsage(enabled: true, used: 0, limit: nil, remaining: nil), pooled: CursorPooledUsage( enabled: true, used: 12_725_135, limit: 28_122_000, remaining: 15_396_865))), userInfo: nil, rawJSON: nil) // Headline: $73.84 / $100 -> 73.84% (matches Cursor's own dashboard). // Allow a tiny tolerance for floating-point division (7384/10000 * 100). #expect(abs(snapshot.planPercentUsed - 73.84) < 0.0001) #expect(snapshot.planUsedUSD == 73.84) #expect(snapshot.planLimitUSD == 100.0) #expect(snapshot.autoPercentUsed == nil) #expect(snapshot.apiPercentUsed == nil) let primaryPercent = try #require(snapshot.toUsageSnapshot().primary?.usedPercent) #expect(abs(primaryPercent - 73.84) < 0.0001) } @Test func `enterprise pooled fallback used when no individual data`() { // When Cursor only reports a shared team pool (no `plan`, no `overall`) we should still surface // a non-zero headline so the menu reflects pool consumption rather than appearing "all clear". let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)) .parseUsageSummary( CursorUsageSummary( billingCycleStart: nil, billingCycleEnd: nil, membershipType: "enterprise", limitType: "team", isUnlimited: false, autoModelSelectedDisplayMessage: nil, namedModelSelectedDisplayMessage: nil, individualUsage: nil, teamUsage: CursorTeamUsage( onDemand: nil, pooled: CursorPooledUsage( enabled: true, used: 12_725_135, limit: 28_122_000, remaining: 15_396_865))), userInfo: nil, rawJSON: nil) #expect(snapshot.planPercentUsed > 45.0) #expect(snapshot.planPercentUsed < 45.5) #expect(snapshot.planUsedUSD == 127_251.35) #expect(snapshot.planLimitUSD == 281_220.0) } @Test func `team on-demand pool is the budget and personal spend rides along`() { // Live team-plan payload (sanitized): the user's own on-demand spend has no personal limit, // so the team pool is the headline budget. The personal spend must still be surfaced. let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)) .parseUsageSummary( CursorUsageSummary( billingCycleStart: "2026-06-01T00:00:00.000Z", billingCycleEnd: "2026-07-01T00:00:00.000Z", membershipType: "enterprise", limitType: "team", isUnlimited: false, autoModelSelectedDisplayMessage: nil, namedModelSelectedDisplayMessage: nil, individualUsage: CursorIndividualUsage( plan: CursorPlanUsage( enabled: true, used: 2000, limit: 2000, remaining: 0, breakdown: nil, autoPercentUsed: 0, apiPercentUsed: 100, totalPercentUsed: 100), onDemand: CursorOnDemandUsage(enabled: true, used: 4471, limit: nil, remaining: nil), overall: nil), teamUsage: CursorTeamUsage( onDemand: CursorOnDemandUsage( enabled: true, used: 1_311_125, limit: 2_000_000, remaining: 688_875), pooled: nil)), userInfo: nil, rawJSON: nil) let cost = snapshot.toUsageSnapshot().providerCost #expect(cost?.used == 13111.25) // team pool used #expect(cost?.limit == 20000.0) // team pool limit #expect(cost?.personalUsed == 44.71) // this account's own on-demand spend } @Test func `personal on-demand limit keeps personal budget with no rider`() { // When the user has their own on-demand limit, that is the budget and there is no separate rider. let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)) .parseUsageSummary( CursorUsageSummary( billingCycleStart: nil, billingCycleEnd: nil, membershipType: "pro", limitType: "user", isUnlimited: false, autoModelSelectedDisplayMessage: nil, namedModelSelectedDisplayMessage: nil, individualUsage: CursorIndividualUsage( plan: nil, onDemand: CursorOnDemandUsage(enabled: true, used: 4471, limit: 10000, remaining: 5529), overall: nil), teamUsage: nil), userInfo: nil, rawJSON: nil) let cost = snapshot.toUsageSnapshot().providerCost #expect(cost?.used == 44.71) #expect(cost?.limit == 100.0) #expect(cost?.personalUsed == nil) } @Test func `existing plan block still wins over overall and pooled`() { // Guard against future drift: when Cursor sends both legacy `plan` and the newer `overall` // blocks, the existing percent precedence must remain intact. let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)) .parseUsageSummary( CursorUsageSummary( billingCycleStart: nil, billingCycleEnd: nil, membershipType: "pro", limitType: "user", isUnlimited: false, autoModelSelectedDisplayMessage: nil, namedModelSelectedDisplayMessage: nil, individualUsage: CursorIndividualUsage( plan: CursorPlanUsage( enabled: true, used: 1500, limit: 5000, remaining: 3500, breakdown: nil, autoPercentUsed: nil, apiPercentUsed: nil, totalPercentUsed: 30.0), onDemand: nil, overall: CursorOverallUsage(enabled: true, used: 7384, limit: 10000, remaining: 2616)), teamUsage: CursorTeamUsage( onDemand: nil, pooled: CursorPooledUsage( enabled: true, used: 12_725_135, limit: 28_122_000, remaining: 15_396_865))), userInfo: nil, rawJSON: nil) #expect(snapshot.planPercentUsed == 30.0) #expect(snapshot.planUsedUSD == 15.0) #expect(snapshot.planLimitUSD == 50.0) } }