Files
hapi/ios/Packages/HapiKit/Tests/HapiClientTests/Settings/UsageMathTests.swift
T

152 lines
5.7 KiB
Swift

import Foundation
import HapiClient
import HapiProtocol
import Testing
/// Tile/format derivations — thresholds mirror the web reference
/// (`formatTokens` in `usage.tsx`, `formatFileSize` in `file-metadata.ts`).
/// Transcribed from the Android `UsageMathTest`.
@Suite("UsageMath")
struct UsageMathTests {
// MARK: - formatTokens
@Test func tokenFormattingMirrorsTheWebThresholds() {
#expect(UsageMath.formatTokens(0) == "0")
#expect(UsageMath.formatTokens(999) == "999")
#expect(UsageMath.formatTokens(1_000) == "1.0K")
#expect(UsageMath.formatTokens(1_234) == "1.2K")
#expect(UsageMath.formatTokens(9_999) == "10.0K")
#expect(UsageMath.formatTokens(10_000) == "10K")
#expect(UsageMath.formatTokens(999_999) == "1000K")
#expect(UsageMath.formatTokens(1_500_000) == "1.5M")
#expect(UsageMath.formatTokens(9_999_999) == "10.0M")
#expect(UsageMath.formatTokens(12_000_000) == "12M")
#expect(UsageMath.formatTokens(2_300_000_000) == "2.3B")
}
// MARK: - formatBytes
@Test func byteFormattingMirrorsTheWebFormatFileSize() {
#expect(UsageMath.formatBytes(0) == "0 B")
#expect(UsageMath.formatBytes(512) == "512 B")
#expect(UsageMath.formatBytes(1_024) == "1 KB")
#expect(UsageMath.formatBytes(1_536) == "1.5 KB")
// < 10 keeps one decimal, trailing .0 stripped; >= 10 rounds whole.
#expect(UsageMath.formatBytes(10_186) == "9.9 KB")
#expect(UsageMath.formatBytes(10_240) == "10 KB")
#expect(UsageMath.formatBytes(4_194_304) == "4 MB")
#expect(UsageMath.formatBytes(84_930_560) == "81 MB")
#expect(UsageMath.formatBytes(2_147_483_648) == "2 GB")
#expect(UsageMath.formatBytes(-5) == "0 B")
}
// MARK: - cacheHitRate
private func totals(input: Int, cacheRead: Int) -> UsageSummaryTotals {
UsageSummaryTotals(
inputTokens: input,
outputTokens: 0,
cacheReadTokens: cacheRead,
cacheCreationTokens: 0,
totalTokens: input,
uncachedTokens: input - cacheRead,
requests: 1,
sessions: 1
)
}
@Test func cacheHitRateIsCacheReadOverInputWithOneDecimal() {
#expect(UsageMath.cacheHitRate(totals(input: 12_400_000, cacheRead: 9_800_000)) == "79.0%")
#expect(UsageMath.cacheHitRate(totals(input: 5, cacheRead: 5)) == "100.0%")
#expect(UsageMath.cacheHitRate(totals(input: 3, cacheRead: 1)) == "33.3%")
}
@Test func cacheHitRateDegradesToZeroPercentWithoutInputTokens() {
#expect(UsageMath.cacheHitRate(totals(input: 0, cacheRead: 0)) == "0%")
}
// MARK: - dailyBars
private func bucket(_ key: String, total: Int) -> UsageSummaryBucket {
UsageSummaryBucket(
key: key,
inputTokens: total,
outputTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
totalTokens: total,
uncachedTokens: total,
requests: 1
)
}
@Test func boundedRangesFillTheCalendarWindowEndingToday() {
let sparse = [bucket("2026-08-13", total: 5), bucket("2026-08-18", total: 9)]
let bars = UsageMath.dailyBars(daily: sparse, days: 7, todayKey: "2026-08-18")
#expect(bars.count == 7)
#expect(bars.first?.key == "2026-08-12")
#expect(bars.last?.key == "2026-08-18")
#expect(bars.first?.totalTokens == 0)
#expect(bars.first?.bucket == nil)
#expect(bars[1].totalTokens == 5)
#expect(bars.last?.totalTokens == 9)
#expect(bars.last?.bucket == sparse[1])
}
@Test func rangeAllKeepsTheSparseBucketsAsIs() {
let sparse = [bucket("2025-01-01", total: 3), bucket("2026-08-18", total: 4)]
let bars = UsageMath.dailyBars(daily: sparse, days: nil, todayKey: "2026-08-18")
#expect(bars.map(\.key) == ["2025-01-01", "2026-08-18"])
#expect(bars.map(\.totalTokens) == [3, 4])
}
@Test func calendarFillSpansMonthBoundaries() {
let bars = UsageMath.dailyBars(daily: [], days: 30, todayKey: "2026-03-05")
#expect(bars.count == 30)
#expect(bars.first?.key == "2026-02-04")
#expect(bars.last?.key == "2026-03-05")
}
@Test func calendarFillHandlesLeapFebruary() {
let bars = UsageMath.dailyBars(daily: [], days: 7, todayKey: "2024-03-01")
#expect(bars.map(\.key) == [
"2024-02-24", "2024-02-25", "2024-02-26", "2024-02-27",
"2024-02-28", "2024-02-29", "2024-03-01",
])
}
@Test func calendarFillSpansYearBoundaries() {
let bars = UsageMath.dailyBars(daily: [], days: 7, todayKey: "2026-01-03")
#expect(bars.first?.key == "2025-12-28")
#expect(bars.last?.key == "2026-01-03")
}
@Test func corruptTodayKeyDegradesToTheSparseMapping() {
let sparse = [bucket("2026-08-18", total: 4)]
let bars = UsageMath.dailyBars(daily: sparse, days: 7, todayKey: "not-a-day")
#expect(bars.map(\.key) == ["2026-08-18"])
}
// MARK: - todayKey
@Test func todayKeyUsesTheGivenZonesCalendarDay() throws {
// 2026-08-17T20:00:00Z.
let now = Date(timeIntervalSince1970: 1_786_996_800)
let utc = try #require(TimeZone(identifier: "UTC"))
let shanghai = try #require(TimeZone(identifier: "Asia/Shanghai"))
#expect(UsageMath.todayKey(now: now, timeZone: utc) == "2026-08-17")
// UTC+8 has already rolled to the next day.
#expect(UsageMath.todayKey(now: now, timeZone: shanghai) == "2026-08-18")
}
// MARK: - shortDayLabel
@Test func shortDayLabelsDropTheYear() {
#expect(UsageMath.shortDayLabel("2026-08-18") == "08-18")
#expect(UsageMath.shortDayLabel("odd-key") == "odd-key")
}
}