851 lines · 29.4 KB · Swift
RawDownload
import Foundationimport Testing @testable import CodexBarCore @Suitestruct CostUsageScannerTests {    @Test    func codexDailyReportParsesTokenCountsAndCaches() 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)        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"])        #expect(first.data[0].totalTokens == 110)        #expect((first.data[0].costUSD ?? 0) > 0)         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].totalTokens == 176)        #expect((second.data[0].costUSD ?? 0) > (first.data[0].costUSD ?? 0))    }     @Test    func claudeDailyReportParsesUsageAndCaches() 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 assistant: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "message": [                "model": "claude-sonnet-4-20250514",                "usage": [                    "input_tokens": 200,                    "cache_creation_input_tokens": 50,                    "cache_read_input_tokens": 25,                    "output_tokens": 80,                ],            ],        ]        _ = try env.writeClaudeProjectFile(            relativePath: "project-a/session-a.jsonl",            contents: env.jsonl([assistant]))         var options = CostUsageScanner.Options(            codexSessionsRoot: nil,            claudeProjectsRoots: [env.claudeProjectsRoot],            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         let report = CostUsageScanner.loadDailyReport(            provider: .claude,            since: day,            until: day,            now: day,            options: options)        #expect(report.data.count == 1)        #expect(report.data[0].modelsUsed == ["claude-sonnet-4-20250514"])        #expect(report.data[0].inputTokens == 200)        #expect(report.data[0].cacheCreationTokens == 50)        #expect(report.data[0].cacheReadTokens == 25)        #expect(report.data[0].outputTokens == 80)        #expect(report.data[0].totalTokens == 355)        #expect((report.data[0].costUSD ?? 0) > 0)    }     @Test    func vertexDailyReportFiltersClaudeLogs() 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 vertexEntry: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "metadata": [                "provider": "vertexai",                "projectId": "vertex-project",                "location": "us-central1",            ],            "message": [                "model": "claude-sonnet-4-20250514",                "usage": [                    "input_tokens": 10,                    "cache_creation_input_tokens": 0,                    "cache_read_input_tokens": 0,                    "output_tokens": 5,                ],            ],        ]        let claudeEntry: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "metadata": [                "provider": "anthropic",            ],            "message": [                "model": "claude-sonnet-4-20250514",                "usage": [                    "input_tokens": 200,                    "cache_creation_input_tokens": 0,                    "cache_read_input_tokens": 0,                    "output_tokens": 100,                ],            ],        ]         _ = try env.writeClaudeProjectFile(            relativePath: "project-a/session-a.jsonl",            contents: env.jsonl([vertexEntry, claudeEntry]))         var options = CostUsageScanner.Options(            codexSessionsRoot: nil,            claudeProjectsRoots: [env.claudeProjectsRoot],            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         let report = CostUsageScanner.loadDailyReport(            provider: .vertexai,            since: day,            until: day,            now: day,            options: options)         #expect(report.data.count == 1)        #expect(report.data[0].inputTokens == 10)        #expect(report.data[0].outputTokens == 5)        #expect(report.data[0].totalTokens == 15)    }     @Test    func vertexDailyReportDetectsByVrtxIdPrefix() 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))         // Vertex AI entries have "_vrtx_" in message.id and requestId        let vertexEntry: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "requestId": "req_vrtx_011CWjK86SWeFuXqZKUtgB1H",            "message": [                "id": "msg_vrtx_0154LUXjFVzQGUca3yK2RUeo",                "model": "claude-opus-4-5-20251101",                "usage": [                    "input_tokens": 100,                    "cache_creation_input_tokens": 0,                    "cache_read_input_tokens": 0,                    "output_tokens": 50,                ],            ],        ]        // Anthropic API entries have regular IDs without "_vrtx_"        let claudeEntry: [String: Any] = [            "type": "assistant",            "timestamp": iso1,            "requestId": "req_011CW7BFSFkbK9qJrV8kiptH",            "message": [                "id": "msg_0152zX6DsQYcwH1qiXi4B3y2",                "model": "claude-opus-4-5-20251101",                "usage": [                    "input_tokens": 200,                    "cache_creation_input_tokens": 0,                    "cache_read_input_tokens": 0,                    "output_tokens": 100,                ],            ],        ]         _ = try env.writeClaudeProjectFile(            relativePath: "project-a/session-a.jsonl",            contents: env.jsonl([vertexEntry, claudeEntry]))         var options = CostUsageScanner.Options(            codexSessionsRoot: nil,            claudeProjectsRoots: [env.claudeProjectsRoot],            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         // Vertex AI report should only include entries with _vrtx_ prefix        let vertexReport = CostUsageScanner.loadDailyReport(            provider: .vertexai,            since: day,            until: day,            now: day,            options: options)         #expect(vertexReport.data.count == 1)        #expect(vertexReport.data[0].inputTokens == 100)        #expect(vertexReport.data[0].outputTokens == 50)        #expect(vertexReport.data[0].totalTokens == 150)         // Claude report with excludeVertexAI should only include non-vrtx entries        var claudeOptions = options        claudeOptions.claudeLogProviderFilter = .excludeVertexAI        let claudeReport = CostUsageScanner.loadDailyReport(            provider: .claude,            since: day,            until: day,            now: day,            options: claudeOptions)         #expect(claudeReport.data.count == 1)        #expect(claudeReport.data[0].inputTokens == 200)        #expect(claudeReport.data[0].outputTokens == 100)        #expect(claudeReport.data[0].totalTokens == 300)    }     @Test    func claudeParsesLargeLinesWithUsageAtTail() throws {        let env = try CostUsageTestEnvironment()        defer { env.cleanup() }         let day = try env.makeLocalNoon(year: 2025, month: 12, day: 28)        let iso0 = env.isoString(for: day)        let largeText = String(repeating: "a", count: 70000)         let assistant: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "message": [                "model": "claude-sonnet-4-20250514",                "content": [                    ["type": "text", "text": largeText],                ],                "usage": [                    "input_tokens": 3714,                    "output_tokens": 1,                ],            ],        ]        _ = try env.writeClaudeProjectFile(            relativePath: "project-a/large-line.jsonl",            contents: env.jsonl([assistant]))         var options = CostUsageScanner.Options(            codexSessionsRoot: nil,            claudeProjectsRoots: [env.claudeProjectsRoot],            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         let report = CostUsageScanner.loadDailyReport(            provider: .claude,            since: day,            until: day,            now: day,            options: options)        #expect(report.data.count == 1)        #expect(report.data[0].inputTokens == 3714)        #expect(report.data[0].outputTokens == 1)        #expect(report.data[0].totalTokens == 3715)    }     @Test    func claudeDailyReportRefreshesWhenFileChanges() 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 model = "claude-sonnet-4-20250514"        let first: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "message": [                "model": model,                "usage": [                    "input_tokens": 200,                    "cache_creation_input_tokens": 50,                    "cache_read_input_tokens": 25,                    "output_tokens": 80,                ],            ],        ]        let fileURL = try env.writeClaudeProjectFile(            relativePath: "project-a/session-a.jsonl",            contents: env.jsonl([first]))         var options = CostUsageScanner.Options(            codexSessionsRoot: nil,            claudeProjectsRoots: [env.claudeProjectsRoot],            cacheRoot: env.cacheRoot)        options.refreshMinIntervalSeconds = 0         let firstReport = CostUsageScanner.loadDailyReport(            provider: .claude,            since: day,            until: day,            now: day,            options: options)        #expect(firstReport.data.first?.totalTokens == 355)         let second: [String: Any] = [            "type": "assistant",            "timestamp": iso1,            "message": [                "model": model,                "usage": [                    "input_tokens": 40,                    "cache_creation_input_tokens": 10,                    "cache_read_input_tokens": 5,                    "output_tokens": 20,                ],            ],        ]        try env.jsonl([first, second]).write(to: fileURL, atomically: true, encoding: .utf8)         let secondReport = CostUsageScanner.loadDailyReport(            provider: .claude,            since: day,            until: day,            now: day,            options: options)        #expect(secondReport.data.first?.totalTokens == 430)    }     @Test    func codexIncrementalParsingUsesPreviousTotals() 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 normalized = CostUsagePricing.normalizeCodexModel(model)        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]))         let range = CostUsageScanner.CostUsageDayRange(since: day, until: day)        let first = CostUsageScanner.parseCodexFile(fileURL: fileURL, range: range)        #expect(first.parsedBytes > 0)        #expect(first.lastTotals?.input == 100)        #expect(first.lastTotals?.cached == 20)        #expect(first.lastTotals?.output == 10)         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 delta = CostUsageScanner.parseCodexFile(            fileURL: fileURL,            range: range,            startOffset: first.parsedBytes,            initialModel: first.lastModel,            initialTotals: first.lastTotals)        let dayKey = CostUsageScanner.CostUsageDayRange.dayKey(from: day)        let packed = delta.days[dayKey]?[normalized] ?? []        #expect(packed.count >= 3)        #expect(packed[0] == 60)        #expect(packed[1] == 20)        #expect(packed[2] == 6)    }     @Test    func claudeIncrementalParsingReadsAppendedLinesOnly() 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 model = "claude-sonnet-4-20250514"        let normalized = CostUsagePricing.normalizeClaudeModel(model)        let first: [String: Any] = [            "type": "assistant",            "timestamp": iso0,            "message": [                "model": model,                "usage": [                    "input_tokens": 200,                    "cache_creation_input_tokens": 50,                    "cache_read_input_tokens": 25,                    "output_tokens": 80,                ],            ],        ]        let fileURL = try env.writeClaudeProjectFile(

Lines 1–500 of 851.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t