import Foundation import Testing @testable import CodexBarCore struct CostUsageScannerClaudeRegressionTests { @Test func `parseClaudeFile snapshots keep the last streaming chunk`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 21) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let fileURL = try env.writeClaudeProjectFile( relativePath: "project-a/parse-stream-last.jsonl", contents: env.jsonl([ [ "type": "assistant", "timestamp": iso0, "sessionId": "parse-session", "requestId": "req_parse_stream", "isSidechain": false, "message": [ "id": "msg_parse_stream", "model": "claude-sonnet-4-20250514", "usage": [ "input_tokens": 50, "cache_creation_input_tokens": 5, "cache_read_input_tokens": 0, "output_tokens": 7, ], ], ], [ "type": "assistant", "timestamp": iso1, "sessionId": "parse-session", "requestId": "req_parse_stream", "isSidechain": false, "message": [ "id": "msg_parse_stream", "model": "claude-sonnet-4-20250514", "usage": [ "input_tokens": 50, "cache_creation_input_tokens": 5, "cache_read_input_tokens": 0, "output_tokens": 19, ], ], ], ])) let parsed = CostUsageScanner.parseClaudeFile( fileURL: fileURL, range: CostUsageScanner.CostUsageDayRange(since: day, until: day), providerFilter: .all) #expect(parsed.rows.count == 1) #expect(parsed.rows[0].sessionId == "parse-session") #expect(parsed.rows[0].messageId == "msg_parse_stream") #expect(parsed.rows[0].requestId == "req_parse_stream") #expect(parsed.rows[0].output == 19) } @Test func `parseClaudeFile snapshots keep missing id rows distinct`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 21) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let fileURL = try env.writeClaudeProjectFile( relativePath: "project-a/parse-missing-ids.jsonl", contents: env.jsonl([ [ "type": "assistant", "timestamp": iso0, "message": [ "model": "claude-sonnet-4-20250514", "usage": [ "input_tokens": 11, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 3, ], ], ], [ "type": "assistant", "timestamp": iso1, "message": [ "model": "claude-sonnet-4-20250514", "usage": [ "input_tokens": 13, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 5, ], ], ], ])) let parsed = CostUsageScanner.parseClaudeFile( fileURL: fileURL, range: CostUsageScanner.CostUsageDayRange(since: day, until: day), providerFilter: .all) #expect(parsed.rows.count == 2) #expect(parsed.rows.map(\.input).sorted() == [11, 13]) #expect(parsed.rows.allSatisfy { $0.messageId == nil && $0.requestId == nil }) } @Test func `claude opus 4 7 issue row gets priced`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2026, month: 4, day: 23) let fileURL = try env.writeClaudeProjectFile( relativePath: "project-a/opus-47.jsonl", contents: env.jsonl([ [ "message": [ "model": "claude-opus-4-7", "id": "msg_01NrvWoSMk2Eig6vkCgyRZqc", "type": "message", "role": "assistant", "usage": [ "input_tokens": 6, "cache_creation_input_tokens": 1389, "cache_read_input_tokens": 50352, "output_tokens": 3922, ], ], "requestId": "req_011CaLLcFQD712ZnCTxHFk71", "type": "assistant", "timestamp": "2026-04-23T07:51:34.428Z", "sessionId": "39d4b923-8273-4c35-ad9c-e098395286f1", ], ])) let parsed = CostUsageScanner.parseClaudeFile( fileURL: fileURL, range: CostUsageScanner.CostUsageDayRange(since: day, until: day), providerFilter: .all) #expect(parsed.rows.count == 1) #expect(parsed.rows[0].model == "claude-opus-4-7") #expect(parsed.rows[0].input == 6) #expect(parsed.rows[0].cacheCreate == 1389) #expect(parsed.rows[0].cacheRead == 50352) #expect(parsed.rows[0].output == 3922) let expected = 0.13193725 #expect(abs((Double(parsed.rows[0].costNanos) / 1_000_000_000) - expected) < 0.000000001) } /// Regression for https://github.com/steipete/CodexBar/issues/1210: an Opus 4.8 row /// priced to an empty cost because the built-in Claude pricing table had no /// claude-opus-4-8 entry (used when the models.dev cache is missing/stale). @Test func `claude opus 4 8 issue row gets priced`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2026, month: 5, day: 29) let fileURL = try env.writeClaudeProjectFile( relativePath: "project-a/opus-48.jsonl", contents: env.jsonl([ [ "message": [ "model": "claude-opus-4-8", "id": "msg_01NrvWoSMk2Eig6vkCgyRZqc", "type": "message", "role": "assistant", "usage": [ "input_tokens": 6, "cache_creation_input_tokens": 1389, "cache_read_input_tokens": 50352, "output_tokens": 3922, ], ], "requestId": "req_011CaLLcFQD712ZnCTxHFk71", "type": "assistant", "timestamp": "2026-05-29T07:51:34.428Z", "sessionId": "39d4b923-8273-4c35-ad9c-e098395286f1", ], ])) let parsed = CostUsageScanner.parseClaudeFile( fileURL: fileURL, range: CostUsageScanner.CostUsageDayRange(since: day, until: day), providerFilter: .all) #expect(parsed.rows.count == 1) #expect(parsed.rows[0].model == "claude-opus-4-8") #expect(parsed.rows[0].input == 6) #expect(parsed.rows[0].cacheCreate == 1389) #expect(parsed.rows[0].cacheRead == 50352) #expect(parsed.rows[0].output == 3922) let expected = 0.13193725 #expect(abs((Double(parsed.rows[0].costNanos) / 1_000_000_000) - expected) < 0.000000001) } @Test func `claude streaming keeps the last cumulative chunk`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 21) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let iso2 = env.isoString(for: day.addingTimeInterval(2)) let model = "claude-sonnet-4-20250514" let sessionId = "session-stream-last-wins" let messageId = "msg_stream_last_wins" let requestId = "req_stream_last_wins" let chunk1: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": sessionId, "requestId": requestId, "isSidechain": false, "message": [ "id": messageId, "model": model, "usage": [ "input_tokens": 120, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "output_tokens": 12, ], ], ] let chunk2: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": sessionId, "requestId": requestId, "isSidechain": false, "message": [ "id": messageId, "model": model, "usage": [ "input_tokens": 120, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "output_tokens": 48, ], ], ] let chunk3: [String: Any] = [ "type": "assistant", "timestamp": iso2, "sessionId": sessionId, "requestId": requestId, "isSidechain": false, "message": [ "id": messageId, "model": model, "usage": [ "input_tokens": 120, "cache_creation_input_tokens": 10, "cache_read_input_tokens": 5, "output_tokens": 90, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/stream-last-wins.jsonl", contents: env.jsonl([chunk1, chunk2, chunk3])) 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 == 120) #expect(report.data[0].cacheCreationTokens == 10) #expect(report.data[0].cacheReadTokens == 5) #expect(report.data[0].outputTokens == 90) #expect(report.data[0].totalTokens == 225) } @Test func `claude cross file dedup prefers parent and keeps unique sidechain rows`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 22) 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 = "claude-sonnet-4-20250514" let sessionId = "session-cross-file" let parentOverlap: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": sessionId, "requestId": "req_overlap", "isSidechain": false, "message": [ "id": "msg_overlap", "model": model, "usage": [ "input_tokens": 100, "cache_creation_input_tokens": 20, "cache_read_input_tokens": 10, "output_tokens": 30, ], ], ] let compactOverlap: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": sessionId, "requestId": "req_overlap", "isSidechain": true, "message": [ "id": "msg_overlap", "model": model, "usage": [ "input_tokens": 100, "cache_creation_input_tokens": 20, "cache_read_input_tokens": 10, "output_tokens": 30, ], ], ] let nonCompactOverlap: [String: Any] = [ "type": "assistant", "timestamp": iso2, "sessionId": sessionId, "requestId": "req_overlap", "isSidechain": true, "message": [ "id": "msg_overlap", "model": model, "usage": [ "input_tokens": 100, "cache_creation_input_tokens": 20, "cache_read_input_tokens": 10, "output_tokens": 30, ], ], ] let uniqueSidechain: [String: Any] = [ "type": "assistant", "timestamp": iso3, "sessionId": sessionId, "requestId": "req_unique_sidechain", "isSidechain": true, "message": [ "id": "msg_unique_sidechain", "model": model, "usage": [ "input_tokens": 70, "cache_creation_input_tokens": 5, "cache_read_input_tokens": 0, "output_tokens": 20, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId).jsonl", contents: env.jsonl([parentOverlap])) _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId)/subagents/agent-acompact-overlap.jsonl", contents: env.jsonl([compactOverlap])) _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId)/subagents/agent-aside_question-overlap.jsonl", contents: env.jsonl([nonCompactOverlap, uniqueSidechain])) 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 == 170) #expect(report.data[0].cacheCreationTokens == 25) #expect(report.data[0].cacheReadTokens == 10) #expect(report.data[0].outputTokens == 50) #expect(report.data[0].totalTokens == 255) } @Test func `claude forked transcript history dedups globally while new fork rows count`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 22) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let iso2 = env.isoString(for: day.addingTimeInterval(2)) let model = "claude-sonnet-4-20250514" let copiedHistoryInOriginal: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": "session-original", "requestId": "req_copied_history", "isSidechain": false, "uuid": "assistant-uuid-copied", "parentUuid": "parent-uuid-copied", "message": [ "id": "msg_copied_history", "model": model, "usage": [ "input_tokens": 100, "cache_creation_input_tokens": 20, "cache_read_input_tokens": 10, "output_tokens": 30, ], ], ] var copiedHistoryInFork = copiedHistoryInOriginal copiedHistoryInFork["sessionId"] = "session-fork" let originalContinuation: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": "session-original", "requestId": "req_original_new", "isSidechain": false, "message": [ "id": "msg_original_new", "model": model, "usage": [ "input_tokens": 40, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 10, ], ], ] let forkContinuation: [String: Any] = [ "type": "assistant", "timestamp": iso2, "sessionId": "session-fork", "requestId": "req_fork_new", "isSidechain": false, "message": [ "id": "msg_fork_new", "model": model, "usage": [ "input_tokens": 70, "cache_creation_input_tokens": 5, "cache_read_input_tokens": 0, "output_tokens": 20, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/session-original.jsonl", contents: env.jsonl([copiedHistoryInOriginal, originalContinuation])) _ = try env.writeClaudeProjectFile( relativePath: "project-a/session-fork.jsonl", contents: env.jsonl([copiedHistoryInFork, forkContinuation])) 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 == 210) #expect(report.data[0].cacheCreationTokens == 25) #expect(report.data[0].cacheReadTokens == 10) #expect(report.data[0].outputTokens == 60) #expect(report.data[0].totalTokens == 305) } @Test func `claude cross file dedup uses stable path order for same rank sidechains`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 23) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let model = "claude-sonnet-4-20250514" let sessionId = "session-sidechain-path-order" let firstSidechain: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": sessionId, "requestId": "req_path_order", "isSidechain": true, "message": [ "id": "msg_path_order", "model": model, "usage": [ "input_tokens": 10, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, ], ], ] let secondSidechain: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": sessionId, "requestId": "req_path_order", "isSidechain": true, "message": [ "id": "msg_path_order", "model": model, "usage": [ "input_tokens": 999, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 999, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId)/subagents/agent-a-first.jsonl", contents: env.jsonl([firstSidechain])) _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId)/subagents/agent-b-second.jsonl", contents: env.jsonl([secondSidechain])) 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 == 10) #expect(report.data[0].outputTokens == 1) #expect(report.data[0].totalTokens == 11) } @Test func `claude cross file dedup uses provider ids when session id is missing`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 23) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let model = "claude-sonnet-4-20250514" let missingSession: [String: Any] = [ "type": "assistant", "timestamp": iso0, "requestId": "req_shared", "isSidechain": false, "message": [ "id": "msg_shared", "model": model, "usage": [ "input_tokens": 10, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, ], ], ] let sessionScoped: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": "session-has-id", "requestId": "req_shared", "isSidechain": true, "message": [ "id": "msg_shared", "model": model, "usage": [ "input_tokens": 20, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 2, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/missing-session-parent.jsonl", contents: env.jsonl([missingSession])) _ = try env.writeClaudeProjectFile( relativePath: "project-a/session-has-id/subagents/agent-a-sidechain.jsonl", contents: env.jsonl([sessionScoped])) 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 == 10) #expect(report.data[0].outputTokens == 1) #expect(report.data[0].totalTokens == 11) } @Test func `claude rescans sessions when a new parent file overlaps cached sidechain data`() throws { let env = try CostUsageTestEnvironment() defer { env.cleanup() } let day = try env.makeLocalNoon(year: 2025, month: 12, day: 24) let iso0 = env.isoString(for: day) let iso1 = env.isoString(for: day.addingTimeInterval(1)) let model = "claude-sonnet-4-20250514" let sessionId = "session-cache-recompute" let sidechainOverlap: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": sessionId, "requestId": "req_overlap", "isSidechain": true, "message": [ "id": "msg_overlap", "model": model, "usage": [ "input_tokens": 40, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 10, ], ], ] let uniqueSidechain: [String: Any] = [ "type": "assistant", "timestamp": iso1, "sessionId": sessionId, "requestId": "req_unique", "isSidechain": true, "message": [ "id": "msg_unique", "model": model, "usage": [ "input_tokens": 5, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId)/subagents/agent-acompact-cached.jsonl", contents: env.jsonl([sidechainOverlap, uniqueSidechain])) 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.count == 1) #expect(firstReport.data[0].inputTokens == 45) #expect(firstReport.data[0].outputTokens == 11) #expect(firstReport.data[0].totalTokens == 56) let parentOverlap: [String: Any] = [ "type": "assistant", "timestamp": iso0, "sessionId": sessionId, "requestId": "req_overlap", "isSidechain": false, "message": [ "id": "msg_overlap", "model": model, "usage": [ "input_tokens": 40, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 10, ], ], ] _ = try env.writeClaudeProjectFile( relativePath: "project-a/\(sessionId).jsonl", contents: env.jsonl([parentOverlap])) let secondReport = CostUsageScanner.loadDailyReport( provider: .claude, since: day, until: day, now: day.addingTimeInterval(10), options: options) #expect(secondReport.data.count == 1) #expect(secondReport.data[0].inputTokens == 45) #expect(secondReport.data[0].outputTokens == 11) #expect(secondReport.data[0].totalTokens == 56) } @Test func `claude sonnet 4 6 pricing is available for base and dated models`() { let baseCost = CostUsagePricing.claudeCostUSD( model: "claude-sonnet-4-6", inputTokens: 1000, cacheReadInputTokens: 100, cacheCreationInputTokens: 50, outputTokens: 25) let datedCost = CostUsagePricing.claudeCostUSD( model: "claude-sonnet-4-6-20260219", inputTokens: 1000, cacheReadInputTokens: 100, cacheCreationInputTokens: 50, outputTokens: 25) #expect(baseCost != nil) #expect(datedCost != nil) #expect(baseCost == datedCost) } }