diff --git a/Sources/CodexBar/IconRemainingResolver.swift b/Sources/CodexBar/IconRemainingResolver.swift index 49fd27b773..6e913b3571 100644 --- a/Sources/CodexBar/IconRemainingResolver.swift +++ b/Sources/CodexBar/IconRemainingResolver.swift @@ -3,10 +3,6 @@ import Foundation enum IconRemainingResolver { private static let visibleZeroPercent = 0.0001 - private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-" - // Antigravity quota summaries expose exact 5-hour session and weekly buckets for the compact icon. - private static let sessionWindowMinutes = 5 * 60 - private static let weeklyWindowMinutes = 7 * 24 * 60 private static func codexProjection(snapshot: UsageSnapshot, now: Date) -> CodexConsumerProjection { CodexConsumerProjection.make( @@ -28,43 +24,6 @@ enum IconRemainingResolver { return projection.visibleRateLanes.compactMap { projection.menuBarSelectableRateWindow(for: $0) } } - private static func antigravityQuotaSummaryWindows( - snapshot: UsageSnapshot) - -> (primary: RateWindow?, secondary: RateWindow?)? - { - let quotaSummaryWindows = snapshot.extraRateWindows? - .filter { - $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) - } ?? [] - guard !quotaSummaryWindows.isEmpty else { return nil } - - return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown)) - } - - private static func antigravityQuotaSummaryPair( - in windows: [NamedRateWindow]) - -> (primary: RateWindow?, secondary: RateWindow?)? - { - let session = self.mostConstrainedWindow(in: windows, windowMinutes: Self.sessionWindowMinutes) - let weekly = self.mostConstrainedWindow(in: windows, windowMinutes: Self.weeklyWindowMinutes) - guard session != nil || weekly != nil else { return nil } - return (primary: session, secondary: weekly) - } - - /// Returns the highest-usage window for an exact Antigravity compact-icon cadence. - private static func mostConstrainedWindow(in windows: [NamedRateWindow], windowMinutes: Int) -> RateWindow? { - windows - .filter { $0.window.windowMinutes == windowMinutes } - .max { lhs, rhs in - if lhs.window.usedPercent != rhs.window.usedPercent { - return lhs.window.usedPercent < rhs.window.usedPercent - } - // max(by:) keeps the right-hand element when this returns true; use `>` so the smallest id wins ties. - return lhs.id > rhs.id - }? - .window - } - static func resolvedWindows( snapshot: UsageSnapshot, style: IconStyle, @@ -79,9 +38,9 @@ enum IconRemainingResolver { secondary: windows.dropFirst().first) } if style == .antigravity { - // Only current quota-summary buckets define the fixed session/weekly icon lanes. - return self.antigravityQuotaSummaryWindows(snapshot: snapshot) - ?? (primary: nil, secondary: nil) + return ( + primary: snapshot.primary?.windowMinutes != nil ? snapshot.primary : nil, + secondary: snapshot.secondary?.windowMinutes != nil ? snapshot.secondary : nil) } if style == .codex { let windows = self.codexVisibleWindows(snapshot: snapshot, now: now) diff --git a/Sources/CodexBar/MenuBarMetricWindowResolver.swift b/Sources/CodexBar/MenuBarMetricWindowResolver.swift index cab021cf87..1c4e571f98 100644 --- a/Sources/CodexBar/MenuBarMetricWindowResolver.swift +++ b/Sources/CodexBar/MenuBarMetricWindowResolver.swift @@ -117,21 +117,6 @@ enum MenuBarMetricWindowResolver { now: Date) -> RateWindow? { - if provider == .antigravity { - if antigravityPrioritizeExhaustedQuotas, - let window = antigravityQuotaSummaryRankingWindow(snapshot: snapshot, now: now) - { - return window - } - if let window = mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) { - return window - } - return self.mostConstrainedWindow( - primary: snapshot.primary, - secondary: snapshot.secondary, - tertiary: snapshot.tertiary) - ?? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot) - } if provider == .perplexity { return snapshot.automaticPerplexityWindow() } @@ -141,10 +126,11 @@ enum MenuBarMetricWindowResolver { secondary: snapshot.tertiary, tertiary: nil) ?? snapshot.secondary } - if provider == .factory || provider == .kimi { - return snapshot.secondary ?? snapshot.primary - } - if provider == .litellm { + if provider == .factory || provider == .kimi || provider == .litellm { + let windows = [snapshot.primary, snapshot.secondary].compactMap(\.self) + if let exhausted = windows.first(where: { $0.usedPercent >= 100 }) { + return exhausted + } return snapshot.secondary ?? snapshot.primary } if provider == .copilot, @@ -154,150 +140,29 @@ enum MenuBarMetricWindowResolver { return primary.usedPercent >= secondary.usedPercent ? primary : secondary } if provider == .cursor { - return Self.mostConstrainedCursorWindow( + return self.mostConstrainedCursorWindow( total: snapshot.primary, auto: snapshot.secondary, api: snapshot.tertiary) } if provider == .minimax { - return Self.mostConstrainedWindow( + return self.mostConstrainedWindow( primary: snapshot.primary, secondary: snapshot.secondary, tertiary: snapshot.tertiary) } - if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) { + if provider == .claude, let spendLimit = claudeSpendLimitWindow(snapshot: snapshot) { return spendLimit } - return snapshot.primary ?? snapshot.secondary - } - - private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-" - private static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-" - - private static func mostConstrainedAntigravityQuotaSummaryWindow(snapshot: UsageSnapshot) -> RateWindow? { - let windows = snapshot.extraRateWindows? - .filter { $0.usageKnown && $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) } - .map(\.window) ?? [] - guard !windows.isEmpty else { return nil } - - let usableWindows = windows.filter { $0.usedPercent < 100 } - if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) { - return maxUsable - } - return windows.max(by: { $0.usedPercent < $1.usedPercent }) - } - - /// Picks the binding supported quota-summary lane for the exhausted-first opt-in. - static func antigravityQuotaSummaryRankingWindow( - snapshot: UsageSnapshot, - now: Date) - -> RateWindow? - { - let candidates = Self.antigravityQuotaSummaryRows(snapshot: snapshot) - .filter { - $0.usageKnown && - $0.window.usedPercent.isFinite && - Self.isSupportedAntigravityQuotaCadence($0.window.windowMinutes) - } - return candidates.max { lhs, rhs in - if lhs.window.usedPercent != rhs.window.usedPercent { - return lhs.window.usedPercent < rhs.window.usedPercent - } - - let lhsFutureReset = lhs.window.resetsAt.flatMap { $0 > now ? $0 : nil } - let rhsFutureReset = rhs.window.resetsAt.flatMap { $0 > now ? $0 : nil } - if (lhsFutureReset != nil) != (rhsFutureReset != nil) { - return lhsFutureReset == nil - } - if let lhsFutureReset, let rhsFutureReset, lhsFutureReset != rhsFutureReset { - return lhsFutureReset > rhsFutureReset - } - return lhs.id < rhs.id - }?.window - } - - /// True only when every fully understood quota family has an exhausted binding lane. - /// Any incomplete or unfamiliar summary row fails open so automatic provider rotation - /// does not hide quota that CodexBar cannot classify safely. - static func antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: UsageSnapshot) -> Bool { - let rows = Self.antigravityQuotaSummaryRows(snapshot: snapshot) - guard !rows.isEmpty else { return false } - - var familyBlocked: [String: Bool] = [:] - for row in rows { - guard row.usageKnown, - row.window.usedPercent.isFinite, - Self.isSupportedAntigravityQuotaCadence(row.window.windowMinutes), - let family = Self.antigravityQuotaFamily(for: row) - else { - return false - } - familyBlocked[family, default: false] = - familyBlocked[family, default: false] || row.window.usedPercent >= 100 - } - return !familyBlocked.isEmpty && familyBlocked.values.allSatisfy(\.self) - } - - private static let antigravitySupportedQuotaCadences: Set = [300, 10080] - - private static func isSupportedAntigravityQuotaCadence(_ windowMinutes: Int?) -> Bool { - guard let windowMinutes else { return false } - return Self.antigravitySupportedQuotaCadences.contains(windowMinutes) - } - - private static func antigravityQuotaSummaryRows(snapshot: UsageSnapshot) -> [NamedRateWindow] { - snapshot.extraRateWindows?.filter { - $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) - } ?? [] - } - - private static func antigravityQuotaFamily(for row: NamedRateWindow) -> String? { - let suffix = row.id.dropFirst(Self.antigravityQuotaSummaryWindowIDPrefix.count) - var normalizedSuffix = suffix - .lowercased() - .replacingOccurrences(of: "_", with: "-") - if normalizedSuffix.hasSuffix(" limit") { - normalizedSuffix.removeLast(" limit".count) - } - let cadenceSuffixes: [String] - switch row.window.windowMinutes { - case 300: - cadenceSuffixes = ["-session", "-5h", "-5-hour", "-five hour", "-five-hour"] - case 10080: - cadenceSuffixes = ["-weekly"] - default: - return nil - } - guard let cadenceSuffix = cadenceSuffixes.first(where: normalizedSuffix.hasSuffix) else { - return nil + // Default path: + // 1. Prioritize any exhausted window (usedPercent >= 100) + let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self) + if let exhausted = windows.filter({ $0.usedPercent >= 100 }).max(by: { $0.usedPercent < $1.usedPercent }) { + return exhausted } - let family = normalizedSuffix - .dropLast(cadenceSuffix.count) - .trimmingCharacters(in: .whitespacesAndNewlines) - guard !family.isEmpty, - family.first != "-", - family.last != "-", - family.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" || $0 == "." }) - else { - return nil - } - return family - } - - private static func mostConstrainedAntigravityLegacyExtraWindow(snapshot: UsageSnapshot) -> RateWindow? { - let windows = snapshot.extraRateWindows? - .filter { - $0.usageKnown && $0.id.hasPrefix(Self.antigravityCompactFallbackWindowIDPrefix) - } - .map(\.window) ?? [] - guard !windows.isEmpty else { return nil } - - let usableWindows = windows.filter { $0.usedPercent < 100 } - if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) { - return maxUsable - } - return windows.max(by: { $0.usedPercent < $1.usedPercent }) + // 2. Otherwise, fall back to the default order + return snapshot.primary ?? snapshot.secondary } private static func requestedWindow( @@ -306,9 +171,6 @@ enum MenuBarMetricWindowResolver { lanes: [Lane]) -> RateWindow? { self.window(in: snapshot, following: lanes) - ?? (provider == .antigravity - ? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot) - : nil) } private static func window(in snapshot: UsageSnapshot, following lanes: [Lane]) -> RateWindow? { diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 8e32ed8ebb..392c35df1a 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -387,7 +387,7 @@ extension UsageMenuCardView.Model { private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String { let formatter = DateFormatter() - formatter.locale = Locale.current + formatter.locale = codexBarLocalizedLocale() formatter.timeZone = self.subscriptionDateTimeZone(provider: provider) formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy") return formatter.string(from: date) diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index 32823b6d22..11001bcc98 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -39,8 +39,9 @@ enum UsagePaceText { static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail { let displayedEstimate = Self.boundedFullWindowCount(forecast.estimatedWindowsToExhaustWeekly) - let numberText = String.localizedStringWithFormat( - L("≈%d full 5h windows of weekly left · %d windows until reset"), + let numberText = String( + format: L("≈%d full 5h windows of weekly left · %d windows until reset"), + locale: codexBarLocalizedLocale(), displayedEstimate, forecast.windowsUntilReset) let verdictText: String @@ -49,8 +50,9 @@ enum UsagePaceText { } else { let windowsEarly = Self.boundedWindowCount( forecast.availableWindowsUntilReset - forecast.estimatedWindowsToExhaustWeekly) - verdictText = String.localizedStringWithFormat( - L("Weekly can run out ≈%d windows early"), + verdictText = String( + format: L("Weekly can run out ≈%d windows early"), + locale: codexBarLocalizedLocale(), max(1, windowsEarly)) } return SessionEquivalentDetail( diff --git a/Sources/CodexBar/UsageStore+HighestUsage.swift b/Sources/CodexBar/UsageStore+HighestUsage.swift index e1f91866f9..aee0c9e592 100644 --- a/Sources/CodexBar/UsageStore+HighestUsage.swift +++ b/Sources/CodexBar/UsageStore+HighestUsage.swift @@ -43,12 +43,6 @@ extension UsageStore { now: Date) -> RateWindow? { let effectivePreference = self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot) - if provider == .antigravity, - effectivePreference == .automatic, - !self.settings.antigravityPrioritizeExhaustedQuotas - { - return Self.mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) - } if provider == .codex { return self.codexMenuBarMetricWindow(snapshot: snapshot, now: now) } @@ -95,14 +89,6 @@ extension UsageStore { guard !percents.isEmpty else { return true } return percents.allSatisfy { $0 >= 100 } } - if provider == .antigravity, effectivePreference == .automatic { - if self.settings.antigravityPrioritizeExhaustedQuotas { - return MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot) - } - let windows = Self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot) - guard !windows.isEmpty else { return true } - return windows.allSatisfy { $0.usedPercent >= 100 } - } if provider == .copilot, effectivePreference == .automatic, let primary = snapshot.primary, @@ -122,29 +108,16 @@ extension UsageStore { guard !percents.isEmpty else { return true } return percents.allSatisfy { $0 >= 100 } } - - return true - } - - private nonisolated static func mostConstrainedAntigravityQuotaSummaryWindow( - snapshot: UsageSnapshot) - -> RateWindow? - { - let windows = self.antigravityRenderedQuotaSummaryWindows(snapshot: snapshot) - guard !windows.isEmpty else { return nil } - - let usableWindows = windows.filter { $0.usedPercent < 100 } - if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) { - return maxUsable + if provider == .antigravity { + // Antigravity has Gemini 5h (primary) and weekly (secondary). Keep it eligible + // when only one lane is exhausted so the user still sees the usable quota, + // regardless of whether the metric preference is automatic or explicit. + let percents = [snapshot.primary?.usedPercent, snapshot.secondary?.usedPercent] + .compactMap(\.self) + guard !percents.isEmpty else { return true } + return percents.allSatisfy { $0 >= 100 } } - return windows.max(by: { $0.usedPercent < $1.usedPercent }) - } - private nonisolated static func antigravityRenderedQuotaSummaryWindows( - snapshot: UsageSnapshot) - -> [RateWindow] - { - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - return [windows.primary, windows.secondary].compactMap(\.self) + return true } } diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift index db210b6069..ab44f56d56 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift @@ -9,8 +9,8 @@ public enum AntigravityProviderDescriptor { metadata: ProviderMetadata( id: .antigravity, displayName: "Antigravity", - sessionLabel: "Gemini Models", - weeklyLabel: "Claude and GPT", + sessionLabel: "Gemini 5-hour", + weeklyLabel: "Gemini weekly", opusLabel: nil, supportsOpus: false, supportsCredits: false, diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift index dbe044f6fd..157046c1fd 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift @@ -155,14 +155,13 @@ public struct AntigravityStatusSnapshot: Sendable { } let primary = primaryQuota.map(Self.rateWindow(for:)) - let secondary = secondaryQuota.map(Self.rateWindow(for:)) + let secondary: RateWindow? = nil let extraWindows = Self.extraRateWindows( from: normalized, summaryCandidates: summaryCandidates, compactFallbackModelID: fallbackQuota?.modelId, representedPools: Set([ primaryQuota.map { _ in AntigravityUsagePool.gemini }, - secondaryQuota.map { _ in AntigravityUsagePool.claudeGPT }, ].compactMap(\.self))) let identity = ProviderIdentitySnapshot( @@ -208,11 +207,15 @@ public struct AntigravityStatusSnapshot: Sendable { } let primary = Self.quotaSummaryRepresentative( - matching: { $0.lowercased().contains("gemini") }, - in: namedWindows) + matching: { name in + name.lowercased().contains("gemini") + }, + in: namedWindows.filter { $0.window.windowMinutes == 5 * 60 }) let secondary = Self.quotaSummaryRepresentative( - matching: { $0.lowercased().contains("claude") || $0.lowercased().contains("gpt") }, - in: namedWindows) + matching: { name in + name.lowercased().contains("gemini") + }, + in: namedWindows.filter { $0.window.windowMinutes == 7 * 24 * 60 }) let identity = ProviderIdentitySnapshot( providerID: .antigravity, @@ -640,7 +643,9 @@ public struct AntigravityStatusSnapshot: Sendable { let distinctWindows = models .filter { - $0.quota.modelId == compactFallbackModelID || Self.shouldShowDistinctExtraWindow($0) + $0.quota.modelId == compactFallbackModelID || Self.shouldShowDistinctExtraWindow( + $0, + representedPools: representedPools) } .sorted(by: Self.modelOrderPrecedes) .map { m in @@ -667,8 +672,13 @@ public struct AntigravityStatusSnapshot: Sendable { "antigravity-compact-fallback-\(modelID)" } - private static func shouldShowDistinctExtraWindow(_ model: AntigravityNormalizedModel) -> Bool { - guard !self.isSummaryCandidate(model) else { return false } + private static func shouldShowDistinctExtraWindow( + _ model: AntigravityNormalizedModel, + representedPools: Set) -> Bool + { + if let pool = usagePool(for: model), representedPools.contains(pool) { + guard !self.isSummaryCandidate(model) else { return false } + } if model.quota.remainingFraction == nil { return model.quota.resetTime != nil || model.quota.resetDescription != nil } diff --git a/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift b/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift index f29e408beb..9fc1d33921 100644 --- a/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift +++ b/Tests/CodexBarTests/AntigravityQuotaSummaryTests.swift @@ -54,8 +54,8 @@ struct AntigravityQuotaSummaryTests { ] #expect(windows.map(\.window.resetsAt) == expectedDates) - #expect(usage.primary?.remainingPercent.rounded() == 82) - #expect(usage.secondary?.remainingPercent.rounded() == 64) + #expect(usage.primary?.remainingPercent.rounded() == 91) + #expect(usage.secondary?.remainingPercent.rounded() == 82) #expect(usage.tertiary == nil) } diff --git a/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift b/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift index d6976c20bf..31a09309e0 100644 --- a/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift +++ b/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift @@ -333,8 +333,12 @@ struct AntigravityRemoteUsageFetcherTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 20) - #expect(usage.secondary?.remainingPercent.rounded() == 50) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 50) } @Test @@ -441,8 +445,9 @@ struct AntigravityRemoteUsageFetcherTests { #expect(quotaCalls.get() == 1) #expect(usage.primary?.remainingPercent == 60.0) - #expect(usage.secondary?.remainingPercent == 100.0) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) } @Test @@ -659,8 +664,9 @@ struct AntigravityRemoteUsageFetcherTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent == 100.0) - #expect(usage.secondary?.remainingPercent == 100.0) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) } @Test diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift new file mode 100644 index 0000000000..2c4e38dcf2 --- /dev/null +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests+Additional.swift @@ -0,0 +1,378 @@ +import Foundation +import Testing +@testable import CodexBarCore + +extension AntigravityStatusProbeTests { + @Test + func `remote source collapses recognized family models and hides unconsumed junk`() throws { + // Fixture B: verified 13 remote models; recognized text models collapse into Gemini, + // and unconsumed junk stays hidden. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + // junk: image + AntigravityModelQuota( + label: "Gemini 2.5 Flash Image", + modelId: "gemini-2-5-flash-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: tab autocomplete + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 2.5 Pro", + modelId: "gemini-2-5-pro", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "gemini-3-pro-high", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: lite + AntigravityModelQuota( + label: "Gemini 2.5 Flash Lite", + modelId: "gemini-2-5-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: image + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: lite + AntigravityModelQuota( + label: "Gemini 3.1 Flash Lite", + modelId: "gemini-3-1-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3.1 Pro (Low)", + modelId: "gemini-3-1-pro-low", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3.1 Pro (High)", + modelId: "gemini-3-1-pro-high", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // junk: tab autocomplete + AntigravityModelQuota( + label: "Tab Jump Flash Lite Vertex", + modelId: "tab_jump_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 3 Pro (Low)", + modelId: "gemini-3-pro-low", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + // survivor + AntigravityModelQuota( + label: "Gemini 2.5 Flash", + modelId: "gemini-2-5-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 100) + #expect(usage.secondary == nil) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `remote source shows consumed junk models despite filter`() throws { + // Fixture C: junk models with remainingFraction < 0.999 must be shown + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + // consumed tab - should be shown + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + // consumed image - should be shown + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 0.4, + resetTime: nil, + resetDescription: nil), + // unconsumed sibling tab (0.9995 >= 0.999) - should be hidden + AntigravityModelQuota( + label: "Tab Jump Flash Lite Vertex", + modelId: "tab_jump_flash_lite_vertex", + remainingFraction: 0.9995, + resetTime: nil, + resetDescription: nil), + // a clean survivor for non-empty guard + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + let extraWindows = try #require(usage.extraRateWindows) + let ids = extraWindows.map(\.id) + + // Consumed junk models shown despite being junk type + #expect(ids.contains("tab_flash_lite_vertex")) + #expect(ids.contains("gemini-3-pro-image")) + + // Unconsumed sibling stays hidden + #expect(!ids.contains("tab_jump_flash_lite_vertex")) + } + + @Test + func `remote source image models do not drive family summary bars`() throws { + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 0.2, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "gemini-3-pro-high", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash Image", + modelId: "gemini-3-flash-image", + remainingFraction: 0.1, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Flash", + modelId: "gemini-3-flash", + remainingFraction: 0.8, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + + #expect(usage.primary?.usedPercent == 20) + #expect(usage.secondary == nil) + #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-pro-image") == true) + #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-flash-image") == true) + } + + @Test + func `remote source yields nil extra windows when all models are unconsumed junk`() throws { + // Fixture D: all-junk-unconsumed -> extraRateWindows nil + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Tab Flash Lite Vertex", + modelId: "tab_flash_lite_vertex", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 2.5 Flash Lite", + modelId: "gemini-2-5-flash-lite", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro Image", + modelId: "gemini-3-pro-image", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Unknown Model X", + modelId: "unknown-model-x", + remainingFraction: 1.0, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary == nil) + #expect(usage.secondary == nil) + #expect(usage.tertiary == nil) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `ordering edge cases collapse to most constrained usage pool`() throws { + // Fixture F: local source; known Gemini Pro rows collapse into the Gemini pool + // using the most constrained remaining fraction. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Gemini 3 Pro (Low)", + modelId: "MODEL_PLACEHOLDER_M70", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini 3 Pro (High)", + modelId: "MODEL_PLACEHOLDER_M71", + remainingFraction: 0.8, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Gemini Pro Experimental", + modelId: "MODEL_PLACEHOLDER_M72", + remainingFraction: 0.3, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Claude Sonnet 4", + modelId: "MODEL_PLACEHOLDER_M73", + remainingFraction: 0.9, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .local) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M73" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 90) + } + + @Test + func `nil version unknown family models sort deterministically by label`() throws { + // Strict-weak-ordering guard: two .unknown models with unparseable versions + // should sort by label without trapping + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "Zebra Unknown Model", + modelId: "MODEL_PLACEHOLDER_MA", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "Alpha Unknown Model", + modelId: "MODEL_PLACEHOLDER_MB", + remainingFraction: 0.5, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .local) + + let usage = try snapshot.toUsageSnapshot() + let extraWindows = try #require(usage.extraRateWindows) + let titles = extraWindows.map(\.title) + + // Deterministic: label tiebreaker -> Alpha before Zebra + #expect(titles == ["Alpha Unknown Model", "Zebra Unknown Model"]) + } + + @Test + func `hyphenated raw model ids without display name still map to gemini group`() throws { + // When the remote catalog omits displayName/label, the raw hyphenated model id + // becomes the label and still participates in the Gemini group. + let snapshot = AntigravityStatusSnapshot( + modelQuotas: [ + AntigravityModelQuota( + label: "gemini-3-pro-preview", + modelId: "gemini-3-pro-preview", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + AntigravityModelQuota( + label: "gemini-2.5-pro", + modelId: "gemini-2.5-pro", + remainingFraction: 1, + resetTime: nil, + resetDescription: nil), + ], + accountEmail: nil, + accountPlan: nil, + source: .remote) + + let usage = try snapshot.toUsageSnapshot() + #expect(usage.primary?.remainingPercent.rounded() == 100) + #expect(usage.extraRateWindows == nil) + } + + @Test + func `http probe errors still count as reachable`() { + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 403: Forbidden"))) + #expect( + AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("HTTP 404: Not Found"))) + #expect( + !AntigravityStatusProbe.isReachableProbeError( + AntigravityStatusProbeError.apiError("Invalid response"))) + #expect(!AntigravityStatusProbe.isReachableProbeError(AntigravityStatusProbeError.notRunning)) + } + + @Test + func `fallback probe port prefers non extension candidate`() { + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: 61775) == 51170) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [61775], + extensionPort: 61775) == 61775) + #expect( + AntigravityStatusProbe.fallbackProbePort( + ports: [51170, 61775], + extensionPort: nil) == 51170) + } +} diff --git a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift index 5182cef097..1f6373ab79 100644 --- a/Tests/CodexBarTests/AntigravityStatusProbeTests.swift +++ b/Tests/CodexBarTests/AntigravityStatusProbeTests.swift @@ -760,8 +760,13 @@ extension AntigravityStatusProbeTests { return } #expect(primary.remainingPercent.rounded() == 20) - #expect(usage.secondary?.remainingPercent.rounded() == 50) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-3-5-sonnet" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 50) + #expect(claudeWindow.title == "Claude 3.5 Sonnet") } @Test @@ -850,7 +855,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let sonnetWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(sonnetWindow.window.remainingPercent.rounded() == 30) } @Test @@ -875,7 +884,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 40) - #expect(usage.secondary?.remainingPercent.rounded() == 70) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let thinkingWindow = try #require(extra.first(where: { $0.id == "claude-thinking" })) + #expect(thinkingWindow.window.remainingPercent.rounded() == 70) } @Test @@ -900,7 +913,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1001,7 +1018,11 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.tertiary == nil) #expect(usage.primary == nil) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "claude-sonnet-4" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1032,8 +1053,12 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 40) - #expect(usage.secondary?.remainingPercent.rounded() == 30) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) + + let extra = try #require(usage.extraRateWindows) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M35" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 30) } @Test @@ -1083,9 +1108,10 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 100) - #expect(usage.secondary?.remainingPercent.rounded() == 100) + #expect(usage.secondary == nil) #expect(usage.tertiary == nil) #expect(usage.identity?.accountEmail == "user@example.com") + #expect(usage.extraRateWindows == nil) } } @@ -1126,8 +1152,13 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 50) - #expect(usage.secondary?.remainingPercent.rounded() == 25) - #expect(usage.extraRateWindows == nil) + #expect(usage.secondary == nil) + + let extra = try #require(usage.extraRateWindows) + let gptWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M55" })) + #expect(gptWindow.window.remainingPercent.rounded() == 25) + let claudeWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M50" })) + #expect(claudeWindow.window.remainingPercent.rounded() == 75) } @Test @@ -1307,377 +1338,12 @@ extension AntigravityStatusProbeTests { let usage = try snapshot.toUsageSnapshot() #expect(usage.primary?.remainingPercent.rounded() == 30) - #expect(usage.secondary?.remainingPercent.rounded() == 70) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `remote source collapses recognized family models and hides unconsumed junk`() throws { - // Fixture B: verified 13 remote models; recognized text models collapse into Gemini, - // and unconsumed junk stays hidden. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - // junk: image - AntigravityModelQuota( - label: "Gemini 2.5 Flash Image", - modelId: "gemini-2-5-flash-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: tab autocomplete - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 2.5 Pro", - modelId: "gemini-2-5-pro", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "gemini-3-pro-high", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: lite - AntigravityModelQuota( - label: "Gemini 2.5 Flash Lite", - modelId: "gemini-2-5-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: image - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: lite - AntigravityModelQuota( - label: "Gemini 3.1 Flash Lite", - modelId: "gemini-3-1-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3.1 Pro (Low)", - modelId: "gemini-3-1-pro-low", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3.1 Pro (High)", - modelId: "gemini-3-1-pro-high", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // junk: tab autocomplete - AntigravityModelQuota( - label: "Tab Jump Flash Lite Vertex", - modelId: "tab_jump_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 3 Pro (Low)", - modelId: "gemini-3-pro-low", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - // survivor - AntigravityModelQuota( - label: "Gemini 2.5 Flash", - modelId: "gemini-2-5-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 100) #expect(usage.secondary == nil) - #expect(usage.extraRateWindows == nil) - } - @Test - func `remote source shows consumed junk models despite filter`() throws { - // Fixture C: junk models with remainingFraction < 0.999 must be shown - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - // consumed tab - should be shown - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 0.4, - resetTime: nil, - resetDescription: nil), - // consumed image - should be shown - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 0.4, - resetTime: nil, - resetDescription: nil), - // unconsumed sibling tab (0.9995 >= 0.999) - should be hidden - AntigravityModelQuota( - label: "Tab Jump Flash Lite Vertex", - modelId: "tab_jump_flash_lite_vertex", - remainingFraction: 0.9995, - resetTime: nil, - resetDescription: nil), - // a clean survivor for non-empty guard - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - let extraWindows = try #require(usage.extraRateWindows) - let ids = extraWindows.map(\.id) - - // Consumed junk models shown despite being junk type - #expect(ids.contains("tab_flash_lite_vertex")) - #expect(ids.contains("gemini-3-pro-image")) - - // Unconsumed sibling stays hidden - #expect(!ids.contains("tab_jump_flash_lite_vertex")) - } - - @Test - func `remote source image models do not drive family summary bars`() throws { - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 0.2, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "gemini-3-pro-high", - remainingFraction: 0.9, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Flash Image", - modelId: "gemini-3-flash-image", - remainingFraction: 0.1, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Flash", - modelId: "gemini-3-flash", - remainingFraction: 0.8, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - - #expect(usage.primary?.usedPercent == 20) - #expect(usage.secondary == nil) - #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-pro-image") == true) - #expect(usage.extraRateWindows?.map(\.id).contains("gemini-3-flash-image") == true) - } - - @Test - func `remote source yields nil extra windows when all models are unconsumed junk`() throws { - // Fixture D: all-junk-unconsumed -> extraRateWindows nil - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Tab Flash Lite Vertex", - modelId: "tab_flash_lite_vertex", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 2.5 Flash Lite", - modelId: "gemini-2-5-flash-lite", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro Image", - modelId: "gemini-3-pro-image", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Unknown Model X", - modelId: "unknown-model-x", - remainingFraction: 1.0, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary == nil) - #expect(usage.secondary == nil) - #expect(usage.tertiary == nil) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `ordering edge cases collapse to most constrained usage pool`() throws { - // Fixture F: local source; known Gemini Pro rows collapse into the Gemini pool - // using the most constrained remaining fraction. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Gemini 3 Pro (Low)", - modelId: "MODEL_PLACEHOLDER_M70", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3 Pro (High)", - modelId: "MODEL_PLACEHOLDER_M71", - remainingFraction: 0.8, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini Pro Experimental", - modelId: "MODEL_PLACEHOLDER_M72", - remainingFraction: 0.3, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Claude Sonnet 4", - modelId: "MODEL_PLACEHOLDER_M73", - remainingFraction: 0.9, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 30) - #expect(usage.secondary?.remainingPercent.rounded() == 90) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `nil version unknown family models sort deterministically by label`() throws { - // Strict-weak-ordering guard: two .unknown models with unparseable versions - // should sort by label without trapping - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Zebra Unknown Model", - modelId: "MODEL_PLACEHOLDER_MA", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "Alpha Unknown Model", - modelId: "MODEL_PLACEHOLDER_MB", - remainingFraction: 0.5, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - - let usage = try snapshot.toUsageSnapshot() - let extraWindows = try #require(usage.extraRateWindows) - let titles = extraWindows.map(\.title) - - // Deterministic: label tiebreaker -> Alpha before Zebra - #expect(titles == ["Alpha Unknown Model", "Zebra Unknown Model"]) - } - - @Test - func `hyphenated raw model ids without display name still map to gemini group`() throws { - // When the remote catalog omits displayName/label, the raw hyphenated model id - // becomes the label and still participates in the Gemini group. - let snapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "gemini-3-pro-preview", - modelId: "gemini-3-pro-preview", - remainingFraction: 1, - resetTime: nil, - resetDescription: nil), - AntigravityModelQuota( - label: "gemini-2.5-pro", - modelId: "gemini-2.5-pro", - remainingFraction: 1, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .remote) - - let usage = try snapshot.toUsageSnapshot() - #expect(usage.primary?.remainingPercent.rounded() == 100) - #expect(usage.extraRateWindows == nil) - } - - @Test - func `http probe errors still count as reachable`() { - #expect( - AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("HTTP 403: Forbidden"))) - #expect( - AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("HTTP 404: Not Found"))) - #expect( - !AntigravityStatusProbe.isReachableProbeError( - AntigravityStatusProbeError.apiError("Invalid response"))) - #expect(!AntigravityStatusProbe.isReachableProbeError(AntigravityStatusProbeError.notRunning)) - } - - @Test - func `fallback probe port prefers non extension candidate`() { - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [51170, 61775], - extensionPort: 61775) == 51170) - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [61775], - extensionPort: 61775) == 61775) - #expect( - AntigravityStatusProbe.fallbackProbePort( - ports: [51170, 61775], - extensionPort: nil) == 51170) + let extra = try #require(usage.extraRateWindows) + let opusWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M61" })) + #expect(opusWindow.window.remainingPercent.rounded() == 70) + let sonnetWindow = try #require(extra.first(where: { $0.id == "MODEL_PLACEHOLDER_M60" })) + #expect(sonnetWindow.window.remainingPercent.rounded() == 80) } } diff --git a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift index e5c995eb56..ec93b7e0e0 100644 --- a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift +++ b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift @@ -628,7 +628,7 @@ struct CodexBarWidgetProviderTests { let rows = WidgetUsageRow.rows(for: entry) #expect(rows.map(\.id) == ["primary", "secondary"]) - #expect(rows.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(rows.map(\.title) == ["Gemini 5-hour", "Gemini weekly"]) #expect(rows.compactMap(\.percentLeft) == [90, 80]) } diff --git a/Tests/CodexBarTests/CodexbarTests.swift b/Tests/CodexBarTests/CodexbarTests.swift index 7502ab9155..6628291028 100644 --- a/Tests/CodexBarTests/CodexbarTests.swift +++ b/Tests/CodexBarTests/CodexbarTests.swift @@ -75,35 +75,17 @@ struct CodexBarTests { @Test func `antigravity quota summary icon shows session on top and weekly on bottom`() { let snapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), + primary: RateWindow(usedPercent: 97, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 84, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 97, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 98, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - ], updatedAt: Date()) let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) #expect(windows.primary?.windowMinutes == 300) - #expect(windows.primary?.remainingPercent == 2) + #expect(windows.primary?.remainingPercent == 3) #expect(windows.secondary?.windowMinutes == 10080) - #expect(windows.secondary?.remainingPercent == 1) + #expect(windows.secondary?.remainingPercent == 16) } @Test @@ -138,178 +120,6 @@ struct CodexBarTests { #expect(visualTopRightAlpha > visualBottomRightAlpha + 0.2) } - @Test - func `antigravity quota summary icon uses most constrained quota summary lanes`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Renamed Weekly", - window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Renamed Session", - window: RateWindow(usedPercent: 40, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 98, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 2) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity quota summary icon can pair gemini session with claude gpt weekly`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 40, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 60) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity quota summary icon ignores unknown rows while ranking known lanes`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 100, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - usageKnown: false), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary == nil) - #expect(windows.secondary?.remainingPercent == 1) - } - - @Test - func `antigravity used icon percent matches constrained claude gpt lane`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Session", - window: RateWindow(usedPercent: 20, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 95, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 40, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let percents = IconRemainingResolver.resolvedPercents( - snapshot: snapshot, - style: .antigravity, - showUsed: true) - - #expect(percents.primary == 95) - #expect(percents.secondary == 40) - } - - @Test - func `antigravity quota summary icon falls back when gemini rows are absent`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude + GPT Session", - window: RateWindow(usedPercent: 75, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude + GPT Weekly", - window: RateWindow(usedPercent: 88, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.remainingPercent == 25) - #expect(windows.secondary?.remainingPercent == 12) - } - - @Test - func `antigravity quota summary icon tie break is stable`() { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - tertiary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-z-5h", - title: "Gemini Session", - window: RateWindow( - usedPercent: 50, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "second-by-id")), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-a-5h", - title: "Gemini Session", - window: RateWindow( - usedPercent: 50, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "first-by-id")), - ], - updatedAt: Date()) - - let windows = IconRemainingResolver.resolvedWindows(snapshot: snapshot, style: .antigravity) - - #expect(windows.primary?.resetDescription == "first-by-id") - #expect(windows.secondary == nil) - } - @Test func `perplexity icon falls back to purchased lane when bonus is exhausted`() { let snapshot = UsageSnapshot( diff --git a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift index 9dfb1ad72f..f322c5ca5b 100644 --- a/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift +++ b/Tests/CodexBarTests/MenuBarMetricWindowResolverTests.swift @@ -211,118 +211,46 @@ struct MenuBarMetricWindowResolverTests { } @Test - func `automatic metric uses constrained antigravity family lane`() { + func `automatic metric prioritizes exhausted litellm personal budget over active team budget`() { let snapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: "Claude"), - secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Pro"), - tertiary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Flash"), + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Personal"), + secondary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: "Team"), updatedAt: Date()) let window = MenuBarMetricWindowResolver.rateWindow( preference: .automatic, - provider: .antigravity, + provider: .litellm, snapshot: snapshot, supportsAverage: false) + #expect(window?.resetDescription == "Personal") #expect(window?.usedPercent == 100) - #expect(window?.resetDescription == "Gemini Pro") } @Test - func `automatic metric preserves usable first by default and prioritizes exhausted lane when enabled`() { + func `automatic metric prioritizes exhausted litellm team budget over active personal budget`() { let snapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 67, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-5h", - title: "Gemini Models Five Hour Limit", - window: RateWindow(usedPercent: 71, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Models Weekly Limit", - window: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-5h", - title: "Claude and GPT models Five Hour Limit", - window: RateWindow(usedPercent: 100, windowMinutes: 300, resetsAt: nil, resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-3p-weekly", - title: "Claude and GPT models Weekly Limit", - window: RateWindow(usedPercent: 67, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)), - ], + primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: "Personal"), + secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Team"), updatedAt: Date()) - let defaultWindow = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false) - let optInWindow = MenuBarMetricWindowResolver.rateWindow( - preference: .automatic, - provider: .antigravity, - snapshot: snapshot, - supportsAverage: false, - antigravityPrioritizeExhaustedQuotas: true) - - #expect(defaultWindow?.remainingPercent == 29) - #expect(defaultWindow?.windowMinutes == 300) - #expect(optInWindow?.remainingPercent == 0) - #expect(optInWindow?.windowMinutes == 300) - } - - @Test - func `automatic metric uses recognized antigravity gemini pool when claude gpt is reset only`() throws { - let resetOnlyReset = Date(timeIntervalSince1970: 1000) - let exhaustedReset = Date(timeIntervalSince1970: 2000) - let antigravitySnapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Claude Sonnet 4.6", - modelId: "claude-sonnet-4-6", - remainingFraction: nil, - resetTime: resetOnlyReset, - resetDescription: nil), - AntigravityModelQuota( - label: "Gemini 3.1 Pro", - modelId: "gemini-3-1-pro", - remainingFraction: 0, - resetTime: exhaustedReset, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - let snapshot = try antigravitySnapshot.toUsageSnapshot() - #expect(snapshot.primary?.usedPercent == 100) - #expect(snapshot.primary?.resetsAt == exhaustedReset) - #expect(snapshot.secondary == nil) - let window = MenuBarMetricWindowResolver.rateWindow( preference: .automatic, - provider: .antigravity, + provider: .litellm, snapshot: snapshot, supportsAverage: false) + #expect(window?.resetDescription == "Team") #expect(window?.usedPercent == 100) - #expect(window?.resetsAt == exhaustedReset) } @Test - func `automatic metric uses unclassified antigravity compact fallback`() throws { - let antigravitySnapshot = AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Experimental Model", - modelId: "MODEL_PLACEHOLDER_NEW", - remainingFraction: 0.36, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - let snapshot = try antigravitySnapshot.toUsageSnapshot() + func `automatic metric uses constrained antigravity family lane`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: "Claude"), + secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Pro"), + tertiary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Flash"), + updatedAt: Date()) let window = MenuBarMetricWindowResolver.rateWindow( preference: .automatic, @@ -330,32 +258,19 @@ struct MenuBarMetricWindowResolverTests { snapshot: snapshot, supportsAverage: false) - #expect(window?.usedPercent == 64) + #expect(window?.usedPercent == 100) + #expect(window?.resetDescription == "Gemini Pro") } @Test - func `automatic metric keeps legacy antigravity compact fallback usable first semantics`() { + func `automatic metric prefers antigravity session window over weekly when none are exhausted`() { let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-compact-fallback-exhausted", - title: "Exhausted", - window: RateWindow( - usedPercent: 100, - windowMinutes: nil, - resetsAt: nil, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-compact-fallback-usable", - title: "Usable", - window: RateWindow( - usedPercent: 64, - windowMinutes: nil, - resetsAt: nil, - resetDescription: nil)), - ], + primary: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5h"), + secondary: RateWindow( + usedPercent: 15, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: "weekly"), updatedAt: Date()) let window = MenuBarMetricWindowResolver.rateWindow( @@ -364,235 +279,10 @@ struct MenuBarMetricWindowResolverTests { snapshot: snapshot, supportsAverage: false) - #expect(window?.usedPercent == 64) - } - - @Test - func `antigravity quota ranking filters unknown and unsupported lanes`() { - let now = Date(timeIntervalSince1970: 100_000) - let expectedReset = now.addingTimeInterval(120) - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-session", - title: "Gemini Session", - window: RateWindow( - usedPercent: 85, - windowMinutes: 300, - resetsAt: expectedReset, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-daily", - title: "Gemini Daily", - window: RateWindow( - usedPercent: 100, - windowMinutes: 1440, - resetsAt: now.addingTimeInterval(60), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gemini-weekly", - title: "Gemini Weekly", - window: RateWindow( - usedPercent: 99, - windowMinutes: 10080, - resetsAt: now.addingTimeInterval(30), - resetDescription: nil), - usageKnown: false), - NamedRateWindow( - id: "antigravity-quota-summary-invalid-session", - title: "Invalid Session", - window: RateWindow( - usedPercent: .nan, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(10), - resetDescription: nil)), - ], - updatedAt: now) - - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.usedPercent == 85) - #expect(window?.resetsAt == expectedReset) - } - - @Test - func `antigravity quota ranking breaks usage ties by valid nearest reset`() { - let now = Date(timeIntervalSince1970: 100_000) - let nearestFutureReset = now.addingTimeInterval(60) - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: [ - NamedRateWindow( - id: "antigravity-quota-summary-gemini-session", - title: "Gemini Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nil, - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-claude-session", - title: "Claude Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(-60), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-gpt-session", - title: "GPT Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: now.addingTimeInterval(120), - resetDescription: nil)), - NamedRateWindow( - id: "antigravity-quota-summary-other-session", - title: "Other Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nearestFutureReset, - resetDescription: nil)), - ], - updatedAt: now) - - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.resetsAt == nearestFutureReset) - } - - @Test - func `antigravity quota ranking breaks complete ties by stable row ID`() { - let now = Date(timeIntervalSince1970: 100_000) - let rows = [ - NamedRateWindow( - id: "antigravity-quota-summary-a-weekly", - title: "A Weekly", - window: RateWindow( - usedPercent: 90, - windowMinutes: 10080, - resetsAt: nil, - resetDescription: "a")), - NamedRateWindow( - id: "antigravity-quota-summary-z-session", - title: "Z Session", - window: RateWindow( - usedPercent: 90, - windowMinutes: 300, - resetsAt: nil, - resetDescription: "z")), - ] - - for orderedRows in [rows, Array(rows.reversed())] { - let snapshot = UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: orderedRows, - updatedAt: now) - let window = MenuBarMetricWindowResolver.antigravityQuotaSummaryRankingWindow( - snapshot: snapshot, - now: now) - - #expect(window?.resetDescription == "z") - } - } - - @Test - func `antigravity families are blocked only when every understood family has an exhausted lane`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini-session", 300, 100, true), - ("gemini-weekly", 10080, 20, true), - ("3p-5-hour", 300, 10, true), - ("3p-weekly", 10080, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - - let availableFamily = Self.antigravitySummarySnapshot(rows: [ - ("gemini-session", 300, 100, true), - ("3p-session", 300, 99, true), - ]) - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: availableFamily)) - } - - @Test - func `antigravity family blocking accepts underscore cadence delimiters`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini_session", 300, 100, true), - ("gemini_weekly", 10080, 20, true), - ("third_party_five_hour", 300, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test - func `antigravity family blocking accepts limit suffixed cadence`() { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("gemini-5h limit", 300, 100, true), - ("gemini-weekly limit", 10080, 20, true), - ("third-party-session limit", 300, 100, true), - ]) - - #expect(MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test(arguments: [ - ("gemini-session", 300, 100.0, false), - ("gemini-daily", 1440, 100.0, true), - ("-session", 300, 100.0, true), - ("gemini-daily", 300, 100.0, true), - ("gem ini-session", 300, 100.0, true), - ("invalid-session", 300, Double.nan, true), - ]) - func `antigravity family blocking fails open for incomplete summary rows`( - idSuffix: String, - windowMinutes: Int, - usedPercent: Double, - usageKnown: Bool) - { - let snapshot = Self.antigravitySummarySnapshot(rows: [ - ("safe-session", 300, 100, true), - (idSuffix, windowMinutes, usedPercent, usageKnown), - ]) - - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - @Test - func `antigravity family blocking fails open without quota summary rows`() { - let snapshot = UsageSnapshot(primary: nil, secondary: nil, updatedAt: Date()) - - #expect(!MenuBarMetricWindowResolver.antigravityQuotaSummaryFamiliesAreAllBlocked(snapshot: snapshot)) - } - - private static func antigravitySummarySnapshot( - rows: [(idSuffix: String, windowMinutes: Int, usedPercent: Double, usageKnown: Bool)]) - -> UsageSnapshot - { - UsageSnapshot( - primary: nil, - secondary: nil, - extraRateWindows: rows.map { row in - NamedRateWindow( - id: "antigravity-quota-summary-\(row.idSuffix)", - title: row.idSuffix, - window: RateWindow( - usedPercent: row.usedPercent, - windowMinutes: row.windowMinutes, - resetsAt: nil, - resetDescription: nil), - usageKnown: row.usageKnown) - }, - updatedAt: Date()) + // Session (5-hour) windows should be preferred even though weekly windows + // have higher usedPercent, because the session limit is the immediate constraint. + #expect(window?.windowMinutes == 300) + #expect(window?.resetDescription == "5h") } @Test @@ -809,4 +499,55 @@ struct MenuBarMetricWindowResolverTests { #expect(window?.resetsAt == reset) } + + @Test + func `automatic metric prioritizes exhausted kimi weekly quota over active rate limit`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "Weekly") + #expect(window?.usedPercent == 100) + } + + @Test + func `automatic metric prioritizes exhausted kimi rate limit over active weekly quota`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 100, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "5-hour") + #expect(window?.usedPercent == 100) + } + + @Test + func `automatic metric defaults to rate limit for kimi when neither is exhausted`() { + let snapshot = UsageSnapshot( + primary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Weekly"), + secondary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"), + updatedAt: Date()) + + let window = MenuBarMetricWindowResolver.rateWindow( + preference: .automatic, + provider: .kimi, + snapshot: snapshot, + supportsAverage: false) + + #expect(window?.resetDescription == "5-hour") + #expect(window?.usedPercent == 4) + } } diff --git a/Tests/CodexBarTests/MenuCardAntigravityTests.swift b/Tests/CodexBarTests/MenuCardAntigravityTests.swift index b07ef767c6..9272b869f7 100644 --- a/Tests/CodexBarTests/MenuCardAntigravityTests.swift +++ b/Tests/CodexBarTests/MenuCardAntigravityTests.swift @@ -86,7 +86,7 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.count == 1) - #expect(model.metrics.map(\.title) == ["Gemini Models"]) + #expect(model.metrics.map(\.title) == ["Gemini 5-hour"]) #expect(model.metrics[0].percent == 95) #expect(model.metrics[0].percentLabel == "95% left") } @@ -188,7 +188,7 @@ struct MenuCardAntigravityTests { hidePersonalInfo: false, now: now)) - #expect(model.metrics.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(model.metrics.map(\.title) == ["Gemini 5-hour", "Claude Thinking"]) #expect(!model.metrics.contains { $0.title == "Gemini 3.1 Pro (Low)" }) } @@ -250,11 +250,13 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.map(\.title) == [ - "Gemini Models", - "Claude and GPT", + "Gemini 5-hour", + "Claude Opus 4.6 (Thinking)", + "GPT-OSS 120B (Medium)", ]) #expect(model.metrics.map(\.percentLabel) == [ "50% left", + "75% left", "25% left", ]) } @@ -307,7 +309,7 @@ struct MenuCardAntigravityTests { // Distinct extra windows remain visible even with optional extras disabled. #expect(model.metrics.contains { $0.title == "Experimental Tool" }) - #expect(model.metrics.contains { $0.title == "Gemini Models" }) + #expect(model.metrics.contains { $0.title == "Gemini 5-hour" }) } @Test @@ -517,7 +519,7 @@ struct MenuCardAntigravityTests { now: now)) #expect(model.metrics.count == 1) - #expect(model.metrics[0].title == "Gemini Models") + #expect(model.metrics[0].title == "Gemini 5-hour") #expect(model.metrics[0].percent == 5) #expect(model.metrics[0].percentLabel == "5% used") } diff --git a/Tests/CodexBarTests/MenuCardTestDateFormatting.swift b/Tests/CodexBarTests/MenuCardTestDateFormatting.swift index 4f94f865d1..b13b161dca 100644 --- a/Tests/CodexBarTests/MenuCardTestDateFormatting.swift +++ b/Tests/CodexBarTests/MenuCardTestDateFormatting.swift @@ -1,8 +1,9 @@ import Foundation +@testable import CodexBar func minimaxRenewDate(_ timestamp: TimeInterval) -> String { let formatter = DateFormatter() - formatter.locale = .current + formatter.locale = codexBarLocalizedLocale() formatter.timeZone = TimeZone(identifier: "Asia/Shanghai") formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy") return formatter.string(from: Date(timeIntervalSince1970: timestamp)) diff --git a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift index a5986037d7..12c3ccdc1b 100644 --- a/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemAnimationSignatureTests.swift @@ -98,8 +98,8 @@ struct StatusItemAnimationSignatureTests { store._setSnapshotForTesting( UsageSnapshot( - primary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 16, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), + primary: RateWindow(usedPercent: 1, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 99, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), tertiary: nil, extraRateWindows: [ NamedRateWindow( @@ -138,7 +138,7 @@ struct StatusItemAnimationSignatureTests { #expect(signature.contains("provider=antigravity")) #expect(signature.contains("style=combined")) - #expect(signature.contains("primary=98.000")) + #expect(signature.contains("primary=99.000")) #expect(signature.contains("weekly=1.000")) } diff --git a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift index b47fd1383d..7af3b3c90e 100644 --- a/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift +++ b/Tests/CodexBarTests/UsageStoreHighestUsageTests.swift @@ -162,11 +162,9 @@ struct UsageStoreHighestUsageTests { } @Test - func `automatic metric ignores unclassified antigravity compact fallback until exhausted priority is enabled`() - throws - { + func `antigravity automatic metric prefers usable session window`() { let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-unclassified"), + configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-prefers-session"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.refreshFrequency = .manual @@ -174,9 +172,6 @@ struct UsageStoreHighestUsageTests { settings.setMenuBarMetricPreference(.automatic, for: .antigravity) let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } if let antigravityMeta = registry.metadata[.antigravity] { settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) } @@ -185,210 +180,18 @@ struct UsageStoreHighestUsageTests { fetcher: UsageFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = try AntigravityStatusSnapshot( - modelQuotas: [ - AntigravityModelQuota( - label: "Experimental Model", - modelId: "MODEL_PLACEHOLDER_NEW", - remainingFraction: 0.36, - resetTime: nil, - resetDescription: nil), - ], - accountEmail: nil, - accountPlan: nil, - source: .local) - .toUsageSnapshot() - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) - - let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 50) - - settings.antigravityPrioritizeExhaustedQuotas = true - let optInHighest = store.providerWithHighestUsage() - #expect(optInHighest?.provider == .antigravity) - #expect(optInHighest?.usedPercent == 64) - } - - @Test - func `automatic metric ignores legacy antigravity family lanes without quota summary`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-constrained-gemini"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let fetcher = UsageFetcher() - let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 70, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: "Claude"), - secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Pro"), - tertiary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: "Gemini Flash"), - updatedAt: Date()) - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) - - let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 70) - } -} - -extension UsageStoreHighestUsageTests { - @Test - func `antigravity automatic ranking keeps usable first until exhausted priority is enabled`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-all-summary"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let store = UsageStore( - fetcher: UsageFetcher(), - browserDetection: BrowserDetection(cacheTTL: 0), - settings: settings) - store._setSnapshotForTesting( - UsageSnapshot( - primary: RateWindow(usedPercent: 80, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()), - provider: .codex) - let antigravity = self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 10, + // Gemini session is 15% used and weekly is 20% used. + let snapshot = self.antigravityQuotaSummarySnapshot( + geminiSessionUsed: 15, geminiWeeklyUsed: 20, - otherSessionUsed: 95, - otherWeeklyUsed: 90) - let unknownCadence = NamedRateWindow( - id: "antigravity-quota-summary-future-daily", - title: "Future daily lane", - window: RateWindow( - usedPercent: 99, - windowMinutes: 24 * 60, - resetsAt: nil, - resetDescription: nil)) - store._setSnapshotForTesting( - antigravity.with(extraRateWindows: (antigravity.extraRateWindows ?? []) + [unknownCadence]), - provider: .antigravity) - - var highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 95) - - store._setSnapshotForTesting( - self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 95, - geminiWeeklyUsed: 20, - otherSessionUsed: 10, - otherWeeklyUsed: 10), - provider: .antigravity) - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 95) - - store._setSnapshotForTesting( - self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 100, - geminiWeeklyUsed: 100, - otherSessionUsed: 50, - otherWeeklyUsed: 50), - provider: .antigravity) - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 80) - - settings.antigravityPrioritizeExhaustedQuotas = true - highest = store.providerWithHighestUsage() - #expect(highest?.provider == .antigravity) - #expect(highest?.usedPercent == 100) - } - - @Test - func `opt in automatic metric excludes antigravity only when every summary family is blocked`() { - let settings = SettingsStore( - configStore: testConfigStore(suiteName: "UsageStoreHighestUsageTests-antigravity-summary-usable"), - zaiTokenStore: NoopZaiTokenStore(), - syntheticTokenStore: NoopSyntheticTokenStore()) - settings.refreshFrequency = .manual - settings.statusChecksEnabled = false - settings.setMenuBarMetricPreference(.automatic, for: .antigravity) - settings.antigravityPrioritizeExhaustedQuotas = true - - let registry = ProviderRegistry.shared - if let codexMeta = registry.metadata[.codex] { - settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) - } - if let antigravityMeta = registry.metadata[.antigravity] { - settings.setProviderEnabled(provider: .antigravity, metadata: antigravityMeta, enabled: true) - } - - let fetcher = UsageFetcher() - let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) - - let codexSnapshot = UsageSnapshot( - primary: RateWindow(usedPercent: 80, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - updatedAt: Date()) - let antigravitySnapshot = self.antigravityQuotaSummarySnapshot( - geminiSessionUsed: 100, - geminiWeeklyUsed: 40, otherSessionUsed: 100, - otherWeeklyUsed: 100) - - store._setSnapshotForTesting(codexSnapshot, provider: .codex) - store._setSnapshotForTesting(antigravitySnapshot, provider: .antigravity) + otherWeeklyUsed: 70) + store._setSnapshotForTesting(snapshot, provider: .antigravity) let highest = store.providerWithHighestUsage() - #expect(highest?.provider == .codex) - #expect(highest?.usedPercent == 80) - - let unsupportedRow = NamedRateWindow( - id: "antigravity-quota-summary-future-daily", - title: "Future daily lane", - window: RateWindow( - usedPercent: 100, - windowMinutes: 1440, - resetsAt: nil, - resetDescription: nil)) - store._setSnapshotForTesting( - antigravitySnapshot.with( - extraRateWindows: (antigravitySnapshot.extraRateWindows ?? []) + [unsupportedRow]), - provider: .antigravity) - - let failOpenHighest = store.providerWithHighestUsage() - #expect(failOpenHighest?.provider == .antigravity) - #expect(failOpenHighest?.usedPercent == 100) + #expect(highest?.provider == .antigravity) + #expect(highest?.usedPercent == 15) } @Test @@ -927,27 +730,30 @@ extension UsageStoreHighestUsageTests { otherSessionUsed: Double, otherWeeklyUsed: Double) -> UsageSnapshot { - UsageSnapshot( - primary: nil, - secondary: nil, + let primary = RateWindow( + usedPercent: geminiSessionUsed, + windowMinutes: 5 * 60, + resetsAt: nil, + resetDescription: "5h") + let secondary = RateWindow( + usedPercent: geminiWeeklyUsed, + windowMinutes: 7 * 24 * 60, + resetsAt: nil, + resetDescription: "weekly") + + return UsageSnapshot( + primary: primary, + secondary: secondary, tertiary: nil, extraRateWindows: [ NamedRateWindow( id: "antigravity-quota-summary-gemini-5h", title: "Gemini Session", - window: RateWindow( - usedPercent: geminiSessionUsed, - windowMinutes: 5 * 60, - resetsAt: nil, - resetDescription: nil)), + window: primary), NamedRateWindow( id: "antigravity-quota-summary-gemini-weekly", title: "Gemini Weekly", - window: RateWindow( - usedPercent: geminiWeeklyUsed, - windowMinutes: 7 * 24 * 60, - resetsAt: nil, - resetDescription: nil)), + window: secondary), NamedRateWindow( id: "antigravity-quota-summary-3p-5h", title: "Claude + GPT Session", @@ -1017,6 +823,18 @@ extension UsageStoreHighestUsageTests { primary: antigravity.primary, secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil)), provider: .antigravity) + // Partially exhausted: secondary is 100% but primary (10%) still has quota — + // Antigravity should remain eligible and surface the exhausted lane as the metric. + highest = store.providerWithHighestUsage() + #expect(highest?.provider == .antigravity) + #expect(highest?.usedPercent == 100) + + // Fully exhausted: both lanes at 100% → Antigravity is excluded. + store._setSnapshotForTesting( + antigravity.with( + primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil)), + provider: .antigravity) highest = store.providerWithHighestUsage() #expect(highest?.provider == .codex) } diff --git a/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift b/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift index 670e68e3ea..b999a26010 100644 --- a/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift +++ b/Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift @@ -162,7 +162,7 @@ struct UsageStoreWidgetSnapshotTests { let entry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .antigravity }) #expect(widgetSnapshots.last?.usageBarsShowUsed == true) #expect(entry.usageRows?.map(\.id) == ["primary", "secondary"]) - #expect(entry.usageRows?.map(\.title) == ["Gemini Models", "Claude and GPT"]) + #expect(entry.usageRows?.map(\.title) == ["Gemini 5-hour", "Gemini weekly"]) #expect(entry.usageRows?.compactMap(\.percentLeft) == [90, 80]) } diff --git a/Tests/CodexBarTests/ZenMuxProviderTests.swift b/Tests/CodexBarTests/ZenMuxProviderTests.swift index f7ca2692e8..703fea60c7 100644 --- a/Tests/CodexBarTests/ZenMuxProviderTests.swift +++ b/Tests/CodexBarTests/ZenMuxProviderTests.swift @@ -243,7 +243,7 @@ struct ZenMuxProviderTests { let now = try #require(Self.date("2026-03-24T07:35:09.000Z")) let expiresAt = try #require(Self.date("2026-04-12T08:26:56.000Z")) let expiryFormatter = DateFormatter() - expiryFormatter.locale = .current + expiryFormatter.locale = codexBarLocalizedLocale() expiryFormatter.timeZone = .current expiryFormatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy") let transport = ProviderHTTPTransportStub { request in