1023 lines
41 KiB
Swift
1023 lines
41 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Observation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@MainActor
|
|
struct UsageStoreCoverageTests {
|
|
private final class ObservationFlag: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var value = false
|
|
|
|
func set() {
|
|
self.lock.lock()
|
|
self.value = true
|
|
self.lock.unlock()
|
|
}
|
|
|
|
func get() -> Bool {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.value
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `provider with highest usage and icon style`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-highest")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
|
|
try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true)
|
|
try settings.setProviderEnabled(provider: .factory, metadata: #require(metadata[.factory]), enabled: true)
|
|
try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: true)
|
|
|
|
let now = Date()
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: now),
|
|
provider: .codex)
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 70, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
updatedAt: now),
|
|
provider: .factory)
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: now),
|
|
provider: .claude)
|
|
|
|
let highest = store.providerWithHighestUsage()
|
|
#expect(highest?.provider == .factory)
|
|
#expect(highest?.usedPercent == 70)
|
|
#expect(store.iconStyle == .combined)
|
|
|
|
try settings.setProviderEnabled(provider: .factory, metadata: #require(metadata[.factory]), enabled: false)
|
|
try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: false)
|
|
#expect(store.iconStyle == store.style(for: .codex))
|
|
|
|
store._setErrorForTesting("error", provider: .codex)
|
|
#expect(store.isStale)
|
|
}
|
|
|
|
@Test
|
|
func `source label adds open AI web`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-source")
|
|
settings.debugDisableKeychainAccess = false
|
|
settings.codexUsageDataSource = .oauth
|
|
settings.codexCookieSource = .manual
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
store.openAIDashboard = OpenAIDashboardSnapshot(
|
|
signedInEmail: "user@example.com",
|
|
codeReviewRemainingPercent: nil,
|
|
creditEvents: [],
|
|
dailyBreakdown: [],
|
|
usageBreakdown: [],
|
|
creditsPurchaseURL: nil,
|
|
updatedAt: Date())
|
|
store.openAIDashboardRequiresLogin = false
|
|
|
|
let label = store.sourceLabel(for: .codex)
|
|
#expect(label.contains("openai-web"))
|
|
}
|
|
|
|
@Test
|
|
func `amp balances are rendered in provider cards`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-amp-credits")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let now = Date()
|
|
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: 51.4,
|
|
windowMinutes: 1440,
|
|
resetsAt: now.addingTimeInterval(12 * 3600),
|
|
resetDescription: nil),
|
|
secondary: nil,
|
|
ampUsage: AmpUsageDetails(
|
|
individualCredits: 25.64,
|
|
workspaceBalances: [AmpWorkspaceBalance(name: "billing@example.test", remaining: 10.22)]),
|
|
updatedAt: now),
|
|
provider: .amp)
|
|
let model = ProvidersPane(settings: settings, store: store)._test_menuCardModel(for: .amp)
|
|
|
|
#expect(model.creditsText == "Individual credits: $25.64\nWorkspace billing@example.test: $10.22")
|
|
#expect(model.creditsRemaining == nil)
|
|
|
|
settings.hidePersonalInfo = true
|
|
let redactedModel = ProvidersPane(settings: settings, store: store)._test_menuCardModel(for: .amp)
|
|
#expect(redactedModel.creditsText == "Individual credits: $25.64\nWorkspace: $10.22")
|
|
}
|
|
|
|
@Test
|
|
func `account info caches codex auth parsing until config revision changes`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-account-info-cache")
|
|
let home = FileManager.default.temporaryDirectory.appendingPathComponent(
|
|
"usage-store-account-info-\(UUID().uuidString)",
|
|
isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: home) }
|
|
|
|
try Self.writeCodexAuthFile(homeURL: home, email: "first@example.com", plan: "plus")
|
|
let env = ["CODEX_HOME": home.path]
|
|
settings._test_codexReconciliationEnvironment = env
|
|
defer { settings._test_codexReconciliationEnvironment = nil }
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(environment: env),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
startupBehavior: .testing,
|
|
environmentBase: env)
|
|
|
|
let first = store.accountInfo(for: .codex)
|
|
try Self.writeCodexAuthFile(homeURL: home, email: "second@example.com", plan: "pro")
|
|
let cached = store.accountInfo(for: .codex)
|
|
settings.configRevision &+= 1
|
|
let refreshed = store.accountInfo(for: .codex)
|
|
|
|
#expect(first.email == "first@example.com")
|
|
#expect(cached.email == "first@example.com")
|
|
#expect(refreshed.email == "second@example.com")
|
|
}
|
|
|
|
@Test
|
|
func `source label uses configured kilo source`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-kilo-source")
|
|
settings.kiloUsageDataSource = .api
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
#expect(store.sourceLabel(for: .kilo) == "api")
|
|
}
|
|
|
|
@Test
|
|
func `clearing copilot budget extras syncs reset baseline`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-copilot-budget-clear")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let live = Self.makeCopilotSnapshot(usedPercent: 20, extraRateWindows: [Self.makeCopilotBudgetWindow()])
|
|
let resetBaseline = Self.makeCopilotSnapshot(usedPercent: 10, extraRateWindows: nil)
|
|
store._setSnapshotForTesting(live, provider: .copilot)
|
|
store.lastKnownResetSnapshots[.copilot] = resetBaseline
|
|
|
|
store.clearCopilotBudgetExtras()
|
|
|
|
#expect(store.snapshot(for: .copilot)?.extraRateWindows == nil)
|
|
#expect(store.lastKnownResetSnapshots[.copilot]?.extraRateWindows == nil)
|
|
#expect(store.lastKnownResetSnapshots[.copilot]?.primary?.usedPercent == 20)
|
|
}
|
|
|
|
@Test
|
|
func `clearing copilot budget extras also clears stale reset baseline`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-copilot-budget-reset-clear")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let live = Self.makeCopilotSnapshot(usedPercent: 20, extraRateWindows: nil)
|
|
let resetBaseline = Self.makeCopilotSnapshot(
|
|
usedPercent: 10,
|
|
extraRateWindows: [Self.makeCopilotBudgetWindow()])
|
|
store._setSnapshotForTesting(live, provider: .copilot)
|
|
store.lastKnownResetSnapshots[.copilot] = resetBaseline
|
|
|
|
store.clearCopilotBudgetExtras()
|
|
|
|
#expect(store.snapshot(for: .copilot)?.extraRateWindows == nil)
|
|
#expect(store.snapshot(for: .copilot)?.primary?.usedPercent == 20)
|
|
#expect(store.lastKnownResetSnapshots[.copilot]?.extraRateWindows == nil)
|
|
#expect(store.lastKnownResetSnapshots[.copilot]?.primary?.usedPercent == 10)
|
|
}
|
|
|
|
@Test
|
|
func `permission prompt errors are detected for notifications`() {
|
|
let errors: [LocalizedTestError] = [
|
|
LocalizedTestError("Waiting for folder trust prompt"),
|
|
LocalizedTestError("Permission prompt is waiting in the CLI"),
|
|
]
|
|
|
|
for error in errors {
|
|
#expect(UsageStore.isPermissionPromptWaiting(error))
|
|
}
|
|
#expect(!UsageStore.isPermissionPromptWaiting(LocalizedTestError("network timeout")))
|
|
}
|
|
|
|
@Test
|
|
func `provider with highest usage prefers kimi rate limit window`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-kimi-highest")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
|
|
try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true)
|
|
try settings.setProviderEnabled(provider: .kimi, metadata: #require(metadata[.kimi]), enabled: true)
|
|
|
|
let now = Date()
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: now),
|
|
provider: .codex)
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 80, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
updatedAt: now),
|
|
provider: .kimi)
|
|
|
|
let highest = store.providerWithHighestUsage()
|
|
#expect(highest?.provider == .kimi)
|
|
#expect(highest?.usedPercent == 80)
|
|
}
|
|
|
|
@Test
|
|
func `provider availability and subscription detection`() {
|
|
let zaiStore = InMemoryZaiTokenStore(value: "zai-token")
|
|
let syntheticStore = InMemorySyntheticTokenStore(value: "synthetic-token")
|
|
let settings = Self.makeSettingsStore(
|
|
suite: "UsageStoreCoverageTests-availability",
|
|
zaiTokenStore: zaiStore,
|
|
syntheticTokenStore: syntheticStore)
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
|
|
#expect(store.isProviderAvailable(.zai))
|
|
#expect(store.isProviderAvailable(.synthetic))
|
|
|
|
let identity = ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: nil,
|
|
accountOrganization: nil,
|
|
loginMethod: "Pro")
|
|
store._setSnapshotForTesting(
|
|
UsageSnapshot(primary: nil, secondary: nil, updatedAt: Date(), identity: identity),
|
|
provider: .claude)
|
|
#expect(store.isClaudeSubscription())
|
|
#expect(UsageStore.isSubscriptionPlan("Team"))
|
|
#expect(!UsageStore.isSubscriptionPlan("api"))
|
|
}
|
|
|
|
@Test
|
|
func `background refresh only tracks enabled providers`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-background-refresh")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let staleSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
store._setSnapshotForTesting(staleSnapshot, provider: .claude)
|
|
store._setErrorForTesting("stale", provider: .claude)
|
|
store.statuses[.claude] = ProviderStatus(indicator: .major, description: "Outage", updatedAt: Date())
|
|
store.statusComponents[.claude] = [
|
|
ProviderStatusComponent(id: "api", name: "API", indicator: .major, status: "major_outage"),
|
|
]
|
|
|
|
#expect(store.enabledProviders() == [.codex])
|
|
|
|
store.clearDisabledProviderState(enabledProviders: Set(store.enabledProvidersForDisplay()))
|
|
|
|
#expect(store.snapshot(for: .claude) == nil)
|
|
#expect(store.errors[.claude] == nil)
|
|
#expect(store.statuses[.claude] == nil)
|
|
#expect(store.statusComponents(for: .claude).isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `cleanup preserves enabled but unavailable provider state`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-preserve-unavailable")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let staleSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
store._setSnapshotForTesting(staleSnapshot, provider: .synthetic)
|
|
store._setErrorForTesting("stale", provider: .synthetic)
|
|
store.statuses[.synthetic] = ProviderStatus(indicator: .major, description: "Outage", updatedAt: Date())
|
|
|
|
#expect(store.enabledProviders().isEmpty)
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
|
|
store.clearDisabledProviderState(enabledProviders: Set(store.enabledProvidersForDisplay()))
|
|
|
|
#expect(store.snapshot(for: .synthetic) != nil)
|
|
#expect(store.errors[.synthetic] == "stale")
|
|
#expect(store.statuses[.synthetic]?.indicator == .major)
|
|
}
|
|
|
|
@Test
|
|
func `background work excludes enabled but unavailable providers`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-background-unavailable")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
#expect(store.enabledProviders().isEmpty)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `visible unavailable provider gets explicit user facing state`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-unavailable-message")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
|
|
#expect(store.errors[.synthetic] == nil)
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
#expect(store.isProviderAvailable(.synthetic) == false)
|
|
#expect(store.userFacingError(for: .synthetic) == SyntheticSettingsError.missingToken.errorDescription)
|
|
#expect(store.unavailableMessage(for: .synthetic) == SyntheticSettingsError.missingToken.errorDescription)
|
|
}
|
|
}
|
|
|
|
extension UsageStoreCoverageTests {
|
|
@Test
|
|
func `sub2api unavailable message identifies the missing setting`() throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-sub2api-unavailable-message")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: provider == .sub2api)
|
|
}
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
#expect(store.unavailableMessage(for: .sub2api) == Sub2APIUsageError.missingCredentials.errorDescription)
|
|
|
|
settings.sub2APIAPIKey = "group-key"
|
|
#expect(store.unavailableMessage(for: .sub2api) == Sub2APIUsageError.missingBaseURL.errorDescription)
|
|
}
|
|
|
|
@Test
|
|
func `refresh clears enabled but unavailable cached state`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-background-cleanup")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let cachedSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
store._setSnapshotForTesting(cachedSnapshot, provider: .synthetic)
|
|
let account = ProviderTokenAccount(id: UUID(), label: "Account", token: "token", addedAt: 0, lastUsed: nil)
|
|
store.accountSnapshots[.synthetic] = [
|
|
TokenAccountUsageSnapshot(
|
|
account: account,
|
|
snapshot: cachedSnapshot,
|
|
error: nil,
|
|
sourceLabel: "api",
|
|
cacheKey: store.tokenAccountSnapshotCacheKey(provider: .synthetic, account: account)),
|
|
]
|
|
store._setTokenSnapshotForTesting(
|
|
CostUsageTokenSnapshot(
|
|
sessionTokens: 10,
|
|
sessionCostUSD: 1.23,
|
|
last30DaysTokens: 100,
|
|
last30DaysCostUSD: 4.56,
|
|
daily: [],
|
|
updatedAt: Date()),
|
|
provider: .synthetic)
|
|
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
#expect(store.enabledProviders().isEmpty)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
|
|
await store.refresh()
|
|
#expect(store.snapshot(for: .synthetic) == nil)
|
|
#expect((store.accountSnapshots[.synthetic] ?? []).isEmpty)
|
|
#expect(store.tokenSnapshots[.synthetic] == nil)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `refresh clears enabled but unavailable failure state`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-background-failure-cleanup")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
store._setErrorForTesting("stale", provider: .synthetic)
|
|
store.statuses[.synthetic] = ProviderStatus(indicator: .major, description: "Outage", updatedAt: Date())
|
|
store.statusComponents[.synthetic] = [
|
|
ProviderStatusComponent(id: "api", name: "API", indicator: .major, status: "major_outage"),
|
|
]
|
|
store.tokenErrors[.synthetic] = "token stale"
|
|
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
#expect(store.enabledProviders().isEmpty)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
|
|
await store.refresh()
|
|
|
|
#expect(store.errors[.synthetic] == nil)
|
|
#expect(store.tokenErrors[.synthetic] == nil)
|
|
#expect(store.statuses[.synthetic] == nil)
|
|
#expect(store.statusComponents(for: .synthetic).isEmpty)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `widget snapshot projects provider derived token usage`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-widget-provider-cost")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let day = MistralDailyUsageBucket(
|
|
day: "2026-05-26",
|
|
cost: 1.2,
|
|
inputTokens: 10,
|
|
cachedTokens: 0,
|
|
outputTokens: 5,
|
|
models: [])
|
|
store._setSnapshotForTesting(MistralUsageSnapshot(
|
|
totalCost: 9,
|
|
currency: "eur",
|
|
currencySymbol: "€",
|
|
totalInputTokens: 10,
|
|
totalOutputTokens: 5,
|
|
totalCachedTokens: 0,
|
|
modelCount: 1,
|
|
daily: [day],
|
|
startDate: nil,
|
|
endDate: nil,
|
|
updatedAt: Date()).toUsageSnapshot(), provider: .mistral)
|
|
|
|
var widgetSnapshots: [WidgetSnapshot] = []
|
|
store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) }
|
|
defer { store._test_widgetSnapshotSaveOverride = nil }
|
|
|
|
store.persistWidgetSnapshot(reason: "provider-cost")
|
|
await store.widgetSnapshotPersistTask?.value
|
|
|
|
let mistralEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .mistral })
|
|
#expect(mistralEntry.tokenUsage?.currencyCode == "EUR")
|
|
#expect(mistralEntry.tokenUsage?.sessionLabel == "Latest billing day")
|
|
#expect(mistralEntry.tokenUsage?.last30DaysLabel == "This month")
|
|
#expect(mistralEntry.tokenUsage?.last30DaysCostUSD == 9)
|
|
}
|
|
|
|
@Test
|
|
func `unavailable provider with only cached status gets single cleanup pass`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-background-status-cleanup")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = true
|
|
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: false)
|
|
}
|
|
try settings.setProviderEnabled(
|
|
provider: .synthetic,
|
|
metadata: #require(metadata[.synthetic]),
|
|
enabled: true)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
store.statuses[.synthetic] = ProviderStatus(indicator: .major, description: "Outage", updatedAt: Date())
|
|
|
|
#expect(store.enabledProvidersForDisplay() == [.synthetic])
|
|
#expect(store.enabledProviders().isEmpty)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
|
|
await store.refresh()
|
|
|
|
#expect(store.statuses[.synthetic] == nil)
|
|
#expect(store.enabledProvidersForBackgroundWork().isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `status indicators and failure gate`() {
|
|
#expect(!ProviderStatusIndicator.none.hasIssue)
|
|
#expect(ProviderStatusIndicator.maintenance.hasIssue)
|
|
CodexBarLocalizationOverride.$appLanguage.withValue("en") {
|
|
#expect(ProviderStatusIndicator.unknown.label == "Status unknown")
|
|
}
|
|
|
|
var gate = ConsecutiveFailureGate()
|
|
let first = gate.shouldSurfaceError(onFailureWithPriorData: true)
|
|
#expect(!first)
|
|
let second = gate.shouldSurfaceError(onFailureWithPriorData: true)
|
|
#expect(second)
|
|
gate.recordSuccess()
|
|
let third = gate.shouldSurfaceError(onFailureWithPriorData: false)
|
|
#expect(third)
|
|
gate.reset()
|
|
#expect(gate.streak == 0)
|
|
}
|
|
|
|
@Test
|
|
func `token account error message ignores cancellation`() {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-token-account-cancel")
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
|
|
#expect(store.tokenAccountErrorMessage(CancellationError()) == nil)
|
|
#expect(store.tokenAccountErrorMessage(ProviderFetchError.noAvailableStrategy(.copilot)) != nil)
|
|
}
|
|
|
|
@Test
|
|
func `isPreservableNetworkTransportError classifies transport failures correctly`() {
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotFindHost)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotConnectToHost)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorDNSLookupFailed)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorNetworkConnectionLost)))
|
|
#expect(UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled)))
|
|
#expect(!UsageStore.isPreservableNetworkTransportError(
|
|
NSError(domain: NSCocoaErrorDomain, code: 0)))
|
|
}
|
|
|
|
@Test
|
|
func `background work settings observation ignores menu provider selection churn`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-switcher-selection-observation")
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = true
|
|
try Self.enableOnly(.codex, settings: settings)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
settings.selectedMenuProvider = .codex
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(didChange.get() == false)
|
|
|
|
let refreshDidChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
refreshDidChange.set()
|
|
}
|
|
|
|
settings.refreshFrequency = .oneMinute
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(refreshDidChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `background work settings observation ignores display only settings churn`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-display-only-observation")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.mergeIcons = false
|
|
settings.randomBlinkEnabled = false
|
|
settings.usageBarsShowUsed = false
|
|
settings.showOptionalCreditsAndExtraUsage = false
|
|
try Self.enableOnly(.codex, settings: settings)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
settings.usageBarsShowUsed = true
|
|
settings.mergeIcons = true
|
|
settings.randomBlinkEnabled = true
|
|
settings.codexSparkUsageVisible.toggle()
|
|
settings.debugLoadingPattern = .pulse
|
|
settings.setProviderOrder(Array(settings.orderedProviders().reversed()))
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(didChange.get() == false)
|
|
|
|
let refreshDidChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
refreshDidChange.set()
|
|
}
|
|
|
|
settings.statusChecksEnabled = true
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(refreshDidChange.get() == true)
|
|
|
|
let layoutDidChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
layoutDidChange.set()
|
|
}
|
|
|
|
settings.multiAccountMenuLayout = .stacked
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(layoutDidChange.get() == true)
|
|
|
|
let optionalUsageDidChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
optionalUsageDidChange.set()
|
|
}
|
|
|
|
settings.showOptionalCreditsAndExtraUsage = true
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(optionalUsageDidChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `display only settings do not invoke provider refresh while background work is active`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-display-only-no-provider-refresh")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.mergeIcons = false
|
|
settings.randomBlinkEnabled = false
|
|
settings.usageBarsShowUsed = false
|
|
try Self.enableOnly(.codex, settings: settings)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
var refreshedProviders: [UsageProvider] = []
|
|
store._test_providerRefreshOverride = { refreshedProviders.append($0) }
|
|
defer { store._test_providerRefreshOverride = nil }
|
|
|
|
func observeBackgroundSettingsForTest() {
|
|
withObservationTracking {
|
|
_ = store.backgroundWorkSettingsObservationToken
|
|
} onChange: {
|
|
Task { @MainActor in
|
|
await store.refreshForSettingsChange()
|
|
}
|
|
}
|
|
}
|
|
|
|
observeBackgroundSettingsForTest()
|
|
|
|
settings.usageBarsShowUsed = true
|
|
settings.mergeIcons = true
|
|
settings.randomBlinkEnabled = true
|
|
settings.codexSparkUsageVisible.toggle()
|
|
settings.debugLoadingPattern = .pulse
|
|
settings.setProviderOrder(Array(settings.orderedProviders().reversed()))
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(refreshedProviders.isEmpty)
|
|
|
|
settings.codexUsageDataSource = .cli
|
|
for _ in 0..<20 where !refreshedProviders.contains(.codex) {
|
|
try? await Task.sleep(nanoseconds: 25_000_000)
|
|
}
|
|
#expect(refreshedProviders.contains(.codex))
|
|
}
|
|
|
|
@Test
|
|
func `startup status network failure schedules bounded retry`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-startup-status-retry")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = true
|
|
try Self.enableOnly(.codex, settings: settings)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
store._test_providerRefreshOverride = { _ in }
|
|
defer { store._test_providerRefreshOverride = nil }
|
|
store._test_providerStatusFetchOverride = { _ in
|
|
throw URLError(.notConnectedToInternet)
|
|
}
|
|
defer { store._test_providerStatusFetchOverride = nil }
|
|
|
|
var scheduled: [(attempt: Int, delay: TimeInterval)] = []
|
|
store._test_startupConnectivityRetryScheduled = { attempt, delay in
|
|
scheduled.append((attempt, delay))
|
|
}
|
|
defer { store._test_startupConnectivityRetryScheduled = nil }
|
|
|
|
await store.refresh()
|
|
defer {
|
|
store.startupConnectivityRetryTask?.cancel()
|
|
store.startupConnectivityRetryTask = nil
|
|
}
|
|
|
|
#expect(scheduled.map(\.attempt) == [1])
|
|
#expect(scheduled.map(\.delay) == [15])
|
|
#expect(store.statuses[.codex]?.indicator == .unknown)
|
|
#expect(store.statuses[.codex]?.description?.isEmpty == false)
|
|
}
|
|
|
|
@Test
|
|
func `startup connectivity retry refreshes status and clears retry task after recovery`() async throws {
|
|
let settings = Self.makeSettingsStore(suite: "UsageStoreCoverageTests-startup-status-recovery")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = true
|
|
try Self.enableOnly(.codex, settings: settings)
|
|
|
|
let store = Self.makeUsageStore(settings: settings)
|
|
store._test_providerRefreshOverride = { _ in }
|
|
defer { store._test_providerRefreshOverride = nil }
|
|
|
|
var statusAttempts = 0
|
|
store._test_providerStatusFetchOverride = { _ in
|
|
statusAttempts += 1
|
|
if statusAttempts == 1 {
|
|
throw URLError(.cannotFindHost)
|
|
}
|
|
return ProviderStatus(indicator: .none, description: "Operational", updatedAt: Date())
|
|
}
|
|
defer { store._test_providerStatusFetchOverride = nil }
|
|
|
|
let sleepGate = StartupConnectivityRetrySleepGate()
|
|
store._test_startupConnectivityRetrySleepOverride = { delay in
|
|
try await sleepGate.sleep(delay)
|
|
}
|
|
defer { store._test_startupConnectivityRetrySleepOverride = nil }
|
|
|
|
await store.refresh()
|
|
await sleepGate.waitUntilSleeping()
|
|
let retryTask = try #require(store.startupConnectivityRetryTask)
|
|
|
|
await sleepGate.resume()
|
|
await retryTask.value
|
|
|
|
#expect(statusAttempts == 2)
|
|
#expect(store.statuses[.codex]?.indicator == ProviderStatusIndicator.none)
|
|
#expect(store.statuses[.codex]?.description == "Operational")
|
|
#expect(store.startupConnectivityRetryTask == nil)
|
|
}
|
|
|
|
@Test
|
|
func `startup connectivity retry classification is bounded and excludes cancellation`() {
|
|
#expect(UsageStore.startupConnectivityRetryDelay(forAttempt: 1) == 15)
|
|
#expect(UsageStore.startupConnectivityRetryDelay(forAttempt: 4) == 300)
|
|
#expect(UsageStore.startupConnectivityRetryDelay(forAttempt: 5) == nil)
|
|
#expect(UsageStore.isStartupConnectivityRetryableError(URLError(.timedOut)))
|
|
#expect(UsageStore.isStartupConnectivityRetryableError(URLError(.notConnectedToInternet)))
|
|
#expect(!UsageStore.isStartupConnectivityRetryableError(URLError(.cancelled)))
|
|
#expect(!UsageStore.isStartupConnectivityRetryableError(CancellationError()))
|
|
}
|
|
|
|
private static func makeSettingsStore(
|
|
suite: String,
|
|
zaiTokenStore: any ZaiTokenStoring = NoopZaiTokenStore(),
|
|
syntheticTokenStore: any SyntheticTokenStoring = NoopSyntheticTokenStore())
|
|
-> SettingsStore
|
|
{
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let settings = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: zaiTokenStore,
|
|
syntheticTokenStore: syntheticTokenStore,
|
|
codexCookieStore: InMemoryCookieHeaderStore(),
|
|
claudeCookieStore: InMemoryCookieHeaderStore(),
|
|
cursorCookieStore: InMemoryCookieHeaderStore(),
|
|
opencodeCookieStore: InMemoryCookieHeaderStore(),
|
|
factoryCookieStore: InMemoryCookieHeaderStore(),
|
|
minimaxCookieStore: InMemoryMiniMaxCookieStore(),
|
|
minimaxAPITokenStore: InMemoryMiniMaxAPITokenStore(),
|
|
kimiTokenStore: InMemoryKimiTokenStore(),
|
|
kimiK2TokenStore: InMemoryKimiK2TokenStore(),
|
|
augmentCookieStore: InMemoryCookieHeaderStore(),
|
|
ampCookieStore: InMemoryCookieHeaderStore(),
|
|
copilotTokenStore: InMemoryCopilotTokenStore(),
|
|
tokenAccountStore: InMemoryTokenAccountStore())
|
|
settings.providerDetectionCompleted = true
|
|
return settings
|
|
}
|
|
|
|
private static func makeUsageStore(settings: SettingsStore) -> UsageStore {
|
|
UsageStore(
|
|
fetcher: UsageFetcher(environment: [:]),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
environmentBase: [:])
|
|
}
|
|
|
|
private static func writeCodexAuthFile(homeURL: URL, email: String, plan: String) throws {
|
|
try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true)
|
|
let auth = try [
|
|
"tokens": [
|
|
"accessToken": "access-token",
|
|
"refreshToken": "refresh-token",
|
|
"idToken": Self.fakeCodexJWT(email: email, plan: plan),
|
|
],
|
|
]
|
|
let data = try JSONSerialization.data(withJSONObject: auth)
|
|
try data.write(to: homeURL.appendingPathComponent("auth.json"), options: .atomic)
|
|
}
|
|
|
|
private static func fakeCodexJWT(email: String, plan: String) throws -> String {
|
|
let header = try JSONSerialization.data(withJSONObject: ["alg": "none"])
|
|
let payload = try JSONSerialization.data(withJSONObject: [
|
|
"email": email,
|
|
"chatgpt_plan_type": plan,
|
|
"https://api.openai.com/auth": [
|
|
"chatgpt_plan_type": plan,
|
|
],
|
|
])
|
|
return "\(Self.base64URL(header)).\(Self.base64URL(payload))."
|
|
}
|
|
|
|
private static func base64URL(_ data: Data) -> String {
|
|
data.base64EncodedString()
|
|
.replacingOccurrences(of: "=", with: "")
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
}
|
|
|
|
private static func makeCopilotSnapshot(
|
|
usedPercent: Double,
|
|
extraRateWindows: [NamedRateWindow]?) -> UsageSnapshot
|
|
{
|
|
UsageSnapshot(
|
|
primary: RateWindow(usedPercent: usedPercent, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
extraRateWindows: extraRateWindows,
|
|
updatedAt: Date(timeIntervalSince1970: 1_780_358_400))
|
|
}
|
|
|
|
private static func makeCopilotBudgetWindow() -> NamedRateWindow {
|
|
NamedRateWindow(
|
|
id: "copilot-budget-test",
|
|
title: "Budget - Copilot",
|
|
window: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil))
|
|
}
|
|
|
|
private static func enableOnly(_ enabledProvider: UsageProvider, settings: SettingsStore) throws {
|
|
let metadata = ProviderRegistry.shared.metadata
|
|
for provider in UsageProvider.allCases {
|
|
try settings.setProviderEnabled(
|
|
provider: provider,
|
|
metadata: #require(metadata[provider]),
|
|
enabled: provider == enabledProvider)
|
|
}
|
|
}
|
|
}
|
|
|
|
private actor StartupConnectivityRetrySleepGate {
|
|
private var continuation: CheckedContinuation<Void, Error>?
|
|
private var waiters: [CheckedContinuation<Void, Never>] = []
|
|
|
|
func sleep(_ delay: TimeInterval) async throws {
|
|
#expect(delay == 15)
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
self.continuation = continuation
|
|
self.resumeWaiters()
|
|
}
|
|
}
|
|
|
|
func waitUntilSleeping() async {
|
|
if self.continuation != nil {
|
|
return
|
|
}
|
|
await withCheckedContinuation { continuation in
|
|
self.waiters.append(continuation)
|
|
}
|
|
}
|
|
|
|
func resume() {
|
|
self.continuation?.resume()
|
|
self.continuation = nil
|
|
}
|
|
|
|
private func resumeWaiters() {
|
|
let waiters = self.waiters
|
|
self.waiters.removeAll()
|
|
for waiter in waiters {
|
|
waiter.resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class InMemoryZaiTokenStore: ZaiTokenStoring, @unchecked Sendable {
|
|
var value: String?
|
|
|
|
init(value: String? = nil) {
|
|
self.value = value
|
|
}
|
|
|
|
func loadToken() throws -> String? {
|
|
self.value
|
|
}
|
|
|
|
func storeToken(_ token: String?) throws {
|
|
self.value = token
|
|
}
|
|
}
|
|
|
|
private final class InMemorySyntheticTokenStore: SyntheticTokenStoring, @unchecked Sendable {
|
|
var value: String?
|
|
|
|
init(value: String? = nil) {
|
|
self.value = value
|
|
}
|
|
|
|
func loadToken() throws -> String? {
|
|
self.value
|
|
}
|
|
|
|
func storeToken(_ token: String?) throws {
|
|
self.value = token
|
|
}
|
|
}
|
|
|
|
private struct LocalizedTestError: LocalizedError {
|
|
let message: String
|
|
|
|
init(_ message: String) {
|
|
self.message = message
|
|
}
|
|
|
|
var errorDescription: String? {
|
|
self.message
|
|
}
|
|
}
|