503 lines · 14.6 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore struct CostUsageDecodingTests {    @Test    func `decodes daily report type format`() throws {        let json = """        {          "type": "daily",          "data": [            {              "date": "2025-12-20",              "inputTokens": 10,              "cacheReadTokens": 2,              "cacheCreationTokens": 3,              "outputTokens": 20,              "totalTokens": 30,              "costUSD": 0.12            }          ],          "summary": {            "totalInputTokens": 10,            "totalOutputTokens": 20,            "cacheReadTokens": 2,            "cacheCreationTokens": 3,            "totalTokens": 30,            "totalCostUSD": 0.12          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data.count == 1)        #expect(report.data[0].date == "2025-12-20")        #expect(report.data[0].totalTokens == 30)        #expect(report.data[0].cacheReadTokens == 2)        #expect(report.data[0].cacheCreationTokens == 3)        #expect(report.data[0].costUSD == 0.12)        #expect(report.summary?.totalCostUSD == 0.12)        #expect(report.summary?.cacheReadTokens == 2)        #expect(report.summary?.cacheCreationTokens == 3)    }     @Test    func `decodes daily report legacy format`() throws {        let json = """        {          "daily": [            {              "date": "2025-12-20",              "inputTokens": 1,              "cacheReadTokens": 2,              "cacheCreationTokens": 3,              "outputTokens": 2,              "totalTokens": 3,              "totalCost": 0.01            }          ],          "totals": {            "totalInputTokens": 1,            "totalOutputTokens": 2,            "cacheReadTokens": 2,            "cacheCreationTokens": 3,            "totalTokens": 3,            "totalCost": 0.01          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data.count == 1)        #expect(report.summary?.totalTokens == 3)        #expect(report.summary?.totalCostUSD == 0.01)        #expect(report.data[0].cacheReadTokens == 2)        #expect(report.data[0].cacheCreationTokens == 3)        #expect(report.summary?.cacheReadTokens == 2)        #expect(report.summary?.cacheCreationTokens == 3)    }     @Test    func `decodes legacy cache token keys`() throws {        let json = """        {          "type": "daily",          "data": [            {              "date": "2025-12-20",              "cacheReadInputTokens": 4,              "cacheCreationInputTokens": 5,              "totalTokens": 9            }          ],          "summary": {            "totalCacheReadTokens": 4,            "totalCacheCreationTokens": 5,            "totalTokens": 9          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].cacheReadTokens == 4)        #expect(report.data[0].cacheCreationTokens == 5)        #expect(report.summary?.cacheReadTokens == 4)        #expect(report.summary?.cacheCreationTokens == 5)    }     @Test    func `decodes daily report legacy format with model map`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "inputTokens": 10,              "outputTokens": 20,              "totalTokens": 30,              "costUSD": 0.12,              "models": {                "gpt-5.2-codex": {                  "inputTokens": 10,                  "outputTokens": 20,                  "totalTokens": 30,                  "isFallback": false                }              }            }          ],          "totals": {            "totalTokens": 30,            "costUSD": 0.12          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data.count == 1)        #expect(report.data[0].costUSD == 0.12)        #expect(report.data[0].modelsUsed == ["gpt-5.2-codex"])    }     @Test    func `decodes daily report legacy format with model map sorted`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 0.12,              "models": {                "z-model": { "totalTokens": 10 },                "a-model": { "totalTokens": 20 },                "m-model": { "totalTokens": 0 }              }            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelsUsed == ["a-model", "m-model", "z-model"])    }     @Test    func `decodes daily report legacy format with empty model map as nil`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 0.12,              "models": {}            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelsUsed == nil)    }     @Test    func `decodes daily report legacy format prefers models used list over models map`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 0.12,              "modelsUsed": ["gpt-5.2-codex"],              "models": {                "ignored-model": { "totalTokens": 30 }              }            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelsUsed == ["gpt-5.2-codex"])    }     @Test    func `decodes daily report legacy format with models list`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 0.12,              "models": ["gpt-5.2-codex", "gpt-5.2-mini"]            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelsUsed == ["gpt-5.2-codex", "gpt-5.2-mini"])    }     @Test    func `decodes model breakdown total tokens`() throws {        let json = """        {          "type": "daily",          "data": [            {              "date": "2025-12-20",              "totalTokens": 30,              "costUSD": 0.12,              "modelBreakdowns": [                {                  "modelName": "gpt-5.2-codex",                  "costUSD": 0.12,                  "totalTokens": 30                }              ]            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelBreakdowns == [            CostUsageDailyReport.ModelBreakdown(modelName: "gpt-5.2-codex", costUSD: 0.12, totalTokens: 30),        ])    }     @Test    func `decodes daily report legacy format with invalid models field`() throws {        let json = """        {          "daily": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 0.12,              "models": "gpt-5.2"            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        #expect(report.data[0].modelsUsed == nil)    }     @Test    func `decodes monthly report legacy format`() throws {        let json = """        {          "monthly": [            {              "month": "Dec 2025",              "totalTokens": 123,              "costUSD": 4.56            }          ],          "totals": {            "totalTokens": 123,            "costUSD": 4.56          }        }        """         let report = try JSONDecoder().decode(CostUsageMonthlyReport.self, from: Data(json.utf8))        #expect(report.data.count == 1)        #expect(report.data[0].month == "Dec 2025")        #expect(report.data[0].costUSD == 4.56)        #expect(report.summary?.totalCostUSD == 4.56)    }     @Test    func `selects most recent session`() throws {        let json = """        {          "type": "session",          "data": [            {              "session": "A",              "totalTokens": 100,              "costUSD": 0.50,              "lastActivity": "2025-12-19"            },            {              "session": "B",              "totalTokens": 50,              "costUSD": 0.20,              "lastActivity": "2025-12-20T12:00:00Z"            },            {              "session": "C",              "totalTokens": 200,              "costUSD": 0.10,              "lastActivity": "2025-12-20T11:00:00Z"            }          ],          "summary": {            "totalCostUSD": 0.80          }        }        """         let report = try JSONDecoder().decode(CostUsageSessionReport.self, from: Data(json.utf8))        let selected = CostUsageFetcher.selectCurrentSession(from: report.data)        #expect(selected?.session == "B")    }     @Test    func `selects most recent supported month format`() throws {        let json = """        {          "type": "monthly",          "data": [            { "month": "Dec 2025", "totalTokens": 100, "costUSD": 1.00 },            { "month": "January 2026", "totalTokens": 200, "costUSD": 2.00 },            { "month": "2026-02", "totalTokens": 300, "costUSD": 3.00 }          ]        }        """         let report = try JSONDecoder().decode(CostUsageMonthlyReport.self, from: Data(json.utf8))        let selected = CostUsageFetcher.selectMostRecentMonth(from: report.data)        #expect(selected?.month == "2026-02")        #expect(selected?.totalTokens == 300)    }     @Test    func `date parsers handle concurrent mixed formats`() async {        let dateInputs = [            "2026-02-03T04:05:06.789Z",            "2026-02-03T04:05:06Z",            "2026-02-03",            "Feb 3, 2026",        ]        let monthInputs = ["Feb 2026", "February 2026", "2026-02"]         await withTaskGroup(of: Bool.self) { group in            for _ in 0..<32 {                group.addTask {                    for _ in 0..<250 {                        guard dateInputs.allSatisfy({ CostUsageDateParser.parse($0) != nil }),                              monthInputs.allSatisfy({ CostUsageDateParser.parseMonth($0) != nil })                        else {                            return false                        }                    }                    return true                }            }             for await succeeded in group {                #expect(succeeded)            }        }    }     @Test    func `token snapshot selects current local day`() throws {        let json = """        {          "type": "daily",          "data": [            {              "date": "Dec 20, 2025",              "totalTokens": 30,              "costUSD": 1.23            },            {              "date": "2025-12-21",              "totalTokens": 10,              "costUSD": 4.56            }          ],          "summary": {            "totalCostUSD": 5.79          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        let now = try Self.localNoon(year: 2025, month: 12, day: 21)        let snapshot = CostUsageFetcher.tokenSnapshot(from: report, now: now)        #expect(snapshot.sessionTokens == 10)        #expect(snapshot.sessionCostUSD == 4.56)        #expect(snapshot.last30DaysCostUSD == 5.79)        #expect(snapshot.daily.count == 2)        #expect(snapshot.updatedAt == now)    }     @Test    func `token snapshot rejects impossible later calendar day`() throws {        let json = """        {          "type": "daily",          "data": [            {              "date": "2026-05-13",              "totalTokens": 30,              "costUSD": 23.45            },            {              "date": "2026-06-31",              "totalTokens": 40,              "costUSD": 99.00            }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        let snapshot = CostUsageFetcher.tokenSnapshot(            from: report,            now: Date(),            useCurrentLocalDayForSession: false)         #expect(snapshot.sessionTokens == 30)        #expect(snapshot.sessionCostUSD == 23.45)    }     @Test    func `token snapshot uses summary total cost when available`() throws {        let json = """        {          "type": "daily",          "data": [            { "date": "2025-12-20", "costUSD": 1.00 },            { "date": "2025-12-21", "costUSD": 2.00 }          ],          "summary": {            "totalCostUSD": 99.00          }        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        let snapshot = CostUsageFetcher.tokenSnapshot(from: report, now: Date())        #expect(snapshot.last30DaysCostUSD == 99.00)    }     @Test    func `token snapshot falls back to summed entries when summary missing`() throws {        let json = """        {          "type": "daily",          "data": [            { "date": "2025-12-20", "costUSD": 1.00 },            { "date": "2025-12-21", "costUSD": 2.00 }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        let snapshot = CostUsageFetcher.tokenSnapshot(from: report, now: Date())        #expect(snapshot.last30DaysCostUSD == 3.00)    }     @Test    func `token snapshot returns nil total when no costs present`() throws {        let json = """        {          "type": "daily",          "data": [            { "date": "2025-12-20", "totalTokens": 10 },            { "date": "2025-12-21", "totalTokens": 20 }          ]        }        """         let report = try JSONDecoder().decode(CostUsageDailyReport.self, from: Data(json.utf8))        let snapshot = CostUsageFetcher.tokenSnapshot(from: report, now: Date())        #expect(snapshot.last30DaysCostUSD == nil)    }     private static func localNoon(year: Int, month: Int, day: Int) throws -> Date {

Lines 1–500 of 503.