112 lines
4.9 KiB
Swift
112 lines
4.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
@testable import NucleicCore
|
|
|
|
/// Locks the parse of Anthropic's `/api/oauth/usage` response (subscription quota).
|
|
/// The body below mirrors captures from before and after the July 2026 `limits[]` migration;
|
|
/// re-capture and diff if the indicator ever reads wrong after a CLI/API change.
|
|
struct SubscriptionUsageTests {
|
|
private static let sample = Data("""
|
|
{
|
|
"five_hour": { "utilization": 37.0, "resets_at": "2026-06-15T04:29:59.851919+00:00" },
|
|
"seven_day": { "utilization": 14.0, "resets_at": "2026-06-21T01:59:59.851939+00:00" },
|
|
"seven_day_oauth_apps": null,
|
|
"seven_day_opus": null,
|
|
"seven_day_sonnet": { "utilization": 0.0, "resets_at": null },
|
|
"limits": [
|
|
{ "kind": "session", "percent": 38, "resets_at": "2026-06-15T04:29:59.851919+00:00", "is_active": false },
|
|
{ "kind": "weekly_all", "percent": 15, "resets_at": "2026-06-21T01:59:59.851939+00:00", "is_active": false },
|
|
{ "kind": "weekly_scoped", "percent": 68, "resets_at": "2026-06-21T01:59:59.851939+00:00",
|
|
"scope": { "model": { "id": null, "display_name": "Fable" } }, "is_active": true },
|
|
{ "kind": "weekly_scoped", "percent": 12, "resets_at": null,
|
|
"scope": { "model": { "display_name": "Sonnet" } }, "is_active": false }
|
|
],
|
|
"extra_usage": { "is_enabled": false, "monthly_limit": null }
|
|
}
|
|
""".utf8)
|
|
|
|
@Test func parsesWindowsAndPeak() throws {
|
|
let usage = try SubscriptionUsageFetcher.parse(Self.sample)
|
|
|
|
// Generic limits[] is authoritative over legacy flat buckets.
|
|
#expect(usage.fiveHour?.utilization == 38.0)
|
|
#expect(usage.fiveHour?.resetsAt != nil)
|
|
#expect(usage.sevenDay?.utilization == 15.0)
|
|
#expect(usage.sevenDayFable?.utilization == 68.0)
|
|
// A null per-model window is absent, not zero.
|
|
#expect(usage.sevenDayOpus == nil)
|
|
// A present window with a null reset keeps the utilization but no date.
|
|
#expect(usage.sevenDaySonnet?.utilization == 12.0)
|
|
#expect(usage.sevenDaySonnet?.resetsAt == nil)
|
|
// The headline tracks the most-constrained window.
|
|
#expect(usage.peakUtilization == 68.0)
|
|
}
|
|
|
|
@Test func microsecondResetTimestampParses() throws {
|
|
let usage = try SubscriptionUsageFetcher.parse(Self.sample)
|
|
let resets = try #require(usage.fiveHour?.resetsAt)
|
|
// 2026-06-15T04:29:59.851919Z — verify it landed in the right minute.
|
|
let components = Calendar(identifier: .gregorian)
|
|
.dateComponents(in: TimeZone(identifier: "UTC")!, from: resets)
|
|
#expect(components.year == 2026)
|
|
#expect(components.month == 6)
|
|
#expect(components.day == 15)
|
|
#expect(components.hour == 4)
|
|
#expect(components.minute == 29)
|
|
}
|
|
|
|
@Test func legacyFlatWindowsRemainSupported() throws {
|
|
let usage = try SubscriptionUsageFetcher.parse(Data("""
|
|
{
|
|
"five_hour": { "utilization": 37.0, "resets_at": null },
|
|
"seven_day": { "utilization": 14.0, "resets_at": null },
|
|
"seven_day_opus": { "utilization": 9.0, "resets_at": null }
|
|
}
|
|
""".utf8))
|
|
|
|
#expect(usage.fiveHour?.utilization == 37)
|
|
#expect(usage.sevenDay?.utilization == 14)
|
|
#expect(usage.sevenDayFable == nil)
|
|
#expect(usage.sevenDayOpus?.utilization == 9)
|
|
}
|
|
|
|
@Test func malformedGenericEntriesAreIgnored() throws {
|
|
let usage = try SubscriptionUsageFetcher.parse(Data("""
|
|
{
|
|
"five_hour": { "utilization": 8.0, "resets_at": null },
|
|
"limits": [
|
|
{ "kind": "weekly_scoped", "percent": null,
|
|
"scope": { "model": { "display_name": "Fable" } } },
|
|
{ "kind": "weekly_scoped", "percent": 45, "scope": null }
|
|
]
|
|
}
|
|
""".utf8))
|
|
|
|
#expect(usage.fiveHour?.utilization == 8)
|
|
#expect(usage.sevenDayFable == nil)
|
|
}
|
|
|
|
@Test func lapsedWindowReadsAsEmptyUntilRefetched() {
|
|
let reset = Date(timeIntervalSince1970: 1_000_000)
|
|
let window = UsageWindow(utilization: 73.0, resetsAt: reset)
|
|
// Before the reset the cached snapshot is current.
|
|
#expect(window.utilization(at: reset.addingTimeInterval(-60)) == 73.0)
|
|
// At/after the reset the window has rolled over; the stale 73% reads as 0 until
|
|
// the next poll refreshes it, instead of sticking at the pre-reset value.
|
|
#expect(window.utilization(at: reset) == 0)
|
|
#expect(window.utilization(at: reset.addingTimeInterval(60)) == 0)
|
|
}
|
|
|
|
@Test func windowWithoutResetIsReportedAsIs() {
|
|
let window = UsageWindow(utilization: 42.0, resetsAt: nil)
|
|
#expect(window.utilization(at: Date(timeIntervalSince1970: 1_000_000)) == 42.0)
|
|
}
|
|
|
|
@Test func malformedBodyThrows() {
|
|
#expect(throws: (any Error).self) {
|
|
try SubscriptionUsageFetcher.parse(Data("not json".utf8))
|
|
}
|
|
}
|
|
}
|