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
24 changes: 17 additions & 7 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ extension UsageMenuCardView.Model {
let primaryLabel = if input.provider == .cursor, snapshot.cursorRequests != nil {
"Requests"
} else if input.provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
GrokProviderDescriptor.primaryLabel(window: snapshot.primary, now: input.now) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .sub2api {
Expand Down Expand Up @@ -507,7 +507,7 @@ extension UsageMenuCardView.Model {
input: Input,
pace: UsagePace? = nil) -> PaceDetail?
{
guard self.supportsResetWindowPace(provider: input.provider, window: window),
guard self.supportsResetWindowPace(provider: input.provider, window: window, now: input.now),
window.remainingPercent > 0
else { return nil }
let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
Expand All @@ -525,18 +525,28 @@ extension UsageMenuCardView.Model {
showUsed: input.usageBarsShowUsed)
}

private static let weeklyWindowMinutes = 7 * 24 * 60
private static let monthlyWindowSentinelMinutes = 30 * 24 * 60

private static func supportsResetWindowPace(provider: UsageProvider, window: RateWindow) -> Bool {
private static func supportsResetWindowPace(provider: UsageProvider, window: RateWindow, now: Date) -> Bool {
switch provider {
case .copilot:
window.resetsAt != nil
return window.resetsAt != nil
case .cursor:
window.windowMinutes != nil
return window.windowMinutes != nil
case .grok:
guard GrokProviderDescriptor.primaryLabel(window: window, now: now) == "Weekly",
let resetsAt = window.resetsAt
else { return false }
let windowMinutes = window.windowMinutes ?? self.weeklyWindowMinutes
let timeUntilReset = resetsAt.timeIntervalSince(now)
return windowMinutes > 0
&& timeUntilReset > 0
&& timeUntilReset <= TimeInterval(windowMinutes) * 60
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
window.windowMinutes == self.monthlyWindowSentinelMinutes
return window.windowMinutes == self.monthlyWindowSentinelMinutes
default:
false
return false
}
}

Expand Down
127 changes: 127 additions & 0 deletions Tests/CodexBarTests/GrokMenuCardModelTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar

struct GrokMenuCardModelTests {
@Test
func `weekly CLI quota shows projection and pace marker`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(4 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Weekly")
#expect(metric.detailLeftText == "7% in deficit")
#expect(metric.detailRightText == "Runs out in 3d")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == false)
}

@Test
func `weekly web quota infers projection from reset date`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(4 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Weekly")
#expect(metric.detailLeftText == "7% in deficit")
#expect(metric.detailRightText == "Runs out in 3d")
#expect(metric.pacePercent != nil)
#expect(metric.paceOnTop == false)
}

@Test
func `weekly web quota beyond default duration does not show projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(8 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Weekly")
#expect(metric.detailLeftText == nil)
#expect(metric.detailRightText == nil)
#expect(metric.pacePercent == nil)
}

@Test
func `monthly quota does not show weekly projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: 30 * 24 * 60,
resetsAt: now.addingTimeInterval(20 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Monthly")
#expect(metric.detailLeftText == nil)
#expect(metric.detailRightText == nil)
#expect(metric.pacePercent == nil)
}

@Test
func `unclassified quota does not show weekly projection`() throws {
let now = Date(timeIntervalSince1970: 0)
let model = try Self.model(
now: now,
window: RateWindow(
usedPercent: 50,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(2 * 24 * 3600),
resetDescription: nil))

let metric = try #require(model.metrics.first { $0.id == "primary" })
#expect(metric.title == "Credits")
#expect(metric.detailLeftText == nil)
#expect(metric.detailRightText == nil)
#expect(metric.pacePercent == nil)
}

private static func model(now: Date, window: RateWindow) throws -> UsageMenuCardView.Model {
let metadata = try #require(ProviderDefaults.metadata[.grok])
let snapshot = UsageSnapshot(
primary: window,
secondary: nil,
tertiary: nil,
updatedAt: now,
identity: nil)
return UsageMenuCardView.Model.make(.init(
provider: .grok,
metadata: metadata,
snapshot: snapshot,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
}
}