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

722 lines
31 KiB
Swift

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<PredictivePaceWarningStateKey> = []
#expect(PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60, runOutProbability: 0.2),
notifiedKeys: &notifiedKeys))
#expect(notifiedKeys.contains(key))
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: true, etaSeconds: nil),
notifiedKeys: &notifiedKeys))
#expect(!notifiedKeys.contains(key))
#expect(PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
}
@Test
func `new reset window identity is independent and prunes expired sibling key`() {
var notifiedKeys: Set<PredictivePaceWarningStateKey> = []
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: &notifiedKeys))
PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys(
activeKey: newKey,
notifiedKeys: &notifiedKeys)
#expect(!notifiedKeys.contains(oldKey))
#expect(PredictivePaceWarningNotificationLogic.recordObservation(
key: newKey,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
}
@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<PredictivePaceWarningStateKey> = []
for key in keys {
#expect(PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
}
for key in keys {
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
}
#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<PredictivePaceWarningStateKey> = []
#expect(PredictivePaceWarningNotificationLogic.recordObservation(
key: firstKey,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys(
activeKey: correctedKey,
notifiedKeys: &notifiedKeys)
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: correctedKey,
pace: self.pace(willLastToReset: false, etaSeconds: 60),
notifiedKeys: &notifiedKeys))
#expect(notifiedKeys == Set([correctedKey]))
PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys(
activeKey: correctedKey,
notifiedKeys: &notifiedKeys)
#expect(!PredictivePaceWarningNotificationLogic.recordObservation(
key: correctedKey,
pace: self.pace(willLastToReset: true, etaSeconds: nil),
notifiedKeys: &notifiedKeys))
#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))
}
}