import Foundation import Testing @testable import CodexBarCore struct OpenAIDashboardFetcherCreditsWaitTests { @Test func `waits after scroll request`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForCreditsHistory(.init( now: now, anyDashboardSignalAt: now.addingTimeInterval(-10), creditsHeaderVisibleAt: nil, creditsHeaderPresent: false, creditsHeaderInViewport: false, didScrollToCredits: true)) #expect(shouldWait == true) } @Test func `waits briefly when header visible but table empty`() { let now = Date() let visibleAt = now.addingTimeInterval(-1.0) let shouldWait = OpenAIDashboardFetcher.shouldWaitForCreditsHistory(.init( now: now, anyDashboardSignalAt: now.addingTimeInterval(-10), creditsHeaderVisibleAt: visibleAt, creditsHeaderPresent: true, creditsHeaderInViewport: true, didScrollToCredits: false)) #expect(shouldWait == true) } @Test func `stops waiting after header has been visible long enough`() { let now = Date() let visibleAt = now.addingTimeInterval(-3.0) let shouldWait = OpenAIDashboardFetcher.shouldWaitForCreditsHistory(.init( now: now, anyDashboardSignalAt: now.addingTimeInterval(-10), creditsHeaderVisibleAt: visibleAt, creditsHeaderPresent: true, creditsHeaderInViewport: true, didScrollToCredits: false)) #expect(shouldWait == false) } @Test func `waits briefly after first dashboard signal even when header not present yet`() { let now = Date() let startedAt = now.addingTimeInterval(-2.0) let shouldWait = OpenAIDashboardFetcher.shouldWaitForCreditsHistory(.init( now: now, anyDashboardSignalAt: startedAt, creditsHeaderVisibleAt: nil, creditsHeaderPresent: false, creditsHeaderInViewport: false, didScrollToCredits: false)) #expect(shouldWait == true) } @Test func `stops waiting eventually when header never appears`() { let now = Date() let startedAt = now.addingTimeInterval(-7.0) let shouldWait = OpenAIDashboardFetcher.shouldWaitForCreditsHistory(.init( now: now, anyDashboardSignalAt: startedAt, creditsHeaderVisibleAt: nil, creditsHeaderPresent: false, creditsHeaderInViewport: false, didScrollToCredits: false)) #expect(shouldWait == false) } @Test func `usage breakdown recovery waits briefly after chart classification error`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForUsageBreakdownRecovery(.init( now: now, errorFirstSeenAt: now.addingTimeInterval(-1.0))) #expect(shouldWait == true) } @Test func `usage breakdown recovery stops blocking partial snapshots`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForUsageBreakdownRecovery(.init( now: now, errorFirstSeenAt: now.addingTimeInterval(-5.0))) #expect(shouldWait == false) } @Test func `probe waits briefly after reaching usage route without email or dashboard signals`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForProbeReadiness(.init( now: now, usageRouteSeenAt: now.addingTimeInterval(-1.0), dashboardSignalSeenAt: nil, signedInEmail: nil, hasDashboardSignal: false)) #expect(shouldWait == true) } @Test func `probe waits briefly for email after dashboard signals appear`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForProbeReadiness(.init( now: now, usageRouteSeenAt: now.addingTimeInterval(-3.0), dashboardSignalSeenAt: now.addingTimeInterval(-1.0), signedInEmail: nil, hasDashboardSignal: true)) #expect(shouldWait == true) } @Test func `probe stops waiting once signed in email is available`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForProbeReadiness(.init( now: now, usageRouteSeenAt: now.addingTimeInterval(-0.2), dashboardSignalSeenAt: now.addingTimeInterval(-0.2), signedInEmail: "user@example.com", hasDashboardSignal: true)) #expect(shouldWait == false) } @Test func `probe handoff preserves page only after confirmed signed in email`() { let result = OpenAIDashboardFetcher.ProbeResult( href: "https://chatgpt.com/codex/cloud/settings/analytics#usage", loginRequired: false, workspacePicker: false, cloudflareInterstitial: false, signedInEmail: "user@example.com", bodyText: "Credits remaining 42") #expect(OpenAIDashboardFetcher.shouldPreserveLoadedPageAfterProbe(result)) } @Test func `probe handoff does not preserve timed out usage page without email`() { let result = OpenAIDashboardFetcher.ProbeResult( href: "https://chatgpt.com/codex/cloud/settings/analytics#usage", loginRequired: false, workspacePicker: false, cloudflareInterstitial: false, signedInEmail: nil, bodyText: "Codex Analytics") #expect(!OpenAIDashboardFetcher.shouldPreserveLoadedPageAfterProbe(result)) } @Test func `probe grace restarts after route reload resets readiness timestamps`() { let now = Date() let shouldWait = OpenAIDashboardFetcher.shouldWaitForProbeReadiness(.init( now: now, usageRouteSeenAt: now, dashboardSignalSeenAt: nil, signedInEmail: nil, hasDashboardSignal: false)) #expect(shouldWait == true) } @Test func `sanitized timeout preserves positive caller deadline`() { #expect(OpenAIDashboardFetcher.sanitizedTimeout(60) == 60) #expect(OpenAIDashboardFetcher.sanitizedTimeout(25) == 25) #expect(OpenAIDashboardFetcher.sanitizedTimeout(0.5) == 0.5) } @Test func `sanitized timeout falls back for invalid values`() { #expect(OpenAIDashboardFetcher.sanitizedTimeout(0) == 1) #expect(OpenAIDashboardFetcher.sanitizedTimeout(-5) == 1) #expect(OpenAIDashboardFetcher.sanitizedTimeout(.infinity) == 1) #expect(OpenAIDashboardFetcher.sanitizedTimeout(.nan) == 1) } @Test func `deadline starts at call start and remaining timeout shrinks from there`() { let start = Date(timeIntervalSinceReferenceDate: 1000) let deadline = OpenAIDashboardFetcher.deadline(startingAt: start, timeout: 15) #expect(deadline.timeIntervalSince(start) == 15) let remaining = OpenAIDashboardFetcher.remainingTimeout( until: deadline, now: start.addingTimeInterval(14.5)) #expect(remaining == 0.5) } @Test func `remaining timeout does not go negative`() { let deadline = Date(timeIntervalSinceReferenceDate: 2000) let remaining = OpenAIDashboardFetcher.remainingTimeout( until: deadline, now: deadline.addingTimeInterval(3)) #expect(remaining == 0) } @Test func `usage route matcher accepts legacy settings route`() { #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/settings/usage")) } @Test func `usage route matcher accepts cloud settings route`() { #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/cloud/settings/usage")) } @Test func `usage route matcher accepts analytics route`() { #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/cloud/settings/analytics")) } @Test func `usage route matcher accepts analytics usage hash route`() { #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/cloud/settings/analytics#usage")) } @Test func `usage route matcher accepts trailing slash variants`() { #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/settings/usage/")) #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/cloud/settings/usage/")) #expect(OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex/cloud/settings/analytics/")) } @Test func `usage route matcher rejects unrelated routes`() { #expect(!OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/")) #expect(!OpenAIDashboardFetcher.isUsageRoute("https://chatgpt.com/codex")) #expect(!OpenAIDashboardFetcher.isUsageRoute(nil)) } @Test(arguments: [ ("https://chatgpt.com/#usage", true, false, false, false), ("https://chatgpt.com/", false, false, true, false), ("https://chatgpt.com/", false, false, false, true) ]) func `usage route reload skips blocking states`( href: String, loginRequired: Bool, workspacePicker: Bool, cloudflareInterstitial: Bool, expected: Bool) { #expect(OpenAIDashboardFetcher.shouldReloadUsageRoute( href: href, loginRequired: loginRequired, workspacePicker: workspacePicker, cloudflareInterstitial: cloudflareInterstitial) == expected) } @Test func `dashboard requests prefer English localization`() throws { let url = try #require(URL(string: "https://chatgpt.com/codex/cloud/settings/analytics#usage")) let request = OpenAIDashboardFetcher.usageURLRequest(url: url) #expect(request.value(forHTTPHeaderField: "Accept-Language") == "en-US,en;q=0.9") } @Test func `usage api request carries cookies and English localization`() { let request = OpenAIDashboardFetcher.dashboardUsageAPIRequest(cookieHeader: "a=b") #expect(request.url?.absoluteString == "https://chatgpt.com/backend-api/wham/usage") #expect(request.value(forHTTPHeaderField: "Cookie") == "a=b") #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") #expect(request.value(forHTTPHeaderField: "Accept-Language") == "en-US,en;q=0.9") #expect(request.cachePolicy == .reloadIgnoringLocalCacheData) } @Test func `identity api request carries cookies and English localization`() throws { let url = try #require(URL(string: "https://chatgpt.com/backend-api/me")) let request = OpenAIDashboardFetcher.dashboardIdentityAPIRequest(url: url, cookieHeader: "a=b") #expect(request.url?.absoluteString == "https://chatgpt.com/backend-api/me") #expect(request.value(forHTTPHeaderField: "Cookie") == "a=b") #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") #expect(request.value(forHTTPHeaderField: "Accept-Language") == "en-US,en;q=0.9") #expect(request.cachePolicy == .reloadIgnoringLocalCacheData) } @Test func `dashboard api requests accept shared deadline timeout clamps`() throws { let url = try #require(URL(string: "https://chatgpt.com/backend-api/me")) let usageRequest = OpenAIDashboardFetcher.dashboardUsageAPIRequest( cookieHeader: "a=b", timeout: 1.25) let identityRequest = OpenAIDashboardFetcher.dashboardIdentityAPIRequest( url: url, cookieHeader: "a=b", timeout: 0.75) #expect(usageRequest.timeoutInterval == 1.25) #expect(identityRequest.timeoutInterval == 0.75) } @Test func `usage api data maps language independent rate limits and credits`() throws { let json = """ { "plan_type": "pro", "rate_limit": { "primary_window": { "used_percent": 12, "reset_at": 1700003600, "limit_window_seconds": 18000 }, "secondary_window": { "used_percent": 34, "reset_at": 1700604800, "limit_window_seconds": 604800 } }, "credits": { "has_credits": true, "unlimited": false, "balance": 42.5 } } """ let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8)) let data = OpenAIDashboardFetcher.dashboardAPIData(from: response) #expect(data.primaryLimit?.usedPercent == 12) #expect(data.primaryLimit?.windowMinutes == 300) #expect(data.secondaryLimit?.usedPercent == 34) #expect(data.secondaryLimit?.windowMinutes == 10080) #expect(data.creditsRemaining == 42.5) #expect(data.accountPlan == "pro") #expect(data.hasUsageData) } @Test func `find first email searches nested api payloads`() { let json = """ { "accounts": [ { "profile": { "name": "Test" } }, { "profile": { "email": "nested@example.com" } } ] } """ #expect(OpenAIDashboardFetcher.findFirstEmail(inJSONData: Data(json.utf8)) == "nested@example.com") } }