import Foundation import Testing @testable import CodexBarCore /// Dashboard-path coverage for Codex `additional_rate_limits` (e.g. GPT-5.3-Codex-Spark): the /// OpenAI web dashboard usage API decodes the same `wham/usage` JSON as the OAuth path, so Spark /// limits must survive the `dashboardAPIData -> DashboardSnapshotComponents -> OpenAIDashboardSnapshot /// -> fromAttachedDashboard -> UsageSnapshot.extraRateWindows` chain without disturbing the /// existing primary/weekly/credits/plan mapping. struct OpenAIDashboardSparkTests { private static func response(from json: String) throws -> CodexUsageResponse { try JSONDecoder().decode(CodexUsageResponse.self, from: Data(json.utf8)) } @Test func `dashboard api data maps additional spark limit into extra windows`() throws { let json = """ { "plan_type": "pro", "rate_limit": { "primary_window": { "used_percent": 22, "reset_at": 1766948068, "limit_window_seconds": 18000 }, "secondary_window": { "used_percent": 43, "reset_at": 1767407914, "limit_window_seconds": 604800 } }, "additional_rate_limits": [ { "limit_name": "GPT-5.3-Codex-Spark", "metered_feature": "gpt_5_3_codex_spark", "rate_limit": { "primary_window": { "used_percent": 30, "reset_at": 1766948068, "limit_window_seconds": 18000 }, "secondary_window": { "used_percent": 100, "reset_at": 1767407914, "limit_window_seconds": 604800 } } } ] } """ let response = try Self.response(from: json) let apiData = OpenAIDashboardFetcher.dashboardAPIData(from: response) // Primary/weekly/credits/plan continue to map exactly as before. #expect(apiData.primaryLimit?.usedPercent == 22) #expect(apiData.secondaryLimit?.usedPercent == 43) #expect(apiData.accountPlan == "pro") // Spark surfaces with stable ids/titles for the additional 5-hour and weekly windows. #expect(apiData.extraRateWindows.count == 2) let spark = try #require(apiData.extraRateWindows.first) #expect(spark.id == "codex-spark") #expect(spark.title == "Codex Spark 5-hour") #expect(spark.window.usedPercent == 30) #expect(spark.window.windowMinutes == 300) #expect(spark.window.resetsAt != nil) let weekly = try #require(apiData.extraRateWindows.last) #expect(weekly.id == "codex-spark-weekly") #expect(weekly.title == "Codex Spark Weekly") #expect(weekly.window.usedPercent == 100) #expect(weekly.window.windowMinutes == 10080) #expect(weekly.window.resetsAt != nil) } @Test func `dashboard api data has empty extra windows when additional limits are absent`() throws { let json = """ { "rate_limit": { "primary_window": { "used_percent": 22, "reset_at": 1766948068, "limit_window_seconds": 18000 } } } """ let response = try Self.response(from: json) let apiData = OpenAIDashboardFetcher.dashboardAPIData(from: response) #expect(apiData.primaryLimit?.usedPercent == 22) #expect(apiData.extraRateWindows.isEmpty) } @Test func `dashboard api data tolerates non array additional limits while keeping primary`() throws { let json = """ { "rate_limit": { "primary_window": { "used_percent": 22, "reset_at": 1766948068, "limit_window_seconds": 18000 } }, "additional_rate_limits": "unexpected" } """ let response = try Self.response(from: json) let apiData = OpenAIDashboardFetcher.dashboardAPIData(from: response) #expect(apiData.primaryLimit?.usedPercent == 22) #expect(apiData.extraRateWindows.isEmpty) } @Test func `dashboard api data keeps valid spark when a malformed sibling is present`() throws { // Lossy per-element decode (shared with the OAuth path via CodexUsageResponse) means a single // malformed entry cannot discard its valid siblings. let json = """ { "rate_limit": { "primary_window": { "used_percent": 22, "reset_at": 1766948068, "limit_window_seconds": 18000 } }, "additional_rate_limits": [ "garbage-not-an-object", { "limit_name": "GPT-5.3-Codex-Spark", "metered_feature": "gpt_5_3_codex_spark", "rate_limit": { "primary_window": { "used_percent": 30, "reset_at": 1766948068, "limit_window_seconds": 18000 } } }, 42 ] } """ let response = try Self.response(from: json) let apiData = OpenAIDashboardFetcher.dashboardAPIData(from: response) #expect(apiData.primaryLimit?.usedPercent == 22) #expect(apiData.extraRateWindows.count == 1) #expect(apiData.extraRateWindows.first?.id == "codex-spark") #expect(apiData.extraRateWindows.first?.window.usedPercent == 30) } @Test func `dashboard snapshot exposes extra rate windows via to usage snapshot`() throws { let now = Date(timeIntervalSince1970: 1_766_948_000) let snapshot = OpenAIDashboardSnapshot( signedInEmail: "codex@example.com", codeReviewRemainingPercent: nil, creditEvents: [], dailyBreakdown: [], usageBreakdown: [], creditsPurchaseURL: nil, primaryLimit: RateWindow( usedPercent: 22, windowMinutes: 300, resetsAt: now.addingTimeInterval(3600), resetDescription: nil), secondaryLimit: RateWindow( usedPercent: 43, windowMinutes: 10080, resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), resetDescription: nil), extraRateWindows: [ NamedRateWindow( id: "codex-spark", title: "Codex Spark 5-hour", window: RateWindow( usedPercent: 30, windowMinutes: 300, resetsAt: now.addingTimeInterval(60 * 60), resetDescription: nil)), NamedRateWindow( id: "codex-spark-weekly", title: "Codex Spark Weekly", window: RateWindow( usedPercent: 100, windowMinutes: 10080, resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), resetDescription: nil)), ], updatedAt: now) let usage = try #require(snapshot.toUsageSnapshot(provider: .codex)) // Primary/weekly behavior preserved. #expect(usage.primary?.usedPercent == 22) #expect(usage.secondary?.usedPercent == 43) // Spark surfaces through UsageSnapshot.extraRateWindows for dashboard-source users. let extras = try #require(usage.extraRateWindows) #expect(extras.map(\.id) == ["codex-spark", "codex-spark-weekly"]) #expect(extras.first?.window.usedPercent == 30) #expect(extras.last?.window.usedPercent == 100) } @Test func `dashboard snapshot codable round trips extra rate windows`() throws { let now = Date(timeIntervalSince1970: 1_766_948_000) let snapshot = OpenAIDashboardSnapshot( signedInEmail: nil, codeReviewRemainingPercent: nil, creditEvents: [], dailyBreakdown: [], usageBreakdown: [], creditsPurchaseURL: nil, extraRateWindows: [ NamedRateWindow( id: "codex-spark", title: "Codex Spark 5-hour", window: RateWindow( usedPercent: 30, windowMinutes: 300, resetsAt: now.addingTimeInterval(60 * 60), resetDescription: nil)), NamedRateWindow( id: "codex-spark-weekly", title: "Codex Spark Weekly", window: RateWindow( usedPercent: 100, windowMinutes: 10080, resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), resetDescription: nil)), ], updatedAt: now) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let data = try encoder.encode(snapshot) let decoded = try decoder.decode(OpenAIDashboardSnapshot.self, from: data) #expect(decoded.extraRateWindows?.map(\.id) == ["codex-spark", "codex-spark-weekly"]) #expect(decoded.extraRateWindows?.first?.window.usedPercent == 30) #expect(decoded.extraRateWindows?.last?.window.usedPercent == 100) } @Test func `dashboard snapshot decoder preserves absence of extra rate windows`() throws { // Older cached snapshots predate the field; decoding such payloads must yield nil and never // throw, so existing dashboard caches keep working. let json = """ { "signedInEmail": "codex@example.com", "codeReviewRemainingPercent": null, "creditEvents": [], "dailyBreakdown": [], "usageBreakdown": [], "creditsPurchaseURL": null, "updatedAt": "2026-04-30T19:27:07Z" } """ let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let snapshot = try decoder.decode(OpenAIDashboardSnapshot.self, from: Data(json.utf8)) #expect(snapshot.extraRateWindows == nil) } }