import CodexBarCore import Foundation import Testing @testable import CodexBar // swiftlint:disable:next type_body_length struct UsageStorePlanUtilizationTests { @Test func `coalesces changed usage within hour into single entry`() throws { let calendar = Calendar(identifier: .gregorian) let hourStart = try #require(calendar.date(from: DateComponents( timeZone: TimeZone(secondsFromGMT: 0), year: 2026, month: 3, day: 17, hour: 10))) let first = planEntry(at: hourStart, usedPercent: 10) let second = planEntry(at: hourStart.addingTimeInterval(25 * 60), usedPercent: 35) let initial = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: [], entry: first)) let updated = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: initial, entry: second)) #expect(updated.count == 1) #expect(updated.last == second) } @Test func `changed reset boundary within hour appends new entry`() throws { let calendar = Calendar(identifier: .gregorian) let hourStart = try #require(calendar.date(from: DateComponents( timeZone: TimeZone(secondsFromGMT: 0), year: 2026, month: 3, day: 17, hour: 10))) let first = planEntry( at: hourStart.addingTimeInterval(5 * 60), usedPercent: 82, resetsAt: hourStart.addingTimeInterval(30 * 60)) let second = planEntry( at: hourStart.addingTimeInterval(35 * 60), usedPercent: 4, resetsAt: hourStart.addingTimeInterval(5 * 60 * 60)) let initial = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: [], entry: first)) let updated = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: initial, entry: second)) #expect(updated.count == 2) #expect(updated[0] == first) #expect(updated[1] == second) } @Test func `first known reset boundary within hour replaces earlier provisional peak even when usage drops`() throws { let calendar = Calendar(identifier: .gregorian) let hourStart = try #require(calendar.date(from: DateComponents( timeZone: TimeZone(secondsFromGMT: 0), year: 2026, month: 3, day: 17, hour: 10))) let first = planEntry( at: hourStart.addingTimeInterval(5 * 60), usedPercent: 82, resetsAt: nil) let second = planEntry( at: hourStart.addingTimeInterval(35 * 60), usedPercent: 4, resetsAt: hourStart.addingTimeInterval(5 * 60 * 60)) let initial = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: [], entry: first)) let updated = try #require( UsageStore._updatedPlanUtilizationEntriesForTesting( existingEntries: initial, entry: second)) #expect(updated.count == 1) #expect(updated[0] == second) } @Test func `trims entry history to retention limit`() throws { let maxSamples = UsageStore._planUtilizationMaxSamplesForTesting let base = Date(timeIntervalSince1970: 1_700_000_000) var entries: [PlanUtilizationHistoryEntry] = [] for offset in 0.. UsageStore { let suiteName = "UsageStorePlanUtilizationTests-\(UUID().uuidString)" guard let defaults = UserDefaults(suiteName: suiteName) else { fatalError("Failed to create isolated UserDefaults suite for tests") } defaults.removePersistentDomain(forName: suiteName) let configStore = testConfigStore(suiteName: suiteName) let planHistoryStore = testPlanUtilizationHistoryStore(suiteName: suiteName) let temporaryRoot = FileManager.default.temporaryDirectory.standardizedFileURL.path let managedStoreURL = FileManager.default.temporaryDirectory .appendingPathComponent("\(suiteName)-managed-codex-accounts.json") precondition(configStore.fileURL.standardizedFileURL.path.hasPrefix(temporaryRoot)) precondition(configStore.fileURL.standardizedFileURL != CodexBarConfigStore.defaultURL().standardizedFileURL) if let historyURL = planHistoryStore.directoryURL?.standardizedFileURL { precondition(historyURL.path.hasPrefix(temporaryRoot)) } let managedStore = FileManagedCodexAccountStore(fileURL: managedStoreURL) try? FileManager.default.removeItem(at: managedStoreURL) do { try managedStore.storeAccounts(ManagedCodexAccountSet( version: FileManagedCodexAccountStore.currentVersion, accounts: [])) } catch { fatalError("Failed to seed isolated managed Codex account store: \(error)") } let isolatedSettings = SettingsStore( userDefaults: defaults, configStore: configStore, tokenAccountStore: InMemoryTokenAccountStore()) let store = UsageStore( fetcher: UsageFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), settings: isolatedSettings, planUtilizationHistoryStore: planHistoryStore, startupBehavior: .testing) isolatedSettings._test_managedCodexAccountStoreURL = managedStoreURL isolatedSettings.codexActiveSource = .liveSystem // Cancel the background plan-utilization decode so it cannot race the // explicit empty assignment below. Production paths still load on the // utility queue; this only short-circuits the test setup. store._cancelPlanUtilizationHistoryLoadForTesting() store.planUtilizationHistory = [:] return store } static func makeSnapshot(provider: UsageProvider, email: String) -> UsageSnapshot { UsageSnapshot( primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil), secondary: RateWindow(usedPercent: 20, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), updatedAt: Date(), identity: ProviderIdentitySnapshot( providerID: provider, accountEmail: email, accountOrganization: nil, loginMethod: "plus")) } static func loadPlanUtilizationFixture(named name: String) throws -> PlanUtilizationHistoryBuckets { let fixtureURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() .appendingPathComponent("Fixtures", isDirectory: true) .appendingPathComponent(name, isDirectory: false) let data = try Data(contentsOf: fixtureURL) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let document = try decoder.decode(FixtureDocument.self, from: data) return PlanUtilizationHistoryBuckets( preferredAccountKey: document.preferredAccountKey, unscoped: document.unscoped, accounts: document.accounts) } } extension UsageStorePlanUtilizationTests { @MainActor @Test func `global refresh tail does not keep completed provider plan card loading`() { let store = Self.makeStore() store._setSnapshotForTesting(nil, provider: .claude) store.isRefreshing = true store.refreshingProviders.insert(.claude) #expect(store.shouldShowRefreshingMenuCard(for: .claude)) #expect(store.shouldShowRefreshingMenuCardIndicator(for: .claude)) #expect(store.shouldHidePlanUtilizationMenuItem(for: .claude)) store.refreshingProviders.remove(.claude) #expect(store.isRefreshing) #expect(store.refreshingProviders.isEmpty) #expect(!store.shouldShowRefreshingMenuCard(for: .claude)) #expect(!store.shouldShowRefreshingMenuCardIndicator(for: .claude)) #expect(!store.shouldHidePlanUtilizationMenuItem(for: .claude)) } } func planEntry(at capturedAt: Date, usedPercent: Double, resetsAt: Date? = nil) -> PlanUtilizationHistoryEntry { PlanUtilizationHistoryEntry(capturedAt: capturedAt, usedPercent: usedPercent, resetsAt: resetsAt) } func planSeries( name: PlanUtilizationSeriesName, windowMinutes: Int, entries: [PlanUtilizationHistoryEntry]) -> PlanUtilizationSeriesHistory { PlanUtilizationSeriesHistory(name: name, windowMinutes: windowMinutes, entries: entries) } func findSeries( _ histories: [PlanUtilizationSeriesHistory], name: PlanUtilizationSeriesName, windowMinutes: Int) -> PlanUtilizationSeriesHistory? { histories.first { $0.name == name && $0.windowMinutes == windowMinutes } } func formattedBoundary(_ date: Date) -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone.current formatter.dateFormat = "yyyy-MM-dd HH:mm" return formatter.string(from: date) }