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

499 lines
18 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct CostUsageFetcherCacheSnapshotTests {
@Test
func `cached codex token snapshot loads from existing cache without rescanning`() 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 cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(cached?.sessionTokens == 42)
#expect(cached?.last30DaysTokens == 42)
#expect(cached?.daily.map(\.date) == ["2026-04-08"])
}
@Test
func `cached codex token snapshot keeps the cache scan time as updatedAt`() 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 cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
#expect(cache.lastScanUnixMs > 0)
let scanTime = Date(timeIntervalSince1970: TimeInterval(cache.lastScanUnixMs) / 1000)
let hydratedAt = day.addingTimeInterval(50 * 60)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: hydratedAt,
historyDays: 1,
scannerOptions: options)
#expect(cached?.snapshot.updatedAt == scanTime)
#expect(cached?.snapshot.updatedAt != hydratedAt)
#expect(cached?.lastRefreshAt == scanTime)
}
@Test
func `cached codex token snapshot keeps the oldest scan time when pi sessions merge`() 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)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions)
let nativeCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
var piCache = PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot)
#expect(nativeCache.lastScanUnixMs > 0)
#expect(piCache.lastScanUnixMs > 0)
piCache.lastScanUnixMs = nativeCache.lastScanUnixMs - 30 * 60 * 1000
PiSessionCostCacheIO.save(cache: piCache, cacheRoot: env.cacheRoot)
let oldestScanTime = Date(timeIntervalSince1970: TimeInterval(piCache.lastScanUnixMs) / 1000)
let hydratedAt = day.addingTimeInterval(50 * 60)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: hydratedAt,
historyDays: 1,
scannerOptions: options)
#expect(cached?.snapshot.sessionTokens == 207)
#expect(cached?.snapshot.updatedAt == oldestScanTime)
#expect(cached?.snapshot.updatedAt != hydratedAt)
#expect(cached?.lastRefreshAt == nil)
}
@Test
func `cached codex token snapshot keeps pi scan time when only pi sessions exist`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions)
let piCache = PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot)
#expect(piCache.lastScanUnixMs > 0)
let piScanTime = Date(timeIntervalSince1970: TimeInterval(piCache.lastScanUnixMs) / 1000)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshotResult(
now: day.addingTimeInterval(50 * 60),
historyDays: 1,
scannerOptions: options)
#expect(cached?.snapshot.sessionTokens == 165)
#expect(cached?.snapshot.updatedAt == piScanTime)
#expect(cached?.lastRefreshAt == nil)
}
@Test
func `cached codex token snapshot keeps native scan time when pi cache lacks one`() 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)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions)
var piCache = PiSessionCostCacheIO.load(cacheRoot: env.cacheRoot)
piCache.lastScanUnixMs = 0
PiSessionCostCacheIO.save(cache: piCache, cacheRoot: env.cacheRoot)
let nativeCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
#expect(nativeCache.lastScanUnixMs > 0)
let nativeScanTime = Date(
timeIntervalSince1970: TimeInterval(nativeCache.lastScanUnixMs) / 1000)
let hydratedAt = day.addingTimeInterval(50 * 60)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: hydratedAt,
historyDays: 1,
scannerOptions: options)
#expect(cached?.sessionTokens == 207)
#expect(cached?.updatedAt == nativeScanTime)
#expect(cached?.updatedAt != hydratedAt)
}
@Test
func `cached codex token snapshot refuses expanded or managed scopes`() 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 expanded = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 7,
scannerOptions: options)
let managed = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
codexHomePath: env.codexHomeRoot.path,
historyDays: 1,
scannerOptions: options)
#expect(expanded == nil)
#expect(managed == nil)
}
@Test
func `cached codex token snapshot omits projects until metadata migration`() 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 current = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(current?.projects.count == 1)
var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
cache.codexProjectMetadataVersion = nil
CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)
let legacy = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(legacy?.sessionTokens == 42)
#expect(legacy?.projects.isEmpty == true)
}
@Test
func `cached codex token snapshot refuses mismatched roots fingerprint`() 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)
var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
cache.roots = [env.root.appendingPathComponent("other/sessions", isDirectory: true).path: 0]
CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(cached == nil)
}
@Test
func `cached codex token snapshot merges cached pi sessions`() 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)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(cached?.sessionTokens == 207)
#expect(cached?.last30DaysTokens == 207)
}
@Test
func `cached codex token snapshot loads cached pi sessions without native codex cache`() async throws {
let env = try CostUsageTestEnvironment()
defer { env.cleanup() }
let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = PiSessionCostScanner.loadDailyReport(
provider: .codex,
since: day,
until: day,
now: day,
options: piOptions)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot))
#expect(cached?.sessionTokens == 165)
#expect(cached?.last30DaysTokens == 165)
}
@Test
func `cached codex token snapshot still loads pi sessions when native cache roots mismatch`() 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)
try Self.writePiCodexSessionFile(env: env, day: day, tokens: 165)
let options = CostUsageScanner.Options(
codexSessionsRoot: env.codexSessionsRoot,
cacheRoot: env.cacheRoot)
let piOptions = PiSessionCostScanner.Options(
piSessionsRoot: env.piSessionsRoot,
cacheRoot: env.cacheRoot,
refreshMinIntervalSeconds: 0)
_ = try await CostUsageFetcher.loadTokenSnapshot(
provider: .codex,
now: day,
historyDays: 1,
refreshPricingInBackground: false,
scannerOptions: options,
piScannerOptions: piOptions)
var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)
cache.roots = [env.root.appendingPathComponent("other/sessions", isDirectory: true).path: 0]
CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)
let cached = await CostUsageFetcher.loadCachedCodexTokenSnapshot(
now: day,
historyDays: 1,
scannerOptions: options)
#expect(cached?.sessionTokens == 165)
#expect(cached?.last30DaysTokens == 165)
}
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)
}
private static func writePiCodexSessionFile(
env: CostUsageTestEnvironment,
day: Date,
tokens: Int) throws
{
_ = try env.writePiSessionFile(
relativePath: "nested/run-0/2026-04-08T10-00-00-000Z_test.jsonl",
contents: env.jsonl([
[
"type": "message",
"timestamp": env.isoString(for: day),
"message": [
"role": "assistant",
"provider": "openai-codex",
"model": "openai/gpt-5.4",
"timestamp": Int(day.timeIntervalSince1970 * 1000),
"usage": [
"input": tokens,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": tokens,
],
],
],
]))
}
}