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. Load more
import Foundation import Testing @testable import CodexBarCore @Suite struct 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(