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

615 lines
20 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct DeepSeekUsageFetcherTests {
private struct TimeoutError: Error {}
private actor SummaryCancellationProbe {
private var started = false
private var cancelled = false
private var startedWaiters: [CheckedContinuation<Void, Never>] = []
private var cancelledWaiters: [CheckedContinuation<Void, Never>] = []
func markStarted() {
self.started = true
for waiter in self.startedWaiters {
waiter.resume()
}
self.startedWaiters.removeAll()
}
func waitUntilStarted() async {
if self.started { return }
await withCheckedContinuation { continuation in
self.startedWaiters.append(continuation)
}
}
func markCancelled() {
self.cancelled = true
for waiter in self.cancelledWaiters {
waiter.resume()
}
self.cancelledWaiters.removeAll()
}
func waitUntilCancelled() async {
if self.cancelled { return }
await withCheckedContinuation { continuation in
self.cancelledWaiters.append(continuation)
}
}
func wasCancelled() -> Bool {
self.cancelled
}
}
private static func withTimeout<T: Sendable>(
_ timeout: Duration,
operation: @escaping @Sendable () async throws -> T) async throws -> T
{
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await operation()
}
group.addTask {
try await Task.sleep(for: timeout)
throw TimeoutError()
}
let result = try await group.next()
group.cancelAll()
guard let result else { throw TimeoutError() }
return result
}
}
private static func waitForCancellation(_ probe: SummaryCancellationProbe) async -> Bool {
for _ in 0..<100 {
if await probe.wasCancelled() { return true }
try? await Task.sleep(for: .milliseconds(20))
}
return await probe.wasCancelled()
}
private static let sampleBalanceJSON = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "50.00",
"granted_balance": "10.00",
"topped_up_balance": "40.00"
}
]
}
"""
private static func sampleSummary(updatedAt: Date = Date()) -> DeepSeekUsageSummary {
DeepSeekUsageSummary(
todayTokens: 123,
currentMonthTokens: 456,
todayCost: 1.23,
currentMonthCost: 4.56,
requestCount: 7,
currentMonthRequestCount: 8,
topModel: "deepseek-v4-flash",
categoryBreakdown: [
DeepSeekCategoryBreakdown(category: .promptCacheHitToken, tokens: 123, cost: 1.23),
],
daily: [],
currency: "USD",
updatedAt: updatedAt)
}
@Test
func `parses USD balance response`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "50.00",
"granted_balance": "10.00",
"topped_up_balance": "40.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
#expect(snapshot.isAvailable == true)
#expect(snapshot.currency == "USD")
#expect(snapshot.totalBalance == 50.0)
#expect(snapshot.grantedBalance == 10.0)
#expect(snapshot.toppedUpBalance == 40.0)
}
@Test
func `parses CNY balance response`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "CNY",
"total_balance": "110.00",
"granted_balance": "10.00",
"topped_up_balance": "100.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
#expect(snapshot.currency == "CNY")
#expect(snapshot.totalBalance == 110.0)
#expect(snapshot.toppedUpBalance == 100.0)
}
@Test
func `prefers USD when both currencies present`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "CNY",
"total_balance": "100.00",
"granted_balance": "0.00",
"topped_up_balance": "100.00"
},
{
"currency": "USD",
"total_balance": "20.00",
"granted_balance": "5.00",
"topped_up_balance": "15.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
#expect(snapshot.currency == "USD")
#expect(snapshot.totalBalance == 20.0)
}
@Test
func `prefers positive CNY balance over empty USD balance`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "0.00",
"granted_balance": "0.00",
"topped_up_balance": "0.00"
},
{
"currency": "CNY",
"total_balance": "100.00",
"granted_balance": "0.00",
"topped_up_balance": "100.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
#expect(snapshot.currency == "CNY")
#expect(snapshot.totalBalance == 100.0)
#expect(usage.primary?.resetDescription?.contains("¥100.00") == true)
}
@Test
func `zero balance prompts top up even when unavailable`() throws {
let json = """
{
"is_available": false,
"balance_infos": [
{
"currency": "USD",
"total_balance": "0.00",
"granted_balance": "0.00",
"topped_up_balance": "0.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
#expect(snapshot.isAvailable == false)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 100)
#expect(usage.primary?.resetDescription == "$0.00 — add credits at platform.deepseek.com")
#expect(usage.identity?.loginMethod == nil)
}
@Test
func `full bar when balance available`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "5.00",
"granted_balance": "0.00",
"topped_up_balance": "5.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 0)
#expect(usage.primary?.resetDescription?.contains("$5.00") == true)
#expect(usage.identity?.loginMethod == nil)
}
@Test
func `throws on malformed balance string`() {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "not-a-number",
"granted_balance": "0.00",
"topped_up_balance": "0.00"
}
]
}
"""
#expect {
_ = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
} throws: { error in
guard case DeepSeekUsageError.parseFailed = error else { return false }
return true
}
}
@Test
func `empty balance_infos returns unavailable snapshot`() throws {
let json = """
{
"is_available": true,
"balance_infos": []
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
#expect(snapshot.isAvailable == false)
#expect(snapshot.totalBalance == 0.0)
}
@Test
func `throws on invalid JSON root`() {
let json = "[{ \"is_available\": true }]"
#expect {
_ = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
} throws: { error in
guard case DeepSeekUsageError.parseFailed = error else { return false }
return true
}
}
@Test
func `balance description includes paid and granted breakdown`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "50.00",
"granted_balance": "10.00",
"topped_up_balance": "40.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
let detail = usage.primary?.resetDescription ?? ""
#expect(detail.contains("$50.00"))
#expect(detail.contains("$40.00"))
#expect(detail.contains("$10.00"))
}
@Test
func `CNY balance uses yen symbol`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "CNY",
"total_balance": "100.00",
"granted_balance": "0.00",
"topped_up_balance": "100.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
let detail = usage.primary?.resetDescription ?? ""
#expect(detail.contains("¥"))
}
@Test
func `balance snapshot has nil usage summary`() throws {
let json = """
{
"is_available": true,
"balance_infos": [
{
"currency": "USD",
"total_balance": "50.00",
"granted_balance": "10.00",
"topped_up_balance": "40.00"
}
]
}
"""
let snapshot = try DeepSeekUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
#expect(usage.deepseekUsage == nil)
}
@Test
func `balance returns promptly when optional usage summary is slow`() async throws {
let probe = SummaryCancellationProbe()
let snapshot = try await Self.withTimeout(.seconds(10)) {
try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .milliseconds(50),
fetchBalanceData: { _ in
Data(Self.sampleBalanceJSON.utf8)
},
fetchSummary: { _ in
await probe.markStarted()
do {
try await Task.sleep(for: .seconds(60))
return Self.sampleSummary()
} catch is CancellationError {
await probe.markCancelled()
throw CancellationError()
}
})
}
#expect(snapshot.totalBalance == 50.0)
#expect(snapshot.usageSummary == nil)
#expect(await Self.waitForCancellation(probe))
}
@Test
func `balance grace does not wait for optional summary that ignores cancellation`() async throws {
let startedAt = ContinuousClock.now
let snapshot = try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .milliseconds(20),
fetchBalanceData: { _ in
Data(Self.sampleBalanceJSON.utf8)
},
fetchSummary: { _ in
await withCheckedContinuation { continuation in
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
continuation.resume(returning: Self.sampleSummary())
}
}
})
let elapsed = startedAt.duration(to: .now)
#expect(snapshot.totalBalance == 50.0)
#expect(snapshot.usageSummary == nil)
#expect(elapsed < .milliseconds(300), "Optional summary delayed balance: \(elapsed)")
// Let the deliberately cancellation-ignoring test task drain before the test exits.
try await Task.sleep(for: .milliseconds(550))
}
@Test
func `balance returns when optional usage summary fails closed`() async throws {
let snapshot = try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(2),
fetchBalanceData: { _ in
Data(Self.sampleBalanceJSON.utf8)
},
fetchSummary: { _ in
throw DeepSeekUsageError.networkError("simulated failure")
})
#expect(snapshot.totalBalance == 50.0)
#expect(snapshot.usageSummary == nil)
}
@Test
func `cancels optional usage summary when balance fetch fails`() async throws {
let probe = SummaryCancellationProbe()
do {
_ = try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(2),
fetchBalanceData: { _ in
await probe.waitUntilStarted()
throw DeepSeekUsageError.networkError("simulated balance failure")
},
fetchSummary: { _ in
await probe.markStarted()
do {
try await Task.sleep(for: .seconds(1))
return Self.sampleSummary()
} catch is CancellationError {
await probe.markCancelled()
throw DeepSeekUsageError.networkError("cancelled")
}
})
Issue.record("Expected balance failure")
} catch DeepSeekUsageError.networkError {
#expect(await Self.waitForCancellation(probe))
}
}
@Test
func `cancels optional usage summary when balance parsing fails`() async throws {
let probe = SummaryCancellationProbe()
do {
_ = try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(2),
fetchBalanceData: { _ in
await probe.waitUntilStarted()
return Data("{\"is_available\":true,\"balance_infos\":[".utf8)
},
fetchSummary: { _ in
await probe.markStarted()
do {
try await Task.sleep(for: .seconds(1))
return Self.sampleSummary()
} catch is CancellationError {
await probe.markCancelled()
throw DeepSeekUsageError.networkError("cancelled")
}
})
Issue.record("Expected balance parse failure")
} catch DeepSeekUsageError.parseFailed {
#expect(await Self.waitForCancellation(probe))
}
}
@Test
func `parent cancellation propagates while waiting for optional usage summary`() async throws {
let probe = SummaryCancellationProbe()
let task = Task {
try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(30),
fetchBalanceData: { _ in
Data(Self.sampleBalanceJSON.utf8)
},
fetchSummary: { _ in
await probe.markStarted()
do {
try await Task.sleep(for: .seconds(60))
return Self.sampleSummary()
} catch is CancellationError {
await probe.markCancelled()
throw CancellationError()
}
})
}
await probe.waitUntilStarted()
task.cancel()
do {
_ = try await Self.withTimeout(.seconds(10)) {
try await task.value
}
Issue.record("Expected cancellation")
} catch is CancellationError {
#expect(await Self.waitForCancellation(probe))
}
}
@Test
func `parent cancellation stops summary while balance transport ignores cancellation`() async throws {
let balanceStarted = AsyncStream<Void>.makeStream(of: Void.self)
let probe = SummaryCancellationProbe()
let task = Task {
try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(30),
fetchBalanceData: { _ in
balanceStarted.continuation.yield(())
return await withCheckedContinuation { continuation in
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
continuation.resume(returning: Data(Self.sampleBalanceJSON.utf8))
}
}
},
fetchSummary: { _ in
await probe.markStarted()
do {
try await Task.sleep(for: .seconds(60))
return Self.sampleSummary()
} catch is CancellationError {
await probe.markCancelled()
throw CancellationError()
}
})
}
var balanceIterator = balanceStarted.stream.makeAsyncIterator()
_ = await balanceIterator.next()
await probe.waitUntilStarted()
let cancellationStartedAt = ContinuousClock.now
task.cancel()
await probe.waitUntilCancelled()
#expect(cancellationStartedAt.duration(to: .now) < .milliseconds(300))
await #expect(throws: CancellationError.self) {
try await task.value
}
}
@Test
func `usage period defaults to Gregorian API calendar`() throws {
let date = try #require(Self.utcDate(year: 2026, month: 5, day: 26))
let period = try DeepSeekUsageFetcher._apiUsagePeriodForTesting(now: date)
#expect(period.month == 5)
#expect(period.year == 2026)
}
@Test
func `usage period supports injected test calendar`() throws {
var calendar = Calendar(identifier: .buddhist)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .gmt
let date = try #require(Self.utcDate(year: 2026, month: 5, day: 26))
let period = try DeepSeekUsageFetcher._apiUsagePeriodForTesting(now: date, calendar: calendar)
#expect(period.month == 5)
#expect(period.year == 2569)
}
@Test
func `production path can populate usage summary when optional fetch succeeds`() async throws {
let expected = Self.sampleSummary()
let snapshot = try await DeepSeekUsageFetcher._fetchUsageForTesting(
apiKey: "test-key",
includeOptionalUsage: true,
optionalSummaryJoinGrace: .seconds(2),
fetchBalanceData: { _ in
Data(Self.sampleBalanceJSON.utf8)
},
fetchSummary: { _ in
expected
})
#expect(snapshot.totalBalance == 50.0)
#expect(snapshot.usageSummary == expected)
}
private static func utcDate(year: Int, month: Int, day: Int) -> Date? {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .gmt
return calendar.date(from: DateComponents(year: year, month: month, day: day))
}
}