317 lines
12 KiB
Swift
317 lines
12 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
struct UsageStoreResetBoundaryRefreshTests {
|
|
@Test
|
|
func `schedules refresh at reset boundary before normal poll`() {
|
|
let now = Date(timeIntervalSince1970: 1000)
|
|
let resetsAt = now.addingTimeInterval(10 * 60)
|
|
let snapshot = Self.snapshot(updatedAt: now, primaryResetsAt: resetsAt)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
now: now)
|
|
|
|
#expect(refreshAt == resetsAt.addingTimeInterval(UsageStore.resetBoundaryRefreshGraceSeconds))
|
|
}
|
|
|
|
@Test
|
|
func `schedules prompt refresh when reset boundary already passed`() {
|
|
let now = Date(timeIntervalSince1970: 2000)
|
|
let resetsAt = now.addingTimeInterval(-3 * 60)
|
|
let snapshot = Self.snapshot(
|
|
updatedAt: resetsAt.addingTimeInterval(-60),
|
|
primaryResetsAt: resetsAt)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
now: now)
|
|
|
|
#expect(refreshAt == now.addingTimeInterval(UsageStore.resetBoundaryRefreshMinimumDelaySeconds))
|
|
}
|
|
|
|
@Test
|
|
func `suppresses repeated prompt refresh after attempted boundary`() {
|
|
let now = Date(timeIntervalSince1970: 2500)
|
|
let resetsAt = now.addingTimeInterval(-3 * 60)
|
|
let boundaryRefreshAt = resetsAt.addingTimeInterval(UsageStore.resetBoundaryRefreshGraceSeconds)
|
|
let snapshot = Self.snapshot(
|
|
updatedAt: resetsAt.addingTimeInterval(-60),
|
|
primaryResetsAt: resetsAt)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
attemptedBoundaryRefreshes: [boundaryRefreshAt],
|
|
now: now)
|
|
|
|
#expect(refreshAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `in flight boundary refresh remains retryable`() {
|
|
let now = Date(timeIntervalSince1970: 2750)
|
|
let resetsAt = now.addingTimeInterval(-3 * 60)
|
|
let boundaryRefreshAt = resetsAt.addingTimeInterval(UsageStore.resetBoundaryRefreshGraceSeconds)
|
|
let snapshot = Self.snapshot(
|
|
updatedAt: resetsAt.addingTimeInterval(-60),
|
|
primaryResetsAt: resetsAt)
|
|
|
|
#expect(UsageStore.shouldRecordResetBoundaryAttempt(isRefreshing: true) == false)
|
|
#expect(UsageStore.shouldRecordResetBoundaryAttempt(isRefreshing: false) == true)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
attemptedBoundaryRefreshes: [],
|
|
now: now)
|
|
|
|
#expect(refreshAt == now.addingTimeInterval(UsageStore.resetBoundaryRefreshMinimumDelaySeconds))
|
|
|
|
let suppressedAfterRecordedAttempt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
attemptedBoundaryRefreshes: [boundaryRefreshAt],
|
|
now: now)
|
|
|
|
#expect(suppressedAfterRecordedAttempt == nil)
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func `in flight boundary refresh clears fired schedule marker`() async {
|
|
let now = Date(timeIntervalSince1970: 2800)
|
|
let resetsAt = now.addingTimeInterval(-3 * 60)
|
|
let boundaryRefreshAt = resetsAt.addingTimeInterval(UsageStore.resetBoundaryRefreshGraceSeconds)
|
|
let retryAt = now.addingTimeInterval(UsageStore.resetBoundaryRefreshMinimumDelaySeconds)
|
|
let snapshot = Self.snapshot(
|
|
updatedAt: resetsAt.addingTimeInterval(-60),
|
|
primaryResetsAt: resetsAt)
|
|
let settings = testSettingsStore(suiteName: "UsageStoreResetBoundaryRefreshTests-inflight-marker")
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(environment: [:]),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
startupBehavior: .testing)
|
|
store.snapshots[.codex] = snapshot
|
|
store.isRefreshing = true
|
|
store.scheduledResetBoundaryRefreshAt = retryAt
|
|
|
|
await store.runResetBoundaryRefresh(boundaryRefreshAt: boundaryRefreshAt)
|
|
|
|
#expect(store.scheduledResetBoundaryRefreshAt == nil)
|
|
#expect(store.attemptedResetBoundaryRefreshes.isEmpty)
|
|
|
|
store.isRefreshing = false
|
|
store.scheduleResetBoundaryRefreshIfNeeded(normalRefreshInterval: 30 * 60, now: now)
|
|
defer { store.cancelResetBoundaryRefresh() }
|
|
|
|
#expect(store.scheduledResetBoundaryRefreshAt == retryAt)
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func `boundary refresh records its attempt before waiting for forced enrichment`() async {
|
|
let settings = testSettingsStore(suiteName: "UsageStoreResetBoundaryRefreshTests-waits-for-tail")
|
|
settings.statusChecksEnabled = false
|
|
settings.costUsageEnabled = true
|
|
settings.openAIWebAccessEnabled = false
|
|
settings.codexCookieSource = .off
|
|
for provider in UsageProvider.allCases {
|
|
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
|
|
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
|
|
}
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(environment: [:]),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
startupBehavior: .testing)
|
|
let tokenGate = BlockingForcedTokenRefresh()
|
|
var providerRefreshes = 0
|
|
var didObserveWait = false
|
|
store._test_providerRefreshOverride = { _ in
|
|
providerRefreshes += 1
|
|
}
|
|
store._test_codexCreditsLoaderOverride = {
|
|
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
|
|
}
|
|
store._test_tokenUsageRefreshOverride = { provider, force in
|
|
guard force else { return }
|
|
await tokenGate.run(
|
|
provider: provider,
|
|
force: force,
|
|
interaction: ProviderInteractionContext.current,
|
|
refreshPhase: ProviderRefreshContext.current,
|
|
browserRetryAllowed: false)
|
|
}
|
|
store._test_forcedRefreshEnrichmentWaitObserver = {
|
|
didObserveWait = true
|
|
}
|
|
defer {
|
|
store._test_providerRefreshOverride = nil
|
|
store._test_codexCreditsLoaderOverride = nil
|
|
store._test_tokenUsageRefreshOverride = nil
|
|
store._test_forcedRefreshEnrichmentWaitObserver = nil
|
|
}
|
|
|
|
await store.refresh(enrichmentMode: .forcedBackground)
|
|
#expect(await tokenGate.waitUntilStarted(count: 1))
|
|
settings.costUsageEnabled = false
|
|
|
|
let boundaryRefreshAt = Date(timeIntervalSince1970: 12345)
|
|
let boundaryTask = Task { @MainActor in
|
|
await store.runResetBoundaryRefresh(boundaryRefreshAt: boundaryRefreshAt)
|
|
}
|
|
for _ in 0..<100 where !didObserveWait {
|
|
await Task.yield()
|
|
}
|
|
|
|
#expect(didObserveWait)
|
|
#expect(providerRefreshes == 1)
|
|
#expect(store.attemptedResetBoundaryRefreshes.contains(boundaryRefreshAt))
|
|
|
|
await tokenGate.resumeNext()
|
|
await boundaryTask.value
|
|
|
|
#expect(providerRefreshes == 2)
|
|
#expect(store.attemptedResetBoundaryRefreshes.contains(boundaryRefreshAt))
|
|
#expect(!store.hasForcedRefreshEnrichmentInFlight)
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func `boundary refresh does not reschedule unchanged stale snapshot`() async {
|
|
let settings = testSettingsStore(suiteName: "UsageStoreResetBoundaryRefreshTests-no-duplicate")
|
|
settings.refreshFrequency = .oneMinute
|
|
settings.statusChecksEnabled = false
|
|
settings.costUsageEnabled = false
|
|
settings.openAIWebAccessEnabled = false
|
|
settings.codexCookieSource = .off
|
|
for provider in UsageProvider.allCases {
|
|
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
|
|
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
|
|
}
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(environment: [:]),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
startupBehavior: .testing)
|
|
let now = Date()
|
|
let boundaryRefreshAt = now.addingTimeInterval(1)
|
|
store.snapshots[.codex] = Self.snapshot(
|
|
updatedAt: now.addingTimeInterval(-60),
|
|
primaryResetsAt: boundaryRefreshAt.addingTimeInterval(-UsageStore.resetBoundaryRefreshGraceSeconds))
|
|
var providerRefreshes = 0
|
|
store._test_providerRefreshOverride = { _ in
|
|
providerRefreshes += 1
|
|
}
|
|
defer {
|
|
store._test_providerRefreshOverride = nil
|
|
store.cancelResetBoundaryRefresh()
|
|
}
|
|
|
|
await store.runResetBoundaryRefresh(boundaryRefreshAt: boundaryRefreshAt)
|
|
|
|
#expect(providerRefreshes == 1)
|
|
#expect(store.attemptedResetBoundaryRefreshes.contains(boundaryRefreshAt))
|
|
#expect(store.resetBoundaryRefreshTask == nil)
|
|
#expect(store.scheduledResetBoundaryRefreshAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `ignores reset boundary after normal poll`() {
|
|
let now = Date(timeIntervalSince1970: 3000)
|
|
let resetsAt = now.addingTimeInterval(40 * 60)
|
|
let snapshot = Self.snapshot(updatedAt: now, primaryResetsAt: resetsAt)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
now: now)
|
|
|
|
#expect(refreshAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `ignores already refreshed reset boundary`() {
|
|
let now = Date(timeIntervalSince1970: 4000)
|
|
let resetsAt = now.addingTimeInterval(-3 * 60)
|
|
let snapshot = Self.snapshot(updatedAt: now, primaryResetsAt: resetsAt)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
now: now)
|
|
|
|
#expect(refreshAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `uses earliest boundary across secondary and extra windows`() {
|
|
let now = Date(timeIntervalSince1970: 5000)
|
|
let secondaryResetsAt = now.addingTimeInterval(8 * 60)
|
|
let extraResetsAt = now.addingTimeInterval(4 * 60)
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: 10,
|
|
windowMinutes: 300,
|
|
resetsAt: now.addingTimeInterval(20 * 60),
|
|
resetDescription: nil),
|
|
secondary: RateWindow(
|
|
usedPercent: 80,
|
|
windowMinutes: 10080,
|
|
resetsAt: secondaryResetsAt,
|
|
resetDescription: nil),
|
|
tertiary: nil,
|
|
extraRateWindows: [
|
|
NamedRateWindow(
|
|
id: "extra",
|
|
title: "Extra",
|
|
window: RateWindow(
|
|
usedPercent: 50,
|
|
windowMinutes: 60,
|
|
resetsAt: extraResetsAt,
|
|
resetDescription: nil)),
|
|
],
|
|
updatedAt: now)
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: 30 * 60,
|
|
now: now)
|
|
|
|
#expect(refreshAt == extraResetsAt.addingTimeInterval(UsageStore.resetBoundaryRefreshGraceSeconds))
|
|
}
|
|
|
|
@Test
|
|
func `manual refresh cadence does not schedule boundary refresh`() {
|
|
let now = Date(timeIntervalSince1970: 6000)
|
|
let snapshot = Self.snapshot(
|
|
updatedAt: now,
|
|
primaryResetsAt: now.addingTimeInterval(10 * 60))
|
|
|
|
let refreshAt = UsageStore.nextResetBoundaryRefreshDate(
|
|
snapshots: [.codex: snapshot],
|
|
normalRefreshInterval: nil,
|
|
now: now)
|
|
|
|
#expect(refreshAt == nil)
|
|
}
|
|
|
|
private static func snapshot(updatedAt: Date, primaryResetsAt: Date) -> UsageSnapshot {
|
|
UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: 100,
|
|
windowMinutes: 300,
|
|
resetsAt: primaryResetsAt,
|
|
resetDescription: nil),
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
updatedAt: updatedAt)
|
|
}
|
|
}
|