Files
steipete--codexbar/Tests/CodexBarTests/StatusItemAnimationTests.swift
2026-07-13 12:22:33 +08:00

1755 lines
77 KiB
Swift

import AppKit
import CodexBarCore
import Testing
@testable import CodexBar
@MainActor
@Suite(.serialized)
// swiftlint:disable:next type_body_length
struct StatusItemAnimationTests {
private func maxAlpha(in rep: NSBitmapImageRep) -> CGFloat {
var maxAlpha: CGFloat = 0
for x in 0..<rep.pixelsWide {
for y in 0..<rep.pixelsHigh {
let alpha = (rep.colorAt(x: x, y: y) ?? .clear).alphaComponent
if alpha > maxAlpha {
maxAlpha = alpha
}
}
}
return maxAlpha
}
private func makeStatusBarForTesting() -> NSStatusBar {
// Use the real system status bar in tests. Creating standalone NSStatusBar instances
// has caused AppKit teardown crashes under swiftpm-testing-helper.
.system
}
@Test
func `known unavailable limits stop loading animation`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-known-unavailable"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
store._setSnapshotForTesting(nil, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
#expect(controller.shouldAnimate(provider: .claude))
store._setKnownLimitsAvailabilityForTesting(.unavailable, provider: .claude)
#expect(!controller.shouldAnimate(provider: .claude))
}
@Test
func `merged icon loading animation tracks selected provider only`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-merged"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
if let openRouterMeta = registry.metadata[.openrouter] {
settings.setProviderEnabled(provider: .openrouter, metadata: openRouterMeta, enabled: true)
}
settings.openRouterAPIToken = "or-token"
if let geminiMeta = registry.metadata[.gemini] {
settings.setProviderEnabled(provider: .gemini, metadata: geminiMeta, enabled: false)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setSnapshotForTesting(nil, provider: .claude)
store._setErrorForTesting(nil, provider: .codex)
store._setErrorForTesting(nil, provider: .claude)
#expect(controller.needsMenuBarIconAnimation() == false)
}
@Test
func `merged icon loading animation does not flip layout when weekly hits zero`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-weekly"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.menuBarShowsBrandIconWithPercent = false
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
if let openRouterMeta = registry.metadata[.openrouter] {
settings.setProviderEnabled(provider: .openrouter, metadata: openRouterMeta, enabled: true)
}
settings.openRouterAPIToken = "or-token"
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
// Seed with data so init doesn't start the animation driver.
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .codex)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
// Enter loading state: no data, no stale error.
store._setSnapshotForTesting(nil, provider: .codex)
store._setSnapshotForTesting(nil, provider: .claude)
store._setErrorForTesting(nil, provider: .codex)
store._setErrorForTesting(nil, provider: .claude)
controller.animationPattern = .knightRider
#expect(controller.needsMenuBarIconAnimation() == true)
// At phase = π/2, the secondary bar hits 0 (weeklyRemaining == 0) due to a π offset.
// Regression: this used to flip IconRenderer into the "weekly exhausted" layout and cause toolbar flicker.
controller.applyIcon(phase: .pi / 2)
guard let image = controller.statusItem.button?.image else {
#expect(Bool(false))
return
}
let rep = image.representations.compactMap { $0 as? NSBitmapImageRep }.first(where: {
$0.pixelsWide == 36 && $0.pixelsHigh == 36
})
#expect(rep != nil)
guard let rep else { return }
let alpha = (rep.colorAt(x: 18, y: 12) ?? .clear).alphaComponent
#expect(alpha > 0.05)
}
@Test
func `warp no bonus layout is preserved in show used mode when bonus is exhausted`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-warp-no-bonus-used"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.menuBarShowsBrandIconWithPercent = false
settings.usageBarsShowUsed = true
let registry = ProviderRegistry.shared
if let warpMeta = registry.metadata[.warp] {
settings.setProviderEnabled(provider: .warp, metadata: warpMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
// Primary used=10%. Bonus exhausted: used=100% (remaining=0%).
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .warp)
store._setErrorForTesting(nil, provider: .warp)
controller.applyIcon(for: .warp, phase: nil)
guard let image = controller.statusItems[.warp]?.button?.image else {
#expect(Bool(false))
return
}
let rep = image.representations.compactMap { $0 as? NSBitmapImageRep }.first(where: {
$0.pixelsWide == 36 && $0.pixelsHigh == 36
})
#expect(rep != nil)
guard let rep else { return }
// In the Warp "no bonus/exhausted bonus" layout, the bottom bar is a dimmed track.
// A pixel near the right side of the bottom bar should remain subdued (not fully opaque).
let alpha = (rep.colorAt(x: 25, y: 9) ?? .clear).alphaComponent
#expect(alpha < 0.6)
}
@Test
func `warp bonus lane is preserved in show used mode when bonus is unused`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-warp-unused-bonus-used"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.menuBarShowsBrandIconWithPercent = false
settings.usageBarsShowUsed = true
let registry = ProviderRegistry.shared
if let warpMeta = registry.metadata[.warp] {
settings.setProviderEnabled(provider: .warp, metadata: warpMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
// Bonus exists but is unused: used=0% (remaining=100%).
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .warp)
store._setErrorForTesting(nil, provider: .warp)
controller.applyIcon(for: .warp, phase: nil)
guard let image = controller.statusItems[.warp]?.button?.image else {
#expect(Bool(false))
return
}
let rep = image.representations.compactMap { $0 as? NSBitmapImageRep }.first(where: {
$0.pixelsWide == 36 && $0.pixelsHigh == 36
})
#expect(rep != nil)
guard let rep else { return }
// When we incorrectly treat "0 used" as "no bonus", the Warp branch makes the top bar full (100%).
// A pixel near the right side of the top bar should remain in the track-only range for 10% usage.
let alpha = (rep.colorAt(x: 31, y: 25) ?? .clear).alphaComponent
#expect(alpha < 0.6)
}
@Test
func `open router without key limit uses meter icon when brand percent is disabled`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-openrouter-no-limit-meter"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.menuBarShowsBrandIconWithPercent = false
let registry = ProviderRegistry.shared
if let openRouterMeta = registry.metadata[.openrouter] {
settings.setProviderEnabled(provider: .openrouter, metadata: openRouterMeta, enabled: true)
}
settings.openRouterAPIToken = "or-token"
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = OpenRouterUsageSnapshot(
totalCredits: 50,
totalUsage: 45,
balance: 5,
usedPercent: 90,
keyDataFetched: true,
keyLimit: nil,
keyUsage: nil,
rateLimit: nil,
updatedAt: Date()).toUsageSnapshot()
store._setSnapshotForTesting(snapshot, provider: .openrouter)
store._setErrorForTesting(nil, provider: .openrouter)
controller.applyIcon(for: .openrouter, phase: nil)
guard let image = controller.statusItems[.openrouter]?.button?.image else {
#expect(Bool(false))
return
}
#expect(image.size.width == 18)
#expect(image.size.height == 18)
#expect(snapshot.openRouterUsage?.keyQuotaStatus == .noLimitConfigured)
#expect(controller.statusItems[.openrouter]?.button?.title.isEmpty == true)
#expect(MenuBarDisplayText.percentText(window: snapshot.primary, showUsed: false) == nil)
// With no key limit, the primary bar has no fill — just the dim track.
// A brand logo would be fully opaque here; the track is not.
let rep = image.representations.compactMap { $0 as? NSBitmapImageRep }.first(where: {
$0.pixelsWide == 36 && $0.pixelsHigh == 36
})
#expect(rep != nil)
if let rep {
let alpha = (rep.colorAt(x: 8, y: 25) ?? .clear).alphaComponent
#expect(alpha < 0.5)
}
}
@Test
func `open router key data not fetched still uses meter icon when brand percent is disabled`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-openrouter-no-fetch-meter"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.menuBarShowsBrandIconWithPercent = false
let registry = ProviderRegistry.shared
if let openRouterMeta = registry.metadata[.openrouter] {
settings.setProviderEnabled(provider: .openrouter, metadata: openRouterMeta, enabled: true)
}
settings.openRouterAPIToken = "or-token"
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = OpenRouterUsageSnapshot(
totalCredits: 50,
totalUsage: 45,
balance: 5,
usedPercent: 90,
keyDataFetched: false,
keyLimit: nil,
keyUsage: nil,
rateLimit: nil,
updatedAt: Date()).toUsageSnapshot()
store._setSnapshotForTesting(snapshot, provider: .openrouter)
store._setErrorForTesting(nil, provider: .openrouter)
controller.applyIcon(for: .openrouter, phase: nil)
guard let image = controller.statusItems[.openrouter]?.button?.image else {
#expect(Bool(false))
return
}
#expect(image.size.width == 18)
#expect(image.size.height == 18)
#expect(snapshot.openRouterUsage?.keyQuotaStatus == .unavailable)
// Even with no key data, OpenRouter still renders a meter rather than the brand logo.
// A brand logo would be fully opaque here; the unfilled track is not.
let rep = image.representations.compactMap { $0 as? NSBitmapImageRep }.first(where: {
$0.pixelsWide == 36 && $0.pixelsHigh == 36
})
#expect(rep != nil)
if let rep {
let alpha = (rep.colorAt(x: 8, y: 25) ?? .clear).alphaComponent
#expect(alpha < 0.5)
}
}
@Test
func `menu bar percent uses configured metric`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-metric"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.setMenuBarMetricPreference(.secondary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 12, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 42, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
let window = controller.menuBarMetricWindow(for: .codex, snapshot: snapshot)
#expect(window?.usedPercent == 42)
}
@Test
func `combined codex menu bar metric window uses most constrained visible lane`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-codex-combined-window"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 91, windowMinutes: 7 * 24 * 60, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
let window = controller.menuBarMetricWindow(for: .codex, snapshot: snapshot)
#expect(window?.usedPercent == 91)
#expect(window?.windowMinutes == 7 * 24 * 60)
}
@Test
func `menu bar percent automatic prefers rate limit for kimi`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-kimi-automatic"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .kimi
settings.setMenuBarMetricPreference(.automatic, for: .kimi)
let registry = ProviderRegistry.shared
if let kimiMeta = registry.metadata[.kimi] {
settings.setProviderEnabled(provider: .kimi, metadata: kimiMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 12, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 42, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .kimi)
store._setErrorForTesting(nil, provider: .kimi)
let window = controller.menuBarMetricWindow(for: .kimi, snapshot: snapshot)
#expect(window?.usedPercent == 42)
}
@Test
func `menu bar percent uses average for gemini`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-average"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .gemini
settings.setMenuBarMetricPreference(.average, for: .gemini)
let registry = ProviderRegistry.shared
if let geminiMeta = registry.metadata[.gemini] {
settings.setProviderEnabled(provider: .gemini, metadata: geminiMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .gemini)
store._setErrorForTesting(nil, provider: .gemini)
let window = controller.menuBarMetricWindow(for: .gemini, snapshot: snapshot)
#expect(window?.usedPercent == 40)
}
@Test
func `menu bar percent automatic keeps gemini primary over higher tertiary`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-gemini-automatic-primary"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .gemini
settings.setMenuBarMetricPreference(.automatic, for: .gemini)
let registry = ProviderRegistry.shared
if let geminiMeta = registry.metadata[.gemini] {
settings.setProviderEnabled(provider: .gemini, metadata: geminiMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 95, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .gemini)
store._setErrorForTesting(nil, provider: .gemini)
let window = controller.menuBarMetricWindow(for: .gemini, snapshot: snapshot)
#expect(window?.usedPercent == 20)
}
@Test
func `menu bar percent automatic picks highest cursor lane including api`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-cursor-automatic-tertiary"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .cursor
settings.setMenuBarMetricPreference(.automatic, for: .cursor)
let registry = ProviderRegistry.shared
if let cursorMeta = registry.metadata[.cursor] {
settings.setProviderEnabled(provider: .cursor, metadata: cursorMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 25, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 90, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .cursor)
store._setErrorForTesting(nil, provider: .cursor)
let window = controller.menuBarMetricWindow(for: .cursor, snapshot: snapshot)
#expect(window?.usedPercent == 90)
}
@Test
func `menu bar percent automatic falls back to purchased perplexity lane when bonus is exhausted`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-perplexity-automatic-purchased"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .perplexity
settings.setMenuBarMetricPreference(.automatic, for: .perplexity)
let registry = ProviderRegistry.shared
if let perplexityMeta = registry.metadata[.perplexity] {
settings.setProviderEnabled(provider: .perplexity, metadata: perplexityMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: nil,
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .perplexity)
store._setErrorForTesting(nil, provider: .perplexity)
let window = controller.menuBarMetricWindow(for: .perplexity, snapshot: snapshot)
#expect(window?.usedPercent == 20)
}
@Test
func `menu bar percent automatic falls through after recurring perplexity credits are exhausted`() {
let settings = SettingsStore(
configStore: testConfigStore(
suiteName: "StatusItemAnimationTests-perplexity-automatic-recurring-exhausted"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .perplexity
settings.setMenuBarMetricPreference(.automatic, for: .perplexity)
let registry = ProviderRegistry.shared
if let perplexityMeta = registry.metadata[.perplexity] {
settings.setProviderEnabled(provider: .perplexity, metadata: perplexityMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 32, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .perplexity)
store._setErrorForTesting(nil, provider: .perplexity)
let window = controller.menuBarMetricWindow(for: .perplexity, snapshot: snapshot)
#expect(window?.usedPercent == 32)
}
@Test
func `menu bar percent automatic prefers purchased perplexity credits before bonus`() {
let settings = SettingsStore(
configStore: testConfigStore(
suiteName: "StatusItemAnimationTests-perplexity-automatic-purchased-before-bonus"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .perplexity
settings.setMenuBarMetricPreference(.automatic, for: .perplexity)
let registry = ProviderRegistry.shared
if let perplexityMeta = registry.metadata[.perplexity] {
settings.setProviderEnabled(provider: .perplexity, metadata: perplexityMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 45, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .perplexity)
store._setErrorForTesting(nil, provider: .perplexity)
let window = controller.menuBarMetricWindow(for: .perplexity, snapshot: snapshot)
#expect(window?.usedPercent == 45)
}
@Test
func `menu bar percent primary preference stays on recurring perplexity credits`() {
let settings = SettingsStore(
configStore: testConfigStore(
suiteName: "StatusItemAnimationTests-perplexity-primary-recurring-exhausted"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .perplexity
settings.setMenuBarMetricPreference(.primary, for: .perplexity)
let registry = ProviderRegistry.shared
if let perplexityMeta = registry.metadata[.perplexity] {
settings.setProviderEnabled(provider: .perplexity, metadata: perplexityMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 32, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .perplexity)
store._setErrorForTesting(nil, provider: .perplexity)
let window = controller.menuBarMetricWindow(for: .perplexity, snapshot: snapshot)
#expect(window?.usedPercent == 100)
}
@Test
func `menu bar percent tertiary preference uses purchased perplexity lane`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-perplexity-tertiary-pref"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .perplexity
settings.setMenuBarMetricPreference(.tertiary, for: .perplexity)
let registry = ProviderRegistry.shared
if let perplexityMeta = registry.metadata[.perplexity] {
settings.setProviderEnabled(provider: .perplexity, metadata: perplexityMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: nil,
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 28, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .perplexity)
store._setErrorForTesting(nil, provider: .perplexity)
let window = controller.menuBarMetricWindow(for: .perplexity, snapshot: snapshot)
#expect(window?.usedPercent == 28)
}
@Test
func `menu bar percent tertiary preference uses api lane for cursor`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-cursor-tertiary-pref"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .cursor
settings.setMenuBarMetricPreference(.tertiary, for: .cursor)
let registry = ProviderRegistry.shared
if let cursorMeta = registry.metadata[.cursor] {
settings.setProviderEnabled(provider: .cursor, metadata: cursorMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 72, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .cursor)
store._setErrorForTesting(nil, provider: .cursor)
let window = controller.menuBarMetricWindow(for: .cursor, snapshot: snapshot)
#expect(window?.usedPercent == 72)
}
@Test
func `menu bar tertiary preference falls back to automatic when cursor api lane is missing`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-cursor-tertiary-missing-api"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .cursor
settings.setMenuBarMetricPreference(.tertiary, for: .cursor)
let registry = ProviderRegistry.shared
if let cursorMeta = registry.metadata[.cursor] {
settings.setProviderEnabled(provider: .cursor, metadata: cursorMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 72, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
tertiary: nil,
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .cursor)
store._setErrorForTesting(nil, provider: .cursor)
let window = controller.menuBarMetricWindow(for: .cursor, snapshot: snapshot)
#expect(window?.usedPercent == 72)
}
@Test
func `menu bar display text formats percent and pace`() {
let now = Date(timeIntervalSince1970: 0)
let percentWindow = RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
let paceWindow = RateWindow(
usedPercent: 30,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(60 * 60 * 24 * 6),
resetDescription: nil)
let paceValue = UsagePace.weekly(window: paceWindow, now: now, defaultWindowMinutes: 10080)
let percent = MenuBarDisplayText.displayText(
mode: .percent,
percentWindow: percentWindow,
pace: paceValue,
showUsed: true)
let pace = MenuBarDisplayText.displayText(
mode: .pace,
percentWindow: percentWindow,
pace: paceValue,
showUsed: true)
let both = MenuBarDisplayText.displayText(
mode: .both,
percentWindow: percentWindow,
pace: paceValue,
showUsed: true)
#expect(percent == "40%")
#expect(pace == "+16%")
#expect(both == "40% · +16%")
}
@Test
func `menu bar display text formats codex combined percent lanes`() {
let sessionWindow = RateWindow(usedPercent: 7, windowMinutes: 300, resetsAt: nil, resetDescription: nil)
let weeklyWindow = RateWindow(usedPercent: 18, windowMinutes: 10080, resetsAt: nil, resetDescription: nil)
let remaining = MenuBarDisplayText.combinedSessionWeeklyPercentText(
sessionWindow: sessionWindow,
weeklyWindow: weeklyWindow,
showUsed: false)
let used = MenuBarDisplayText.combinedSessionWeeklyPercentText(
sessionWindow: sessionWindow,
weeklyWindow: weeklyWindow,
showUsed: true)
let weeklyOnly = MenuBarDisplayText.combinedSessionWeeklyPercentText(
sessionWindow: nil,
weeklyWindow: weeklyWindow,
showUsed: false)
let nineHour = MenuBarDisplayText.combinedSessionWeeklyPercentText(
sessionWindow: RateWindow(
usedPercent: 7,
windowMinutes: 540,
resetsAt: nil,
resetDescription: nil),
weeklyWindow: weeklyWindow,
showUsed: false)
let unknownSessionDuration = MenuBarDisplayText.combinedSessionWeeklyPercentText(
sessionWindow: RateWindow(
usedPercent: 7,
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil),
weeklyWindow: weeklyWindow,
showUsed: false)
#expect(remaining == "5h 93% · W 82%")
#expect(used == "5h 7% · W 18%")
#expect(weeklyOnly == "W 82%")
#expect(nineHour == "9h 93% · W 82%")
#expect(unknownSessionDuration == "S 93% · W 82%")
}
@Test
func `menu bar display text falls back to percent when pace unavailable`() {
let percentWindow = RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
let pace = MenuBarDisplayText.displayText(
mode: .pace,
percentWindow: percentWindow,
showUsed: true)
let both = MenuBarDisplayText.displayText(
mode: .both,
percentWindow: percentWindow,
showUsed: true)
#expect(pace == "40%")
#expect(both == "40%")
}
@Test
func `menu bar display text falls back to percent when pace nil for codex`() {
let percentWindow = RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
let pace = MenuBarDisplayText.displayText(
mode: .pace,
percentWindow: percentWindow,
pace: nil,
showUsed: true)
let both = MenuBarDisplayText.displayText(
mode: .both,
percentWindow: percentWindow,
pace: nil,
showUsed: true)
#expect(pace == "40%")
#expect(both == "40%")
}
@Test
func `claude primary menu bar metric computes pace from selected session window`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-primary-pace"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .both
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 80,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 20,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(24 * 60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
#expect(displayText == "20% · +60%")
}
@Test
func `claude combined menu bar metric shows session and weekly lanes`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
#expect(settings.menuBarMetricPreference(for: .claude) == .primaryAndSecondary)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 12,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 45,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(24 * 60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
#expect(displayText == "5h 12% · W 45%")
}
@Test
func `claude combined menu bar metric shows weekly only when session lane is absent`() {
// Mirrors the Claude OAuth path where `five_hour` is missing: the mapper parks the 7-day
// window in BOTH `primary` and `secondary`. The combined metric must not relabel the
// weekly window as a session lane (e.g. "168h 42% · W 42%") — it should show weekly only.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-no-session"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let weekly = RateWindow(
usedPercent: 42,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(24 * 60 * 60),
resetDescription: nil)
let snapshot = UsageSnapshot(primary: weekly, secondary: weekly, updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
#expect(displayText == "W 42%")
}
@Test
func `claude combined menu bar metric paces the weekly lane in both mode`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-pace"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .both
settings.menuBarShowsResetTimeWhenExhausted = false
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
// Session lane is fully consumed, so its own pace is nil; the weekly lane still has room.
// The combined metric must pace the weekly lane, so a pace component must appear even though
// the displayed percent comes from the session lane (here fully consumed).
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 100,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 50,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
// "0% · ±N%": percent from the session lane (here exhausted), pace from the weekly lane.
#expect(displayText?.hasPrefix("0% · ") == true)
}
@Test
func `claude combined menu bar metric pairs session usage with weekly pace`() {
// Regression: in pace/both modes the combined metric must pair the SESSION usage with the
// WEEKLY pace. Previously the usage component came from the most-constrained lane, so when the
// weekly lane was busier than the session lane it showed weekly usage + weekly pace.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-session-pace"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .both
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
// Weekly lane (45%) is busier than the session lane (12%). The usage component must still be the
// session lane, while the pace is computed on the weekly lane (mostly elapsed → pace present).
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 12,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 45,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
// Usage is the session lane (12% used), not the most-constrained weekly lane (45%).
#expect(displayText?.hasPrefix("12% · ") == true)
#expect(displayText?.hasPrefix("45%") == false)
}
@Test
func `codex combined menu bar metric pairs session usage with weekly pace`() {
// The combined metric is shared with Codex, which resolves its lanes through the consumer
// projection. The session usage must headline the pace/both readout there too — not the busier
// weekly lane that drives the icon/bar.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-codex-combined-session-pace"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.menuBarDisplayMode = .both
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
// Weekly lane (91%) is busier than the session lane (12%), but neither is exhausted. The usage
// component must be the session lane while the pace is computed on the weekly lane.
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 12,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 91,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
let displayText = controller.menuBarDisplayText(for: .codex, snapshot: snapshot)
#expect(displayText?.hasPrefix("12% · ") == true)
#expect(displayText?.hasPrefix("91%") == false)
}
@Test
func `claude combined menu bar metric surfaces an exhausted weekly lane in both mode`() {
// When the weekly lane is exhausted it is the binding cap and has no pace, so the combined metric
// must surface it instead of a roomy session number that would hide the spent weekly limit.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-weekly-exhausted"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .both
settings.menuBarShowsResetTimeWhenExhausted = false
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
// Session lane has room (88% remaining); weekly lane is fully consumed (0% remaining).
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 12,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 100,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(60 * 60),
resetDescription: nil),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
// Shows the exhausted weekly lane (0% remaining), not the roomy session lane (88%).
#expect(displayText == "0%")
#expect(displayText?.hasPrefix("88%") == false)
}
@Test
func `claude combined menu bar metric falls back to weekly lane in both mode when session absent`() {
// Five_hour OAuth fallback: the mapper parks the 7-day window in both primary and secondary, so no
// session lane exists. The pace/both usage component must land on the weekly lane, not collapse to
// nil.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-no-session-both"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .both
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let weekly = RateWindow(
usedPercent: 42,
windowMinutes: 7 * 24 * 60,
resetsAt: now.addingTimeInterval(60 * 60),
resetDescription: nil)
let snapshot = UsageSnapshot(primary: weekly, secondary: weekly, updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
// Usage lands on the weekly lane (42% used) rather than collapsing to nil.
#expect(displayText?.hasPrefix("42%") == true)
}
@Test
func `claude combined menu bar metric shows spend limit for a spend-limit-only account`() {
// A Claude account that only exposes an enterprise/extra-usage spend limit has no real
// session/weekly lanes (here a 0% 5h placeholder + a spend limit). With Session + Weekly selected,
// it must surface the spend-limit usage, not the meaningless "5h 0%" placeholder lane.
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-claude-combined-spend-limit"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = true
settings.setMenuBarMetricPreference(.primaryAndSecondary, for: .claude)
let registry = ProviderRegistry.shared
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 0,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil,
isSyntheticPlaceholder: true),
secondary: nil,
providerCost: ProviderCostSnapshot(
used: 45,
limit: 100,
currencyCode: "USD",
period: "Spend limit",
updatedAt: now),
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setErrorForTesting(nil, provider: .claude)
let displayText = controller.menuBarDisplayText(for: .claude, snapshot: snapshot)
// Spend-limit usage (45% of the cap), not the "5h 0%" placeholder lane.
#expect(displayText == "45%")
#expect(displayText?.contains("5h") == false)
}
@Test
func `codex menu bar pace does not fall back to session when weekly projection is unavailable`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-codex-no-weekly-pace"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.menuBarDisplayMode = .both
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let now = Date()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 80,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(4 * 60 * 60),
resetDescription: nil),
secondary: nil,
updatedAt: now)
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
let displayText = controller.menuBarDisplayText(for: .codex, snapshot: snapshot)
#expect(displayText == "20%")
}
@Test
func `menu bar display text uses credits when codex weekly is exhausted`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-credits-fallback"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.secondary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 20, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
let remainingCredits = (snapshot.primary?.usedPercent ?? 0) * 4.5 + (snapshot.secondary?.usedPercent ?? 0) / 10
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
store.credits = CreditsSnapshot(remaining: remainingCredits, events: [], updatedAt: Date())
let displayText = controller.menuBarDisplayText(for: .codex, snapshot: snapshot)
let expected = UsageFormatter
.creditsString(from: remainingCredits)
.replacingOccurrences(of: " left", with: "")
#expect(displayText == expected)
}
@Test
func `menu bar display text uses credits when codex session is exhausted`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-credits-fallback-session"),
zaiTokenStore: NoopZaiTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primary, for: .codex)
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 100, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 40, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
let remainingCredits = (snapshot.primary?.usedPercent ?? 0) - (snapshot.secondary?.usedPercent ?? 0) / 2
store._setSnapshotForTesting(snapshot, provider: .codex)
store._setErrorForTesting(nil, provider: .codex)
store.credits = CreditsSnapshot(remaining: remainingCredits, events: [], updatedAt: Date())
let displayText = controller.menuBarDisplayText(for: .codex, snapshot: snapshot)
let expected = UsageFormatter
.creditsString(from: remainingCredits)
.replacingOccurrences(of: " left", with: "")
#expect(displayText == expected)
}
@Test
func `menu bar display text shows zero percent for kilo zero total edge`() {
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-kilo-zero-edge"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .kilo
settings.menuBarDisplayMode = .percent
settings.usageBarsShowUsed = false
settings.setMenuBarMetricPreference(.primary, for: .kilo)
let registry = ProviderRegistry.shared
if let kiloMeta = registry.metadata[.kilo] {
settings.setProviderEnabled(provider: .kilo, metadata: kiloMeta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let snapshot = KiloUsageSnapshot(
creditsUsed: 0,
creditsTotal: 0,
creditsRemaining: 0,
planName: "Kilo Pass Pro",
autoTopUpEnabled: true,
autoTopUpMethod: "visa",
updatedAt: Date()).toUsageSnapshot()
store._setSnapshotForTesting(snapshot, provider: .kilo)
store._setErrorForTesting(nil, provider: .kilo)
let displayText = controller.menuBarDisplayText(for: .kilo, snapshot: snapshot)
#expect(displayText == "0%")
}
@Test
func `brand image with status overlay returns original image when no issue`() {
let brand = NSImage(size: NSSize(width: 16, height: 16))
brand.isTemplate = true
let output = StatusItemController.brandImageWithStatusOverlay(brand: brand, statusIndicator: .none)
#expect(output === brand)
}
@Test
func `brand image with status overlay draws issue mark`() throws {
let size = NSSize(width: 16, height: 16)
let brand = NSImage(size: size)
brand.lockFocus()
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
brand.unlockFocus()
brand.isTemplate = true
let baselineData = try #require(brand.tiffRepresentation)
let baselineRep = try #require(NSBitmapImageRep(data: baselineData))
let baselineAlpha = self.maxAlpha(in: baselineRep)
let output = StatusItemController.brandImageWithStatusOverlay(brand: brand, statusIndicator: .major)
#expect(output !== brand)
let outputData = try #require(output.tiffRepresentation)
let outputRep = try #require(NSBitmapImageRep(data: outputData))
let outputAlpha = self.maxAlpha(in: outputRep)
#expect(baselineAlpha < 0.01)
#expect(outputAlpha > 0.01)
}
}