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

876 lines
37 KiB
Swift

import Foundation
import Testing
@testable import CodexBar
@testable import CodexBarCore
@MainActor
struct UsageStoreSessionQuotaTransitionTests {
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 posts: [(transition: SessionQuotaTransition, provider: UsageProvider)] = []
private(set) var quotaWarningPosts: [(
event: QuotaWarningEvent,
provider: UsageProvider,
soundEnabled: Bool,
onScreenAlertEnabled: Bool)] = []
func post(transition: SessionQuotaTransition, provider: UsageProvider, badge _: NSNumber?) {
self.posts.append((transition: transition, provider: provider))
}
func postQuotaWarning(
event: QuotaWarningEvent,
provider: UsageProvider,
soundEnabled: Bool,
onScreenAlertEnabled: Bool)
{
self.quotaWarningPosts.append((
event: event,
provider: provider,
soundEnabled: soundEnabled,
onScreenAlertEnabled: onScreenAlertEnabled))
}
}
@Test
func `copilot switch from primary to secondary resets baseline`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-primary-secondary")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let primarySnapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .copilot, snapshot: primarySnapshot)
let secondarySnapshot = UsageSnapshot(
primary: nil,
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .copilot, snapshot: secondarySnapshot)
#expect(notifier.posts.isEmpty)
}
@Test
func `copilot switch from secondary to primary resets baseline`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-secondary-primary")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let secondarySnapshot = UsageSnapshot(
primary: nil,
secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .copilot, snapshot: secondarySnapshot)
let primarySnapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .copilot, snapshot: primarySnapshot)
#expect(notifier.posts.isEmpty)
}
@Test
func `claude weekly primary fallback does not emit session quota notifications`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-claude-weekly")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let baseline = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: 7 * 24 * 60, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .claude, snapshot: baseline)
let depleted = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: 7 * 24 * 60, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .claude, snapshot: depleted)
#expect(notifier.posts.isEmpty)
}
@Test
func `claude spend limit fallback does not emit session or quota warning notifications`() throws {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-claude-spend-limit")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50, 20]
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let json = """
{
"extra_usage": {
"is_enabled": true,
"monthly_limit": 600,
"used_credits": 434.43,
"utilization": 72,
"currency": "USD"
}
}
"""
let claude = try ClaudeUsageFetcher._mapOAuthUsageForTesting(
Data(json.utf8),
subscriptionType: "enterprise")
let snapshot = ClaudeOAuthFetchStrategy._snapshotForTesting(from: claude)
store.handleSessionQuotaTransition(provider: .claude, snapshot: snapshot)
store.handleQuotaWarningTransitions(provider: .claude, snapshot: snapshot)
#expect(snapshot.primary == nil)
#expect(snapshot.providerCost?.period == "Spend limit")
#expect(notifier.posts.isEmpty)
#expect(notifier.quotaWarningPosts.isEmpty)
}
@Test
func `mimo balance and monthly credits do not emit quota notifications`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-mimo-balance")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50, 20]
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let balanceSnapshot = MiMoUsageSnapshot(
balance: 0,
currency: "USD",
updatedAt: Date())
.toUsageSnapshot()
let tokenPlanSnapshot = MiMoUsageSnapshot(
balance: 25.51,
currency: "USD",
planCode: "standard",
tokenUsed: 100,
tokenLimit: 100,
tokenPercent: 1,
updatedAt: Date())
.toUsageSnapshot()
for snapshot in [balanceSnapshot, tokenPlanSnapshot] {
store.handleSessionQuotaTransition(provider: .mimo, snapshot: snapshot)
store.handleQuotaWarningTransitions(provider: .mimo, snapshot: snapshot)
}
#expect(notifier.posts.isEmpty)
#expect(notifier.quotaWarningPosts.isEmpty)
}
@Test
func `claude five hour primary still emits session quota notifications`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-claude-session")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let baseline = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: 5 * 60, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .claude, snapshot: baseline)
let depleted = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: 5 * 60, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleSessionQuotaTransition(provider: .claude, snapshot: depleted)
#expect(notifier.posts.map(\.provider) == [.claude])
}
@Test
func `antigravity session notification uses quota summary duration instead of family representative`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-antigravity-session")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 20, weeklyUsed: 100))
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 100, weeklyUsed: 100))
#expect(notifier.posts.map(\.provider) == [.antigravity])
#expect(notifier.posts.map(\.transition) == [.depleted])
}
@Test
func `antigravity preserves session notifications for durationless legacy family lanes`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-antigravity-legacy-session")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 20, claudeUsed: 20))
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 20, claudeUsed: 100))
#expect(notifier.posts.map(\.provider) == [.antigravity])
#expect(notifier.posts.map(\.transition) == [.depleted])
}
@Test
func `antigravity snapshot mode change resets session notification baseline`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-antigravity-mode-change")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 20, weeklyUsed: 20))
store.handleSessionQuotaTransition(
provider: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 100, claudeUsed: 100))
#expect(notifier.posts.isEmpty)
}
@Test
func `quota warning disabled does not post`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-disabled")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = false
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 90, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store.handleQuotaWarningTransitions(provider: .codex, snapshot: snapshot)
#expect(notifier.quotaWarningPosts.isEmpty)
}
@Test
func `quota warning posts once per downward threshold crossing`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-once")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningOnScreenAlertEnabled = 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: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "person@example.com",
accountOrganization: nil,
loginMethod: nil)))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 55, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "person@example.com",
accountOrganization: nil,
loginMethod: nil)))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "person@example.com",
accountOrganization: nil,
loginMethod: nil)))
#expect(notifier.quotaWarningPosts.count == 1)
#expect(notifier.quotaWarningPosts.first?.event.window == .session)
#expect(notifier.quotaWarningPosts.first?.event.threshold == 50)
#expect(notifier.quotaWarningPosts.first?.event.accountDisplayName == "person@example.com")
#expect(notifier.quotaWarningPosts.first?.onScreenAlertEnabled == true)
}
@Test
func `quota warning omits account when personal info is hidden`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-account-hidden")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.hidePersonalInfo = true
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
settings.setQuotaWarningWindowEnabled(.session, enabled: true)
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
let identity = ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "person@example.com",
accountOrganization: nil,
loginMethod: nil)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: identity))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 55, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: identity))
#expect(notifier.quotaWarningPosts.count == 1)
#expect(notifier.quotaWarningPosts.first?.event.accountDisplayName == nil)
}
@Test
func `hidden quota warning markers do not disable warning notifications`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-markers-hidden")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningMarkersVisible = false
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: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 55, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.count == 1)
#expect(notifier.quotaWarningPosts.first?.event.threshold == 50)
}
@Test
func `quota warning crossing multiple thresholds posts most severe only`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-severe")
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: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 85, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.map(\.event.threshold) == [20])
}
@Test
func `quota warning recovers and can fire again`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-recover")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
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)
for used in [40, 55, 10, 55] {
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(
usedPercent: Double(used),
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date()))
}
#expect(notifier.quotaWarningPosts.map(\.event.threshold) == [50, 50])
}
@Test
func `quota warning provider override beats global thresholds`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-override")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
settings.setQuotaWarningWindowEnabled(.session, enabled: true)
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
settings.setQuotaWarningThresholds(provider: .codex, window: .session, thresholds: [10])
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 95, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.map(\.event.threshold) == [10])
}
@Test
func `quota warning session only config ignores weekly crossings`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-session-only")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
settings.setQuotaWarningWindowEnabled(.session, enabled: true)
settings.setQuotaWarningWindowEnabled(.weekly, enabled: false)
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.map(\.event.window) == [.session])
}
@Test
func `quota warning weekly only config ignores session crossings`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-weekly-only")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
settings.setQuotaWarningWindowEnabled(.session, enabled: false)
settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.map(\.event.window) == [.weekly])
}
@Test
func `minimax quota warning posts for session and weekly windows`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-minimax")
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: .minimax,
snapshot: self.minimaxSnapshot(sessionUsed: 40, weeklyUsed: 40))
store.handleQuotaWarningTransitions(
provider: .minimax,
snapshot: self.minimaxSnapshot(sessionUsed: 55, weeklyUsed: 55))
#expect(notifier.quotaWarningPosts.map(\.provider) == [.minimax, .minimax])
#expect(notifier.quotaWarningPosts.map(\.event.window) == [.session, .weekly])
#expect(notifier.quotaWarningPosts.map(\.event.threshold) == [50, 50])
}
@Test
func `antigravity quota warnings use named session and weekly durations`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-antigravity")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
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: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 40, weeklyUsed: 40))
store.handleQuotaWarningTransitions(
provider: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 60, weeklyUsed: 60))
#expect(notifier.quotaWarningPosts.map(\.provider) == [.antigravity, .antigravity])
#expect(notifier.quotaWarningPosts.map(\.event.window) == [.session, .weekly])
#expect(notifier.quotaWarningPosts.map(\.event.threshold) == [50, 50])
}
@Test
func `antigravity legacy quota warnings do not infer weekly from family slots`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-antigravity-legacy")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
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: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 40, claudeUsed: 40))
store.handleQuotaWarningTransitions(
provider: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 60, claudeUsed: 60))
#expect(notifier.quotaWarningPosts.map(\.event.window) == [.session])
}
@Test
func `antigravity quota warning mode change resets warning baseline`() {
let settings = self.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-antigravity-mode")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
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: .antigravity,
snapshot: self.antigravityQuotaSummarySnapshot(sessionUsed: 20, weeklyUsed: 20))
store.handleQuotaWarningTransitions(
provider: .antigravity,
snapshot: self.antigravityLegacySnapshot(geminiUsed: 80, claudeUsed: 40))
#expect(notifier.quotaWarningPosts.isEmpty)
let key = UsageStore.QuotaWarningStateKey(provider: .antigravity, window: .session)
#expect(store.quotaWarningState[key]?.lastRemaining == 20)
#expect(store.quotaWarningState[key]?.source == .antigravityLegacy)
}
@Test
func `disabling quota warning window clears fired state`() {
let settings = self
.makeSettings(suiteName: "UsageStoreSessionQuotaTransitionTests-warning-disabled-clears-state")
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.quotaWarningNotificationsEnabled = true
settings.quotaWarningThresholds = [50]
let notifier = SessionQuotaNotifierSpy()
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
settings.setQuotaWarningWindowEnabled(.session, enabled: false)
store.handleQuotaWarningTransitions(
provider: .codex,
snapshot: UsageSnapshot(
primary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date()))
#expect(notifier.quotaWarningPosts.count == 1)
#expect(store.quotaWarningState[UsageStore.QuotaWarningStateKey(provider: .codex, window: .session)] == nil)
}
private func minimaxSnapshot(sessionUsed: Double, weeklyUsed: Double) -> UsageSnapshot {
let now = Date()
return MiniMaxUsageSnapshot(
planName: "Plus",
availablePrompts: nil,
currentPrompts: nil,
remainingPrompts: nil,
windowMinutes: nil,
usedPercent: nil,
resetsAt: nil,
updatedAt: now,
services: [
MiniMaxServiceUsage(
serviceType: "text-generation",
windowType: "5 hours",
timeRange: "15:00-20:00(UTC+8)",
usage: Int(sessionUsed),
limit: 100,
percent: sessionUsed,
resetsAt: now.addingTimeInterval(3600),
resetDescription: "Resets in 1 hour"),
MiniMaxServiceUsage(
serviceType: "text-generation",
windowType: "Weekly",
timeRange: "06/01 00:00 - 06/08 00:00(UTC+8)",
usage: Int(weeklyUsed),
limit: 100,
percent: weeklyUsed,
resetsAt: now.addingTimeInterval(6 * 24 * 3600),
resetDescription: "Resets in 6 days"),
]).toUsageSnapshot()
}
private func antigravityQuotaSummarySnapshot(sessionUsed: Double, weeklyUsed: Double) -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: weeklyUsed,
windowMinutes: 7 * 24 * 60,
resetsAt: nil,
resetDescription: nil),
secondary: RateWindow(
usedPercent: sessionUsed,
windowMinutes: 5 * 60,
resetsAt: nil,
resetDescription: nil),
tertiary: nil,
extraRateWindows: [
NamedRateWindow(
id: "antigravity-quota-summary-gemini-5h",
title: "Gemini Models Five Hour Limit",
window: RateWindow(
usedPercent: sessionUsed,
windowMinutes: 5 * 60,
resetsAt: nil,
resetDescription: nil)),
NamedRateWindow(
id: "antigravity-quota-summary-gemini-weekly",
title: "Gemini Models Weekly Limit",
window: RateWindow(
usedPercent: weeklyUsed,
windowMinutes: 7 * 24 * 60,
resetsAt: nil,
resetDescription: nil)),
],
updatedAt: Date())
}
private func antigravityLegacySnapshot(geminiUsed: Double, claudeUsed: Double) -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: geminiUsed,
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil),
secondary: RateWindow(
usedPercent: claudeUsed,
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil),
updatedAt: Date())
}
}