966 lines · 35.0 KB · Swift
RawDownload
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.