import CodexBarCore import Commander import Foundation import Testing @testable import CodexBarCLI struct CLICostTests { @Test func `cost json shortcut does not enable json logs`() throws { let signature = CodexBarCLI._costSignatureForTesting() let parser = CommandParser(signature: signature) let parsed = try parser.parse(arguments: ["--json"]) #expect(parsed.flags.contains("jsonShortcut")) #expect(!parsed.flags.contains("jsonOutput")) #expect(CodexBarCLI._decodeFormatForTesting(from: parsed) == .json) } @Test func `renders cost text snapshot`() { let snap = CostUsageTokenSnapshot( sessionTokens: 1200, sessionCostUSD: 1.25, last30DaysTokens: 9000, last30DaysCostUSD: 9.99, historyDays: 90, daily: [], updatedAt: Date(timeIntervalSince1970: 0)) let output = CodexBarCLI.renderCostText(provider: .claude, snapshot: snap, useColor: false) .replacingOccurrences(of: "\u{00A0}", with: " ") .replacingOccurrences(of: "$ ", with: "$") #expect(output.contains("Claude Cost (API-rate estimate)")) #expect(output.contains("Today: $1.25 · 1.2K tokens")) #expect(output.contains("Last 90 days: $9.99 · 9K tokens")) #expect(output.contains("cache read/write tokens")) #expect(output.contains("Claude Code /status")) } @Test func `renders codex project grouped cost text`() { let snap = CostUsageTokenSnapshot( sessionTokens: 1200, sessionCostUSD: 1.25, last30DaysTokens: 9000, last30DaysCostUSD: 9.99, historyDays: 30, daily: [], projects: [ CostUsageProjectBreakdown( name: "client-a", path: "/work/client-a", totalTokens: 7000, totalCostUSD: 7.5, daily: [], modelBreakdowns: nil, sources: [ CostUsageProjectSourceBreakdown( name: "client-a", path: "/work/client-a", totalTokens: 5000, totalCostUSD: 5.25, daily: [], modelBreakdowns: nil), CostUsageProjectSourceBreakdown( name: "client-a", path: "/Users/test/.codex/worktrees/abcd/client-a", totalTokens: 2000, totalCostUSD: 2.25, daily: [], modelBreakdowns: nil), ]), CostUsageProjectBreakdown( name: CostUsageProjectBreakdown.unknownProjectName, path: nil, totalTokens: 2000, totalCostUSD: 2.49, daily: [], modelBreakdowns: nil), ], updatedAt: Date(timeIntervalSince1970: 0)) let output = CodexBarCLI.renderCostText( provider: .codex, snapshot: snap, groupBy: .project, useColor: false) .replacingOccurrences(of: "\u{00A0}", with: " ") .replacingOccurrences(of: "$ ", with: "$") #expect(output.contains("Codex Cost (API-rate estimate)")) #expect(output.contains("Projects (Last 30 days):")) #expect(output.contains("client-a: $7.50 · 7K tokens")) #expect(output.contains("/work/client-a")) #expect(output.contains(" - client-a: $5.25 · 5K tokens")) #expect(output.contains(" - client-a: $2.25 · 2K tokens")) #expect(output.contains("/Users/test/.codex/worktrees/abcd/client-a")) #expect(output.contains("Unknown project: $2.49 · 2K tokens")) } @Test func `encodes cost payload JSON`() throws { let payload = CostPayload( provider: "claude", source: "local", updatedAt: Date(timeIntervalSince1970: 1_700_000_000), sessionTokens: 100, sessionCostUSD: 0.5, historyDays: 90, last30DaysTokens: 200, last30DaysCostUSD: 1.5, daily: [ CostDailyEntryPayload( date: "2025-12-20", inputTokens: 10, outputTokens: 5, cacheReadTokens: 2, cacheCreationTokens: 3, totalTokens: 15, costUSD: 0.01, modelsUsed: ["claude-sonnet-4-20250514"], modelBreakdowns: [ CostModelBreakdownPayload( modelName: "claude-sonnet-4-20250514", costUSD: 0.01, totalTokens: 15), ]), ], totals: CostTotalsPayload( totalInputTokens: 10, totalOutputTokens: 5, cacheReadTokens: 2, cacheCreationTokens: 3, totalTokens: 15, totalCostUSD: 0.01), error: nil) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .secondsSince1970 let data = try encoder.encode(payload) guard let json = String(data: data, encoding: .utf8) else { Issue.record("Failed to decode cost payload JSON") return } #expect(json.contains("\"provider\":\"claude\"")) #expect(json.contains("\"source\":\"local\"")) #expect(json.contains("\"historyDays\":90")) #expect(json.contains("\"daily\"")) #expect(json.contains("\"totals\"")) #expect(json.contains("\"cacheReadTokens\":2")) #expect(json.contains("\"cacheCreationTokens\":3")) #expect(json.contains("\"totalCost\"")) #expect(json.contains("\"totalTokens\":15")) #expect(json.contains("1700000000")) } @Test func `codex cost payload includes project rollups`() throws { let snapshot = CostUsageTokenSnapshot( sessionTokens: 10, sessionCostUSD: 0.01, last30DaysTokens: 40, last30DaysCostUSD: 0.04, daily: [ CostUsageDailyReport.Entry( date: "2026-04-02", inputTokens: 30, outputTokens: 10, totalTokens: 40, costUSD: 0.04, modelsUsed: ["gpt-5.4"], modelBreakdowns: [ CostUsageDailyReport.ModelBreakdown( modelName: "gpt-5.4", costUSD: 0.04, totalTokens: 40), ]), ], projects: [ CostUsageProjectBreakdown( name: "client-a", path: "/work/client-a", totalTokens: 40, totalCostUSD: 0.04, daily: [ CostUsageDailyReport.Entry( date: "2026-04-02", inputTokens: 30, outputTokens: 10, totalTokens: 40, costUSD: 0.04, modelsUsed: ["gpt-5.4"], modelBreakdowns: nil), ], modelBreakdowns: [ CostUsageDailyReport.ModelBreakdown( modelName: "gpt-5.4", costUSD: 0.04, totalTokens: 40), ], sources: [ CostUsageProjectSourceBreakdown( name: "client-a", path: "/work/client-a", totalTokens: 40, totalCostUSD: 0.04, daily: [ CostUsageDailyReport.Entry( date: "2026-04-02", inputTokens: 30, outputTokens: 10, totalTokens: 40, costUSD: 0.04, modelsUsed: ["gpt-5.4"], modelBreakdowns: nil), ], modelBreakdowns: nil), ]), ], updatedAt: Date(timeIntervalSince1970: 1_700_000_000)) let payload = CodexBarCLI.makeCostPayload(provider: .codex, snapshot: snapshot, error: nil) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .secondsSince1970 let data = try encoder.encode(payload) guard let json = String(data: data, encoding: .utf8) else { Issue.record("Failed to decode cost payload JSON") return } #expect(json.contains("\"projects\"")) #expect(json.contains("\"sources\"")) #expect(json.contains("\"name\":\"client-a\"")) #expect(json.contains("/work/client-a") || json.contains("\\/work\\/client-a")) #expect(json.contains("\"totalCost\":0.04")) #expect(json.contains("\"daily\"")) #expect(json.contains("\"gpt-5.4\"")) } @Test func `encodes exact codex model I ds and zero cost breakdowns`() throws { let payload = CostPayload( provider: "codex", source: "local", updatedAt: Date(timeIntervalSince1970: 1_700_000_000), sessionTokens: 155, sessionCostUSD: 0, historyDays: 30, last30DaysTokens: 155, last30DaysCostUSD: 0, daily: [ CostDailyEntryPayload( date: "2025-12-21", inputTokens: 120, outputTokens: 15, cacheReadTokens: 20, cacheCreationTokens: nil, totalTokens: 155, costUSD: 0, modelsUsed: ["gpt-5.3-codex-spark", "gpt-5.2-codex"], modelBreakdowns: [ CostModelBreakdownPayload(modelName: "gpt-5.3-codex-spark", costUSD: 0, totalTokens: 15), CostModelBreakdownPayload(modelName: "gpt-5.2-codex", costUSD: 1.23, totalTokens: 140), ]), ], totals: CostTotalsPayload( totalInputTokens: 120, totalOutputTokens: 15, cacheReadTokens: 20, cacheCreationTokens: nil, totalTokens: 155, totalCostUSD: 0), error: nil) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .secondsSince1970 let data = try encoder.encode(payload) guard let json = String(data: data, encoding: .utf8) else { Issue.record("Failed to decode cost payload JSON") return } #expect(json.contains("\"gpt-5.3-codex-spark\"")) #expect(json.contains("\"gpt-5.2-codex\"")) #expect(!json.contains("\"gpt-5.2\"")) #expect(json.contains("\"cost\":0")) #expect(json.contains("\"totalTokens\":140")) } @Test func `cost estimate hint is stable string`() { let hint = UsageFormatter.costEstimateHint #expect(!hint.isEmpty) #expect(hint.contains("Estimated")) #expect(UsageFormatter.costEstimateHint(provider: .claude).contains("cache read/write tokens")) } }