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

487 lines
17 KiB
Swift

import CodexBarCore
import Foundation
import Testing
struct PoeUsageHistorySnapshotTests {
// MARK: - summary(days:)
@Test
func `summary over empty daily returns zeroed summary with nil cost`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [],
updatedAt: Date())
let summary = snapshot.summary(days: 7)
#expect(summary.points == 0)
#expect(summary.requests == 0)
#expect(summary.costUSD == nil)
}
@Test
func `summary over single day reports that day's points and requests`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [PoeUsageHistorySnapshot.DailyBucket(
day: "2026-05-31",
points: 250,
requests: 3,
costUSD: 0.05)],
updatedAt: Date())
let summary = snapshot.summary(days: 1)
#expect(summary.points == 250)
#expect(summary.requests == 3)
#expect(summary.costUSD == 0.05)
}
@Test
func `summary over seven days uses the last seven daily buckets`() {
let daily: [PoeUsageHistorySnapshot.DailyBucket] = (1...10).map { offset in
PoeUsageHistorySnapshot.DailyBucket(
day: String(format: "2026-05-%02d", offset),
points: Double(offset * 10),
requests: offset,
costUSD: nil)
}
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: daily,
updatedAt: Date())
// Last 7 buckets: offsets 4..10 → 40+50+60+70+80+90+100 = 490
let summary = snapshot.summary(days: 7)
#expect(summary.points == 490)
#expect(summary.requests == 4 + 5 + 6 + 7 + 8 + 9 + 10)
#expect(summary.costUSD == nil)
}
@Test
func `summary clamps zero and negative day counts up to one`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-29", points: 100, requests: 1, costUSD: nil),
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-30", points: 200, requests: 2, costUSD: nil),
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-31", points: 300, requests: 3, costUSD: nil),
],
updatedAt: Date())
#expect(snapshot.summary(days: 0).points == 300) // last bucket only
#expect(snapshot.summary(days: 0).requests == 3)
#expect(snapshot.summary(days: -5).points == 300) // clamped up to 1
}
@Test
func `summary ignores daily buckets beyond the requested window`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: (1...30).map { offset in
PoeUsageHistorySnapshot.DailyBucket(
day: String(format: "2026-04-%02d", offset),
points: 1,
requests: 1,
costUSD: nil)
},
updatedAt: Date())
let last30 = snapshot.summary(days: 30)
let last7 = snapshot.summary(days: 7)
#expect(last30.points == 30)
#expect(last7.points == 7)
}
@Test
func `summary reports nil cost when every daily bucket has nil cost`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: (1...3).map { offset in
PoeUsageHistorySnapshot.DailyBucket(
day: "2026-05-\(28 + offset)",
points: 50,
requests: 1,
costUSD: nil)
},
updatedAt: Date())
#expect(snapshot.summary(days: 7).costUSD == nil)
}
@Test
func `summary sums only the non-nil cost buckets and keeps the rest invisible`() throws {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-29", points: 100, requests: 1, costUSD: nil),
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-30", points: 200, requests: 1, costUSD: 0.10),
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-31", points: 300, requests: 1, costUSD: 0.20),
],
updatedAt: Date())
// Skips the nil bucket, sums 0.10 + 0.20 (allow IEEE-754 round-trip)
let cost = try #require(snapshot.summary(days: 7).costUSD)
#expect(abs(cost - 0.30) < 1e-9)
}
// MARK: - latestDay / last7Days / last30Days shortcuts
@Test
func `latest day, last 7 and last 30 days agree with summary by day count`() {
let daily: [PoeUsageHistorySnapshot.DailyBucket] = (1...40).map { offset in
PoeUsageHistorySnapshot.DailyBucket(
day: String(format: "2026-04-%02d", offset),
points: Double(offset),
requests: 1,
costUSD: nil)
}
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: daily,
updatedAt: Date())
#expect(snapshot.latestDay == snapshot.summary(days: 1))
#expect(snapshot.last7Days == snapshot.summary(days: 7))
#expect(snapshot.last30Days == snapshot.summary(days: 30))
}
@Test
func `current day does not reuse a stale latest bucket`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(identifier: "Europe/London"))
let now = try #require(ISO8601DateFormatter().date(from: "2026-06-23T12:00:00Z"))
let yesterday = try #require(ISO8601DateFormatter().date(from: "2026-06-22T12:00:00Z"))
let snapshot = PoeUsageHistorySnapshot(
entries: [
self.makeEntry(
id: "stale",
createdAt: yesterday,
model: "GPT-4o",
usageType: "chat",
points: 100,
costUSD: 0.10),
],
daily: [
PoeUsageHistorySnapshot.DailyBucket(
day: "2026-06-22",
points: 100,
requests: 1,
costUSD: 0.10),
],
updatedAt: now)
#expect(snapshot.latestDay.points == 100)
#expect(snapshot.currentDay(now: now, calendar: calendar).points == 0)
#expect(snapshot.currentDay(now: now, calendar: calendar).requests == 0)
#expect(snapshot.currentDay(now: now, calendar: calendar).costUSD == nil)
}
@Test
func `current day filters raw entries across a UTC bucket boundary`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(identifier: "America/Los_Angeles"))
let now = try #require(ISO8601DateFormatter().date(from: "2026-06-23T01:00:00Z"))
let localToday = try #require(ISO8601DateFormatter().date(from: "2026-06-22T20:00:00Z"))
let localYesterday = try #require(ISO8601DateFormatter().date(from: "2026-06-22T06:00:00Z"))
let snapshot = PoeUsageHistorySnapshot(
entries: [
self.makeEntry(
id: "today",
createdAt: localToday,
model: "GPT-4o",
usageType: "chat",
points: 80,
costUSD: 0.08),
self.makeEntry(
id: "yesterday",
createdAt: localYesterday,
model: "Claude",
usageType: "chat",
points: 20,
costUSD: 0.02),
],
daily: [
PoeUsageHistorySnapshot.DailyBucket(
day: "2026-06-22",
points: 100,
requests: 2,
costUSD: 0.10),
],
updatedAt: now)
let current = snapshot.currentDay(now: now, calendar: calendar)
#expect(current.points == 80)
#expect(current.requests == 1)
#expect(current.costUSD == 0.08)
}
// MARK: - topModels / topModel
@Test
func `top models is empty when entries is empty`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [],
updatedAt: Date())
#expect(snapshot.topModels.isEmpty)
#expect(snapshot.topModel == nil)
}
@Test
func `top models groups by model and sums points and requests`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 10, costUSD: 0.01),
self.makeEntry(id: "2", model: "GPT-4o", usageType: "chat", points: 5, costUSD: 0.01),
self.makeEntry(id: "3", model: "Claude-3.7", usageType: "chat", points: 20, costUSD: 0.02),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
let top = snapshot.topModels
#expect(top.count == 2)
#expect(top[0].name == "Claude-3.7")
#expect(top[0].points == 20)
#expect(top[0].requests == 1)
#expect(top[1].name == "GPT-4o")
#expect(top[1].points == 15)
#expect(top[1].requests == 2)
}
@Test
func `top models breaks ties by name ascending`() {
let entries = [
self.makeEntry(id: "1", model: "Z-Model", usageType: "chat", points: 10, costUSD: nil),
self.makeEntry(id: "2", model: "A-Model", usageType: "chat", points: 10, costUSD: nil),
self.makeEntry(id: "3", model: "M-Model", usageType: "chat", points: 10, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
let top = snapshot.topModels
#expect(top.map(\.name) == ["A-Model", "M-Model", "Z-Model"])
}
@Test
func `top models falls back to unknown for empty or whitespace model strings`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 5, costUSD: nil),
self.makeEntry(id: "2", model: "", usageType: "chat", points: 5, costUSD: nil),
self.makeEntry(id: "3", model: " ", usageType: "chat", points: 5, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
let top = snapshot.topModels
let names = top.map(\.name)
#expect(names.contains("unknown"))
#expect(names.contains("GPT-4o"))
}
@Test
func `top models omits cost when no entry reported cost`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 5, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
#expect(snapshot.topModels.first?.costUSD == nil)
}
@Test
func `top models sums cost across entries for the same model`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 5, costUSD: 0.01),
self.makeEntry(id: "2", model: "GPT-4o", usageType: "chat", points: 5, costUSD: 0.02),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
#expect(snapshot.topModels.first?.costUSD == 0.03)
}
// MARK: - topUsageTypes / topUsageType
@Test
func `top usage types groups by usage type independent of model`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 5, costUSD: nil),
self.makeEntry(id: "2", model: "Claude", usageType: "chat", points: 10, costUSD: nil),
self.makeEntry(id: "3", model: "GPT-4o", usageType: "api", points: 8, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
let top = snapshot.topUsageTypes
#expect(top.map(\.name) == ["chat", "api"])
#expect(top[0].points == 15)
#expect(top[0].requests == 2)
}
@Test
func `top usage type is the first entry in top usage types`() {
let entries = [
self.makeEntry(id: "1", model: "GPT-4o", usageType: "chat", points: 5, costUSD: nil),
self.makeEntry(id: "2", model: "GPT-4o", usageType: "api", points: 10, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: Date())
#expect(snapshot.topUsageType == "api")
#expect(snapshot.topUsageType == snapshot.topUsageTypes.first?.name)
}
@Test
func `top usage type is nil for empty entries`() {
let snapshot = PoeUsageHistorySnapshot(
entries: [],
daily: [],
updatedAt: Date())
#expect(snapshot.topUsageType == nil)
}
// MARK: - recentEntries(limit:)
@Test
func `recent entries returns up to the requested limit, newest first`() {
let now = Date(timeIntervalSince1970: 1_717_000_000)
let entries = (0..<5).map { offset in
self.makeEntry(
id: "\(offset)",
createdAt: now.addingTimeInterval(TimeInterval(offset * 60)),
model: "GPT-4o",
usageType: "chat",
points: 1,
costUSD: nil)
}
// entries are passed in order they came back; init should sort
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: now)
let recent = snapshot.recentEntries(limit: 3)
#expect(recent.count == 3)
// Newest three (offsets 4, 3, 2) should be first
#expect(recent[0].id == "4")
#expect(recent[1].id == "3")
#expect(recent[2].id == "2")
}
@Test
func `recent entries clamps non-positive limit up to one`() {
let now = Date(timeIntervalSince1970: 1_717_000_000)
let entries = (0..<3).map { offset in
self.makeEntry(
id: "\(offset)",
createdAt: now.addingTimeInterval(TimeInterval(offset * 60)),
model: "GPT-4o",
usageType: "chat",
points: 1,
costUSD: nil)
}
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: now)
#expect(snapshot.recentEntries(limit: 0).count == 1)
#expect(snapshot.recentEntries(limit: -3).count == 1)
#expect(snapshot.recentEntries(limit: 0).first?.id == "2")
}
@Test
func `recent entries returns everything when limit exceeds entries count`() {
let now = Date(timeIntervalSince1970: 1_717_000_000)
let entries = [
self.makeEntry(id: "1", createdAt: now, model: "A", usageType: "t", points: 1, costUSD: nil),
self.makeEntry(
id: "2",
createdAt: now.addingTimeInterval(60),
model: "A",
usageType: "t",
points: 1,
costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: [],
updatedAt: now)
let recent = snapshot.recentEntries(limit: 10)
#expect(recent.count == 2)
}
// MARK: - Init sorting invariants
@Test
func `init sorts entries ascending by created at and daily ascending by day string`() {
let now = Date(timeIntervalSince1970: 1_717_000_000)
let entries = [
self.makeEntry(
id: "newer",
createdAt: now.addingTimeInterval(120),
model: "A",
usageType: "t",
points: 1,
costUSD: nil),
self.makeEntry(id: "older", createdAt: now, model: "A", usageType: "t", points: 1, costUSD: nil),
]
let daily = [
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-31", points: 1, requests: 1, costUSD: nil),
PoeUsageHistorySnapshot.DailyBucket(day: "2026-05-30", points: 1, requests: 1, costUSD: nil),
]
let snapshot = PoeUsageHistorySnapshot(
entries: entries,
daily: daily,
updatedAt: now)
// Public init sorts entries ASC by createdAt, daily ASC by day
// (consumers wanting newest-first should use recentEntries(limit:))
#expect(snapshot.entries.first?.id == "older")
#expect(snapshot.entries.last?.id == "newer")
#expect(snapshot.daily.first?.day == "2026-05-30")
#expect(snapshot.daily.last?.day == "2026-05-31")
}
// MARK: - Helpers
private func makeEntry(
id: String,
createdAt: Date = Date(timeIntervalSince1970: 1_717_000_000),
model: String,
usageType: String,
points: Double,
costUSD: Double?) -> PoeUsageHistorySnapshot.Entry
{
PoeUsageHistorySnapshot.Entry(
id: id,
createdAt: createdAt,
model: model,
usageType: usageType,
points: points,
costUSD: costUSD)
}
}