chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
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 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 {
|
||||
try #require(Calendar.current.date(from: DateComponents(year: year, month: month, day: day, hour: 12)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user