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

260 lines
9.7 KiB
Swift

import Foundation
import Testing
@testable import CodexBar
@testable import CodexBarCore
@MainActor
@Suite(.serialized)
struct UsageStoreCachedTokenHydrationTests {
@Test
func `cached codex token hydration populates startup token snapshot`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
try Self.writeCodexSessionFile(
homeRoot: env.codexHomeRoot,
env: env,
day: day,
filename: "cached.jsonl",
tokens: 42)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
scannerOptions: options)
let settings = Self.makeCodexOnlySettings(historyDays: 1)
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
costUsageFetcher: CostUsageFetcher(scannerOptions: options),
settings: settings,
startupBehavior: .testing,
environmentBase: [:])
store.hydrateCachedTokenSnapshots(now: day)
for _ in 0..<100 where store.tokenSnapshot(for: .codex) == nil {
try await Task.sleep(for: .milliseconds(10))
}
#expect(store.tokenSnapshot(for: .codex)?.sessionTokens == 42)
#expect(store.tokenSnapshot(for: .codex)?.daily.map(\.date) == ["2026-04-08"])
#expect(store.tokenError(for: .codex) == nil)
}
@Test
func `cached codex token hydration skips managed codex homes`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
try Self.writeCodexSessionFile(
homeRoot: env.codexHomeRoot,
env: env,
day: day,
filename: "cached.jsonl",
tokens: 42)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
scannerOptions: options)
let settings = Self.makeCodexOnlySettings(historyDays: 1)
let managedAccount = ManagedCodexAccount(
id: UUID(),
email: "managed@example.com",
managedHomePath: env.codexHomeRoot.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
settings._test_activeManagedCodexAccount = managedAccount
settings.codexActiveSource = .managedAccount(id: managedAccount.id)
defer { settings._test_activeManagedCodexAccount = nil }
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
costUsageFetcher: CostUsageFetcher(scannerOptions: options),
settings: settings,
startupBehavior: .testing,
environmentBase: [:])
store.hydrateCachedTokenSnapshots(now: day)
for _ in 0..<20 {
try await Task.sleep(for: .milliseconds(10))
}
#expect(store.tokenSnapshot(for: .codex) == nil)
}
@Test
func `fresh cached hydration suppresses the redundant startup token refresh`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let now = Date()
try Self.writeCodexSessionFile(
homeRoot: env.codexHomeRoot,
env: env,
day: now,
filename: "cached.jsonl",
tokens: 42)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: now,
historyDays: 1,
scannerOptions: options)
let settings = Self.makeCodexOnlySettings(historyDays: 1)
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
costUsageFetcher: CostUsageFetcher(scannerOptions: options),
settings: settings,
startupBehavior: .testing,
environmentBase: [:])
var tokenRefreshCount = 0
store._test_tokenUsageRefreshOverride = { _, _ in tokenRefreshCount += 1 }
store.hydrateCachedTokenSnapshots(now: now)
for _ in 0..<100 where store.tokenSnapshot(for: .codex) == nil {
try await Task.sleep(for: .milliseconds(10))
}
await store.refreshTokenUsageNow(for: .codex, force: false)
#expect(store.tokenSnapshot(for: .codex)?.sessionTokens == 42)
#expect(store.tokenLastAttemptAt(for: .codex).map { abs($0.timeIntervalSince(now)) < 0.001 } == true)
#expect(tokenRefreshCount == 0)
}
@Test
func `stale cached hydration still allows the startup token refresh`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let now = Date()
try Self.writeCodexSessionFile(
homeRoot: env.codexHomeRoot,
env: env,
day: now,
filename: "cached.jsonl",
tokens: 42)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: now,
historyDays: 1,
scannerOptions: options)
var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
cache.lastScanUnixMs = Int64(now.addingTimeInterval(-2 * 60 * 60).timeIntervalSince1970 * 1000)
CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)
let settings = Self.makeCodexOnlySettings(historyDays: 1)
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
costUsageFetcher: CostUsageFetcher(scannerOptions: options),
settings: settings,
startupBehavior: .testing,
environmentBase: [:])
var tokenRefreshCount = 0
store._test_tokenUsageRefreshOverride = { _, _ in tokenRefreshCount += 1 }
store.hydrateCachedTokenSnapshots(now: now)
for _ in 0..<100 where store.tokenSnapshot(for: .codex) == nil {
try await Task.sleep(for: .milliseconds(10))
}
await store.refreshTokenUsageNow(for: .codex, force: false)
#expect(store.tokenSnapshot(for: .codex)?.sessionTokens == 42)
#expect(store.tokenLastAttemptAt(for: .codex) != nil)
#expect(tokenRefreshCount == 1)
}
private static func makeCodexOnlySettings(historyDays: Int) -> SettingsStore {
let suite = "UsageStoreCachedTokenHydrationTests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.costUsageHistoryDays = historyDays
settings.openAIWebAccessEnabled = false
settings.codexCookieSource = .off
settings.providerDetectionCompleted = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
return settings
}
private static func writeCodexSessionFile(
homeRoot: URL,
env: CostUsageTestEnvironment,
day: Date,
filename: String,
tokens: Int) throws
{
let comps = Calendar.current.dateComponents([.year, .month, .day], from: day)
let dir = homeRoot
.appendingPathComponent("sessions", isDirectory: true)
.appendingPathComponent(String(format: "%04d", comps.year ?? 1970), isDirectory: true)
.appendingPathComponent(String(format: "%02d", comps.month ?? 1), isDirectory: true)
.appendingPathComponent(String(format: "%02d", comps.day ?? 1), isDirectory: true)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let model = "openai/gpt-5.4"
let url = dir.appendingPathComponent(filename, isDirectory: false)
try env.jsonl([
[
"type": "turn_context",
"timestamp": env.isoString(for: day),
"payload": ["model": model],
],
[
"type": "event_msg",
"timestamp": env.isoString(for: day.addingTimeInterval(1)),
"payload": [
"type": "token_count",
"info": [
"last_token_usage": [
"input_tokens": tokens,
"cached_input_tokens": 0,
"output_tokens": 0,
],
"model": model,
],
],
],
]).write(to: url, atomically: true, encoding: .utf8)
}
}