259 lines
11 KiB
Swift
259 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|