Files
steipete--codexbar/Tests/CodexBarTests/DeepSeekUsageCostParserTests.swift
2026-07-13 12:22:33 +08:00

856 lines
26 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct DeepSeekUsageCostParserTests {
// Fixtures use date 2026-05-26
private let fixtureNow = Date(timeIntervalSince1970: 1_779_796_800) // 2026-05-26 12:00:00 UTC
private let fixtureCalendar: Calendar = {
var cal = Calendar.current
cal.timeZone = TimeZone(identifier: "UTC") ?? .current
return cal
}()
// MARK: - Amount Parser Tests
@Test
func `amount parser decodes total and days`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1305432"},
{"type": "RESPONSE_TOKEN", "amount": "656338"},
{"type": "REQUEST", "amount": "1212"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1305432"},
{"type": "RESPONSE_TOKEN", "amount": "656338"},
{"type": "REQUEST", "amount": "1212"}
]
}
]
}
]
}
}
}
"""
let payload = try DeepSeekUsageCostParser.decodeAmountPayload(data: Data(json.utf8))
#expect(payload.code == 0)
#expect(payload.data?.bizCode == 0)
#expect(payload.data?.bizData?.total?.count == 1)
#expect(payload.data?.bizData?.total?[0].model == "deepseek-v4-flash")
#expect(payload.data?.bizData?.days?.count == 1)
#expect(payload.data?.bizData?.days?[0].date == "2026-05-26")
}
@Test
func `amount parser handles missing biz_data gracefully`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": null
}
"""
let payload = try DeepSeekUsageCostParser.decodeAmountPayload(data: Data(json.utf8))
#expect(payload.code == 0)
#expect(payload.data?.bizData?.total == nil)
#expect(payload.data?.bizData?.days == nil)
}
// MARK: - Cost Parser Tests
@Test
func `cost parser decodes total, days, and currency`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1.3054320000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"},
{"type": "REQUEST", "amount": "0"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1.3054320000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"},
{"type": "REQUEST", "amount": "0"}
]
}
]
}
],
"currency": "CNY"
}
]
}
}
"""
let payload = try DeepSeekUsageCostParser.decodeCostPayload(data: Data(json.utf8))
#expect(payload.code == 0)
#expect(payload.data?.bizCode == 0)
#expect(payload.data?.bizData?[0].currency == "CNY")
#expect(payload.data?.bizData?[0].total?.count == 1)
#expect(payload.data?.bizData?[0].days?.count == 1)
#expect(payload.data?.bizData?[0].days?[0].date == "2026-05-26")
}
@Test
func `cost parser handles empty biz_data`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": []
}
}
"""
let payload = try DeepSeekUsageCostParser.decodeCostPayload(data: Data(json.utf8))
#expect(payload.code == 0)
#expect(payload.data?.bizData?.isEmpty == true)
}
// MARK: - String Parsing Tests
@Test
func `string token parsing works`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "RESPONSE_TOKEN", "amount": "656338"}
]
}
],
"days": []
}
}
}
"""
let payload = try DeepSeekUsageCostParser.decodeAmountPayload(data: Data(json.utf8))
#expect(payload.data?.bizData?.total?[0].usage?[0].type == "PROMPT_CACHE_HIT_TOKEN")
#expect(payload.data?.bizData?.total?[0].usage?[0].amount == "100686720")
}
@Test
func `decimal cost parsing works`() throws {
let json = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"}
]
}
],
"days": [],
"currency": "CNY"
}
]
}
}
"""
let payload = try DeepSeekUsageCostParser.decodeCostPayload(data: Data(json.utf8))
#expect(payload.data?.bizData?[0].total?[0].usage?[0].amount == "2.0137344000000000")
}
// MARK: - Aggregation Tests
@Test
func `aggregation computes today token totals`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1305432"},
{"type": "RESPONSE_TOKEN", "amount": "656338"},
{"type": "REQUEST", "amount": "1212"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1305432"},
{"type": "RESPONSE_TOKEN", "amount": "656338"},
{"type": "REQUEST", "amount": "1212"}
]
}
]
}
]
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1.3054320000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"},
{"type": "REQUEST", "amount": "0"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1.3054320000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"},
{"type": "REQUEST", "amount": "0"}
]
}
]
}
],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
// Today is 2026-05-26 per the test data
#expect(summary.todayTokens == 102_648_490) // 100_686_720 + 1_305_432 + 656_338
#expect(summary.requestCount == 1212)
#expect(summary.currency == "CNY")
}
@Test
func `aggregation uses injected now and calendar for today bucket`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100"},
{"type": "REQUEST", "amount": "1"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100"},
{"type": "REQUEST", "amount": "1"}
]
}
]
}
]
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [],
"days": [],
"currency": "CNY"
}
]
}
}
"""
var nextMonthUTC = Calendar(identifier: .gregorian)
nextMonthUTC.timeZone = TimeZone(identifier: "UTC") ?? .current
let injectedNow = try #require(nextMonthUTC.date(from: DateComponents(year: 2026, month: 6, day: 1, hour: 12)))
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: injectedNow,
calendar: nextMonthUTC)
#expect(summary.todayTokens == 0)
#expect(summary.currentMonthTokens == 0)
}
@Test
func `aggregation computes today cost totals`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "RESPONSE_TOKEN", "amount": "656338"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "RESPONSE_TOKEN", "amount": "656338"}
]
}
]
}
]
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"}
]
}
]
}
],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
#expect(abs((summary.todayCost ?? 0) - 3.3264104) < 0.0001)
#expect(summary.currentMonthCost != nil)
}
@Test
func `aggregation computes model and category breakdown`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100686720"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1305432"},
{"type": "RESPONSE_TOKEN", "amount": "656338"}
]
}
],
"days": []
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0137344000000000"},
{"type": "PROMPT_CACHE_MISS_TOKEN", "amount": "1.3054320000000000"},
{"type": "RESPONSE_TOKEN", "amount": "1.3126760000000000"}
]
}
],
"days": [],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
#expect(summary.topModel == "deepseek-v4-flash")
#expect(summary.categoryBreakdown.count == 3)
let cacheHit = summary.categoryBreakdown.first { $0.category == DeepSeekUsageCategory.promptCacheHitToken }
#expect(cacheHit?.tokens == 100_686_720)
#expect(abs((cacheHit?.cost ?? 0) - 2.0137344) < 0.0001)
let cacheMiss = summary.categoryBreakdown.first { $0.category == DeepSeekUsageCategory.promptCacheMissToken }
#expect(cacheMiss?.tokens == 1_305_432)
#expect(abs((cacheMiss?.cost ?? 0) - 1.305432) < 0.0001)
let response = summary.categoryBreakdown.first { $0.category == DeepSeekUsageCategory.responseToken }
#expect(response?.tokens == 656_338)
#expect(abs((response?.cost ?? 0) - 1.312676) < 0.0001)
}
// MARK: - Unknown Types Handling
@Test
func `unknown usage types are ignored safely`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100"},
{"type": "UNKNOWN_TYPE", "amount": "999"},
{"type": "RESPONSE_TOKEN", "amount": "200"}
]
}
],
"days": []
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "1.0"},
{"type": "UNKNOWN_TYPE", "amount": "99.0"},
{"type": "RESPONSE_TOKEN", "amount": "2.0"}
]
}
],
"days": [],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
// Unknown type should be ignored - only known categories with non-zero tokens appear in breakdown
// todayTokens comes from daily data which is empty in this test, so it's 0
#expect(summary.todayTokens == 0)
#expect(summary.categoryBreakdown.count == 3) // Always 3 categories, even if some have 0 tokens
}
// MARK: - Error Handling
@Test
func `missing fields fails closed`() {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": null
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [],
"days": [],
"currency": "CNY"
}
]
}
}
"""
#expect {
_ = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: fixtureNow,
calendar: fixtureCalendar)
} throws: { error in
guard case DeepSeekUsageError.parseFailed = error else { return false }
return true
}
}
@Test
func `non-zero biz_code fails closed`() {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 1001,
"biz_msg": "some error",
"biz_data": {
"total": [],
"days": []
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [],
"days": [],
"currency": "CNY"
}
]
}
}
"""
#expect {
_ = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8))
} throws: { error in
guard case DeepSeekUsageError.apiError = error else { return false }
return true
}
}
@Test
func `invalid JSON fails closed`() {
#expect {
_ = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data("not json".utf8),
costData: Data("{}".utf8))
} throws: { error in
guard case DeepSeekUsageError.parseFailed = error else { return false }
return true
}
}
// MARK: - Edge Cases
@Test
func `empty days array works`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [],
"days": []
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [],
"days": [],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
#expect(summary.todayTokens == 0)
#expect(summary.currentMonthTokens == 0)
#expect(summary.todayCost == nil)
#expect(summary.currentMonthCost == nil)
#expect(summary.daily.isEmpty)
}
@Test
func `multiple models works`() throws {
let amountJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": {
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100"},
{"type": "RESPONSE_TOKEN", "amount": "50"}
]
},
{
"model": "deepseek-chat",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "200"},
{"type": "RESPONSE_TOKEN", "amount": "100"}
]
}
],
"days": [
{
"date": "2026-05-26",
"data": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "100"},
{"type": "RESPONSE_TOKEN", "amount": "50"}
]
},
{
"model": "deepseek-chat",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "200"},
{"type": "RESPONSE_TOKEN", "amount": "100"}
]
}
]
}
]
}
}
}
"""
let costJSON = """
{
"code": 0,
"msg": "",
"data": {
"biz_code": 0,
"biz_msg": "",
"biz_data": [
{
"total": [
{
"model": "deepseek-v4-flash",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "1.0"},
{"type": "RESPONSE_TOKEN", "amount": "0.5"}
]
},
{
"model": "deepseek-chat",
"usage": [
{"type": "PROMPT_CACHE_HIT_TOKEN", "amount": "2.0"},
{"type": "RESPONSE_TOKEN", "amount": "1.0"}
]
}
],
"days": [],
"currency": "CNY"
}
]
}
}
"""
let summary = try DeepSeekUsageFetcher._parseUsageSummaryForTesting(
amountData: Data(amountJSON.utf8),
costData: Data(costJSON.utf8),
now: self.fixtureNow,
calendar: self.fixtureCalendar)
#expect(summary.topModel == "deepseek-chat") // 300 tokens vs 150 tokens
#expect(summary.todayTokens == 450) // 150 + 300
}
}