164 lines
5.8 KiB
Swift
164 lines
5.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct CostUsageCacheTests {
|
|
@Test
|
|
func `cache file URL uses provider artifact versions`() {
|
|
let root = URL(fileURLWithPath: "/tmp/codexbar-cost-cache", isDirectory: true)
|
|
|
|
let codexURL = CostUsageCacheIO.cacheFileURL(provider: .codex, cacheRoot: root)
|
|
let claudeURL = CostUsageCacheIO.cacheFileURL(provider: .claude, cacheRoot: root)
|
|
let vertexURL = CostUsageCacheIO.cacheFileURL(provider: .vertexai, cacheRoot: root)
|
|
|
|
#expect(codexURL.lastPathComponent == "codex-v9.json")
|
|
#expect(claudeURL.lastPathComponent == "claude-v4.json")
|
|
#expect(vertexURL.lastPathComponent == "vertexai-v4.json")
|
|
}
|
|
|
|
@Test
|
|
func `cache load requires matching producer key`() throws {
|
|
let root = try self.makeTemporaryCacheRoot()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
var cache = CostUsageCache()
|
|
cache.lastScanUnixMs = 123
|
|
cache.days = ["2026-05-18": ["gpt-5.5": [1, 2, 3]]]
|
|
|
|
CostUsageCacheIO.save(
|
|
provider: .codex,
|
|
cache: cache,
|
|
cacheRoot: root,
|
|
producerKey: "codex:cu:p1111111111111111")
|
|
|
|
let loaded = CostUsageCacheIO.load(
|
|
provider: .codex,
|
|
cacheRoot: root,
|
|
producerKey: "codex:cu:p1111111111111111")
|
|
#expect(loaded.producerKey == "codex:cu:p1111111111111111")
|
|
#expect(loaded.lastScanUnixMs == 123)
|
|
#expect(loaded.days["2026-05-18"]?["gpt-5.5"] == [1, 2, 3])
|
|
|
|
let stale = CostUsageCacheIO.load(
|
|
provider: .codex,
|
|
cacheRoot: root,
|
|
producerKey: "codex:cu:p2222222222222222")
|
|
#expect(stale.lastScanUnixMs == 0)
|
|
#expect(stale.files.isEmpty)
|
|
#expect(stale.days.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `legacy cache without producer key is ignored`() throws {
|
|
let root = try self.makeTemporaryCacheRoot()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let url = CostUsageCacheIO.cacheFileURL(provider: .codex, cacheRoot: root)
|
|
try FileManager.default.createDirectory(
|
|
at: url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
let legacy = """
|
|
{
|
|
"version": 1,
|
|
"lastScanUnixMs": 999,
|
|
"files": {},
|
|
"days": {
|
|
"2026-05-18": {
|
|
"gpt-5": [1, 0, 0]
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
try legacy.write(to: url, atomically: false, encoding: .utf8)
|
|
|
|
let loaded = CostUsageCacheIO.load(
|
|
provider: .codex,
|
|
cacheRoot: root,
|
|
producerKey: "codex:cu:p1111111111111111")
|
|
|
|
#expect(loaded.lastScanUnixMs == 0)
|
|
#expect(loaded.days.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `current codex cache rejects pre interleave containment producers`() throws {
|
|
// Interleave containment (#2037) changed cumulative delta semantics, so caches from
|
|
// previously compatible parser hashes must be rebuilt instead of reused.
|
|
let root = try self.makeTemporaryCacheRoot()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
for legacyProducerKey in ["codex:cu:p3c27f997569eb3c5", "codex:cu:pc54070a94f6419ea"] {
|
|
var cache = CostUsageCache()
|
|
cache.lastScanUnixMs = 123
|
|
cache.days = ["2026-05-18": ["gpt-5.5": [1, 2, 3]]]
|
|
CostUsageCacheIO.save(
|
|
provider: .codex,
|
|
cache: cache,
|
|
cacheRoot: root,
|
|
producerKey: legacyProducerKey)
|
|
|
|
let loaded = CostUsageCacheIO.load(provider: .codex, cacheRoot: root)
|
|
|
|
#expect(loaded.lastScanUnixMs == 0)
|
|
#expect(loaded.days.isEmpty)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `non codex cache does not require producer key`() throws {
|
|
let root = try self.makeTemporaryCacheRoot()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let url = CostUsageCacheIO.cacheFileURL(provider: .claude, cacheRoot: root)
|
|
try FileManager.default.createDirectory(
|
|
at: url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
let legacy = """
|
|
{
|
|
"version": 1,
|
|
"lastScanUnixMs": 999,
|
|
"files": {},
|
|
"days": {
|
|
"2026-05-18": {
|
|
"claude-sonnet-4-5": [1, 0, 0]
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
try legacy.write(to: url, atomically: false, encoding: .utf8)
|
|
|
|
let loaded = CostUsageCacheIO.load(provider: .claude, cacheRoot: root)
|
|
|
|
#expect(loaded.lastScanUnixMs == 999)
|
|
#expect(loaded.days["2026-05-18"]?["claude-sonnet-4-5"] == [1, 0, 0])
|
|
}
|
|
|
|
@Test
|
|
func `current producer key uses generated parser hash for codex only`() {
|
|
let codexKey = CostUsageCacheIO.currentProducerKey(
|
|
provider: .codex,
|
|
parserHash: "abc1234567890def")
|
|
let standaloneKey = CostUsageCacheIO.currentProducerKey(
|
|
provider: .claude,
|
|
parserHash: "abc1234567890def")
|
|
|
|
#expect(codexKey == "codex:cu:pabc1234567890def")
|
|
#expect(standaloneKey == nil)
|
|
}
|
|
|
|
@Test
|
|
func `generated parser hash is stable short lowercase hex`() {
|
|
let hash = CodexParserHash.value
|
|
|
|
#expect(hash.range(of: #"^[0-9a-f]{16}$"#, options: .regularExpression) != nil)
|
|
#expect(CostUsageCacheIO.currentProducerKey(provider: .codex) == "codex:cu:p\(hash)")
|
|
}
|
|
|
|
private func makeTemporaryCacheRoot() throws -> URL {
|
|
let root = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("codexbar-cost-cache-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
return root
|
|
}
|
|
}
|