import Foundationimport Testing@testable import CodexBarCore struct CostUsageScannerTests { @Test func `codex file metadata detects append truncation and replacement`() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-codex-metadata-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } let fileURL = root.appendingPathComponent("session.jsonl") try Data("abc".utf8).write(to: fileURL) let initial = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(initial.size == 3) #expect(initial.fileId != nil) let linkURL = root.appendingPathComponent("linked-session.jsonl") try FileManager.default.createSymbolicLink(at: linkURL, withDestinationURL: fileURL) let linked = CostUsageScanner.codexFileMetadata(fileURL: linkURL) #expect(linked.size == initial.size) #expect(linked.fileId == initial.fileId) let handle = try FileHandle(forWritingTo: fileURL) try handle.seekToEnd() try handle.write(contentsOf: Data("def".utf8)) try handle.close() let appended = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(appended.size == 6) #expect(appended.fileId == initial.fileId) let truncateHandle = try FileHandle(forWritingTo: fileURL) try truncateHandle.truncate(atOffset: 2) try truncateHandle.close() let truncated = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(truncated.size == 2) #expect(truncated.fileId == initial.fileId) try FileManager.default.removeItem(at: fileURL) try Data("replacement".utf8).write(to: fileURL) let replaced = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(replaced.size == 11) #expect(replaced.fileId != initial.fileId) } @Test func `vertex daily report filters claude logs`() 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 `vertex daily report detects by vrtx id prefix`() 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 `claude report preserves per-request threshold pricing`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2026, month: 5, day: 9) let first = env.isoString(for: day) let second = env.isoString(for: day.addingTimeInterval(1)) let model = "claude-sonnet-4-5" let firstEntry: [String: Any] = [ "type": "assistant", "timestamp": first, "requestId": "req_one", "message": [ "id": "msg_one", "model": model, "usage": [ "input_tokens": 150_000, "output_tokens": 0, ], ], ] let secondEntry: [String: Any] = [ "type": "assistant", "timestamp": second, "requestId": "req_two", "message": [ "id": "msg_two", "model": model, "usage": [ "input_tokens": 150_000, "output_tokens": 0, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/threshold.jsonl", contents: env.jsonl([firstEntry, secondEntry])) 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) let expectedRequestCost = CostUsagePricing.claudeCostUSD( model: model, inputTokens: 150_000, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, outputTokens: 0, modelsDevCacheRoot: env.cacheRoot) ?? 0 let aggregateCost = CostUsagePricing.claudeCostUSD( model: model, inputTokens: 300_000, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, outputTokens: 0, modelsDevCacheRoot: env.cacheRoot) ?? 0 let expectedCost = expectedRequestCost * 2 #expect(report.data.count == 1) #expect(report.data.first?.inputTokens == 300_000) #expect(abs((report.data.first?.costUSD ?? 0) - expectedCost) < 0.000001) #expect(abs((report.data.first?.costUSD ?? 0) - aggregateCost) > 0.000001) #expect(abs((report.data.first?.modelBreakdowns?.first?.costUSD ?? 0) - expectedCost) < 0.000001) } @Test func `claude parses large lines with usage at tail`() 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 `claude daily report refreshes when file changes`() 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 `codex incremental parsing uses previous totals`() 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) #expect(normalized == "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])) 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 `codex incremental parsing keeps current turn id`() 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 iso2 = env.isoString(for: day.addingTimeInterval(2)) let iso3 = env.isoString(for: day.addingTimeInterval(3)) let model = "openai/gpt-5.5" let turnID = "22222222-2222-2222-2222-222222222222" let turnContext: [String: Any] = [ "type": "turn_context", "timestamp": iso0, "payload": [ "model": model, ], ] let taskStarted: [String: Any] = [ "type": "event_msg", "timestamp": iso1, "payload": [ "type": "task_started", "id": turnID, ], ] let firstTokenCount: [String: Any] = [ "type": "event_msg", "timestamp": iso2, "payload": [ "type": "token_count", "info": [ "total_token_usage": [ "input_tokens": 100, "cached_input_tokens": 20, "output_tokens": 10, ], ], ], ] Lines 1–500 of 966. Load more
import Foundation import Testing @testable import CodexBarCore struct CostUsageScannerTests { @Test func `codex file metadata detects append truncation and replacement`() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-codex-metadata-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } let fileURL = root.appendingPathComponent("session.jsonl") try Data("abc".utf8).write(to: fileURL) let initial = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(initial.size == 3) #expect(initial.fileId != nil) let linkURL = root.appendingPathComponent("linked-session.jsonl") try FileManager.default.createSymbolicLink(at: linkURL, withDestinationURL: fileURL) let linked = CostUsageScanner.codexFileMetadata(fileURL: linkURL) #expect(linked.size == initial.size) #expect(linked.fileId == initial.fileId) let handle = try FileHandle(forWritingTo: fileURL) try handle.seekToEnd() try handle.write(contentsOf: Data("def".utf8)) try handle.close() let appended = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(appended.size == 6) #expect(appended.fileId == initial.fileId) let truncateHandle = try FileHandle(forWritingTo: fileURL) try truncateHandle.truncate(atOffset: 2) try truncateHandle.close() let truncated = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(truncated.size == 2) #expect(truncated.fileId == initial.fileId) try FileManager.default.removeItem(at: fileURL) try Data("replacement".utf8).write(to: fileURL) let replaced = CostUsageScanner.codexFileMetadata(fileURL: fileURL) #expect(replaced.size == 11) #expect(replaced.fileId != initial.fileId) } @Test func `vertex daily report filters claude logs`() 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 `vertex daily report detects by vrtx id prefix`() 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 `claude report preserves per-request threshold pricing`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2026, month: 5, day: 9) let first = env.isoString(for: day) let second = env.isoString(for: day.addingTimeInterval(1)) let model = "claude-sonnet-4-5" let firstEntry: [String: Any] = [ "type": "assistant", "timestamp": first, "requestId": "req_one", "message": [ "id": "msg_one", "model": model, "usage": [ "input_tokens": 150_000, "output_tokens": 0, ], ], ] let secondEntry: [String: Any] = [ "type": "assistant", "timestamp": second, "requestId": "req_two", "message": [ "id": "msg_two", "model": model, "usage": [ "input_tokens": 150_000, "output_tokens": 0, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/threshold.jsonl", contents: env.jsonl([firstEntry, secondEntry])) 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) let expectedRequestCost = CostUsagePricing.claudeCostUSD( model: model, inputTokens: 150_000, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, outputTokens: 0, modelsDevCacheRoot: env.cacheRoot) ?? 0 let aggregateCost = CostUsagePricing.claudeCostUSD( model: model, inputTokens: 300_000, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, outputTokens: 0, modelsDevCacheRoot: env.cacheRoot) ?? 0 let expectedCost = expectedRequestCost * 2 #expect(report.data.count == 1) #expect(report.data.first?.inputTokens == 300_000) #expect(abs((report.data.first?.costUSD ?? 0) - expectedCost) < 0.000001) #expect(abs((report.data.first?.costUSD ?? 0) - aggregateCost) > 0.000001) #expect(abs((report.data.first?.modelBreakdowns?.first?.costUSD ?? 0) - expectedCost) < 0.000001) } @Test func `claude parses large lines with usage at tail`() 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 `claude daily report refreshes when file changes`() 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 `codex incremental parsing uses previous totals`() 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) #expect(normalized == "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])) 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 `codex incremental parsing keeps current turn id`() 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 iso2 = env.isoString(for: day.addingTimeInterval(2)) let iso3 = env.isoString(for: day.addingTimeInterval(3)) let model = "openai/gpt-5.5" let turnID = "22222222-2222-2222-2222-222222222222" let turnContext: [String: Any] = [ "type": "turn_context", "timestamp": iso0, "payload": [ "model": model, ], ] let taskStarted: [String: Any] = [ "type": "event_msg", "timestamp": iso1, "payload": [ "type": "task_started", "id": turnID, ], ] let firstTokenCount: [String: Any] = [ "type": "event_msg", "timestamp": iso2, "payload": [ "type": "token_count", "info": [ "total_token_usage": [ "input_tokens": 100, "cached_input_tokens": 20, "output_tokens": 10, ], ], ], ]