Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Sources/Providers/GrokBotCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ struct GrokBotReading {
let limitCents: Double
}

var isExpired: Bool { expiresAt.map { $0 <= Date() } ?? false }
/// Past Grok Bot's own expiry, or past the reset it names: Grok Bot can run
/// for hours after a reset without rewriting the cache, and a reading of a
/// week that is over is not the present truth either.
var isExpired: Bool {
let now = Date()
return (expiresAt.map { $0 <= now } ?? false) || (resetsAt.map { $0 <= now } ?? false)
}
}

enum GrokBotCache {
Expand Down Expand Up @@ -77,9 +83,11 @@ enum GrokBotCache {
key.hasSuffix(weeklyUsageSuffix),
key.hasPrefix("sand.client.slice.account.")
else { return nil }
let slot = key.dropFirst("sand.client.slice.account.".count)
.dropLast(weeklyUsageSuffix.count)
return (String(slot), url)
let slot = String(key.dropFirst("sand.client.slice.account.".count)
.dropLast(weeklyUsageSuffix.count))
// The key spells the slot URL-encoded (`auth0%7Cuser_…`); the slot
// blob names it plainly (`auth0|user_…`).
return (slot.removingPercentEncoding ?? slot, url)
}
guard !caches.isEmpty else { throw UsageProviderError.needsAuth }

Expand Down
36 changes: 34 additions & 2 deletions Tests/GrokBotUsageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ final class GrokBotUsageTests: XCTestCase {
}

func testTheWeeklyWindowCarriesTheReset() throws {
let reading = try GrokBotCache.reading(from: Data(Self.cache().utf8), fallbackDate: .distantPast)
let reading = try GrokBotCache.reading(
from: Data(Self.cache(nextResetMs: Self.recordedResetMs).utf8), fallbackDate: .distantPast)
let windows = GrokBotUsage.windows(from: reading)
let weekly = try XCTUnwrap(windows.first { $0.id == "weekly" })
XCTAssertEqual(weekly.label, "Weekly usage")
Expand Down Expand Up @@ -88,6 +89,19 @@ final class GrokBotUsageTests: XCTestCase {
XCTAssertEqual(snapshot.usedFraction ?? -1, 0.85898948, accuracy: 0.000001)
}

/// Grok Bot only rewrites the cache when it fetches, and it can sit
/// running for hours past a reset without doing so. A reading whose reset
/// has passed describes a week that is over, so it reads stale even while
/// Grok Bot's own day-long expiry has not.
func testAReadingPastItsResetReadsStale() async throws {
let now = Int(Date().timeIntervalSince1970 * 1000)
try writeBlobs(cache: Self.cache(nextResetMs: now - 3_600_000,
readAtMs: now - 7_200_000))
let snapshot = try await GrokBotProvider(persistenceDir: directory).fetchSnapshot()
XCTAssertEqual(snapshot.status,
.stale(since: Date(timeIntervalSince1970: Double(now - 7_200_000) / 1000)))
}

/// Anything but `kind: "present"` with a usage in it is not a reading —
/// the fetch behind the cache answers null for pooled enterprise
/// allowances and for accounts whose percent never arrived.
Expand Down Expand Up @@ -153,6 +167,19 @@ final class GrokBotUsageTests: XCTestCase {
XCTAssertEqual(snapshot.usedFraction ?? -1, 0.10, accuracy: 0.0001)
}

/// Observed on a real Mac: the slot blob names the account as
/// `auth0|user_…`, while the cache key spells the same slot URL-encoded as
/// `auth0%7Cuser_…`. The two must still match.
func testTheActiveSlotMatchesItsURLEncodedCacheKey() async throws {
try writeBlobs(cache: Self.cache(percentUsed: 10), slot: "auth0%7Cuser_active")
try writeBlobs(cache: Self.cache(percentUsed: 90), slot: "auth0%7Cuser_other")
try selectSlot("auth0|user_active")
try touch(weeklyURL(slot: "auth0%7Cuser_other"), daysFromNow: 1)

let snapshot = try await GrokBotProvider(persistenceDir: directory).fetchSnapshot()
XCTAssertEqual(snapshot.usedFraction ?? -1, 0.10, accuracy: 0.0001)
}

/// Without a selected slot the freshest cache answers — a file Grok Bot
/// wrote moments ago over one from a version that kept no slot.
func testTheFreshestCacheWinsWithoutASelectedSlot() async throws {
Expand All @@ -179,14 +206,19 @@ final class GrokBotUsageTests: XCTestCase {

/// The recorded shape. `readAtMs`/`expiresAtMs` default to a reading taken
/// now, good for another day — the way a running Grok Bot leaves it.
/// The recorded reset (2026-10-13 UTC) is kept for the test that pins it;
/// every other reading resets three days out, so it stays current.
private static let recordedResetMs = 1791876398146

private static func cache(percentUsed: Double = 85.898948,
onDemand: String? = nil,
nextResetMs: Int? = nil,
readAtMs: Int? = nil, expiresAtMs: Int? = nil) -> String {
let now = Int(Date().timeIntervalSince1970 * 1000)
return """
{"schemaVersion":2,"value":{"kind":"present","selectedTeamId":null,\
"reading":{"usage":{"percentUsed":\(percentUsed),\
"nextResetMs":1791876398146,"isSandTrial":false,\
"nextResetMs":\(nextResetMs ?? (now + 3 * 86_400_000)),"isSandTrial":false,\
"hasNonZeroIncludedLimit":true,"isTeamSeat":false,\
"onDemand":\(onDemand ?? "null"),"grokPlanLabel":"SuperGrok Plus"},\
"readAtMs":\(readAtMs ?? now)},"expiresAtMs":\(expiresAtMs ?? (now + 86_400_000))}}
Expand Down
Loading