import CodexBarCoreimport Foundationimport Testing@testable import CodexBar /// Covers the timer plumbing added on top of the pure `AdaptiveRefreshPolicy` (see/// `AdaptiveRefreshPolicyTests`): how `UsageStore.startTimer()` wires live signals into the/// policy, and how manual/fixed/adaptive modes drive (or don't drive) `refresh()` over time.@MainActorstruct AdaptiveRefreshTimerTests { @Test func `launch with no menu history begins at thirty minutes`() { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-launch", frequency: .adaptive) let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) #expect(store.lastMenuOpenAt == nil) let decision = UsageStore.adaptiveRefreshDecision( now: Date(), lastMenuOpenAt: store.lastMenuOpenAt, lowPowerModeEnabled: false, thermalState: .nominal) #expect(decision.reason == .longIdle) #expect(decision.delay == .seconds(30 * 60)) } @Test func `menu-open signal changes the next adaptive decision`() { let now = Date(timeIntervalSinceReferenceDate: 900_000_000) let beforeOpen = UsageStore.adaptiveRefreshDecision( now: now, lastMenuOpenAt: nil, lowPowerModeEnabled: false, thermalState: .nominal) #expect(beforeOpen.reason == .longIdle) let afterOpen = UsageStore.adaptiveRefreshDecision( now: now, lastMenuOpenAt: now, lowPowerModeEnabled: false, thermalState: .nominal) #expect(afterOpen.reason == .recentInteraction) } @Test func `menu open advances a long idle timer during refresh without postponing an earlier tick`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-advance", frequency: .adaptive) let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) store.restartTimerWithSleepOverrideForTesting(.seconds(10)) try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil } let longIdleSchedule = try #require(store.adaptiveRefreshScheduledAt) let now = Date() store._setSnapshotForTesting( UsageSnapshot( primary: RateWindow( usedPercent: 50, windowMinutes: 300, resetsAt: now.addingTimeInterval(30), resetDescription: nil), secondary: nil, updatedAt: now, identity: nil), provider: .codex) store.scheduleResetBoundaryRefreshIfNeeded(normalRefreshInterval: 30 * 60, now: now) defer { store.cancelResetBoundaryRefresh() } let resetBoundarySchedule = try #require(store.scheduledResetBoundaryRefreshAt) store.isRefreshing = true defer { store.isRefreshing = false } store.noteMenuOpened() try await Self.waitUntil { guard let scheduledAt = store.adaptiveRefreshScheduledAt else { return false } return scheduledAt < longIdleSchedule } let interactionSchedule = try #require(store.adaptiveRefreshScheduledAt) #expect(store.isRefreshing) #expect(store.scheduledResetBoundaryRefreshAt == resetBoundarySchedule) store.noteMenuOpened(at: Date().addingTimeInterval(30)) #expect(store.adaptiveRefreshScheduledAt == interactionSchedule) } @Test func `coding activity advances a long idle timer without postponing an earlier tick`() async throws { let settings = Self.makeSettingsStore( suite: "AdaptiveRefreshTimerTests-activity-advance", frequency: .adaptiveAgentAware) settings.adaptiveActivityScanConsent = .allowed let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) store.restartTimerWithSleepOverrideForTesting(.seconds(10)) try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil } let longIdleSchedule = try #require(store.adaptiveRefreshScheduledAt) let observedAt = Date() store.noteCodingActivityObserved(at: observedAt, now: observedAt) try await Self.waitUntil { guard let scheduledAt = store.adaptiveRefreshScheduledAt else { return false } return scheduledAt < longIdleSchedule } let activitySchedule = try #require(store.adaptiveRefreshScheduledAt) #expect(store.lastCodingActivityAt == observedAt) // An older observation is ignored. A newer observation is retained, but cannot push an // already earlier provider refresh later. store.noteCodingActivityObserved( at: observedAt.addingTimeInterval(-1), now: observedAt.addingTimeInterval(30)) #expect(store.lastCodingActivityAt == observedAt) store.noteCodingActivityObserved( at: observedAt.addingTimeInterval(1), now: observedAt.addingTimeInterval(30)) #expect(store.lastCodingActivityAt == observedAt.addingTimeInterval(1)) #expect(store.adaptiveRefreshScheduledAt == activitySchedule) } @Test func `plain adaptive ignores coding activity`() async throws { let settings = Self.makeSettingsStore( suite: "AdaptiveRefreshTimerTests-plain-adaptive-activity", frequency: .adaptive) settings.adaptiveActivityScanConsent = .allowed let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) store.restartTimerWithSleepOverrideForTesting(.seconds(10)) try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil } let scheduledAt = try #require(store.adaptiveRefreshScheduledAt) store.noteCodingActivityObserved(at: Date()) #expect(store.adaptiveRefreshScheduledAt == scheduledAt) #expect(store.lastCodingActivityAt == nil) } @Test func `noting a menu open records the signal without starting a refresh`() { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-noteMenuOpened", frequency: .manual) let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) #expect(store.completedRefreshCountForTesting == 0) #expect(store.isRefreshing == false) store.noteMenuOpened() #expect(store.lastMenuOpenAt != nil) #expect(store.completedRefreshCountForTesting == 0) #expect(store.isRefreshing == false) } @Test func `noting coding activity outside agent aware mode is a no-op`() { let settings = Self.makeSettingsStore( suite: "AdaptiveRefreshTimerTests-noteCodingActivity", frequency: .fiveMinutes) let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) let observedAt = Date() store.noteCodingActivityObserved(at: observedAt) #expect(store.lastCodingActivityAt == nil) #expect(store.adaptiveRefreshScheduledAt == nil) #expect(store.completedRefreshCountForTesting == 0) } @Test func `clearing coding activity removes the adaptive input`() { let settings = Self.makeSettingsStore( suite: "AdaptiveRefreshTimerTests-clearCodingActivity", frequency: .adaptiveAgentAware) settings.adaptiveActivityScanConsent = .allowed let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) store.noteCodingActivityObserved(at: Date(timeIntervalSinceReferenceDate: 100)) #expect(store.lastCodingActivityAt != nil) store.clearCodingActivityObservation() #expect(store.lastCodingActivityAt == nil) } @Test func `opportunistic timer refresh is a no-op while another refresh is already in flight`() async { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-coalesce", frequency: .manual) let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing) store.isRefreshing = true await store.refresh(enrichmentMode: .automatic) // The guard at the top of runRefresh() returned immediately: no completion was recorded and the // flag was left untouched by this call. This is the invariant every timer tick (fixed or // adaptive) relies on to avoid overlapping with a refresh already in flight. #expect(store.completedRefreshCountForTesting == 0) #expect(store.isRefreshing == true) } @Test func `manual mode performs the initial refresh but no recurring ticks`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-manual", frequency: .manual) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 } // Manual mode never starts a timer, so nothing can push the count past the one launch refresh // no matter how long we wait; a short settle window is enough to catch a regression. try await Task.sleep(for: .milliseconds(300)) #expect(store.completedRefreshCountForTesting == 1) } @Test func `fixed mode ticks recur at the overridden cadence`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-fixed", frequency: .oneMinute) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) store.restartTimerWithSleepOverrideForTesting(.milliseconds(20)) // 1 initial launch refresh plus at least one 20ms-cadence tick; proves the loop recurs // rather than sleeping once and stopping. Each refresh cycle here costs low single-digit // seconds of wall time even with every provider disabled, so the timeout is generous. try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting >= 2 } #expect(store.completedRefreshCountForTesting >= 2) } @Test func `fixed cadence advances from scheduled tick instead of refresh completion`() { let interval = Duration.milliseconds(100) let start = ContinuousClock.now let firstScheduledAt = start + interval let nextAfterExactTick = UsageStore.nextFixedTimerScheduledAt( previousScheduledAt: firstScheduledAt, completedAt: firstScheduledAt, interval: interval) #expect(nextAfterExactTick == start + .milliseconds(200)) let nextJustBeforeFollowingTick = UsageStore.nextFixedTimerScheduledAt( previousScheduledAt: firstScheduledAt, completedAt: firstScheduledAt + .milliseconds(100) - .nanoseconds(1), interval: interval) #expect(nextJustBeforeFollowingTick == start + .milliseconds(200)) let nextAtFollowingTick = UsageStore.nextFixedTimerScheduledAt( previousScheduledAt: firstScheduledAt, completedAt: firstScheduledAt + .milliseconds(100), interval: interval) #expect(nextAtFollowingTick == start + .milliseconds(300)) let nextAfterSlowRefresh = UsageStore.nextFixedTimerScheduledAt( previousScheduledAt: firstScheduledAt, completedAt: firstScheduledAt + .milliseconds(60), interval: interval) #expect(nextAfterSlowRefresh == start + .milliseconds(200)) let nextAfterMissedTicks = UsageStore.nextFixedTimerScheduledAt( previousScheduledAt: firstScheduledAt, completedAt: firstScheduledAt + .milliseconds(260), interval: interval) #expect(nextAfterMissedTicks == start + .milliseconds(400)) } @Test func `fixed timer loop stays interval aligned after a slow refresh`() async { let harness = FixedTimerLoopHarness() await UsageStore.runFixedRefreshTimer( interval: .milliseconds(100), now: { await harness.now() }, sleep: { duration in await harness.sleep(for: duration) }, refresh: { await harness.refresh() }) #expect(await harness.recordedStarts() == [.milliseconds(100), .milliseconds(300)]) #expect(await harness.maximumConcurrentRefreshes() == 1) } @Test func `adaptive mode keeps recomputing and refreshing across menu-open changes`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-adaptive", frequency: .adaptive) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) store.restartTimerWithSleepOverrideForTesting(.milliseconds(20)) try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting >= 1 } let countBeforeMenuOpen = store.completedRefreshCountForTesting store.noteMenuOpened() // The loop kept looping (recomputing the decision from a fresh Input) after lastMenuOpenAt // changed, rather than sleeping once on a captured delay and stopping. try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting > countBeforeMenuOpen } #expect(store.completedRefreshCountForTesting > countBeforeMenuOpen) } @Test func `changing frequency away from fixed cancels the pending tick without an extra refresh`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel", frequency: .oneMinute) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) // Deliberately much longer than anything else in this test: the assertion only needs this // sleep to still be pending (uncompleted) when we switch away, not to time anything precisely. store.restartTimerWithSleepOverrideForTesting(.seconds(5)) // Only the initial launch refresh can land this quickly; the fixed-mode timer's first tick // needs the full 5s override to elapse, so it cannot have fired yet. try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 } let countBeforeSwitch = store.completedRefreshCountForTesting settings.refreshFrequency = .manual // The settings-change path (outside adaptive-refresh scope) may fire its own refresh(es) for // reasons unrelated to the timer under test; wait for the count to stop moving rather than // assuming it fires exactly once. Windows are doubled from an earlier version that flaked once // under full parallel `make test` load. let countAfterSettling = try await Self.waitForStableCount(store: store, settleWindow: .milliseconds(800)) #expect(countAfterSettling > countBeforeSwitch) // Settle comfortably within the 5s override window. If the old fixed-mode timer had not been // canceled, its pending tick would eventually land and push the count past the settled value — // but not within this window, so any further increase here indicates a real cancellation bug, // not settings-change noise. try await Task.sleep(for: .milliseconds(1600)) #expect(store.completedRefreshCountForTesting == countAfterSettling) } // The test above goes through `settings.refreshFrequency = .manual`, which also triggers the // settings-observer's own `refreshForSettingsChange()` — a legitimate refresh unrelated to the // timer. That confound means it cannot, by itself, prove the `guard !Task.isCancelled else { return }` // after each branch's sleep is load-bearing (deleting either guard still leaves this test green, // since the settings-observer refresh already accounts for the "count increased" expectation). // The two tests below isolate `startTimer()`'s cancel-and-replace path directly, by calling // `restartTimerWithSleepOverrideForTesting` a second time at the *same* frequency — which goes // straight through `startTimer()` with no settings observation involved — so no refresh is // legitimately expected at all, and any extra one proves a canceled sleep still ran its body. @Test func `restarting the timer cancels a pending fixed tick without an extra refresh`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel-fixed", frequency: .oneMinute) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) store.restartTimerWithSleepOverrideForTesting(.seconds(5)) try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 } let countBeforeRestart = store.completedRefreshCountForTesting // Cancels the pending 5s sleep above and starts a fresh one, still at .oneMinute. No settings // mutation, so no settings-observer refresh is expected here at all. store.restartTimerWithSleepOverrideForTesting(.seconds(5)) // Neither the old (canceled) timer's tick nor the new timer's first tick can land within this // window — both need the full 5s override. Any refresh here can only be the canceled sleep's // body running anyway. try await Task.sleep(for: .milliseconds(800)) #expect(store.completedRefreshCountForTesting == countBeforeRestart) } @Test func `restarting the timer cancels a pending adaptive tick without an extra refresh`() async throws { let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel-adaptive", frequency: .adaptive) let store = Self.makeUsageStore(settings: settings, startupBehavior: .full) store.restartTimerWithSleepOverrideForTesting(.seconds(5)) try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 } let countBeforeRestart = store.completedRefreshCountForTesting store.restartTimerWithSleepOverrideForTesting(.seconds(5)) try await Task.sleep(for: .milliseconds(800)) #expect(store.completedRefreshCountForTesting == countBeforeRestart) } /// Polls `condition` until it's true or `timeout` elapses, without assuming how long setup or /// scheduling takes. Throws `CancellationError` (surfaced as a test failure) on timeout. private static func waitUntil( timeout: Duration = .seconds(30), pollInterval: Duration = .milliseconds(20), _ condition: () -> Bool) async throws { let deadline = ContinuousClock.now + timeout while !condition() { if ContinuousClock.now >= deadline { throw CancellationError() } try await Task.sleep(for: pollInterval) } } /// Polls `store.completedRefreshCountForTesting` until it stops changing for `settleWindow`, /// tolerating an unknown number of in-flight refreshes (e.g. settings-change side effects /// unrelated to the timer under test) before returning the final, stable count. private static func waitForStableCount( store: UsageStore, settleWindow: Duration, timeout: Duration = .seconds(30), pollInterval: Duration = .milliseconds(20)) async throws -> Int { let deadline = ContinuousClock.now + timeout var lastCount = store.completedRefreshCountForTesting var lastChangedAt = ContinuousClock.now while true { try await Task.sleep(for: pollInterval) let current = store.completedRefreshCountForTesting let now = ContinuousClock.now if current != lastCount { lastCount = current lastChangedAt = now } else if now - lastChangedAt >= settleWindow { return lastCount } if now >= deadline { throw CancellationError() } } } private static func makeSettingsStore(suite: String, frequency: RefreshFrequency) -> SettingsStore { let defaults = UserDefaults(suiteName: suite)! defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let settings = SettingsStore( userDefaults: defaults, configStore: configStore, zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore(), codexCookieStore: InMemoryCookieHeaderStore(), claudeCookieStore: InMemoryCookieHeaderStore(), cursorCookieStore: InMemoryCookieHeaderStore(), opencodeCookieStore: InMemoryCookieHeaderStore(), factoryCookieStore: InMemoryCookieHeaderStore(), minimaxCookieStore: InMemoryMiniMaxCookieStore(), minimaxAPITokenStore: InMemoryMiniMaxAPITokenStore(), kimiTokenStore: InMemoryKimiTokenStore(), augmentCookieStore: InMemoryCookieHeaderStore(), ampCookieStore: InMemoryCookieHeaderStore(), copilotTokenStore: InMemoryCopilotTokenStore(), tokenAccountStore: InMemoryTokenAccountStore()) settings.providerDetectionCompleted = true settings.refreshFrequency = frequency Self.disableAllProviders(settings: settings) return settings } /// Codex is enabled by default; disabling every provider (including it) keeps `refresh()` cheap /// and deterministic in these tests, which care about tick cadence, not provider fetch results. private static func disableAllProviders(settings: SettingsStore) { let metadata = ProviderRegistry.shared.metadata for provider in UsageProvider.allCases { guard let providerMetadata = metadata[provider] else { continue } settings.setProviderEnabled(provider: provider, metadata: providerMetadata, enabled: false) } } private static func makeUsageStore( settings: SettingsStore, startupBehavior: UsageStore.StartupBehavior) -> UsageStore { UsageStore( fetcher: UsageFetcher(environment: [:]), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings, startupBehavior: startupBehavior, environmentBase: [:]) }} private actor FixedTimerLoopHarness { private let origin = ContinuousClock.now private var elapsed = Duration.zero private var starts: [Duration] = [] private var activeRefreshes = 0 private var maximumActiveRefreshes = 0 func now() -> ContinuousClock.Instant { self.origin + self.elapsed } func sleep(for duration: Duration) { self.elapsed += duration } func refresh() { self.activeRefreshes += 1 self.maximumActiveRefreshes = max(self.maximumActiveRefreshes, self.activeRefreshes) self.starts.append(self.elapsed) if self.starts.count == 1 { self.elapsed += .milliseconds(160) } self.activeRefreshes -= 1 if self.starts.count == 2 { withUnsafeCurrentTask { $0?.cancel() } } } func recordedStarts() -> [Duration] { self.starts } func maximumConcurrentRefreshes() -> Int { self.maximumActiveRefreshes }}
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
/// Covers the timer plumbing added on top of the pure `AdaptiveRefreshPolicy` (see
/// `AdaptiveRefreshPolicyTests`): how `UsageStore.startTimer()` wires live signals into the
/// policy, and how manual/fixed/adaptive modes drive (or don't drive) `refresh()` over time.
@MainActor
struct AdaptiveRefreshTimerTests {
@Test
func `launch with no menu history begins at thirty minutes`() {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-launch", frequency: .adaptive)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
#expect(store.lastMenuOpenAt == nil)
let decision = UsageStore.adaptiveRefreshDecision(
now: Date(),
lastMenuOpenAt: store.lastMenuOpenAt,
lowPowerModeEnabled: false,
thermalState: .nominal)
#expect(decision.reason == .longIdle)
#expect(decision.delay == .seconds(30 * 60))
}
@Test
func `menu-open signal changes the next adaptive decision`() {
let now = Date(timeIntervalSinceReferenceDate: 900_000_000)
let beforeOpen = UsageStore.adaptiveRefreshDecision(
now: now, lastMenuOpenAt: nil, lowPowerModeEnabled: false, thermalState: .nominal)
#expect(beforeOpen.reason == .longIdle)
let afterOpen = UsageStore.adaptiveRefreshDecision(
now: now, lastMenuOpenAt: now, lowPowerModeEnabled: false, thermalState: .nominal)
#expect(afterOpen.reason == .recentInteraction)
}
@Test
func `menu open advances a long idle timer during refresh without postponing an earlier tick`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-advance", frequency: .adaptive)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
store.restartTimerWithSleepOverrideForTesting(.seconds(10))
try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil }
let longIdleSchedule = try #require(store.adaptiveRefreshScheduledAt)
let now = Date()
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 50,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(30),
resetDescription: nil),
secondary: nil,
updatedAt: now,
identity: nil),
provider: .codex)
store.scheduleResetBoundaryRefreshIfNeeded(normalRefreshInterval: 30 * 60, now: now)
defer { store.cancelResetBoundaryRefresh() }
let resetBoundarySchedule = try #require(store.scheduledResetBoundaryRefreshAt)
store.isRefreshing = true
defer { store.isRefreshing = false }
store.noteMenuOpened()
try await Self.waitUntil {
guard let scheduledAt = store.adaptiveRefreshScheduledAt else { return false }
return scheduledAt < longIdleSchedule
}
let interactionSchedule = try #require(store.adaptiveRefreshScheduledAt)
#expect(store.isRefreshing)
#expect(store.scheduledResetBoundaryRefreshAt == resetBoundarySchedule)
store.noteMenuOpened(at: Date().addingTimeInterval(30))
#expect(store.adaptiveRefreshScheduledAt == interactionSchedule)
}
@Test
func `coding activity advances a long idle timer without postponing an earlier tick`() async throws {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-activity-advance",
frequency: .adaptiveAgentAware)
settings.adaptiveActivityScanConsent = .allowed
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
store.restartTimerWithSleepOverrideForTesting(.seconds(10))
try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil }
let longIdleSchedule = try #require(store.adaptiveRefreshScheduledAt)
let observedAt = Date()
store.noteCodingActivityObserved(at: observedAt, now: observedAt)
try await Self.waitUntil {
guard let scheduledAt = store.adaptiveRefreshScheduledAt else { return false }
return scheduledAt < longIdleSchedule
}
let activitySchedule = try #require(store.adaptiveRefreshScheduledAt)
#expect(store.lastCodingActivityAt == observedAt)
// An older observation is ignored. A newer observation is retained, but cannot push an
// already earlier provider refresh later.
store.noteCodingActivityObserved(
at: observedAt.addingTimeInterval(-1),
now: observedAt.addingTimeInterval(30))
#expect(store.lastCodingActivityAt == observedAt)
store.noteCodingActivityObserved(
at: observedAt.addingTimeInterval(1),
now: observedAt.addingTimeInterval(30))
#expect(store.lastCodingActivityAt == observedAt.addingTimeInterval(1))
#expect(store.adaptiveRefreshScheduledAt == activitySchedule)
}
@Test
func `plain adaptive ignores coding activity`() async throws {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-plain-adaptive-activity",
frequency: .adaptive)
settings.adaptiveActivityScanConsent = .allowed
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
store.restartTimerWithSleepOverrideForTesting(.seconds(10))
try await Self.waitUntil { store.adaptiveRefreshScheduledAt != nil }
let scheduledAt = try #require(store.adaptiveRefreshScheduledAt)
store.noteCodingActivityObserved(at: Date())
#expect(store.adaptiveRefreshScheduledAt == scheduledAt)
#expect(store.lastCodingActivityAt == nil)
}
@Test
func `noting a menu open records the signal without starting a refresh`() {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-noteMenuOpened", frequency: .manual)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
#expect(store.completedRefreshCountForTesting == 0)
#expect(store.isRefreshing == false)
store.noteMenuOpened()
#expect(store.lastMenuOpenAt != nil)
#expect(store.completedRefreshCountForTesting == 0)
#expect(store.isRefreshing == false)
}
@Test
func `noting coding activity outside agent aware mode is a no-op`() {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-noteCodingActivity",
frequency: .fiveMinutes)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
let observedAt = Date()
store.noteCodingActivityObserved(at: observedAt)
#expect(store.lastCodingActivityAt == nil)
#expect(store.adaptiveRefreshScheduledAt == nil)
#expect(store.completedRefreshCountForTesting == 0)
}
@Test
func `clearing coding activity removes the adaptive input`() {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-clearCodingActivity",
frequency: .adaptiveAgentAware)
settings.adaptiveActivityScanConsent = .allowed
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
store.noteCodingActivityObserved(at: Date(timeIntervalSinceReferenceDate: 100))
#expect(store.lastCodingActivityAt != nil)
store.clearCodingActivityObservation()
#expect(store.lastCodingActivityAt == nil)
}
@Test
func `opportunistic timer refresh is a no-op while another refresh is already in flight`() async {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-coalesce", frequency: .manual)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
store.isRefreshing = true
await store.refresh(enrichmentMode: .automatic)
// The guard at the top of runRefresh() returned immediately: no completion was recorded and the
// flag was left untouched by this call. This is the invariant every timer tick (fixed or
// adaptive) relies on to avoid overlapping with a refresh already in flight.
#expect(store.completedRefreshCountForTesting == 0)
#expect(store.isRefreshing == true)
}
@Test
func `manual mode performs the initial refresh but no recurring ticks`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-manual", frequency: .manual)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 }
// Manual mode never starts a timer, so nothing can push the count past the one launch refresh
// no matter how long we wait; a short settle window is enough to catch a regression.
try await Task.sleep(for: .milliseconds(300))
#expect(store.completedRefreshCountForTesting == 1)
}
@Test
func `fixed mode ticks recur at the overridden cadence`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-fixed", frequency: .oneMinute)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
store.restartTimerWithSleepOverrideForTesting(.milliseconds(20))
// 1 initial launch refresh plus at least one 20ms-cadence tick; proves the loop recurs
// rather than sleeping once and stopping. Each refresh cycle here costs low single-digit
// seconds of wall time even with every provider disabled, so the timeout is generous.
try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting >= 2 }
#expect(store.completedRefreshCountForTesting >= 2)
}
@Test
func `fixed cadence advances from scheduled tick instead of refresh completion`() {
let interval = Duration.milliseconds(100)
let start = ContinuousClock.now
let firstScheduledAt = start + interval
let nextAfterExactTick = UsageStore.nextFixedTimerScheduledAt(
previousScheduledAt: firstScheduledAt,
completedAt: firstScheduledAt,
interval: interval)
#expect(nextAfterExactTick == start + .milliseconds(200))
let nextJustBeforeFollowingTick = UsageStore.nextFixedTimerScheduledAt(
previousScheduledAt: firstScheduledAt,
completedAt: firstScheduledAt + .milliseconds(100) - .nanoseconds(1),
interval: interval)
#expect(nextJustBeforeFollowingTick == start + .milliseconds(200))
let nextAtFollowingTick = UsageStore.nextFixedTimerScheduledAt(
previousScheduledAt: firstScheduledAt,
completedAt: firstScheduledAt + .milliseconds(100),
interval: interval)
#expect(nextAtFollowingTick == start + .milliseconds(300))
let nextAfterSlowRefresh = UsageStore.nextFixedTimerScheduledAt(
previousScheduledAt: firstScheduledAt,
completedAt: firstScheduledAt + .milliseconds(60),
interval: interval)
#expect(nextAfterSlowRefresh == start + .milliseconds(200))
let nextAfterMissedTicks = UsageStore.nextFixedTimerScheduledAt(
previousScheduledAt: firstScheduledAt,
completedAt: firstScheduledAt + .milliseconds(260),
interval: interval)
#expect(nextAfterMissedTicks == start + .milliseconds(400))
}
@Test
func `fixed timer loop stays interval aligned after a slow refresh`() async {
let harness = FixedTimerLoopHarness()
await UsageStore.runFixedRefreshTimer(
interval: .milliseconds(100),
now: { await harness.now() },
sleep: { duration in await harness.sleep(for: duration) },
refresh: { await harness.refresh() })
#expect(await harness.recordedStarts() == [.milliseconds(100), .milliseconds(300)])
#expect(await harness.maximumConcurrentRefreshes() == 1)
}
@Test
func `adaptive mode keeps recomputing and refreshing across menu-open changes`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-adaptive", frequency: .adaptive)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
store.restartTimerWithSleepOverrideForTesting(.milliseconds(20))
try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting >= 1 }
let countBeforeMenuOpen = store.completedRefreshCountForTesting
store.noteMenuOpened()
// The loop kept looping (recomputing the decision from a fresh Input) after lastMenuOpenAt
// changed, rather than sleeping once on a captured delay and stopping.
try await Self.waitUntil(timeout: .seconds(45)) { store.completedRefreshCountForTesting > countBeforeMenuOpen }
#expect(store.completedRefreshCountForTesting > countBeforeMenuOpen)
}
@Test
func `changing frequency away from fixed cancels the pending tick without an extra refresh`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel", frequency: .oneMinute)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
// Deliberately much longer than anything else in this test: the assertion only needs this
// sleep to still be pending (uncompleted) when we switch away, not to time anything precisely.
store.restartTimerWithSleepOverrideForTesting(.seconds(5))
// Only the initial launch refresh can land this quickly; the fixed-mode timer's first tick
// needs the full 5s override to elapse, so it cannot have fired yet.
try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 }
let countBeforeSwitch = store.completedRefreshCountForTesting
settings.refreshFrequency = .manual
// The settings-change path (outside adaptive-refresh scope) may fire its own refresh(es) for
// reasons unrelated to the timer under test; wait for the count to stop moving rather than
// assuming it fires exactly once. Windows are doubled from an earlier version that flaked once
// under full parallel `make test` load.
let countAfterSettling = try await Self.waitForStableCount(store: store, settleWindow: .milliseconds(800))
#expect(countAfterSettling > countBeforeSwitch)
// Settle comfortably within the 5s override window. If the old fixed-mode timer had not been
// canceled, its pending tick would eventually land and push the count past the settled value —
// but not within this window, so any further increase here indicates a real cancellation bug,
// not settings-change noise.
try await Task.sleep(for: .milliseconds(1600))
#expect(store.completedRefreshCountForTesting == countAfterSettling)
}
// The test above goes through `settings.refreshFrequency = .manual`, which also triggers the
// settings-observer's own `refreshForSettingsChange()` — a legitimate refresh unrelated to the
// timer. That confound means it cannot, by itself, prove the `guard !Task.isCancelled else { return }`
// after each branch's sleep is load-bearing (deleting either guard still leaves this test green,
// since the settings-observer refresh already accounts for the "count increased" expectation).
// The two tests below isolate `startTimer()`'s cancel-and-replace path directly, by calling
// `restartTimerWithSleepOverrideForTesting` a second time at the *same* frequency — which goes
// straight through `startTimer()` with no settings observation involved — so no refresh is
// legitimately expected at all, and any extra one proves a canceled sleep still ran its body.
@Test
func `restarting the timer cancels a pending fixed tick without an extra refresh`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel-fixed", frequency: .oneMinute)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
store.restartTimerWithSleepOverrideForTesting(.seconds(5))
try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 }
let countBeforeRestart = store.completedRefreshCountForTesting
// Cancels the pending 5s sleep above and starts a fresh one, still at .oneMinute. No settings
// mutation, so no settings-observer refresh is expected here at all.
store.restartTimerWithSleepOverrideForTesting(.seconds(5))
// Neither the old (canceled) timer's tick nor the new timer's first tick can land within this
// window — both need the full 5s override. Any refresh here can only be the canceled sleep's
// body running anyway.
try await Task.sleep(for: .milliseconds(800))
#expect(store.completedRefreshCountForTesting == countBeforeRestart)
}
@Test
func `restarting the timer cancels a pending adaptive tick without an extra refresh`() async throws {
let settings = Self.makeSettingsStore(suite: "AdaptiveRefreshTimerTests-cancel-adaptive", frequency: .adaptive)
let store = Self.makeUsageStore(settings: settings, startupBehavior: .full)
store.restartTimerWithSleepOverrideForTesting(.seconds(5))
try await Self.waitUntil { store.completedRefreshCountForTesting >= 1 }
let countBeforeRestart = store.completedRefreshCountForTesting
store.restartTimerWithSleepOverrideForTesting(.seconds(5))
try await Task.sleep(for: .milliseconds(800))
#expect(store.completedRefreshCountForTesting == countBeforeRestart)
}
/// Polls `condition` until it's true or `timeout` elapses, without assuming how long setup or
/// scheduling takes. Throws `CancellationError` (surfaced as a test failure) on timeout.
private static func waitUntil(
timeout: Duration = .seconds(30),
pollInterval: Duration = .milliseconds(20),
_ condition: () -> Bool) async throws
{
let deadline = ContinuousClock.now + timeout
while !condition() {
if ContinuousClock.now >= deadline {
throw CancellationError()
}
try await Task.sleep(for: pollInterval)
}
}
/// Polls `store.completedRefreshCountForTesting` until it stops changing for `settleWindow`,
/// tolerating an unknown number of in-flight refreshes (e.g. settings-change side effects
/// unrelated to the timer under test) before returning the final, stable count.
private static func waitForStableCount(
store: UsageStore,
settleWindow: Duration,
timeout: Duration = .seconds(30),
pollInterval: Duration = .milliseconds(20)) async throws -> Int
{
let deadline = ContinuousClock.now + timeout
var lastCount = store.completedRefreshCountForTesting
var lastChangedAt = ContinuousClock.now
while true {
try await Task.sleep(for: pollInterval)
let current = store.completedRefreshCountForTesting
let now = ContinuousClock.now
if current != lastCount {
lastCount = current
lastChangedAt = now
} else if now - lastChangedAt >= settleWindow {
return lastCount
}
if now >= deadline {
throw CancellationError()
}
}
}
private static func makeSettingsStore(suite: String, frequency: RefreshFrequency) -> SettingsStore {
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
let configStore = testConfigStore(suiteName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: configStore,
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore(),
codexCookieStore: InMemoryCookieHeaderStore(),
claudeCookieStore: InMemoryCookieHeaderStore(),
cursorCookieStore: InMemoryCookieHeaderStore(),
opencodeCookieStore: InMemoryCookieHeaderStore(),
factoryCookieStore: InMemoryCookieHeaderStore(),
minimaxCookieStore: InMemoryMiniMaxCookieStore(),
minimaxAPITokenStore: InMemoryMiniMaxAPITokenStore(),
kimiTokenStore: InMemoryKimiTokenStore(),
augmentCookieStore: InMemoryCookieHeaderStore(),
ampCookieStore: InMemoryCookieHeaderStore(),
copilotTokenStore: InMemoryCopilotTokenStore(),
tokenAccountStore: InMemoryTokenAccountStore())
settings.providerDetectionCompleted = true
settings.refreshFrequency = frequency
Self.disableAllProviders(settings: settings)
return settings
}
/// Codex is enabled by default; disabling every provider (including it) keeps `refresh()` cheap
/// and deterministic in these tests, which care about tick cadence, not provider fetch results.
private static func disableAllProviders(settings: SettingsStore) {
let metadata = ProviderRegistry.shared.metadata
for provider in UsageProvider.allCases {
guard let providerMetadata = metadata[provider] else { continue }
settings.setProviderEnabled(provider: provider, metadata: providerMetadata, enabled: false)
}
}
private static func makeUsageStore(
settings: SettingsStore,
startupBehavior: UsageStore.StartupBehavior) -> UsageStore
{
UsageStore(
fetcher: UsageFetcher(environment: [:]),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
startupBehavior: startupBehavior,
environmentBase: [:])
}
}
private actor FixedTimerLoopHarness {
private let origin = ContinuousClock.now
private var elapsed = Duration.zero
private var starts: [Duration] = []
private var activeRefreshes = 0
private var maximumActiveRefreshes = 0
func now() -> ContinuousClock.Instant {
self.origin + self.elapsed
}
func sleep(for duration: Duration) {
self.elapsed += duration
}
func refresh() {
self.activeRefreshes += 1
self.maximumActiveRefreshes = max(self.maximumActiveRefreshes, self.activeRefreshes)
self.starts.append(self.elapsed)
if self.starts.count == 1 {
self.elapsed += .milliseconds(160)
}
self.activeRefreshes -= 1
if self.starts.count == 2 {
withUnsafeCurrentTask { $0?.cancel() }
}
}
func recordedStarts() -> [Duration] {
self.starts
}
func maximumConcurrentRefreshes() -> Int {
self.maximumActiveRefreshes
}
}