487 lines
17 KiB
Swift
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)
|
|
}
|
|
}
|