278 lines
13 KiB
Swift
278 lines
13 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
@testable import CodexBarCore
|
|
|
|
@MainActor
|
|
struct ClaudeExtraWindowQuotaWarningTests {
|
|
private func makeSettings(suiteName: String) -> SettingsStore {
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
return SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suiteName),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
}
|
|
|
|
@MainActor
|
|
final class SessionQuotaNotifierSpy: SessionQuotaNotifying {
|
|
private(set) var quotaWarningPosts: [(
|
|
event: QuotaWarningEvent,
|
|
provider: UsageProvider,
|
|
soundEnabled: Bool,
|
|
onScreenAlertEnabled: Bool)] = []
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `claude scoped weekly and routines extra windows fire independent weekly warnings`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-independent")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50, 20]
|
|
settings.setQuotaWarningWindowEnabled(.session, enabled: true)
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude,
|
|
snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: 40))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude,
|
|
snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: 55))
|
|
|
|
#expect(notifier.quotaWarningPosts.count == 2)
|
|
let fable = notifier.quotaWarningPosts.first { $0.event.windowID == "claude-weekly-scoped-fable" }
|
|
let routines = notifier.quotaWarningPosts.first { $0.event.windowID == "claude-routines" }
|
|
#expect(fable?.event.window == .weekly)
|
|
#expect(fable?.event.threshold == 50)
|
|
#expect(fable?.event.windowDisplayLabel == "Fable only")
|
|
#expect(routines?.event.threshold == 50)
|
|
#expect(routines?.event.windowDisplayLabel == "Daily Routines")
|
|
|
|
// Each window keeps independent fired-threshold state instead of clobbering the shared weekly key.
|
|
let fableKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-weekly-scoped-fable")
|
|
let routinesKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-routines")
|
|
#expect(store.quotaWarningState[fableKey]?.firedThresholds.contains(50) == true)
|
|
#expect(store.quotaWarningState[routinesKey]?.firedThresholds.contains(50) == true)
|
|
}
|
|
|
|
@Test
|
|
func `antigravity summary extra windows do not trigger the claude extra-window lane`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-antigravity-guard")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50, 20]
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
func snapshot(used: Double) -> UsageSnapshot {
|
|
UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
extraRateWindows: [
|
|
NamedRateWindow(
|
|
id: "antigravity-quota-summary-model-weekly",
|
|
title: "Weekly",
|
|
window: RateWindow(
|
|
usedPercent: used,
|
|
windowMinutes: 7 * 24 * 60,
|
|
resetsAt: nil,
|
|
resetDescription: nil)),
|
|
],
|
|
updatedAt: Date())
|
|
}
|
|
store.handleQuotaWarningTransitions(provider: .claude, snapshot: snapshot(used: 40))
|
|
store.handleQuotaWarningTransitions(provider: .claude, snapshot: snapshot(used: 55))
|
|
|
|
#expect(notifier.quotaWarningPosts.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `claude scoped weekly window refires after recovering above threshold`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-refire")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50]
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
// 60% remaining -> 45% (fires 50) -> 60% (clears 50) -> 45% (refires 50).
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: nil))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: nil))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: nil))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: nil))
|
|
|
|
#expect(notifier.quotaWarningPosts.count == 2)
|
|
#expect(notifier.quotaWarningPosts.allSatisfy { $0.event.windowID == "claude-weekly-scoped-fable" })
|
|
}
|
|
|
|
@Test
|
|
func `claude extra-window fired state is pruned when a window disappears but others remain`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-prune")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50]
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: 40))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: 55))
|
|
let fableKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-weekly-scoped-fable")
|
|
let routinesKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-routines")
|
|
#expect(store.quotaWarningState[fableKey] != nil)
|
|
#expect(store.quotaWarningState[routinesKey] != nil)
|
|
|
|
// Fable ends while Routines is still present: this refresh carries authoritative extras, so
|
|
// Fable's stale state is dropped and Routines is kept.
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: nil, routinesUsed: 55))
|
|
#expect(store.quotaWarningState[fableKey] == nil)
|
|
#expect(store.quotaWarningState[routinesKey] != nil)
|
|
}
|
|
|
|
@Test
|
|
func `disabling weekly warnings clears all claude extra-window state`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-disable")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50]
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: 40))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: 55))
|
|
let fableKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-weekly-scoped-fable")
|
|
let routinesKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-routines")
|
|
#expect(store.quotaWarningState[fableKey] != nil)
|
|
#expect(store.quotaWarningState[routinesKey] != nil)
|
|
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: false)
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude,
|
|
snapshot: UsageSnapshot(primary: nil, secondary: nil, extraRateWindows: nil, updatedAt: Date()))
|
|
#expect(store.quotaWarningState[fableKey] == nil)
|
|
#expect(store.quotaWarningState[routinesKey] == nil)
|
|
}
|
|
|
|
@Test
|
|
func `claude extra-window state survives a transient extras miss without re-posting`() {
|
|
let settings = self.makeSettings(suiteName: "ClaudeExtraWindowQuotaWarningTests-transient-miss")
|
|
settings.refreshFrequency = .manual
|
|
settings.statusChecksEnabled = false
|
|
settings.quotaWarningNotificationsEnabled = true
|
|
settings.quotaWarningThresholds = [50]
|
|
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
|
|
|
|
let notifier = SessionQuotaNotifierSpy()
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
sessionQuotaNotifier: notifier)
|
|
|
|
// Fable crosses 50% and warns once.
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 40, routinesUsed: nil))
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: nil))
|
|
#expect(notifier.quotaWarningPosts.count == 1)
|
|
let fableKey = UsageStore.QuotaWarningStateKey(
|
|
provider: .claude, window: .weekly, windowID: "claude-weekly-scoped-fable")
|
|
|
|
// A failed web-extras fetch delivers nil extras while the main snapshot is intact. The fired
|
|
// state must persist so the warning is not re-posted when extras recover.
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude,
|
|
snapshot: UsageSnapshot(primary: nil, secondary: nil, extraRateWindows: nil, updatedAt: Date()))
|
|
#expect(store.quotaWarningState[fableKey] != nil)
|
|
|
|
store.handleQuotaWarningTransitions(
|
|
provider: .claude, snapshot: self.claudeExtraWindowSnapshot(fableUsed: 55, routinesUsed: nil))
|
|
#expect(notifier.quotaWarningPosts.count == 1)
|
|
}
|
|
|
|
private func claudeExtraWindowSnapshot(fableUsed: Double?, routinesUsed: Double?) -> UsageSnapshot {
|
|
var windows: [NamedRateWindow] = []
|
|
if let fableUsed {
|
|
windows.append(NamedRateWindow(
|
|
id: "claude-weekly-scoped-fable",
|
|
title: "Fable only",
|
|
window: RateWindow(
|
|
usedPercent: fableUsed, windowMinutes: 7 * 24 * 60, resetsAt: nil, resetDescription: nil)))
|
|
}
|
|
if let routinesUsed {
|
|
windows.append(NamedRateWindow(
|
|
id: "claude-routines",
|
|
title: "Daily Routines",
|
|
window: RateWindow(
|
|
usedPercent: routinesUsed, windowMinutes: 7 * 24 * 60, resetsAt: nil, resetDescription: nil)))
|
|
}
|
|
return UsageSnapshot(primary: nil, secondary: nil, extraRateWindows: windows, updatedAt: Date())
|
|
}
|
|
}
|