142 lines
5.8 KiB
Swift
142 lines
5.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
/// Coverage for `RateWindow.isSyntheticPlaceholder` — the boundary marker that lets lane classifiers
|
|
/// distinguish Claude web's `five_hour: null` placeholder from a real zero-usage session. Verifies the
|
|
/// marker is set at the web boundary, survives Codable (with backward compatibility), and survives the
|
|
/// reset backfill that previously defeated a shape-only heuristic.
|
|
struct RateWindowSyntheticPlaceholderTests {
|
|
@Test
|
|
func `synthetic placeholder flag round-trips through Codable`() throws {
|
|
let window = RateWindow(
|
|
usedPercent: 0,
|
|
windowMinutes: 300,
|
|
resetsAt: nil,
|
|
resetDescription: nil,
|
|
isSyntheticPlaceholder: true)
|
|
|
|
let data = try JSONEncoder().encode(window)
|
|
let decoded = try JSONDecoder().decode(RateWindow.self, from: data)
|
|
|
|
#expect(decoded.isSyntheticPlaceholder == true)
|
|
#expect(decoded.usedPercent == 0)
|
|
#expect(decoded.windowMinutes == 300)
|
|
}
|
|
|
|
@Test
|
|
func `older payload without the flag decodes as not a placeholder`() throws {
|
|
// Cached payloads written before the flag existed have no `isSyntheticPlaceholder` key.
|
|
let json = #"{"usedPercent": 50, "windowMinutes": 300}"#
|
|
let decoded = try JSONDecoder().decode(RateWindow.self, from: Data(json.utf8))
|
|
|
|
#expect(decoded.isSyntheticPlaceholder == false)
|
|
#expect(decoded.usedPercent == 50)
|
|
#expect(decoded.windowMinutes == 300)
|
|
}
|
|
|
|
@Test
|
|
func `a real window omits the placeholder flag when encoded`() throws {
|
|
let window = RateWindow(usedPercent: 50, windowMinutes: 300, resetsAt: nil, resetDescription: nil)
|
|
|
|
let data = try JSONEncoder().encode(window)
|
|
let json = String(bytes: data, encoding: .utf8) ?? ""
|
|
|
|
// The flag is only persisted when true, so real windows keep their prior on-disk shape.
|
|
#expect(json.contains("isSyntheticPlaceholder") == false)
|
|
}
|
|
|
|
@Test
|
|
func `backfilling a reset preserves the synthetic placeholder flag`() {
|
|
// Regression: backfilling a still-future cached reset onto the placeholder (which has no reset)
|
|
// must NOT let it masquerade as a real session — the marker has to survive the backfill.
|
|
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
|
let cached = RateWindow(
|
|
usedPercent: 12,
|
|
windowMinutes: 300,
|
|
resetsAt: now.addingTimeInterval(3600),
|
|
resetDescription: "Resets in 1h")
|
|
let placeholder = RateWindow(
|
|
usedPercent: 0,
|
|
windowMinutes: 300,
|
|
resetsAt: nil,
|
|
resetDescription: nil,
|
|
isSyntheticPlaceholder: true)
|
|
|
|
let result = placeholder.backfillingResetTime(from: cached, now: now)
|
|
|
|
#expect(result.resetsAt == now.addingTimeInterval(3600))
|
|
#expect(result.isSyntheticPlaceholder == true)
|
|
}
|
|
|
|
@Test
|
|
func `web mapping flags the null five-hour session as a synthetic placeholder`() throws {
|
|
let json = """
|
|
{
|
|
"five_hour": null,
|
|
"seven_day": { "utilization": 42, "resets_at": "2025-12-29T23:00:00.000Z" }
|
|
}
|
|
"""
|
|
let webData = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(Data(json.utf8))
|
|
let primary = ClaudeUsageFetcher.webPrimaryWindow(from: webData)
|
|
|
|
#expect(primary.isSyntheticPlaceholder == true)
|
|
#expect(primary.usedPercent == 0)
|
|
#expect(primary.windowMinutes == 300)
|
|
#expect(primary.resetsAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `web mapping keeps a real five-hour session unflagged`() throws {
|
|
let json = """
|
|
{
|
|
"five_hour": { "utilization": 11, "resets_at": "2025-12-29T20:00:00.000Z" },
|
|
"seven_day": { "utilization": 42, "resets_at": "2025-12-29T23:00:00.000Z" }
|
|
}
|
|
"""
|
|
let webData = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(Data(json.utf8))
|
|
let primary = ClaudeUsageFetcher.webPrimaryWindow(from: webData)
|
|
|
|
#expect(primary.isSyntheticPlaceholder == false)
|
|
#expect(primary.usedPercent == 11)
|
|
}
|
|
|
|
@Test
|
|
func `web mapping keeps fractional session and weekly utilization`() throws {
|
|
let json = """
|
|
{
|
|
"five_hour": { "utilization": 45.5, "resets_at": "2025-12-29T20:00:00.000Z" },
|
|
"seven_day": { "utilization": 12.25, "resets_at": "2025-12-29T23:00:00.000Z" }
|
|
}
|
|
"""
|
|
let webData = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(Data(json.utf8))
|
|
#expect(webData.sessionPercentUsed == 45.5)
|
|
#expect(webData.weeklyPercentUsed == 12.25)
|
|
#expect(webData.hasLiveSessionWindow == true)
|
|
|
|
let primary = ClaudeUsageFetcher.webPrimaryWindow(from: webData)
|
|
#expect(primary.isSyntheticPlaceholder == false)
|
|
#expect(primary.usedPercent == 45.5)
|
|
#expect(primary.remainingPercent == 54.5)
|
|
}
|
|
|
|
@Test
|
|
func `web mapping keeps a real zero-usage session that omits a reset`() throws {
|
|
// A reported `five_hour` object at 0% with no `resets_at` is a real idle session, not the
|
|
// `five_hour: null` placeholder. The flag keys off object presence (not percent/reset), so this
|
|
// must stay unflagged — otherwise the combined metric would hide a genuine empty session.
|
|
let json = """
|
|
{
|
|
"five_hour": { "utilization": 0 },
|
|
"seven_day": { "utilization": 42, "resets_at": "2025-12-29T23:00:00.000Z" }
|
|
}
|
|
"""
|
|
let webData = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(Data(json.utf8))
|
|
let primary = ClaudeUsageFetcher.webPrimaryWindow(from: webData)
|
|
|
|
#expect(primary.isSyntheticPlaceholder == false)
|
|
#expect(primary.usedPercent == 0)
|
|
#expect(primary.resetsAt == nil)
|
|
}
|
|
}
|