3478 lines · 133.9 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore // swiftlint:disable file_length// swiftlint:disable type_body_lengthstruct CostUsageScannerBreakdownTests {    private typealias Usage = (input: Int, cached: Int, output: Int)     private func codexTurnContext(timestamp: String, model: String) -> [String: Any] {        [            "type": "turn_context",            "timestamp": timestamp,            "payload": [                "model": model,            ],        ]    }     private func codexTokenCount(        timestamp: String,        model: String,        total: Usage? = nil,        last: Usage? = nil) -> [String: Any]    {        var info: [String: Any] = [            "model": model,        ]        if let total {            info["total_token_usage"] = [                "input_tokens": total.input,                "cached_input_tokens": total.cached,                "output_tokens": total.output,            ]        }        if let last {            info["last_token_usage"] = [                "input_tokens": last.input,                "cached_input_tokens": last.cached,                "output_tokens": last.output,            ]        }        return [            "type": "event_msg",            "timestamp": timestamp,            "payload": [                "type": "token_count",                "info": info,            ],        ]    }     private func codexTokenCountWithoutModel(timestamp: String, last: Usage) -> [String: Any] {        [            "type": "event_msg",            "timestamp": timestamp,            "payload": [                "type": "token_count",                "info": [                    "last_token_usage": [                        "input_tokens": last.input,                        "cached_input_tokens": last.cached,                        "output_tokens": last.output,                    ],                ],            ],        ]    }     private func oversizedCodexTurnContextLine(timestamp: String, model: String) -> String {        let largeInstructions = String(repeating: "x", count: 300 * 1024)        return #"{"type":"turn_context","timestamp":""#            + timestamp            + #"","payload":{"model":""#            + model            + #"","instructions":""#            + largeInstructions            + #""}}"#    }     private func oversizedCodexTurnContextInfoModelLine(timestamp: String, model: String) -> String {        let largeInstructions = String(repeating: "x", count: 300 * 1024)        return #"{"type":"turn_context","timestamp":""#            + timestamp            + #"","payload":{"empty":"","info":{"model":""#            + model            + #""},"instructions":""#            + largeInstructions            + #""}}"#    }     private func oversizedCodexTurnContextPromptOnlyLine(timestamp: String, promptModel: String) -> String {        let prompt = #"example: {\"type\":\"turn_context\",\"payload\":{\"model\":\"\#(promptModel)\"}}"#            + String(repeating: "x", count: 300 * 1024)        return #"{"type":"turn_context","timestamp":""#            + timestamp            + #"","payload":{"instructions":""#            + prompt            + #""}}"#    }     @Test    func `codex daily report parses token counts and caches`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2025, month: 12, day: 20)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))        let iso2 = env.isoString(for: day.addingTimeInterval(2))         let model = "openai/gpt-5.2-codex"        let turnContext: [String: Any] = [            "type": "turn_context",            "timestamp": iso0,            "payload": [                "model": model,            ],        ]        let firstTokenCount: [String: Any] = [            "type": "event_msg",            "timestamp": iso1,            "payload": [                "type": "token_count",                "info": [                    "total_token_usage": [                        "input_tokens": 100,                        "cached_input_tokens": 20,                        "output_tokens": 10,                    ],                    "model": model,                ],            ],        ]         let fileURL = try env.writeCodexSessionFile(            day: day,            filename: "session.jsonl",            contents: env.jsonl([turnContext, firstTokenCount]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0         let first = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        #expect(first.data.count == 1)        #expect(first.data[0].modelsUsed == ["gpt-5.2-codex"])        #expect(first.data[0].modelBreakdowns == [            CostUsageDailyReport.ModelBreakdown(                modelName: "gpt-5.2-codex",                costUSD: first.data[0].costUSD,                totalTokens: 110),        ])        #expect(first.data[0].totalTokens == 110)        #expect((first.data[0].costUSD ?? 0) > 0)        let firstCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        #expect(firstCache.codexPricingKey?.hasPrefix("builtin-") == true)         let secondTokenCount: [String: Any] = [            "type": "event_msg",            "timestamp": iso2,            "payload": [                "type": "token_count",                "info": [                    "total_token_usage": [                        "input_tokens": 160,                        "cached_input_tokens": 40,                        "output_tokens": 16,                    ],                    "model": model,                ],            ],        ]        try env.jsonl([turnContext, firstTokenCount, secondTokenCount])            .write(to: fileURL, atomically: true, encoding: .utf8)         let second = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        #expect(second.data.count == 1)        #expect(second.data[0].modelsUsed == ["gpt-5.2-codex"])        #expect(second.data[0].totalTokens == 176)        #expect((second.data[0].costUSD ?? 0) > (first.data[0].costUSD ?? 0))    }     @Test    func `codex incremental append falls back to rescan when fork metadata appears late`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 3, day: 11)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))        let iso2 = env.isoString(for: day.addingTimeInterval(2))        let iso3 = env.isoString(for: day.addingTimeInterval(3))        let model = "gpt-5.4"        let sessionMeta: [String: Any] = [            "type": "session_meta",            "timestamp": iso0,            "payload": ["id": "late-fork-child"],        ]        let turnContext = self.codexTurnContext(timestamp: iso0, model: model)        let firstTokenCount = self.codexTokenCount(            timestamp: iso1,            model: model,            total: (input: 10, cached: 0, output: 0),            last: (input: 10, cached: 0, output: 0))        let fileURL = try env.writeCodexSessionFile(            day: day,            filename: "late-fork-child.jsonl",            contents: env.jsonl([sessionMeta, turnContext, firstTokenCount]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0         let first = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        #expect(first.data.first?.totalTokens == 10)         let lateForkMeta: [String: Any] = [            "type": "session_meta",            "timestamp": iso2,            "payload": [                "id": "late-fork-child",                "forked_from_id": "missing-parent",                "timestamp": iso2,            ],        ]        let replayedForkUsage = self.codexTokenCount(            timestamp: iso3,            model: model,            total: (input: 1000, cached: 900, output: 100),            last: (input: 1000, cached: 900, output: 100))        let handle = try FileHandle(forWritingTo: fileURL)        try handle.seekToEnd()        try handle.write(contentsOf: Data(("\n" + env.jsonl([lateForkMeta, replayedForkUsage])).utf8))        try handle.close()         let second = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        #expect(second.data.first?.totalTokens == 10)    }     @Test    func `codex daily report reprices cached sessions when models dev pricing changes`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 5, day: 10)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))        let olderDay = try env.makeLocalNoon(year: 2026, month: 5, day: 5)        let olderFileURL = try env.writeCodexSessionFile(            day: olderDay,            filename: "older-session.jsonl",            contents: env.jsonl([                self.codexTurnContext(timestamp: env.isoString(for: olderDay), model: "custom-codex-model"),                self.codexTokenCount(                    timestamp: env.isoString(for: olderDay.addingTimeInterval(1)),                    model: "custom-codex-model",                    last: (input: 100, cached: 20, output: 10)),            ]))        try FileManager.default.setAttributes(            [.modificationDate: olderDay],            ofItemAtPath: olderFileURL.path)         let model = "custom-codex-model"        _ = try env.writeCodexSessionFile(            day: day,            filename: "session.jsonl",            contents: env.jsonl([                self.codexTurnContext(timestamp: iso0, model: model),                self.codexTokenCount(timestamp: iso1, model: model, last: (input: 100, cached: 20, output: 10)),            ]))         try ModelsDevCache.save(            catalog: Self.modelsDevCatalog(model: model, input: 1, output: 2, cacheRead: 0.5),            fetchedAt: Date(timeIntervalSince1970: 1),            cacheRoot: env.cacheRoot)         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0         let first = CostUsageScanner.loadDailyReport(            provider: .codex,            since: olderDay,            until: day,            now: day,            options: options)        let oldDailyCost = (80.0 / 1_000_000.0) + (20.0 * 0.5 / 1_000_000.0)            + (10.0 * 2.0 / 1_000_000.0)        #expect(first.summary?.totalCostUSD == oldDailyCost * 2)         try ModelsDevCache.save(            catalog: Self.modelsDevCatalog(model: model, input: 1, output: 2, cacheRead: 0.5),            fetchedAt: Date(timeIntervalSince1970: 2),            cacheRoot: env.cacheRoot)         options.refreshMinIntervalSeconds = 60        let samePricing = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(1),            options: options)        let samePricingCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        #expect(samePricing.summary?.totalCostUSD == oldDailyCost)        #expect(samePricingCache.scanSinceKey == "2026-05-04")         try ModelsDevCache.save(            catalog: Self.modelsDevCatalog(model: model, input: 10, output: 20, cacheRead: 5),            fetchedAt: Date(timeIntervalSince1970: 2),            cacheRoot: env.cacheRoot)         options.refreshMinIntervalSeconds = 60        let narrowRepriced = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(2),            options: options)        let newDailyCost = (80.0 * 10.0 / 1_000_000.0)            + (20.0 * 5.0 / 1_000_000.0)            + (10.0 * 20.0 / 1_000_000.0)        #expect(narrowRepriced.summary?.totalCostUSD == newDailyCost)         let wideRepriced = CostUsageScanner.loadDailyReport(            provider: .codex,            since: olderDay,            until: day,            now: day.addingTimeInterval(3),            options: options)         #expect(wideRepriced.summary?.totalCostUSD == newDailyCost * 2)    }     @Test    func `codex incremental cache preserves divergent total baseline`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))        let iso2 = env.isoString(for: day.addingTimeInterval(2))        let model = "openai/gpt-5.4"        let sessionMeta: [String: Any] = [            "type": "session_meta",            "timestamp": iso0,            "payload": ["session_id": "divergent-session"],        ]        let turnContext = self.codexTurnContext(timestamp: iso0, model: model)        let firstTokenCount = self.codexTokenCount(            timestamp: iso1,            model: model,            total: (input: 50, cached: 0, output: 0),            last: (input: 100, cached: 0, output: 0))        let fileURL = try env.writeCodexSessionFile(            day: day,            filename: "session.jsonl",            contents: env.jsonl([sessionMeta, turnContext, firstTokenCount]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0         let first = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)        #expect(first.data.first?.totalTokens == 100)         let secondTokenCount = self.codexTokenCount(            timestamp: iso2,            model: model,            total: (input: 80, cached: 0, output: 0))        try env.jsonl([sessionMeta, turnContext, firstTokenCount, secondTokenCount])            .write(to: fileURL, atomically: true, encoding: .utf8)         let second = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day.addingTimeInterval(1),            options: options)        let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let usage = cache.files.first { URL(fileURLWithPath: $0.key).lastPathComponent == fileURL.lastPathComponent }?            .value         #expect(second.data.first?.totalTokens == 130)        #expect(usage?.lastTotals == nil)        #expect(usage?.lastCountedTotals?.input == 130)        #expect(usage?.lastRawTotalsBaseline?.input == 80)        #expect(usage?.hasDivergentTotals == true)    }     @Test    func `codex incremental cache migrates legacy rows before appending delta costs`() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 5, day: 18)        let olderDay = try env.makeLocalNoon(year: 2026, month: 5, day: 10)        let olderDayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: olderDay)        let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))        let iso2 = env.isoString(for: day.addingTimeInterval(2))        let model = "gpt-5.4"        let sessionMeta: [String: Any] = [            "type": "session_meta",            "timestamp": iso0,            "payload": ["session_id": "legacy-cost-session"],        ]        let turnContext = self.codexTurnContext(timestamp: iso0, model: model)        let firstTokenCount = self.codexTokenCount(            timestamp: iso1,            model: model,            total: (input: 10, cached: 0, output: 0))        let fileURL = try env.writeCodexSessionFile(            day: day,            filename: "session.jsonl",            contents: env.jsonl([sessionMeta, turnContext, firstTokenCount]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            claudeProjectsRoots: nil,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0         _ = CostUsageScanner.loadDailyReport(            provider: .codex,            since: day,            until: day,            now: day,            options: options)         var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let path = try #require(cache.files.keys.first)        var cachedUsage = try #require(cache.files[path])        #expect(cachedUsage.sessionId == "legacy-cost-session")        #expect(cachedUsage.lastCountedTotals?.input == 10)        cachedUsage.codexCostNanos = nil        cachedUsage.codexRows = [            CostUsageScanner.CodexUsageRow(                day: olderDayKey,                model: CostUsagePricing.normalizeCodexModel(model),                turnID: nil,                input: 20,                cached: 0,                output: 0),            CostUsageScanner.CodexUsageRow(                day: dayKey,                model: CostUsagePricing.normalizeCodexModel(model),                turnID: nil,                input: 10,                cached: 0,                output: 0),        ]        cache.files[path] = cachedUsage        CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)        let savedUsage = try #require(CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot).files[path])        #expect(savedUsage.codexRows?.map(\.day) == [olderDayKey, dayKey])         let secondTokenCount = self.codexTokenCount(            timestamp: iso2,            model: model,

Lines 1–500 of 3478.