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

258 lines
10 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@MainActor
@Suite("Claude synthetic session placeholder notifications")
struct ClaudeSyntheticPlaceholderNotificationTests {
private let start = Date(timeIntervalSince1970: 1_780_000_000)
@Test
func `placeholder preserves depleted state without a reset boundary`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-depleted-no-boundary",
notifier: notifier)
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 20))
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 100))
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(sessionUsed: 0, sessionIsSyntheticPlaceholder: true))
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 100))
#expect(notifier.transitions == [.depleted])
#expect(store.sessionQuotaTransitionStates[.claude]?.remaining == 0)
}
@Test
func `placeholder stays non authoritative after the prior boundary elapses`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-depleted-elapsed-boundary",
notifier: notifier)
let boundary = self.start.addingTimeInterval(5 * 60)
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(sessionUsed: 20, sessionReset: boundary))
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(sessionUsed: 100, sessionReset: boundary, secondsAfterStart: 60))
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 0,
sessionReset: boundary,
sessionIsSyntheticPlaceholder: true,
secondsAfterStart: 6 * 60))
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(sessionUsed: 100, sessionReset: boundary, secondsAfterStart: 7 * 60))
#expect(notifier.transitions == [.depleted])
#expect(store.sessionQuotaTransitionStates[.claude]?.remaining == 0)
}
@Test
func `placeholder cannot rearm depletion while notifications are disabled`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-disabled",
notifier: notifier)
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 20))
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 100))
store.settings.sessionQuotaNotificationsEnabled = false
store.handleSessionQuotaTransition(
provider: .claude,
snapshot: self.snapshot(sessionUsed: 0, sessionIsSyntheticPlaceholder: true))
store.settings.sessionQuotaNotificationsEnabled = true
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 100))
#expect(notifier.transitions == [.depleted])
#expect(store.sessionQuotaTransitionStates[.claude]?.remaining == 0)
}
@Test
func `real zero usage remains an authoritative restore`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-real-zero",
notifier: notifier)
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 20))
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 100))
store.handleSessionQuotaTransition(provider: .claude, snapshot: self.snapshot(sessionUsed: 0))
#expect(notifier.transitions == [.depleted, .restored])
#expect(store.sessionQuotaTransitionStates[.claude]?.remaining == 100)
}
@Test
func `placeholder preserves threshold state while weekly warnings continue`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-threshold",
notifier: notifier)
store.settings.quotaWarningNotificationsEnabled = true
store.settings.quotaWarningThresholds = [50]
store.settings.setQuotaWarningWindowEnabled(.session, enabled: true)
store.settings.setQuotaWarningWindowEnabled(.weekly, enabled: true)
let sessionReset = self.start.addingTimeInterval(2 * 60 * 60)
let weeklyReset = self.start.addingTimeInterval(2 * 24 * 60 * 60)
store.handleQuotaWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 40,
weeklyUsed: 40,
sessionReset: sessionReset,
weeklyReset: weeklyReset))
store.handleQuotaWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 60,
weeklyUsed: 40,
sessionReset: sessionReset,
weeklyReset: weeklyReset,
secondsAfterStart: 60))
store.handleQuotaWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 0,
weeklyUsed: 60,
sessionReset: sessionReset,
weeklyReset: weeklyReset,
sessionIsSyntheticPlaceholder: true,
secondsAfterStart: 120))
store.handleQuotaWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 60,
weeklyUsed: 60,
sessionReset: sessionReset,
weeklyReset: weeklyReset,
secondsAfterStart: 180))
#expect(notifier.quotaWarnings.map(\.window) == [.session, .weekly])
#expect(notifier.quotaWarnings.map(\.threshold) == [50, 50])
}
@Test
func `placeholder preserves predictive episode while weekly risk continues`() {
let notifier = NotifierSpy()
let store = self.makeStore(
suiteName: "ClaudeSyntheticPlaceholderNotificationTests-predictive",
notifier: notifier)
store.settings.predictivePaceWarningNotificationsEnabled = true
let sessionReset = self.start.addingTimeInterval(2 * 60 * 60)
let weeklyReset = self.start.addingTimeInterval(2 * 24 * 60 * 60)
store.handlePredictivePaceWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 80,
weeklyUsed: 20,
sessionReset: sessionReset,
weeklyReset: weeklyReset))
store.handlePredictivePaceWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 0,
weeklyUsed: 90,
sessionReset: sessionReset,
weeklyReset: weeklyReset,
sessionIsSyntheticPlaceholder: true,
secondsAfterStart: 60))
store.handlePredictivePaceWarningTransitions(
provider: .claude,
snapshot: self.snapshot(
sessionUsed: 80,
weeklyUsed: 90,
sessionReset: sessionReset,
weeklyReset: weeklyReset,
secondsAfterStart: 120))
#expect(notifier.predictiveWarnings == [.session, .weekly])
}
private func makeStore(suiteName: String, notifier: NotifierSpy) -> UsageStore {
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suiteName),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.sessionQuotaNotificationsEnabled = true
return UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
sessionQuotaNotifier: notifier)
}
private func snapshot(
sessionUsed: Double,
weeklyUsed: Double = 20,
sessionReset: Date? = nil,
weeklyReset: Date? = nil,
sessionIsSyntheticPlaceholder: Bool = false,
secondsAfterStart: TimeInterval = 0) -> UsageSnapshot
{
UsageSnapshot(
primary: RateWindow(
usedPercent: sessionUsed,
windowMinutes: 5 * 60,
resetsAt: sessionReset,
resetDescription: nil,
isSyntheticPlaceholder: sessionIsSyntheticPlaceholder),
secondary: RateWindow(
usedPercent: weeklyUsed,
windowMinutes: 7 * 24 * 60,
resetsAt: weeklyReset,
resetDescription: nil),
updatedAt: self.start.addingTimeInterval(secondsAfterStart),
identity: ProviderIdentitySnapshot(
providerID: .claude,
accountEmail: "placeholder@example.com",
accountOrganization: nil,
loginMethod: "web"))
}
}
@MainActor
private final class NotifierSpy: SessionQuotaNotifying {
private(set) var transitions: [SessionQuotaTransition] = []
private(set) var quotaWarnings: [QuotaWarningEvent] = []
private(set) var predictiveWarnings: [QuotaWarningWindow] = []
func post(transition: SessionQuotaTransition, provider _: UsageProvider, badge _: NSNumber?) {
self.transitions.append(transition)
}
func postQuotaWarning(
event: QuotaWarningEvent,
provider _: UsageProvider,
soundEnabled _: Bool,
onScreenAlertEnabled _: Bool)
{
self.quotaWarnings.append(event)
}
func postPredictivePaceWarning(
event: PredictivePaceWarningEvent,
provider _: UsageProvider,
soundEnabled _: Bool,
onScreenAlertEnabled _: Bool,
now _: Date)
{
self.predictiveWarnings.append(event.window)
}
}