1500 lines
55 KiB
Swift
1500 lines
55 KiB
Swift
import Foundation
|
|
import SQLite3
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct CursorStatusProbeTests {
|
|
// MARK: - Usage Summary Parsing
|
|
|
|
@Test
|
|
func `parses basic usage summary`() throws {
|
|
let json = """
|
|
{
|
|
"billingCycleStart": "2025-01-01T00:00:00.000Z",
|
|
"billingCycleEnd": "2025-02-01T00:00:00.000Z",
|
|
"membershipType": "pro",
|
|
"individualUsage": {
|
|
"plan": {
|
|
"enabled": true,
|
|
"used": 1500,
|
|
"limit": 5000,
|
|
"remaining": 3500,
|
|
"totalPercentUsed": 30.0
|
|
},
|
|
"onDemand": {
|
|
"enabled": true,
|
|
"used": 500,
|
|
"limit": 10000,
|
|
"remaining": 9500
|
|
}
|
|
},
|
|
"teamUsage": {
|
|
"onDemand": {
|
|
"enabled": true,
|
|
"used": 2000,
|
|
"limit": 50000,
|
|
"remaining": 48000
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let data = try #require(json.data(using: .utf8))
|
|
let summary = try JSONDecoder().decode(CursorUsageSummary.self, from: data)
|
|
|
|
#expect(summary.membershipType == "pro")
|
|
#expect(summary.individualUsage?.plan?.used == 1500)
|
|
#expect(summary.individualUsage?.plan?.limit == 5000)
|
|
#expect(summary.individualUsage?.plan?.totalPercentUsed == 30.0)
|
|
#expect(summary.individualUsage?.onDemand?.used == 500)
|
|
#expect(summary.teamUsage?.onDemand?.used == 2000)
|
|
#expect(summary.teamUsage?.onDemand?.limit == 50000)
|
|
}
|
|
|
|
@Test
|
|
func `parses minimal usage summary`() throws {
|
|
let json = """
|
|
{
|
|
"membershipType": "hobby",
|
|
"individualUsage": {
|
|
"plan": {
|
|
"used": 0,
|
|
"limit": 2000
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let data = try #require(json.data(using: .utf8))
|
|
let summary = try JSONDecoder().decode(CursorUsageSummary.self, from: data)
|
|
|
|
#expect(summary.membershipType == "hobby")
|
|
#expect(summary.individualUsage?.plan?.used == 0)
|
|
#expect(summary.individualUsage?.plan?.limit == 2000)
|
|
#expect(summary.teamUsage == nil)
|
|
}
|
|
|
|
@Test
|
|
func `parses enterprise usage summary`() throws {
|
|
let json = """
|
|
{
|
|
"membershipType": "enterprise",
|
|
"isUnlimited": true,
|
|
"individualUsage": {
|
|
"plan": {
|
|
"enabled": true,
|
|
"used": 50000,
|
|
"limit": 100000,
|
|
"totalPercentUsed": 50.0
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
let data = try #require(json.data(using: .utf8))
|
|
let summary = try JSONDecoder().decode(CursorUsageSummary.self, from: data)
|
|
|
|
#expect(summary.membershipType == "enterprise")
|
|
#expect(summary.isUnlimited == true)
|
|
#expect(summary.individualUsage?.plan?.totalPercentUsed == 50.0)
|
|
}
|
|
|
|
// MARK: - User Info Parsing
|
|
|
|
@Test
|
|
func `parses user info`() throws {
|
|
let json = """
|
|
{
|
|
"email": "user@example.com",
|
|
"email_verified": true,
|
|
"name": "Test User",
|
|
"sub": "auth0|12345"
|
|
}
|
|
"""
|
|
let data = try #require(json.data(using: .utf8))
|
|
let userInfo = try JSONDecoder().decode(CursorUserInfo.self, from: data)
|
|
|
|
#expect(userInfo.email == "user@example.com")
|
|
#expect(userInfo.emailVerified == true)
|
|
#expect(userInfo.name == "Test User")
|
|
#expect(userInfo.sub == "auth0|12345")
|
|
}
|
|
|
|
// MARK: - Snapshot Conversion
|
|
|
|
@Test
|
|
func `prefers plan ratio over percent field`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "enterprise",
|
|
limitType: nil,
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 4900,
|
|
limit: 50000,
|
|
remaining: nil,
|
|
breakdown: nil,
|
|
autoPercentUsed: nil,
|
|
apiPercentUsed: nil,
|
|
totalPercentUsed: 0.40625),
|
|
onDemand: nil),
|
|
teamUsage: nil),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
// totalPercentUsed is already expressed in percentage units.
|
|
#expect(snapshot.planPercentUsed == 0.40625)
|
|
}
|
|
|
|
@Test
|
|
func `uses percent field when limit missing`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
limitType: nil,
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 0,
|
|
limit: nil,
|
|
remaining: nil,
|
|
breakdown: nil,
|
|
autoPercentUsed: nil,
|
|
apiPercentUsed: nil,
|
|
totalPercentUsed: 0.5),
|
|
onDemand: nil),
|
|
teamUsage: nil),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.planPercentUsed == 0.5)
|
|
}
|
|
|
|
@Test
|
|
func `headline total prefers provided total percent over lane average`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
limitType: nil,
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 5400,
|
|
limit: 2000,
|
|
remaining: nil,
|
|
breakdown: nil,
|
|
autoPercentUsed: 0,
|
|
apiPercentUsed: 0.01,
|
|
totalPercentUsed: 0.27),
|
|
onDemand: nil),
|
|
teamUsage: nil),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.planPercentUsed == 0.27)
|
|
#expect(snapshot.autoPercentUsed == 0)
|
|
#expect(snapshot.apiPercentUsed == 0.01)
|
|
}
|
|
|
|
@Test
|
|
func `sub pool percents accept plain percent scale`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
limitType: nil,
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 0,
|
|
limit: 2000,
|
|
remaining: nil,
|
|
breakdown: nil,
|
|
autoPercentUsed: 12.5,
|
|
apiPercentUsed: 3.0,
|
|
totalPercentUsed: nil),
|
|
onDemand: nil),
|
|
teamUsage: nil),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.autoPercentUsed == 12.5)
|
|
#expect(snapshot.apiPercentUsed == 3.0)
|
|
// Dashboard-style total ≈ average of Auto and API lanes
|
|
#expect(snapshot.planPercentUsed == 7.75)
|
|
}
|
|
|
|
@Test
|
|
func `headline total matches dashboard blend when lanes match totalPercentUsed`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
limitType: nil,
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 0,
|
|
limit: 2000,
|
|
remaining: nil,
|
|
breakdown: nil,
|
|
autoPercentUsed: 35,
|
|
apiPercentUsed: 97,
|
|
totalPercentUsed: 66),
|
|
onDemand: nil),
|
|
teamUsage: nil),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.planPercentUsed == 66)
|
|
#expect(snapshot.autoPercentUsed == 35)
|
|
#expect(snapshot.apiPercentUsed == 97)
|
|
}
|
|
|
|
@Test
|
|
func `live cursor payload keeps fractional percents without scaling`() {
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
.parseUsageSummary(
|
|
CursorUsageSummary(
|
|
billingCycleStart: "2026-03-18T20:45:42.000Z",
|
|
billingCycleEnd: "2026-04-18T20:45:42.000Z",
|
|
membershipType: "pro",
|
|
limitType: "user",
|
|
isUnlimited: false,
|
|
autoModelSelectedDisplayMessage: "You've used 1% of your included total usage",
|
|
namedModelSelectedDisplayMessage: "You've used 1% of your included API usage",
|
|
individualUsage: CursorIndividualUsage(
|
|
plan: CursorPlanUsage(
|
|
enabled: true,
|
|
used: 86,
|
|
limit: 2000,
|
|
remaining: 1914,
|
|
breakdown: CursorPlanBreakdown(
|
|
included: 86,
|
|
bonus: 0,
|
|
total: 86),
|
|
autoPercentUsed: 0.36,
|
|
apiPercentUsed: 0.7111111111111111,
|
|
totalPercentUsed: 0.441025641025641),
|
|
onDemand: CursorOnDemandUsage(
|
|
enabled: false,
|
|
used: 0,
|
|
limit: nil,
|
|
remaining: nil)),
|
|
teamUsage: CursorTeamUsage(onDemand: nil)),
|
|
userInfo: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.planPercentUsed == 0.441025641025641)
|
|
#expect(snapshot.autoPercentUsed == 0.36)
|
|
#expect(snapshot.apiPercentUsed == 0.7111111111111111)
|
|
#expect(snapshot.billingCycleStart != nil)
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
#expect(usageSnapshot.primary?.remainingPercent == 99.55897435897436)
|
|
#expect(usageSnapshot.primary?.windowMinutes == 44640)
|
|
#expect(usageSnapshot.secondary?.windowMinutes == 44640)
|
|
#expect(usageSnapshot.tertiary?.windowMinutes == 44640)
|
|
}
|
|
|
|
@Test
|
|
func `converts snapshot to usage snapshot`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 45.0,
|
|
autoPercentUsed: 5.0,
|
|
apiPercentUsed: nil,
|
|
planUsedUSD: 22.50,
|
|
planLimitUSD: 50.0,
|
|
onDemandUsedUSD: 5.0,
|
|
onDemandLimitUSD: 100.0,
|
|
teamOnDemandUsedUSD: 25.0,
|
|
teamOnDemandLimitUSD: 500.0,
|
|
billingCycleStart: Date(timeIntervalSince1970: 1_735_689_600), // Jan 1, 2025
|
|
billingCycleEnd: Date(timeIntervalSince1970: 1_738_368_000), // Feb 1, 2025
|
|
membershipType: "pro",
|
|
accountEmail: "user@example.com",
|
|
accountName: "Test User",
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usageSnapshot.primary?.usedPercent == 45.0)
|
|
#expect(usageSnapshot.accountEmail(for: .cursor) == "user@example.com")
|
|
#expect(usageSnapshot.loginMethod(for: .cursor) == "Cursor Pro")
|
|
#expect(usageSnapshot.secondary != nil)
|
|
#expect(usageSnapshot.secondary?.usedPercent == 5.0)
|
|
#expect(usageSnapshot.primary?.windowMinutes == 44640)
|
|
#expect(usageSnapshot.secondary?.windowMinutes == 44640)
|
|
#expect(usageSnapshot.providerCost?.used == 5.0)
|
|
#expect(usageSnapshot.providerCost?.limit == 100.0)
|
|
#expect(usageSnapshot.providerCost?.currencyCode == "USD")
|
|
}
|
|
|
|
@Test
|
|
func `provider cost includes on demand budget before first spend`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 10.0,
|
|
autoPercentUsed: 5.0,
|
|
apiPercentUsed: nil,
|
|
planUsedUSD: 5.0,
|
|
planLimitUSD: 50.0,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: 75.0,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usageSnapshot.providerCost != nil)
|
|
#expect(usageSnapshot.providerCost?.used == 0.0)
|
|
#expect(usageSnapshot.providerCost?.limit == 75.0)
|
|
#expect(usageSnapshot.providerCost?.period == "Monthly")
|
|
}
|
|
|
|
@Test
|
|
func `uses individual on demand when no team usage`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 10.0,
|
|
autoPercentUsed: 20.0,
|
|
apiPercentUsed: nil,
|
|
planUsedUSD: 5.0,
|
|
planLimitUSD: 50.0,
|
|
onDemandUsedUSD: 12.0,
|
|
onDemandLimitUSD: 60.0,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usageSnapshot.secondary?.usedPercent == 20.0)
|
|
#expect(usageSnapshot.providerCost?.used == 12.0)
|
|
#expect(usageSnapshot.providerCost?.limit == 60.0)
|
|
}
|
|
|
|
@Test
|
|
func `uses team on demand budget when individual usage has no cap`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 0,
|
|
autoPercentUsed: 0,
|
|
apiPercentUsed: 0,
|
|
planUsedUSD: 0,
|
|
planLimitUSD: 20,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: 0,
|
|
teamOnDemandLimitUSD: 2349,
|
|
billingCycleEnd: nil,
|
|
membershipType: "enterprise",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usageSnapshot.providerCost?.used == 0)
|
|
#expect(usageSnapshot.providerCost?.limit == 2349)
|
|
#expect(usageSnapshot.providerCost?.currencyCode == "USD")
|
|
}
|
|
|
|
@Test
|
|
func `formats membership types`() {
|
|
let testCases: [(input: String, expected: String)] = [
|
|
("pro", "Cursor Pro"),
|
|
("hobby", "Cursor Hobby"),
|
|
("enterprise", "Cursor Enterprise"),
|
|
("team", "Cursor Team"),
|
|
("custom", "Cursor Custom"),
|
|
]
|
|
|
|
for testCase in testCases {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 0,
|
|
planUsedUSD: 0,
|
|
planLimitUSD: 0,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: testCase.input,
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
#expect(usageSnapshot.loginMethod(for: .cursor) == testCase.expected)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `handles nil on demand limit`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 50.0,
|
|
planUsedUSD: 25.0,
|
|
planLimitUSD: 50.0,
|
|
onDemandUsedUSD: 10.0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
// Should still have provider cost
|
|
#expect(usageSnapshot.providerCost != nil)
|
|
#expect(usageSnapshot.providerCost?.used == 10.0)
|
|
#expect(usageSnapshot.providerCost?.limit == 0.0)
|
|
// Secondary should be nil when no on-demand limit
|
|
#expect(usageSnapshot.secondary == nil)
|
|
}
|
|
|
|
// MARK: - Legacy Request-Based Plan
|
|
|
|
@Test
|
|
func `parses legacy request based plan`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 100.0,
|
|
planUsedUSD: 0,
|
|
planLimitUSD: 0,
|
|
onDemandUsedUSD: 43.64,
|
|
onDemandLimitUSD: 200.0,
|
|
teamOnDemandUsedUSD: 92.91,
|
|
teamOnDemandLimitUSD: 20000.0,
|
|
billingCycleEnd: nil,
|
|
membershipType: "enterprise",
|
|
accountEmail: "user@company.com",
|
|
accountName: "Test User",
|
|
rawJSON: nil,
|
|
requestsUsed: 500,
|
|
requestsLimit: 500)
|
|
|
|
#expect(snapshot.isLegacyRequestPlan == true)
|
|
#expect(snapshot.requestsUsed == 500)
|
|
#expect(snapshot.requestsLimit == 500)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usageSnapshot.cursorRequests != nil)
|
|
#expect(usageSnapshot.cursorRequests?.used == 500)
|
|
#expect(usageSnapshot.cursorRequests?.limit == 500)
|
|
#expect(usageSnapshot.cursorRequests?.usedPercent == 100.0)
|
|
#expect(usageSnapshot.cursorRequests?.remainingPercent == 0.0)
|
|
|
|
// Primary RateWindow should use request-based percentage for legacy plans
|
|
#expect(usageSnapshot.primary?.usedPercent == 100.0)
|
|
}
|
|
|
|
@Test
|
|
func `legacy plan primary uses requests not dollars`() {
|
|
// Regression: Legacy plans report planPercentUsed as 0 while requests are used
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 0.0, // Dollar-based shows 0
|
|
planUsedUSD: 0,
|
|
planLimitUSD: 0,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "enterprise",
|
|
accountEmail: "user@company.com",
|
|
accountName: nil,
|
|
rawJSON: nil,
|
|
requestsUsed: 250,
|
|
requestsLimit: 500)
|
|
|
|
#expect(snapshot.isLegacyRequestPlan == true)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
|
|
// Primary should reflect request usage (50%), not dollar usage (0%)
|
|
#expect(usageSnapshot.primary?.usedPercent == 50.0)
|
|
#expect(usageSnapshot.cursorRequests?.usedPercent == 50.0)
|
|
}
|
|
|
|
@Test
|
|
func `parse usage summary prefers request total`() {
|
|
let summary = CursorUsageSummary(
|
|
billingCycleStart: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: nil,
|
|
limitType: nil,
|
|
isUnlimited: nil,
|
|
autoModelSelectedDisplayMessage: nil,
|
|
namedModelSelectedDisplayMessage: nil,
|
|
individualUsage: nil,
|
|
teamUsage: nil)
|
|
let requestUsage = CursorUsageResponse(
|
|
gpt4: CursorModelUsage(
|
|
numRequests: 120,
|
|
numRequestsTotal: 240,
|
|
numTokens: nil,
|
|
maxRequestUsage: 500,
|
|
maxTokenUsage: nil),
|
|
startOfMonth: nil)
|
|
|
|
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0)).parseUsageSummary(
|
|
summary,
|
|
userInfo: nil,
|
|
rawJSON: nil,
|
|
requestUsage: requestUsage)
|
|
|
|
#expect(snapshot.requestsUsed == 240)
|
|
#expect(snapshot.requestsLimit == 500)
|
|
}
|
|
|
|
// MARK: - Imported Session Scanning
|
|
|
|
@Test
|
|
func `imported session scan continues after non auth failure until later success`() async {
|
|
let probe = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
let expected = CursorStatusSnapshot(
|
|
planPercentUsed: 0.441025641025641,
|
|
autoPercentUsed: 0.36,
|
|
apiPercentUsed: 0.7111111111111111,
|
|
planUsedUSD: 0.86,
|
|
planLimitUSD: 20.0,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
let result = await probe.scanImportedSessions([
|
|
Self.makeSessionInfo(sourceLabel: "Chrome"),
|
|
Self.makeSessionInfo(sourceLabel: "Safari"),
|
|
]) { session in
|
|
switch session.sourceLabel {
|
|
case "Chrome":
|
|
.failed(.networkError("HTTP 500"))
|
|
case "Safari":
|
|
.succeeded(expected)
|
|
default:
|
|
.tryNextBrowser
|
|
}
|
|
}
|
|
|
|
switch result {
|
|
case let .succeeded(snapshot):
|
|
#expect(snapshot.planPercentUsed == expected.planPercentUsed)
|
|
#expect(snapshot.autoPercentUsed == expected.autoPercentUsed)
|
|
#expect(snapshot.apiPercentUsed == expected.apiPercentUsed)
|
|
case .exhausted:
|
|
Issue.record("Expected scan to continue to the later successful browser session")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `imported session scan preserves first non auth failure after exhausting sessions`() async {
|
|
let probe = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
|
|
let result = await probe.scanImportedSessions([
|
|
Self.makeSessionInfo(sourceLabel: "Chrome"),
|
|
Self.makeSessionInfo(sourceLabel: "Safari"),
|
|
Self.makeSessionInfo(sourceLabel: "Arc"),
|
|
]) { session in
|
|
switch session.sourceLabel {
|
|
case "Chrome":
|
|
.failed(.networkError("HTTP 500"))
|
|
case "Safari":
|
|
.tryNextBrowser
|
|
case "Arc":
|
|
.failed(.parseFailed("bad payload"))
|
|
default:
|
|
.tryNextBrowser
|
|
}
|
|
}
|
|
|
|
switch result {
|
|
case .succeeded:
|
|
Issue.record("Expected scan to report the first recoverable error after exhausting sessions")
|
|
case let .exhausted(error):
|
|
guard let error else {
|
|
Issue.record("Expected first recoverable error to be preserved")
|
|
return
|
|
}
|
|
guard case let .networkError(message) = error else {
|
|
Issue.record("Expected first recoverable error to be the Chrome network failure")
|
|
return
|
|
}
|
|
#expect(message == "HTTP 500")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `browser scan stops importing after later browser succeeds`() async {
|
|
let probe = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
let expected = CursorStatusSnapshot(
|
|
planPercentUsed: 42,
|
|
autoPercentUsed: 12,
|
|
apiPercentUsed: 85,
|
|
planUsedUSD: 8.4,
|
|
planLimitUSD: 20,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
var importedLabels: [String] = []
|
|
|
|
let result = await probe.scanBrowsers(
|
|
[.chrome, .safari, .chromeBeta],
|
|
importSessions: { browser in
|
|
importedLabels.append(browser.displayName)
|
|
switch browser {
|
|
case .chrome:
|
|
return [Self.makeSessionInfo(sourceLabel: "Chrome")]
|
|
case .safari:
|
|
return [Self.makeSessionInfo(sourceLabel: "Safari")]
|
|
case .chromeBeta:
|
|
return [Self.makeSessionInfo(sourceLabel: "Chrome Beta")]
|
|
default:
|
|
return []
|
|
}
|
|
},
|
|
attemptFetch: { session in
|
|
switch session.sourceLabel {
|
|
case "Chrome":
|
|
.failed(.networkError("HTTP 500"))
|
|
case "Safari":
|
|
.succeeded(expected)
|
|
default:
|
|
.tryNextBrowser
|
|
}
|
|
})
|
|
|
|
switch result {
|
|
case let .succeeded(snapshot):
|
|
#expect(snapshot.planPercentUsed == expected.planPercentUsed)
|
|
#expect(importedLabels == ["Chrome", "Safari"])
|
|
case .exhausted:
|
|
Issue.record("Expected browser scan to stop after the later successful browser")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `browser scan keeps trying later sources within the same browser`() async {
|
|
let probe = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
let expected = CursorStatusSnapshot(
|
|
planPercentUsed: 12,
|
|
autoPercentUsed: 3,
|
|
apiPercentUsed: 45,
|
|
planUsedUSD: 2.4,
|
|
planLimitUSD: 20,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: nil,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
var attemptedSources: [String] = []
|
|
|
|
let result = await probe.scanBrowsers(
|
|
[.chrome, .safari],
|
|
importSessions: { browser in
|
|
switch browser {
|
|
case .chrome:
|
|
[
|
|
Self.makeSessionInfo(sourceLabel: "Chrome Profile 1"),
|
|
Self.makeSessionInfo(sourceLabel: "Chrome Profile 2 (domain cookies)"),
|
|
]
|
|
case .safari:
|
|
[Self.makeSessionInfo(sourceLabel: "Safari")]
|
|
default:
|
|
[]
|
|
}
|
|
},
|
|
attemptFetch: { session in
|
|
attemptedSources.append(session.sourceLabel)
|
|
switch session.sourceLabel {
|
|
case "Chrome Profile 1":
|
|
return CursorStatusProbe.ImportedSessionFetchOutcome.failed(.networkError("HTTP 500"))
|
|
case "Chrome Profile 2 (domain cookies)":
|
|
return CursorStatusProbe.ImportedSessionFetchOutcome.succeeded(expected)
|
|
default:
|
|
return CursorStatusProbe.ImportedSessionFetchOutcome.tryNextBrowser
|
|
}
|
|
})
|
|
|
|
switch result {
|
|
case let .succeeded(snapshot):
|
|
#expect(snapshot.planPercentUsed == expected.planPercentUsed)
|
|
#expect(attemptedSources == ["Chrome Profile 1", "Chrome Profile 2 (domain cookies)"])
|
|
case .exhausted:
|
|
Issue.record("Expected browser scan to continue to later sources within the same browser")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `detects non legacy plan`() {
|
|
let snapshot = CursorStatusSnapshot(
|
|
planPercentUsed: 50.0,
|
|
planUsedUSD: 25.0,
|
|
planLimitUSD: 50.0,
|
|
onDemandUsedUSD: 0,
|
|
onDemandLimitUSD: 100.0,
|
|
teamOnDemandUsedUSD: nil,
|
|
teamOnDemandLimitUSD: nil,
|
|
billingCycleEnd: nil,
|
|
membershipType: "pro",
|
|
accountEmail: nil,
|
|
accountName: nil,
|
|
rawJSON: nil)
|
|
|
|
#expect(snapshot.isLegacyRequestPlan == false)
|
|
#expect(snapshot.requestsUsed == nil)
|
|
#expect(snapshot.requestsLimit == nil)
|
|
|
|
let usageSnapshot = snapshot.toUsageSnapshot()
|
|
#expect(usageSnapshot.cursorRequests == nil)
|
|
}
|
|
|
|
// MARK: - Session Store Serialization
|
|
|
|
@Test
|
|
func `session store saves and loads cookies`() async {
|
|
let store = CursorSessionStore.shared
|
|
|
|
// Clear any existing cookies
|
|
await store.clearCookies()
|
|
|
|
// Create test cookies with Date properties
|
|
let cookieProps: [HTTPCookiePropertyKey: Any] = [
|
|
.name: "testCookie",
|
|
.value: "testValue",
|
|
.domain: "cursor.com",
|
|
.path: "/",
|
|
.expires: Date(timeIntervalSince1970: 1_800_000_000),
|
|
.secure: true,
|
|
]
|
|
|
|
guard let cookie = HTTPCookie(properties: cookieProps) else {
|
|
Issue.record("Failed to create test cookie")
|
|
return
|
|
}
|
|
|
|
// Save cookies
|
|
await store.setCookies([cookie])
|
|
|
|
// Verify cookies are stored
|
|
let storedCookies = await store.getCookies()
|
|
#expect(storedCookies.count == 1)
|
|
#expect(storedCookies.first?.name == "testCookie")
|
|
#expect(storedCookies.first?.value == "testValue")
|
|
|
|
// Clean up
|
|
await store.clearCookies()
|
|
}
|
|
|
|
@Test
|
|
func `session store reloads from disk when needed`() async {
|
|
let store = CursorSessionStore.shared
|
|
await store.resetForTesting()
|
|
|
|
let cookieProps: [HTTPCookiePropertyKey: Any] = [
|
|
.name: "diskCookie",
|
|
.value: "diskValue",
|
|
.domain: "cursor.com",
|
|
.path: "/",
|
|
.expires: Date(timeIntervalSince1970: 1_800_000_000),
|
|
.secure: true,
|
|
]
|
|
|
|
guard let cookie = HTTPCookie(properties: cookieProps) else {
|
|
Issue.record("Failed to create test cookie")
|
|
return
|
|
}
|
|
|
|
await store.setCookies([cookie])
|
|
await store.resetForTesting(clearDisk: false)
|
|
|
|
let reloaded = await store.getCookies()
|
|
#expect(reloaded.count == 1)
|
|
#expect(reloaded.first?.name == "diskCookie")
|
|
#expect(reloaded.first?.value == "diskValue")
|
|
|
|
await store.clearCookies()
|
|
}
|
|
|
|
@Test
|
|
func `session store has valid session loads from disk`() async {
|
|
let store = CursorSessionStore.shared
|
|
await store.resetForTesting()
|
|
|
|
let cookieProps: [HTTPCookiePropertyKey: Any] = [
|
|
.name: "validCookie",
|
|
.value: "validValue",
|
|
.domain: "cursor.com",
|
|
.path: "/",
|
|
.expires: Date(timeIntervalSince1970: 1_800_000_000),
|
|
.secure: true,
|
|
]
|
|
|
|
guard let cookie = HTTPCookie(properties: cookieProps) else {
|
|
Issue.record("Failed to create test cookie")
|
|
return
|
|
}
|
|
|
|
await store.setCookies([cookie])
|
|
await store.resetForTesting(clearDisk: false)
|
|
|
|
let hasSession = await store.hasValidSession()
|
|
#expect(hasSession)
|
|
|
|
await store.clearCookies()
|
|
}
|
|
|
|
private static func makeSessionInfo(sourceLabel: String) -> CursorCookieImporter.SessionInfo {
|
|
let cookieProps: [HTTPCookiePropertyKey: Any] = [
|
|
.name: "WorkosCursorSessionToken",
|
|
.value: sourceLabel.lowercased(),
|
|
.domain: "cursor.com",
|
|
.path: "/",
|
|
.secure: true,
|
|
]
|
|
|
|
let cookie = HTTPCookie(properties: cookieProps)!
|
|
return CursorCookieImporter.SessionInfo(cookies: [cookie], sourceLabel: sourceLabel)
|
|
}
|
|
}
|
|
|
|
private final class CursorStatusProbeTestSession {
|
|
let urlSession: URLSession
|
|
private let sessionID: String
|
|
|
|
init(handler: @escaping @Sendable (URLRequest) throws -> (HTTPURLResponse, Data)) {
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.protocolClasses = [CursorStatusProbeStubURLProtocol.self]
|
|
self.sessionID = CursorStatusProbeStubURLProtocol.configure(config, handler: handler)
|
|
self.urlSession = URLSession(configuration: config)
|
|
}
|
|
|
|
deinit {
|
|
self.urlSession.invalidateAndCancel()
|
|
CursorStatusProbeStubURLProtocol.removeSession(self.sessionID)
|
|
}
|
|
|
|
var requestCount: Int {
|
|
CursorStatusProbeStubURLProtocol.requests(for: self.sessionID).count
|
|
}
|
|
|
|
var requestPaths: [String] {
|
|
CursorStatusProbeStubURLProtocol.requests(for: self.sessionID).compactMap { $0.url?.path }
|
|
}
|
|
|
|
var requestCookies: [String] {
|
|
CursorStatusProbeStubURLProtocol.requests(for: self.sessionID)
|
|
.compactMap { $0.value(forHTTPHeaderField: "Cookie") }
|
|
}
|
|
}
|
|
|
|
private func makeCursorStatusProbeResponse(
|
|
url: URL,
|
|
body: String,
|
|
statusCode: Int,
|
|
contentType: String = "application/json") -> (HTTPURLResponse, Data)
|
|
{
|
|
let response = HTTPURLResponse(
|
|
url: url,
|
|
statusCode: statusCode,
|
|
httpVersion: nil,
|
|
headerFields: ["Content-Type": contentType])!
|
|
return (response, Data(body.utf8))
|
|
}
|
|
|
|
extension CursorStatusProbeTests {
|
|
@Test
|
|
func `app auth store reads Cursor global state database`() throws {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("cursor-app-auth-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
|
|
let dbURL = directory.appendingPathComponent("state.vscdb")
|
|
var db: OpaquePointer?
|
|
try #require(sqlite3_open(dbURL.path, &db) == SQLITE_OK)
|
|
defer { sqlite3_close(db) }
|
|
|
|
let sql = """
|
|
CREATE TABLE ItemTable(key TEXT PRIMARY KEY, value BLOB);
|
|
INSERT INTO ItemTable VALUES('cursorAuth/accessToken', 'app-token');
|
|
"""
|
|
try #require(sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK)
|
|
|
|
let session = try #require(try CursorAppAuthStore(dbPath: dbURL.path).loadSession())
|
|
#expect(session == CursorAppAuthSession(accessToken: "app-token"))
|
|
}
|
|
|
|
@Test
|
|
func `fetch ignores user info failure when usage summary succeeds`() async throws {
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
|
|
switch requestURL.path {
|
|
case "/api/usage-summary":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"membershipType": "pro",
|
|
"individualUsage": {
|
|
"plan": {
|
|
"used": 1500,
|
|
"limit": 5000,
|
|
"totalPercentUsed": 30.0
|
|
}
|
|
}
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
case "/api/auth/me":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"error":"nope"}"#,
|
|
statusCode: 500)
|
|
default:
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor.test"))
|
|
let snapshot = try await CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
urlSession: testSession.urlSession).fetchWithManualCookies("auth=test")
|
|
|
|
#expect(snapshot.planPercentUsed == 30.0)
|
|
#expect(snapshot.accountEmail == nil)
|
|
#expect(testSession.requestCount == 2)
|
|
}
|
|
|
|
@Test
|
|
func `fetch fails cleanly when usage summary fails`() async {
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
|
|
switch requestURL.path {
|
|
case "/api/usage-summary":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"error":"denied"}"#,
|
|
statusCode: 500)
|
|
case "/api/auth/me":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"email": "user@example.com",
|
|
"email_verified": true,
|
|
"name": "Test User",
|
|
"sub": "auth0|12345"
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
default:
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
do {
|
|
let baseURL = try #require(URL(string: "https://cursor.test"))
|
|
_ = try await CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
urlSession: testSession.urlSession).fetchWithManualCookies("auth=test")
|
|
Issue.record("Expected usage summary failure to be surfaced")
|
|
} catch let error as CursorStatusProbeError {
|
|
guard case let .networkError(message) = error else {
|
|
Issue.record("Expected networkError, got: \(error)")
|
|
return
|
|
}
|
|
#expect(message == "HTTP 500")
|
|
#expect(testSession.requestPaths.contains("/api/usage-summary"))
|
|
} catch {
|
|
Issue.record("Expected CursorStatusProbeError, got: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `fetch uses Cursor app local auth when browser cookies are unavailable`() async throws {
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let expectedCookie = "WorkosCursorSessionToken=user_test%3A%3A\(accessToken)"
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == nil)
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == expectedCookie)
|
|
#expect(request.httpMethod == "GET")
|
|
|
|
switch requestURL.path {
|
|
case "/api/usage-summary":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"membershipType": "pro",
|
|
"billingCycleStart": "2026-05-23T10:27:04.000Z",
|
|
"billingCycleEnd": "2026-06-23T10:27:04.000Z",
|
|
"individualUsage": {
|
|
"plan": {
|
|
"used": 388,
|
|
"limit": 2000,
|
|
"totalPercentUsed": 19.4
|
|
},
|
|
"onDemand": {
|
|
"used": 450,
|
|
"limit": 1000
|
|
}
|
|
}
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
case "/api/auth/me":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"email":"user@example.com","name":"Test User"}"#,
|
|
statusCode: 200)
|
|
case "/api/usage":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"gpt-4":{},"startOfMonth":"2026-05-23"}"#,
|
|
statusCode: 200)
|
|
default:
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor-web.test"))
|
|
let snapshot = try await CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken))).fetch(allowCachedSessions: false)
|
|
|
|
#expect(abs(snapshot.planPercentUsed - 19.4) < 0.0001)
|
|
#expect(snapshot.planUsedUSD == 3.88)
|
|
#expect(snapshot.planLimitUSD == 20.0)
|
|
#expect(snapshot.onDemandUsedUSD == 4.5)
|
|
#expect(snapshot.onDemandLimitUSD == 10.0)
|
|
#expect(snapshot.membershipType == "pro")
|
|
#expect(snapshot.accountEmail == "user@example.com")
|
|
#expect(snapshot.accountName == "Test User")
|
|
#expect(testSession.requestPaths.sorted() == [
|
|
"/api/auth/me",
|
|
"/api/usage",
|
|
"/api/usage-summary",
|
|
])
|
|
}
|
|
|
|
@Test
|
|
func `fetch can disable Cursor app auth during browser login verification`() async throws {
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
Issue.record("Disabled app auth unexpectedly requested \(request.url?.path ?? "<unknown>")")
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor-web.test"))
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let probe = CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken)))
|
|
|
|
await #expect(throws: CursorStatusProbeError.self) {
|
|
_ = try await probe.fetch(
|
|
allowCachedSessions: false,
|
|
allowAppAuthFallback: false)
|
|
}
|
|
#expect(testSession.requestCount == 0)
|
|
}
|
|
|
|
@Test
|
|
func `fetch prefers stored session cookies before Cursor app auth fallback`() async throws {
|
|
let store = CursorSessionStore.shared
|
|
await store.clearCookies()
|
|
defer {
|
|
Task { await store.clearCookies() }
|
|
}
|
|
|
|
guard let cookie = HTTPCookie(properties: [
|
|
.name: "WorkosCursorSessionToken",
|
|
.value: "stored-session",
|
|
.domain: "cursor.com",
|
|
.path: "/",
|
|
.secure: true,
|
|
]) else {
|
|
Issue.record("Failed to create stored Cursor session cookie")
|
|
return
|
|
}
|
|
await store.setCookies([cookie])
|
|
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == nil)
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "WorkosCursorSessionToken=stored-session")
|
|
|
|
switch requestURL.path {
|
|
case "/api/usage-summary":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"membershipType": "pro",
|
|
"individualUsage": {
|
|
"plan": {
|
|
"used": 1500,
|
|
"limit": 5000,
|
|
"totalPercentUsed": 30.0
|
|
}
|
|
}
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
case "/api/auth/me":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"email":"stored@example.com","name":"Stored User"}"#,
|
|
statusCode: 200)
|
|
default:
|
|
Issue.record("Stored-session precedence test unexpectedly requested \(requestURL.path)")
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor.test"))
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let snapshot = try await CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken))).fetch()
|
|
|
|
#expect(snapshot.planPercentUsed == 30.0)
|
|
#expect(snapshot.accountEmail == "stored@example.com")
|
|
#expect(testSession.requestPaths.sorted() == [
|
|
"/api/auth/me",
|
|
"/api/usage-summary",
|
|
])
|
|
}
|
|
|
|
@Test
|
|
func `fetch with Cursor app auth preserves legacy request quotas`() async throws {
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let expectedCookie = "WorkosCursorSessionToken=user_test%3A%3A\(accessToken)"
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == expectedCookie)
|
|
|
|
switch requestURL.path {
|
|
case "/api/usage-summary":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"membershipType": "enterprise",
|
|
"individualUsage": {}
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
case "/api/auth/me":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"error":"temporary"}"#,
|
|
statusCode: 500)
|
|
case "/api/usage":
|
|
#expect(URLComponents(url: requestURL, resolvingAgainstBaseURL: false)?
|
|
.queryItems?.first(where: { $0.name == "user" })?.value == "user_test")
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: """
|
|
{
|
|
"gpt-4": {
|
|
"numRequests": 200,
|
|
"numRequestsTotal": 240,
|
|
"maxRequestUsage": 500
|
|
}
|
|
}
|
|
""",
|
|
statusCode: 200)
|
|
default:
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor.test"))
|
|
let probe = CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
urlSession: testSession.urlSession)
|
|
|
|
let snapshot = try await probe.fetchWithAppAuthSession(CursorAppAuthSession(accessToken: accessToken))
|
|
#expect(snapshot.requestsUsed == 240)
|
|
#expect(snapshot.requestsLimit == 500)
|
|
#expect(snapshot.toUsageSnapshot().primary?.usedPercent == 48)
|
|
#expect(snapshot.accountEmail == nil)
|
|
#expect(testSession.requestPaths.sorted() == [
|
|
"/api/auth/me",
|
|
"/api/usage",
|
|
"/api/usage-summary",
|
|
])
|
|
}
|
|
|
|
@Test
|
|
func `malformed Cursor app auth token is rejected before network access`() {
|
|
let session = CursorAppAuthSession(accessToken: "not-a-jwt")
|
|
#expect(throws: CursorStatusProbeError.self) {
|
|
_ = try session.cookieHeader()
|
|
}
|
|
#expect(!session.isUsable)
|
|
}
|
|
|
|
@Test
|
|
func `expired Cursor app auth token is skipped before network access`() async throws {
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
Issue.record("Expired app auth unexpectedly requested \(request.url?.path ?? "<unknown>")")
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
let accessToken = try makeCursorAppAuthToken(expiration: Date(timeIntervalSinceNow: -60))
|
|
let baseURL = try #require(URL(string: "https://cursor-web.test"))
|
|
let probe = CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken)))
|
|
|
|
await #expect(throws: CursorStatusProbeError.self) {
|
|
_ = try await probe.fetch(allowCachedSessions: false)
|
|
}
|
|
#expect(testSession.requestCount == 0)
|
|
}
|
|
|
|
@Test
|
|
func `Cursor app auth transient failure is preserved`() async throws {
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"error":"temporary"}"#,
|
|
statusCode: 500)
|
|
}
|
|
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let baseURL = try #require(URL(string: "https://cursor-web.test"))
|
|
let probe = CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken)))
|
|
|
|
do {
|
|
_ = try await probe.fetch(allowCachedSessions: false)
|
|
Issue.record("Expected Cursor.app auth request to fail")
|
|
} catch let error as CursorStatusProbeError {
|
|
guard case .networkError = error else {
|
|
Issue.record("Expected network error, got \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `cached session transient failure does not switch to Cursor app auth`() async throws {
|
|
CookieHeaderCache.store(provider: .cursor, cookieHeader: "cached=bad", sourceLabel: "test")
|
|
defer {
|
|
CookieHeaderCache.clear(provider: .cursor)
|
|
}
|
|
|
|
let accessToken = try makeCursorAppAuthToken()
|
|
let appCookie = "WorkosCursorSessionToken=user_test%3A%3A\(accessToken)"
|
|
let testSession = CursorStatusProbeTestSession { request in
|
|
let requestURL = try #require(request.url)
|
|
let cookie = request.value(forHTTPHeaderField: "Cookie")
|
|
switch requestURL.path {
|
|
case "/api/usage-summary" where cookie == "cached=bad":
|
|
return makeCursorStatusProbeResponse(
|
|
url: requestURL,
|
|
body: #"{"error":"temporary"}"#,
|
|
statusCode: 500)
|
|
case _ where cookie == appCookie:
|
|
Issue.record("Transient cached-session failure unexpectedly switched to Cursor.app auth")
|
|
throw URLError(.userAuthenticationRequired)
|
|
default:
|
|
throw URLError(.badURL)
|
|
}
|
|
}
|
|
|
|
let baseURL = try #require(URL(string: "https://cursor-web.test"))
|
|
let probe = CursorStatusProbe(
|
|
baseURL: baseURL,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
browserCookieImportOrder: [],
|
|
urlSession: testSession.urlSession,
|
|
appAuthStore: CursorAppAuthSessionProviderStub(session: CursorAppAuthSession(
|
|
accessToken: accessToken)))
|
|
|
|
await #expect(throws: CursorStatusProbeError.self) {
|
|
_ = try await probe.fetch()
|
|
}
|
|
#expect(testSession.requestCookies.contains("cached=bad"))
|
|
#expect(!testSession.requestCookies.contains(appCookie))
|
|
}
|
|
}
|
|
|
|
private func makeCursorAppAuthToken(
|
|
subject: String = "auth0|user_test",
|
|
expiration: Date = Date(timeIntervalSinceNow: 3600)) throws -> String
|
|
{
|
|
let payload = try JSONSerialization.data(
|
|
withJSONObject: [
|
|
"exp": Int(expiration.timeIntervalSince1970),
|
|
"sub": subject,
|
|
],
|
|
options: [.sortedKeys])
|
|
let encodedPayload = payload.base64EncodedString()
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
.replacingOccurrences(of: "=", with: "")
|
|
return "header.\(encodedPayload).signature"
|
|
}
|
|
|
|
private struct CursorAppAuthSessionProviderStub: CursorAppAuthSessionProviding {
|
|
let session: CursorAppAuthSession?
|
|
|
|
func loadSession() throws -> CursorAppAuthSession? {
|
|
self.session
|
|
}
|
|
}
|
|
|
|
final class CursorStatusProbeStubURLProtocol: URLProtocol {
|
|
private struct SessionState {
|
|
var requests: [URLRequest] = []
|
|
let handler: @Sendable (URLRequest) throws -> (HTTPURLResponse, Data)
|
|
}
|
|
|
|
private static let sessionHeader = "X-CodexBar-Cursor-Test-Session"
|
|
private static let lock = NSLock()
|
|
private nonisolated(unsafe) static var sessions: [String: SessionState] = [:]
|
|
|
|
static func configure(
|
|
_ configuration: URLSessionConfiguration,
|
|
handler: @escaping @Sendable (URLRequest) throws -> (HTTPURLResponse, Data)) -> String
|
|
{
|
|
let sessionID = UUID().uuidString
|
|
self.lock.lock()
|
|
self.sessions[sessionID] = SessionState(handler: handler)
|
|
self.lock.unlock()
|
|
configuration.httpAdditionalHeaders = [self.sessionHeader: sessionID]
|
|
return sessionID
|
|
}
|
|
|
|
static func removeSession(_ sessionID: String) {
|
|
self.lock.lock()
|
|
self.sessions.removeValue(forKey: sessionID)
|
|
self.lock.unlock()
|
|
}
|
|
|
|
static func requests(for sessionID: String) -> [URLRequest] {
|
|
self.lock.lock()
|
|
defer { Self.lock.unlock() }
|
|
return self.sessions[sessionID]?.requests ?? []
|
|
}
|
|
|
|
override static func canInit(with request: URLRequest) -> Bool {
|
|
true
|
|
}
|
|
|
|
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
request
|
|
}
|
|
|
|
override func startLoading() {
|
|
let handler: (@Sendable (URLRequest) throws -> (HTTPURLResponse, Data))?
|
|
let sessionID = self.request.value(forHTTPHeaderField: Self.sessionHeader)
|
|
Self.lock.lock()
|
|
if let sessionID, var state = Self.sessions[sessionID] {
|
|
state.requests.append(self.request)
|
|
handler = state.handler
|
|
Self.sessions[sessionID] = state
|
|
} else {
|
|
handler = nil
|
|
}
|
|
Self.lock.unlock()
|
|
|
|
do {
|
|
guard let handler else {
|
|
throw URLError(.cancelled)
|
|
}
|
|
let (response, data) = try handler(self.request)
|
|
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
self.client?.urlProtocol(self, didLoad: data)
|
|
self.client?.urlProtocolDidFinishLoading(self)
|
|
} catch {
|
|
self.client?.urlProtocol(self, didFailWithError: error)
|
|
}
|
|
}
|
|
|
|
override func stopLoading() {}
|
|
}
|