867 lines · 32.7 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore struct CostUsageFetcherTests {    @Test    func `fetcher scopes codex history to selected codex home`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let otherHome = env.root.appendingPathComponent("other-codex-home", isDirectory: true)        try Self.writeCodexSessionFile(            homeRoot: env.codexHomeRoot,            env: env,            day: day,            filename: "ambient.jsonl",            tokens: 100)        try Self.writeCodexSessionFile(homeRoot: otherHome, env: env, day: day, filename: "managed.jsonl", tokens: 10)         let options = CostUsageScanner.Options(cacheRoot: env.cacheRoot)        let ambient = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day,            codexHomePath: env.codexHomeRoot.path,            scannerOptions: options)        let managed = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day,            codexHomePath: otherHome.path,            scannerOptions: options)         #expect(ambient.sessionTokens == 100)        #expect(managed.sessionTokens == 10)    }     @Test    func `fetcher refreshes codex cache when legacy roots metadata is missing`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let managedHome = env.root.appendingPathComponent("managed-codex-home", isDirectory: true)        try Self.writeCodexSessionFile(            homeRoot: env.codexHomeRoot,            env: env,            day: day,            filename: "ambient.jsonl",            tokens: 100)        try Self.writeCodexSessionFile(homeRoot: managedHome, env: env, day: day, filename: "managed.jsonl", tokens: 10)         let options = CostUsageScanner.Options(cacheRoot: env.cacheRoot)        let piOptions = PiSessionCostScanner.Options(piSessionsRoot: env.piSessionsRoot, cacheRoot: env.cacheRoot)        let ambient = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day,            codexHomePath: env.codexHomeRoot.path,            scannerOptions: options,            piScannerOptions: piOptions)        #expect(ambient.sessionTokens == 100)         var cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        cache.roots = nil        CostUsageCacheIO.save(provider: .codex, cache: cache, cacheRoot: env.cacheRoot)         let managed = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day.addingTimeInterval(1),            codexHomePath: managedHome.path,            scannerOptions: options,            piScannerOptions: piOptions)         #expect(managed.sessionTokens == 10)    }     @Test    func `fetcher refreshes codex cache when history window expands`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let oldDay = try env.makeLocalNoon(year: 2026, month: 4, day: 2)        let newDay = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        try Self.writeCodexSessionFile(            homeRoot: env.codexHomeRoot,            env: env,            day: oldDay,            filename: "old.jsonl",            tokens: 15)        try Self.writeCodexSessionFile(            homeRoot: env.codexHomeRoot,            env: env,            day: newDay,            filename: "new.jsonl",            tokens: 30)         var options = CostUsageScanner.Options(cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 3600         let narrow = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay,            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            scannerOptions: options)        #expect(narrow.daily.map(\.date) == ["2026-04-08"])        #expect(narrow.last30DaysTokens == 30)         var legacyCache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        legacyCache.scanSinceKey = nil        legacyCache.scanUntilKey = nil        CostUsageCacheIO.save(provider: .codex, cache: legacyCache, cacheRoot: env.cacheRoot)         let expanded = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay.addingTimeInterval(1),            codexHomePath: env.codexHomeRoot.path,            historyDays: 7,            scannerOptions: options)        #expect(expanded.daily.map(\.date) == ["2026-04-02", "2026-04-08"])        #expect(expanded.last30DaysTokens == 45)    }     @Test    func `fetcher resolves fork parent outside requested codex window`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let parentDay = try env.makeLocalNoon(year: 2026, month: 4, day: 2)        let childDay = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let model = "openai/gpt-5.4"        let parentID = "parent-session"        let parentTimestamp = env.isoString(for: parentDay.addingTimeInterval(1))        let childTimestamp = env.isoString(for: childDay.addingTimeInterval(1))        _ = try env.writeCodexSessionFile(            day: parentDay,            filename: "parent.jsonl",            contents: env.jsonl([                [                    "type": "session_meta",                    "timestamp": env.isoString(for: parentDay),                    "payload": ["session_id": parentID],                ],                [                    "type": "event_msg",                    "timestamp": parentTimestamp,                    "payload": [                        "type": "token_count",                        "info": [                            "model": model,                            "total_token_usage": [                                "input_tokens": 100,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))        _ = try env.writeCodexSessionFile(            day: childDay,            filename: "child.jsonl",            contents: env.jsonl([                [                    "type": "session_meta",                    "timestamp": env.isoString(for: childDay),                    "payload": [                        "session_id": "child-session",                        "forked_from_id": parentID,                        "timestamp": parentTimestamp,                    ],                ],                [                    "type": "event_msg",                    "timestamp": childTimestamp,                    "payload": [                        "type": "token_count",                        "info": [                            "model": model,                            "total_token_usage": [                                "input_tokens": 125,                                "cached_input_tokens": 0,                                "output_tokens": 5,                            ],                        ],                    ],                ],            ]))         let options = CostUsageScanner.Options(cacheRoot: env.cacheRoot)        let snapshot = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: childDay,            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            scannerOptions: options)         #expect(snapshot.daily.map(\.date) == ["2026-04-08"])        #expect(snapshot.last30DaysTokens == 30)    }     @Test    func `force refresh only scans requested codex date window`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let oldDay = try env.makeLocalNoon(year: 2026, month: 3, day: 1)        let newDay = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let oldURL = try env.writeCodexSessionFile(            day: oldDay,            filename: "old.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: oldDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 10,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))        try FileManager.default.setAttributes([.modificationDate: oldDay], ofItemAtPath: oldURL.path)        _ = try env.writeCodexSessionFile(            day: newDay,            filename: "new.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: newDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 30,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))         let options = CostUsageScanner.Options(            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        let snapshot = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay,            forceRefresh: true,            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            scannerOptions: options)        let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)        let cacheFileExists = FileManager.default.fileExists(            atPath: CostUsageCacheIO.cacheFileURL(provider: .codex, cacheRoot: env.cacheRoot).path)         #expect(snapshot.daily.map(\.date) == ["2026-04-08"])        #expect(snapshot.last30DaysTokens == 30)        #expect(cacheFileExists)        #expect(cache.files.keys.sorted().map(URL.init(fileURLWithPath:)).map(\.lastPathComponent) == ["new.jsonl"])    }     @Test    func `narrow codex refresh preserves wider cache window`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let oldDay = try env.makeLocalNoon(year: 2026, month: 4, day: 2)        let newDay = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        _ = try env.writeCodexSessionFile(            day: oldDay,            filename: "old.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: oldDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 15,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))        _ = try env.writeCodexSessionFile(            day: newDay,            filename: "new.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: newDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 30,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0        _ = CostUsageScanner.loadDailyReport(            provider: .codex,            since: newDay,            until: newDay,            now: newDay,            options: options)        let wide = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay.addingTimeInterval(1),            codexHomePath: env.codexHomeRoot.path,            historyDays: 7,            refreshPricingInBackground: false,            scannerOptions: options)        let narrow = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay.addingTimeInterval(2),            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            refreshPricingInBackground: false,            scannerOptions: options)        let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)         #expect(wide.last30DaysTokens == 45)        #expect(narrow.last30DaysTokens == 30)        #expect(cache.files.keys.map(URL.init(fileURLWithPath:)).map(\.lastPathComponent).sorted() == [            "new.jsonl",            "old.jsonl",        ])        #expect(cache.scanSinceKey == "2026-04-01")        #expect(cache.scanUntilKey == "2026-04-09")    }     @Test    func `force codex rescan narrows cache window to refreshed range`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let oldDay = try env.makeLocalNoon(year: 2026, month: 4, day: 2)        let newDay = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        _ = try env.writeCodexSessionFile(            day: oldDay,            filename: "old.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: oldDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 15,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))        _ = try env.writeCodexSessionFile(            day: newDay,            filename: "new.jsonl",            contents: env.jsonl([                [                    "type": "event_msg",                    "timestamp": env.isoString(for: newDay),                    "payload": [                        "type": "token_count",                        "info": [                            "model": "openai/gpt-5.4",                            "last_token_usage": [                                "input_tokens": 30,                                "cached_input_tokens": 0,                                "output_tokens": 0,                            ],                        ],                    ],                ],            ]))         var options = CostUsageScanner.Options(            codexSessionsRoot: env.codexSessionsRoot,            cacheRoot: env.cacheRoot,            codexTraceDatabaseURL: env.root.appendingPathComponent("missing-traces.sqlite"))        options.refreshMinIntervalSeconds = 0        _ = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: newDay,            codexHomePath: env.codexHomeRoot.path,            historyDays: 7,            refreshPricingInBackground: false,            scannerOptions: options)         var rescanOptions = options        rescanOptions.forceRescan = true        _ = CostUsageScanner.loadDailyReport(            provider: .codex,            since: newDay,            until: newDay,            now: newDay.addingTimeInterval(1),            options: rescanOptions)        let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)         #expect(cache.files.keys.map(URL.init(fileURLWithPath:)).map(\.lastPathComponent).sorted() == ["new.jsonl"])        #expect(cache.scanSinceKey == "2026-04-07")        #expect(cache.scanUntilKey == "2026-04-09")    }     @Test    func `codex refresh drops stale cache entry when session moves to archive`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let contents = try env.jsonl([            [                "type": "session_meta",                "timestamp": env.isoString(for: day),                "payload": ["session_id": "moved-session"],            ],            [                "type": "event_msg",                "timestamp": env.isoString(for: day.addingTimeInterval(1)),                "payload": [                    "type": "token_count",                    "info": [                        "model": "openai/gpt-5.4",                        "last_token_usage": [                            "input_tokens": 30,                            "cached_input_tokens": 0,                            "output_tokens": 0,                        ],                    ],                ],            ],        ])        let originalURL = try env.writeCodexSessionFile(day: day, filename: "moved.jsonl", contents: contents)         var options = CostUsageScanner.Options(cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0        let first = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day,            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            scannerOptions: options)         let archivedURL = env.codexArchivedSessionsRoot.appendingPathComponent("moved.jsonl", isDirectory: false)        try FileManager.default.moveItem(at: originalURL, to: archivedURL)         let second = try await CostUsageFetcher.loadTokenSnapshot(            provider: .codex,            now: day.addingTimeInterval(1),            codexHomePath: env.codexHomeRoot.path,            historyDays: 1,            scannerOptions: options)        let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot)         #expect(first.last30DaysTokens == 30)        #expect(second.last30DaysTokens == 30)        #expect(cache.files.count == 1)        #expect(cache.files.keys.first.map { URL(fileURLWithPath: $0).resolvingSymlinksInPath().path } ==            archivedURL.resolvingSymlinksInPath().path)    }     @Test    func `fetcher merges native and pi codex history with normalized model names`() async throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2026, month: 4, day: 8)        let iso0 = env.isoString(for: day)        let iso1 = env.isoString(for: day.addingTimeInterval(1))         let nativeTurnContext: [String: Any] = [            "type": "turn_context",            "timestamp": iso0,            "payload": [

Lines 1–500 of 867.