Files
2026-07-13 12:22:33 +08:00

256 lines
9.4 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct WidgetSnapshotTests {
@Test
func `widget snapshot round trip`() throws {
let entry = WidgetSnapshot.ProviderEntry(
provider: .codex,
updatedAt: Date(),
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: nil,
usageRows: [
WidgetSnapshot.WidgetUsageRowSnapshot(id: "session", title: "Session", percentLeft: 90),
WidgetSnapshot.WidgetUsageRowSnapshot(id: "weekly", title: "Weekly", percentLeft: 80),
],
creditsRemaining: 123.4,
codeReviewRemainingPercent: 80,
tokenUsage: WidgetSnapshot.TokenUsageSummary(
sessionCostUSD: 12.3,
sessionTokens: 1200,
last30DaysCostUSD: 456.7,
last30DaysTokens: 9800,
currencyCode: "eur",
sessionLabel: "Latest billing day",
last30DaysLabel: "This month"),
dailyUsage: [
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-20", totalTokens: 1200, costUSD: 12.3),
])
let snapshot = WidgetSnapshot(
entries: [entry],
enabledProviders: [.codex, .claude],
usageBarsShowUsed: true,
generatedAt: Date())
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(snapshot)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(WidgetSnapshot.self, from: data)
#expect(decoded.entries.count == 1)
#expect(decoded.entries.first?.provider == .codex)
#expect(decoded.entries.first?.tokenUsage?.sessionTokens == 1200)
#expect(decoded.entries.first?.tokenUsage?.currencyCode == "EUR")
#expect(decoded.entries.first?.tokenUsage?.sessionLabel == "Latest billing day")
#expect(decoded.entries.first?.tokenUsage?.last30DaysLabel == "This month")
#expect(decoded.entries.first?.usageRows?.map(\.id) == ["session", "weekly"])
#expect(decoded.enabledProviders == [.codex, .claude])
#expect(decoded.usageBarsShowUsed)
}
@Test
func `widget snapshot round trip preserves kilo provider`() throws {
let entry = WidgetSnapshot.ProviderEntry(
provider: .kilo,
updatedAt: Date(),
primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "40/100 credits"),
secondary: nil,
tertiary: nil,
creditsRemaining: nil,
codeReviewRemainingPercent: nil,
tokenUsage: WidgetSnapshot.TokenUsageSummary(
sessionCostUSD: 1.25,
sessionTokens: 4200,
last30DaysCostUSD: 19.75,
last30DaysTokens: 58000),
dailyUsage: [
WidgetSnapshot.DailyUsagePoint(dayKey: "2026-02-27", totalTokens: 4200, costUSD: 1.25),
])
let snapshot = WidgetSnapshot(
entries: [entry],
enabledProviders: [.kilo, .codex],
generatedAt: Date())
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(snapshot)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(WidgetSnapshot.self, from: data)
#expect(decoded.entries.first?.provider == .kilo)
#expect(decoded.entries.first?.primary?.resetDescription == "40/100 credits")
#expect(decoded.enabledProviders == [.kilo, .codex])
}
@Test
func `widget snapshot round trip preserves kilo zero total edge state`() throws {
let now = Date()
let kiloSnapshot = KiloUsageSnapshot(
creditsUsed: 0,
creditsTotal: 0,
creditsRemaining: 0,
planName: "Kilo Pass Pro",
autoTopUpEnabled: true,
autoTopUpMethod: "visa",
updatedAt: now).toUsageSnapshot()
let entry = WidgetSnapshot.ProviderEntry(
provider: .kilo,
updatedAt: now,
primary: kiloSnapshot.primary,
secondary: kiloSnapshot.secondary,
tertiary: kiloSnapshot.tertiary,
creditsRemaining: nil,
codeReviewRemainingPercent: nil,
tokenUsage: nil,
dailyUsage: [])
let snapshot = WidgetSnapshot(
entries: [entry],
enabledProviders: [.kilo],
generatedAt: now)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(snapshot)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(WidgetSnapshot.self, from: data)
#expect(decoded.entries.first?.provider == .kilo)
#expect(decoded.entries.first?.primary?.usedPercent == 100)
#expect(decoded.entries.first?.primary?.remainingPercent == 0)
#expect(decoded.entries.first?.primary?.resetDescription == "0/0 credits")
#expect(decoded.enabledProviders == [.kilo])
}
@Test
func `widget snapshot decodes legacy payload without usage rows`() throws {
let json = """
{
"entries": [
{
"provider": "codex",
"updatedAt": "2026-04-04T06:30:00Z",
"primary": null,
"secondary": {
"usedPercent": 25,
"windowMinutes": 10080,
"resetsAt": null,
"resetDescription": null
},
"tertiary": null,
"creditsRemaining": null,
"codeReviewRemainingPercent": null,
"tokenUsage": null,
"dailyUsage": []
}
],
"enabledProviders": ["codex"],
"generatedAt": "2026-04-04T06:30:00Z"
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(WidgetSnapshot.self, from: Data(json.utf8))
#expect(decoded.entries.count == 1)
#expect(decoded.entries.first?.usageRows == nil)
#expect(decoded.entries.first?.secondary?.usedPercent == 25)
#expect(!decoded.usageBarsShowUsed)
}
@Test
func `widget snapshot decodes legacy token usage as usd`() throws {
let json = """
{
"entries": [
{
"provider": "codex",
"updatedAt": "2026-04-04T06:30:00Z",
"primary": null,
"secondary": null,
"tertiary": null,
"creditsRemaining": null,
"codeReviewRemainingPercent": null,
"tokenUsage": {
"sessionCostUSD": 1.25,
"sessionTokens": 1200,
"last30DaysCostUSD": 9.50,
"last30DaysTokens": 4200
},
"dailyUsage": []
}
],
"generatedAt": "2026-04-04T06:30:00Z"
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(WidgetSnapshot.self, from: Data(json.utf8))
#expect(decoded.entries.first?.tokenUsage?.currencyCode == "USD")
#expect(decoded.entries.first?.tokenUsage?.sessionLabel == "Today")
#expect(decoded.entries.first?.tokenUsage?.last30DaysLabel == "30d")
#expect(decoded.enabledProviders == [.codex])
}
@Test
func `token usage summary round trips updatedAt and tolerates legacy payloads`() throws {
let updatedAt = Date(timeIntervalSince1970: 1_760_000_000)
let summary = WidgetSnapshot.TokenUsageSummary(
sessionCostUSD: 1.5,
sessionTokens: 100,
last30DaysCostUSD: 30,
last30DaysTokens: 2000,
updatedAt: updatedAt)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(
WidgetSnapshot.TokenUsageSummary.self,
from: encoder.encode(summary))
#expect(decoded.updatedAt == updatedAt)
let legacy = try decoder.decode(
WidgetSnapshot.TokenUsageSummary.self,
from: Data(#"{"sessionCostUSD": 1.5, "sessionTokens": 100}"#.utf8))
#expect(legacy.updatedAt == nil)
}
@Test
func `token usage staleness discloses only meaningful lag`() {
let entryUpdatedAt = Date()
func summary(updatedAt: Date?) -> WidgetSnapshot.TokenUsageSummary {
WidgetSnapshot.TokenUsageSummary(
sessionCostUSD: nil,
sessionTokens: nil,
last30DaysCostUSD: nil,
last30DaysTokens: nil,
updatedAt: updatedAt)
}
#expect(!summary(updatedAt: entryUpdatedAt.addingTimeInterval(-5 * 60))
.isStale(comparedTo: entryUpdatedAt))
#expect(summary(updatedAt: entryUpdatedAt.addingTimeInterval(-61 * 60))
.isStale(comparedTo: entryUpdatedAt))
#expect(!summary(updatedAt: nil).isStale(comparedTo: entryUpdatedAt))
}
}