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 most recent 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 = Date(timeIntervalSince1970: 1_766_275_200) // 2025-12-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()) #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) }}
import Foundation
import 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 most recent 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 = Date(timeIntervalSince1970: 1_766_275_200) // 2025-12-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())
#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)
}
}