Files
steipete--codexbar/Tests/CodexBarTests/OpenAIDashboardFetcherCreditsWaitTests.swift
2026-07-13 12:22:33 +08:00

351 lines
13 KiB
Swift

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")
}
}