import Foundation import Testing @testable import CodexBar @testable import CodexBarCore @MainActor struct PredictivePaceWarningTests { @MainActor final class NotifierSpy: SessionQuotaNotifying { struct PredictivePost { let event: PredictivePaceWarningEvent let provider: UsageProvider let soundEnabled: Bool let onScreenAlertEnabled: Bool let now: Date } private(set) var quotaWarningPosts: [( event: QuotaWarningEvent, provider: UsageProvider, soundEnabled: Bool, onScreenAlertEnabled: Bool)] = [] private(set) var predictivePosts: [PredictivePost] = [] func post(transition _: SessionQuotaTransition, provider _: UsageProvider, badge _: NSNumber?) {} func postQuotaWarning( event: QuotaWarningEvent, provider: UsageProvider, soundEnabled: Bool, onScreenAlertEnabled: Bool) { self.quotaWarningPosts.append(( event: event, provider: provider, soundEnabled: soundEnabled, onScreenAlertEnabled: onScreenAlertEnabled)) } func postPredictivePaceWarning( event: PredictivePaceWarningEvent, provider: UsageProvider, soundEnabled: Bool, onScreenAlertEnabled: Bool, now: Date) { self.predictivePosts.append(PredictivePost( event: event, provider: provider, soundEnabled: soundEnabled, onScreenAlertEnabled: onScreenAlertEnabled, now: now)) } } @Test func `predictive pace warnings default off and persist when enabled`() throws { let suite = "PredictivePaceWarningTests-default-\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let settings = self.makeSettings(suiteName: suite, clear: false) #expect(settings.predictivePaceWarningNotificationsEnabled == false) #expect(defaults.object(forKey: "predictivePaceWarningNotificationsEnabled") == nil) settings.predictivePaceWarningNotificationsEnabled = true #expect(defaults.bool(forKey: "predictivePaceWarningNotificationsEnabled") == true) #expect(self.makeSettings(suiteName: suite, clear: false).predictivePaceWarningNotificationsEnabled == true) } @Test func `predictive pace preference refreshes background work only when it changes`() { let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-settings-revision") let initialRevision = settings.backgroundWorkSettingsRevision settings.predictivePaceWarningNotificationsEnabled = true #expect(settings.backgroundWorkSettingsRevision == initialRevision + 1) settings.predictivePaceWarningNotificationsEnabled = true #expect(settings.backgroundWorkSettingsRevision == initialRevision + 1) settings.predictivePaceWarningNotificationsEnabled = false #expect(settings.backgroundWorkSettingsRevision == initialRevision + 2) } @Test func `predictive only settings expose delivery controls without threshold editors`() { let disabled = QuotaWarningSettingsVisibility( thresholdWarningsEnabled: false, predictiveWarningsEnabled: false) #expect(!disabled.showsThresholdControls) #expect(!disabled.showsDeliveryControls) let predictiveOnly = QuotaWarningSettingsVisibility( thresholdWarningsEnabled: false, predictiveWarningsEnabled: true) #expect(!predictiveOnly.showsThresholdControls) #expect(predictiveOnly.showsDeliveryControls) let thresholdWarnings = QuotaWarningSettingsVisibility( thresholdWarningsEnabled: true, predictiveWarningsEnabled: false) #expect(thresholdWarnings.showsThresholdControls) #expect(thresholdWarnings.showsDeliveryControls) } @Test func `trigger only accepts at risk pace with positive eta and confident probability`() { #expect(PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: false, etaSeconds: 60, runOutProbability: nil))) #expect(PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: false, etaSeconds: 60, runOutProbability: 0.5))) #expect(!PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: true, etaSeconds: 60, runOutProbability: nil))) #expect(!PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: false, etaSeconds: nil, runOutProbability: nil))) #expect(!PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: false, etaSeconds: 0, runOutProbability: nil))) #expect(!PredictivePaceWarningNotificationLogic.shouldNotify(pace: self.pace( willLastToReset: false, etaSeconds: 60, runOutProbability: 0.49))) } @Test func `state machine suppresses repeats until authoritative recovery`() { let key = PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "email:person@example.com", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_000)) var notifiedKeys: Set = [] #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60, runOutProbability: 0.2), notifiedKeys: ¬ifiedKeys)) #expect(notifiedKeys.contains(key)) #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: true, etaSeconds: nil), notifiedKeys: ¬ifiedKeys)) #expect(!notifiedKeys.contains(key)) #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) } @Test func `new reset window identity is independent and prunes expired sibling key`() { var notifiedKeys: Set = [] let oldKey = PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "email:person@example.com", window: .weekly, resetWindow: self.resetWindow(minutes: 10080, resetsAt: 1_780_000_000)) let newKey = PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "email:person@example.com", window: .weekly, resetWindow: self.resetWindow(minutes: 10080, resetsAt: 1_780_604_800)) #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: oldKey, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys( activeKey: newKey, notifiedKeys: ¬ifiedKeys) #expect(!notifiedKeys.contains(oldKey)) #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: newKey, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) } @Test func `provider account and window risk episodes are isolated`() { let keys = [ PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "account-a", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_000)), PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "account-a", window: .weekly, resetWindow: self.resetWindow(minutes: 10080, resetsAt: 1_780_000_000)), PredictivePaceWarningStateKey( provider: .claude, accountDiscriminator: "account-b", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_000)), PredictivePaceWarningStateKey( provider: .codex, accountDiscriminator: "account-a", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_000)), ] var notifiedKeys: Set = [] for key in keys { #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) } for key in keys { #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: key, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) } #expect(notifiedKeys == Set(keys)) } @Test func `reset time jitter follows the same risk episode without repeating`() { let firstKey = PredictivePaceWarningStateKey( provider: .codex, accountDiscriminator: "account-a", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_000)) let correctedKey = PredictivePaceWarningStateKey( provider: .codex, accountDiscriminator: "account-a", window: .session, resetWindow: self.resetWindow(minutes: 300, resetsAt: 1_780_000_120)) var notifiedKeys: Set = [] #expect(PredictivePaceWarningNotificationLogic.recordObservation( key: firstKey, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys( activeKey: correctedKey, notifiedKeys: ¬ifiedKeys) #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: correctedKey, pace: self.pace(willLastToReset: false, etaSeconds: 60), notifiedKeys: ¬ifiedKeys)) #expect(notifiedKeys == Set([correctedKey])) PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys( activeKey: correctedKey, notifiedKeys: ¬ifiedKeys) #expect(!PredictivePaceWarningNotificationLogic.recordObservation( key: correctedKey, pace: self.pace(willLastToReset: true, etaSeconds: nil), notifiedKeys: ¬ifiedKeys)) #expect(notifiedKeys.isEmpty) } @Test func `store posts once for Claude session and weekly risk then re-arms after recovery`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-claude-store") settings.predictivePaceWarningNotificationsEnabled = true settings.quotaWarningSoundEnabled = false settings.quotaWarningOnScreenAlertEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let atRisk = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 90, accountEmail: "person@example.com") store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: atRisk) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: atRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly]) #expect(notifier.predictivePosts.allSatisfy { $0.provider == .claude }) #expect(notifier.predictivePosts.allSatisfy { $0.soundEnabled == false }) #expect(notifier.predictivePosts.allSatisfy { $0.onScreenAlertEnabled == true }) #expect(notifier.predictivePosts.allSatisfy { $0.event.accountDisplayName == "person@example.com" }) let jitteredAtRisk = self.snapshot( now: now.addingTimeInterval(120), sessionUsed: 80, weeklyUsed: 90, accountEmail: "person@example.com") store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: jitteredAtRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly]) let recovered = self.snapshot( now: now, sessionUsed: 20, weeklyUsed: 20, accountEmail: "person@example.com") store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: recovered) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: atRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly, .session, .weekly]) } @Test func `missing incomplete and failed observations preserve warned state`() async { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-preserve-state") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let atRisk = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 90, accountEmail: "person@example.com") store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: atRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly]) let incomplete = UsageSnapshot( primary: nil, secondary: nil, updatedAt: now, identity: ProviderIdentitySnapshot( providerID: .claude, accountEmail: "person@example.com", accountOrganization: nil, loginMethod: nil)) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: incomplete) let missingIdentity = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 90, accountEmail: nil) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: missingIdentity) await store.applySelectedOutcome( ProviderFetchOutcome( result: .failure(ProviderFetchError.noAvailableStrategy(.claude)), attempts: []), provider: .claude, account: nil, fallbackSnapshot: atRisk) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: atRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly]) } @Test func `new store starts with memory only warning state`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-memory-only") settings.predictivePaceWarningNotificationsEnabled = true let snapshot = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: "person@example.com") let firstNotifier = NotifierSpy() let firstStore = self.makeStore(settings: settings, notifier: firstNotifier) firstStore.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: snapshot) firstStore.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: snapshot) #expect(firstNotifier.predictivePosts.count == 1) let secondNotifier = NotifierSpy() let secondStore = self.makeStore(settings: settings, notifier: secondNotifier) secondStore.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: snapshot) #expect(secondNotifier.predictivePosts.count == 1) } @Test func `store posts for Codex session and weekly risk`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-codex-store") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let atRisk = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 90, accountEmail: "codex@example.com", provider: .codex) store.handlePredictivePaceWarningTransitions(provider: .codex, snapshot: atRisk) store.handlePredictivePaceWarningTransitions(provider: .codex, snapshot: atRisk) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .weekly]) #expect(notifier.predictivePosts.allSatisfy { $0.provider == .codex }) #expect(notifier.predictivePosts.allSatisfy { $0.event.accountDisplayName == "codex@example.com" }) } @Test func `store isolates risk episodes by account`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-account-isolation") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let firstAccount = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: "first@example.com") let secondAccount = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: "second@example.com") store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: firstAccount) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: firstAccount) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: secondAccount) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: firstAccount) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .session]) #expect(notifier.predictivePosts.map(\.event.accountDisplayName) == [ "first@example.com", "second@example.com", ]) } @Test func `stable Claude account identity spans OAuth and CLI observations`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-claude-active-account") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let firstAccount = UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .oauth, observation: .stable(identity: "account-a")) let secondAccount = UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .cli, observation: .stable(identity: "account-b")) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .oauth, observation: .stable(identity: nil)) == nil) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .cli, observation: .changed) == nil) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .web, observation: .stable(identity: "account-a")) == nil) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .apiToken, observation: .stable(identity: "account-a")) == nil) let noEmailRisk = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: nil) let emailRisk = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: "person@example.com") let emailRecovery = self.snapshot( now: now, sessionUsed: 20, weeklyUsed: 20, accountEmail: "person@example.com") store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: noEmailRisk, accountDiscriminatorOverride: firstAccount) store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: emailRisk, accountDiscriminatorOverride: firstAccount) store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: emailRecovery, accountDiscriminatorOverride: firstAccount) store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: noEmailRisk, accountDiscriminatorOverride: firstAccount) store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: noEmailRisk, accountDiscriminatorOverride: secondAccount) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .session, .session]) #expect(notifier.predictivePosts.allSatisfy { $0.provider == .claude }) } @Test func `Claude OAuth owner keeps no email warnings account scoped when active metadata is missing`() { let owner = String(repeating: "a", count: 64) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .oauth, observation: .stable(identity: nil), oauthHistoryOwnerIdentifier: owner) == "claude-oauth-owner:\(owner)") #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .oauth, observation: .changed, oauthHistoryOwnerIdentifier: " \(owner.uppercased()) ") == "claude-oauth-owner:\(owner)") #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .cli, observation: .stable(identity: nil), oauthHistoryOwnerIdentifier: owner) == nil) #expect(UsageStore.predictivePaceWarningClaudeAccountDiscriminator( strategyKind: .web, observation: .stable(identity: nil), oauthHistoryOwnerIdentifier: owner) == nil) } @Test func `selected Claude account identity is stable across OAuth owner changes`() async throws { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-selected-claude-account") settings.predictivePaceWarningNotificationsEnabled = true let firstAccount = try ProviderTokenAccount( id: #require(UUID(uuidString: "00000000-0000-0000-0000-000000000001")), label: "First", token: "token", addedAt: 0, lastUsed: nil) let secondAccount = try ProviderTokenAccount( id: #require(UUID(uuidString: "00000000-0000-0000-0000-000000000002")), label: "Second", token: "token", addedAt: 0, lastUsed: nil) let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let snapshot = self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: nil) await store.applySelectedOutcome( self.claudeOAuthOutcome(snapshot: snapshot, ownerIdentifier: "owner-a"), provider: .claude, account: firstAccount, fallbackSnapshot: nil) await store.applySelectedOutcome( self.claudeOAuthOutcome(snapshot: snapshot, ownerIdentifier: "owner-b"), provider: .claude, account: firstAccount, fallbackSnapshot: nil) await store.applySelectedOutcome( self.claudeOAuthOutcome(snapshot: snapshot, ownerIdentifier: "owner-b"), provider: .claude, account: secondAccount, fallbackSnapshot: nil) #expect(notifier.predictivePosts.map(\.event.window) == [.session, .session]) #expect(notifier.predictivePosts.allSatisfy { $0.provider == .claude }) #expect(notifier.predictivePosts.map(\.event.accountDisplayName) == ["First", "Second"]) } @Test func `store keeps identity out of copy when personal info is hidden`() throws { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-hidden-info") settings.predictivePaceWarningNotificationsEnabled = true settings.hidePersonalInfo = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) store.handlePredictivePaceWarningTransitions( provider: .claude, snapshot: self.snapshot( now: now, sessionUsed: 80, weeklyUsed: 20, accountEmail: "person@example.com")) #expect(notifier.predictivePosts.first?.event.accountDisplayName == nil) let copy = try PredictivePaceWarningNotificationLogic.notificationCopy( providerName: "Claude", event: #require(notifier.predictivePosts.first?.event), now: now) #expect(!copy.body.contains("person@example.com")) } @Test func `store ignores providers outside accepted scope`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-scope") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) store.handlePredictivePaceWarningTransitions( provider: .zai, snapshot: self.snapshot(now: now, sessionUsed: 80, weeklyUsed: 90, accountEmail: "person@example.com")) #expect(notifier.predictivePosts.isEmpty) } @Test func `store ignores unsupported tertiary windows`() { let now = Date(timeIntervalSince1970: 1_780_000_000) let settings = self.makeSettings(suiteName: "PredictivePaceWarningTests-window-scope") settings.predictivePaceWarningNotificationsEnabled = true let notifier = NotifierSpy() let store = self.makeStore(settings: settings, notifier: notifier) let snapshot = UsageSnapshot( primary: nil, secondary: nil, tertiary: RateWindow( usedPercent: 90, windowMinutes: 30 * 24 * 60, resetsAt: now.addingTimeInterval(2 * 24 * 3600), resetDescription: nil), updatedAt: now, identity: ProviderIdentitySnapshot( providerID: .claude, accountEmail: "person@example.com", accountOrganization: nil, loginMethod: nil)) store.handlePredictivePaceWarningTransitions(provider: .claude, snapshot: snapshot) #expect(notifier.predictivePosts.isEmpty) } private func makeSettings(suiteName: String, clear: Bool = true) -> SettingsStore { let defaults = UserDefaults(suiteName: suiteName)! if clear { defaults.removePersistentDomain(forName: suiteName) } return SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suiteName), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore(), tokenAccountStore: InMemoryTokenAccountStore()) } private func makeStore(settings: SettingsStore, notifier: NotifierSpy) -> UsageStore { UsageStore( fetcher: UsageFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings, sessionQuotaNotifier: notifier) } private func claudeOAuthOutcome(snapshot: UsageSnapshot, ownerIdentifier: String) -> ProviderFetchOutcome { ProviderFetchOutcome( result: .success(ProviderFetchResult( usage: snapshot, credits: nil, dashboard: nil, sourceLabel: "oauth", strategyID: "claude-oauth", strategyKind: .oauth, claudeOAuthHistoryOwnerIdentifier: ownerIdentifier)), attempts: []) } private func snapshot( now: Date, sessionUsed: Double, weeklyUsed: Double, accountEmail: String?, provider: UsageProvider = .claude) -> UsageSnapshot { UsageSnapshot( primary: RateWindow( usedPercent: sessionUsed, windowMinutes: 300, resetsAt: now.addingTimeInterval(2 * 3600), resetDescription: nil), secondary: RateWindow( usedPercent: weeklyUsed, windowMinutes: 7 * 24 * 60, resetsAt: now.addingTimeInterval(2 * 24 * 3600), resetDescription: nil), updatedAt: now, identity: ProviderIdentitySnapshot( providerID: provider, accountEmail: accountEmail, accountOrganization: nil, loginMethod: nil)) } private func pace( willLastToReset: Bool, etaSeconds: TimeInterval?, runOutProbability: Double? = nil) -> UsagePace { UsagePace( stage: willLastToReset ? .onTrack : .ahead, deltaPercent: willLastToReset ? 0 : 20, expectedUsedPercent: 50, actualUsedPercent: willLastToReset ? 40 : 70, etaSeconds: etaSeconds, willLastToReset: willLastToReset, runOutProbability: runOutProbability) } private func resetWindow(minutes: Int?, resetsAt: TimeInterval) -> PredictivePaceWarningResetWindow { PredictivePaceWarningResetWindow( windowMinutes: minutes, resetsAt: Date(timeIntervalSince1970: resetsAt)) } }