Files
2026-07-13 12:22:33 +08:00

1708 lines
72 KiB
Swift

import AppKit
import CodexBarCore
import Testing
@testable import CodexBar
@MainActor
@Suite(.serialized)
struct StatusMenuTests {
func disableMenuCardsForTesting() {
StatusItemController.menuCardRenderingEnabled = false
StatusItemController.setMenuRefreshEnabledForTesting(false)
}
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
}
func makeSettings() -> SettingsStore {
let suite = "StatusMenuTests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
let configStore = testConfigStore(suiteName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: configStore,
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.providerDetectionCompleted = true
return settings
}
func makeCodexStore(settings: SettingsStore, dashboardAuthorized: Bool) -> UsageStore {
let now = Date()
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 22,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(1800),
resetDescription: nil),
secondary: nil,
tertiary: nil,
updatedAt: now,
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "codex@example.com",
accountOrganization: nil,
loginMethod: "Plus Plan")),
provider: .codex)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "other@example.com",
codeReviewRemainingPercent: 88,
codeReviewLimit: RateWindow(
usedPercent: 12,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
updatedAt: now)
store.openAIDashboardAttachmentAuthorized = dashboardAuthorized
store.openAIDashboardRequiresLogin = false
return store
}
private func switcherButtons(in menu: NSMenu) -> [NSButton] {
guard let switcherView = menu.items.first?.view as? ProviderSwitcherView else { return [] }
return switcherView.subviews
.compactMap { $0 as? NSButton }
.sorted { $0.tag < $1.tag }
}
private func representedIDs(in menu: NSMenu) -> [String] {
menu.items.compactMap { $0.representedObject as? String }
}
@Test
func `alibaba dashboard action follows selected region`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.alibabaCodingPlanAPIRegion = .chinaMainland
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())
#expect(controller.dashboardURL(for: .alibaba) == AlibabaCodingPlanAPIRegion.chinaMainland.dashboardURL)
}
@Test
func `zai dashboard action follows selected region`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = 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())
settings.zaiAPIRegion = .global
#expect(controller.dashboardURL(for: .zai) == ZaiAPIRegion.global.dashboardURL)
#expect(
controller.dashboardURL(for: .zai)?.absoluteString ==
"https://z.ai/manage-apikey/coding-plan/personal/my-plan")
#expect(
controller.dashboardURL(
for: .zai,
environment: [ZaiSettingsReader.apiHostKey: "open.bigmodel.cn"]) ==
ZaiAPIRegion.bigmodelCN.dashboardURL)
settings.zaiAPIRegion = .bigmodelCN
#expect(controller.dashboardURL(for: .zai) == ZaiAPIRegion.bigmodelCN.dashboardURL)
#expect(controller.dashboardURL(for: .zai)?.absoluteString == "https://bigmodel.cn/coding-plan/personal/usage")
settings.addTokenAccount(provider: .zai, label: "Team", token: "team-token", usageScope: "team")
#expect(controller.dashboardURL(for: .zai) == ZaiAPIRegion.bigmodelCN.teamDashboardURL)
#expect(
controller.dashboardURL(for: .zai)?.absoluteString ==
"https://bigmodel.cn/coding-plan/team/usage-stats")
}
@Test
func `opencode go dashboard action follows configured workspace`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.opencodegoWorkspaceID = "https://opencode.ai/workspace/wrk_abc123/go"
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())
#expect(controller.dashboardURL(for: .opencodego)?
.absoluteString == "https://opencode.ai/workspace/wrk_abc123/go")
}
@Test
func `claude subscription dashboard action opens usage page`() {
for plan in ["Claude Pro", "Claude Team"] {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
let fetcher = UsageFetcher()
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings)
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 12,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
tertiary: nil,
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .claude,
accountEmail: nil,
accountOrganization: nil,
loginMethod: plan)),
provider: .claude)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
#expect(controller.dashboardURL(for: .claude)?.absoluteString == "https://claude.ai/settings/usage")
}
}
@Test
func `remembers provider when menu opens`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: false)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
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 claudeMenu = controller.makeMenu()
controller.menuWillOpen(claudeMenu)
#expect(controller.lastMenuProvider == .claude)
// No providers enabled: fall back to Codex.
for provider in UsageProvider.allCases {
if let meta = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: meta, enabled: false)
}
}
let unmappedMenu = controller.makeMenu()
controller.menuWillOpen(unmappedMenu)
#expect(controller.lastMenuProvider == .codex)
}
@Test
func `merged menu open does not persist resolved provider when selection is nil`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = nil
let registry = ProviderRegistry.shared
let selectedProviders: Set<UsageProvider> = [.codex, .claude]
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = selectedProviders.contains(provider)
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 expectedResolved = store.enabledProviders().first ?? .codex
#expect(store.enabledProviders().count > 1)
#expect(controller.shouldMergeIcons == true)
let menu = controller.makeMenu()
#expect(settings.selectedMenuProvider == nil)
controller.menuWillOpen(menu)
#expect(settings.selectedMenuProvider == nil)
#expect(controller.lastMenuProvider == expectedResolved)
}
@Test
func `shortcut closes tracked menu instead of queueing another open`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = 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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
#expect(controller.openMenus[key] != nil)
#expect(controller.closeOpenMenusFromShortcutIfNeeded() == true)
#expect(controller.openMenus.isEmpty)
#expect(controller.menuRefreshTasks.isEmpty)
#expect(controller.closeOpenMenusFromShortcutIfNeeded() == false)
}
@Test
func `open menu defers store data refresh until next open`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
StatusItemController.setMenuRefreshEnabledForTesting(true)
defer { StatusItemController.resetMenuRefreshEnabledForTesting() }
let openedVersion = controller.menuVersions[key]
let now = Date()
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 11,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(1800),
resetDescription: nil),
secondary: nil,
tertiary: nil,
updatedAt: now,
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "codex@example.com",
accountOrganization: nil,
loginMethod: "Plus Plan")),
provider: .codex)
for _ in 0..<50 where controller.menuContentVersion == openedVersion {
await Task.yield()
}
let staleVersion = controller.menuContentVersion
controller.refreshOpenMenusIfNeeded()
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
controller.menuDidClose(menu)
controller.menuWillOpen(menu)
#expect(controller.menuVersions[key] == staleVersion)
}
@Test
func `merged menu refresh uses resolved enabled provider when selection is cleared`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.openAIWebAccessEnabled = true
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: false)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
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 event = CreditEvent(date: Date(), service: "CLI", creditsUsed: 1)
let breakdown = OpenAIDashboardSnapshot.makeDailyBreakdown(from: [event], maxDays: 30)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: [event],
dailyBreakdown: breakdown,
usageBreakdown: breakdown,
creditsPurchaseURL: nil,
updatedAt: Date())
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let expectedResolved = store.enabledProviders().first ?? .codex
#expect(store.enabledProviders().count > 1)
#expect(controller.shouldMergeIcons == true)
func hasOpenAIWebSubmenus(_ menu: NSMenu) -> Bool {
let usageItem = menu.items.first { ($0.representedObject as? String) == "menuCardUsage" }
let creditsItem = menu.items.first { ($0.representedObject as? String) == "menuCardCredits" }
let hasUsageBreakdown = usageItem?.submenu?.items
.contains { ($0.representedObject as? String) == "usageBreakdownChart" } == true
let hasCreditsHistory = creditsItem?.submenu?.items
.contains { ($0.representedObject as? String) == "creditsHistoryChart" } == true
return hasUsageBreakdown || hasCreditsHistory
}
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
#expect(controller.lastMenuProvider == expectedResolved)
#expect(settings.selectedMenuProvider == nil)
#expect(hasOpenAIWebSubmenus(menu) == false)
controller.menuContentVersion &+= 1
controller.refreshOpenMenusIfNeeded()
#expect(hasOpenAIWebSubmenus(menu) == false)
}
@Test
func `delayed menu refresh skips when refresh disabled during delay`() async {
StatusItemController.menuCardRenderingEnabled = false
StatusItemController.setMenuRefreshEnabledForTesting(true)
StatusItemController.setMenuOpenRefreshDelayForTesting(.milliseconds(50))
defer {
StatusItemController.resetMenuOpenRefreshDelayForTesting()
StatusItemController.resetMenuRefreshEnabledForTesting()
}
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
var delayedRefreshWakeCount = 0
await withStatusItemControllerForTesting(
store: store,
settings: settings,
fetcher: fetcher,
statusBar: self.makeStatusBarForTesting())
{ controller in
controller.onDelayedMenuRefreshAttemptForTesting = {
delayedRefreshWakeCount += 1
}
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.menuRefreshEnabledOverrideForTesting = false
try? await Task.sleep(for: .milliseconds(180))
}
#expect(delayedRefreshWakeCount == 0)
}
@Test
func `login state callbacks do not attach menus after release`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = 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())
controller.releaseStatusItemsForTesting()
#expect(controller.statusItem.menu == nil)
#expect(controller.statusItems.isEmpty)
controller.activeLoginProvider = .codex
let loginTask = Task<Void, Never> {}
controller.loginTask = loginTask
loginTask.cancel()
controller.loginTask = nil
controller.activeLoginProvider = nil
#expect(controller.statusItem.menu == nil)
#expect(controller.statusItems.isEmpty)
}
@Test
func `display only dashboard does not show code review in status menu card`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
let fetcher = UsageFetcher()
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let model = try #require(controller.menuCardModel(for: .codex))
#expect(model.metrics.contains { $0.id == "code-review" } == false)
}
@Test
func `display only dashboard does not show code review in providers pane`() {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
let pane = ProvidersPane(settings: settings, store: store)
let model = pane._test_menuCardModel(for: .codex)
#expect(model.metrics.contains { $0.id == "code-review" } == false)
}
@Test
func `attached dashboard still shows code review in providers pane`() {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: true)
let pane = ProvidersPane(settings: settings, store: store)
let model = pane._test_menuCardModel(for: .codex)
#expect(model.metrics.contains { $0.id == "code-review" && $0.percent == 88 })
}
@Test
func `open merged menu rebuilds switcher when usage bars mode changes`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.usageBarsShowUsed = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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())
#expect(store.enabledProviders().count == 2)
#expect(controller.shouldMergeIcons == true)
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.openMenus[ObjectIdentifier(menu)] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let initialSwitcher = menu.items.first?.view as? ProviderSwitcherView
#expect(initialSwitcher != nil)
let initialSwitcherID = initialSwitcher.map(ObjectIdentifier.init)
settings.usageBarsShowUsed = true
controller.handleProviderConfigChange(reason: "usageBarsShowUsed")
for _ in 0..<20 {
await Task.yield()
}
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(ObjectIdentifier(menu)))
if let initialSwitcherID, let currentSwitcher = menu.items.first?.view as? ProviderSwitcherView {
#expect(initialSwitcherID == ObjectIdentifier(currentSwitcher))
}
controller.menuDidClose(menu)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let updatedSwitcher = menu.items.first?.view as? ProviderSwitcherView
#expect(updatedSwitcher != nil)
if let initialSwitcherID, let updatedSwitcher {
#expect(initialSwitcherID != ObjectIdentifier(updatedSwitcher))
}
}
@Test
func `merged switcher includes overview tab when multiple providers enabled`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.mergedMenuLastSelectedWasOverview = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let buttons = self.switcherButtons(in: menu)
#expect(buttons.count == store.enabledProvidersForDisplay().count + 1)
#expect(buttons.contains(where: { $0.tag == 0 }))
#expect(buttons.first(where: { $0.state == .on })?.tag == 2)
}
@Test
func `merged switcher overview selection persists without overwriting provider selection`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.mergedMenuLastSelectedWasOverview = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let overviewButton = self.switcherButtons(in: menu).first(where: { $0.tag == 0 })
#expect(overviewButton != nil)
overviewButton?.performClick(nil)
#expect(settings.mergedMenuLastSelectedWasOverview == true)
#expect(settings.selectedMenuProvider == .claude)
controller.menuDidClose(menu)
let reopenedMenu = controller.makeMenu()
controller.menuWillOpen(reopenedMenu)
let reopenedSelectedTag = self.switcherButtons(in: reopenedMenu).first(where: { $0.state == .on })?.tag
#expect(reopenedSelectedTag == 0)
#expect(settings.selectedMenuProvider == .claude)
}
@Test
func `open menu rebuilds switcher when overview availability changes`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.mergedMenuLastSelectedWasOverview = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
let activeProviders: [UsageProvider] = [.codex, .claude]
_ = settings.setMergedOverviewProviderSelection(
provider: .codex,
isSelected: false,
activeProviders: activeProviders)
_ = settings.setMergedOverviewProviderSelection(
provider: .claude,
isSelected: false,
activeProviders: activeProviders)
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.openMenus[ObjectIdentifier(menu)] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let initialButtons = self.switcherButtons(in: menu)
#expect(initialButtons.count == activeProviders.count)
_ = settings.setMergedOverviewProviderSelection(
provider: .codex,
isSelected: true,
activeProviders: activeProviders)
controller.menuContentVersion &+= 1
controller.menuDidClose(menu)
controller.menuWillOpen(menu)
let updatedButtons = self.switcherButtons(in: menu)
#expect(updatedButtons.count == activeProviders.count + 1)
}
@Test
func `overview tab omits contextual provider actions`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.mergedMenuLastSelectedWasOverview = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude || provider == .cursor
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let titles = Set(menu.items.map(\.title))
#expect(!titles.contains("Add Account..."))
#expect(!titles.contains("Switch Account..."))
#expect(!titles.contains("Usage Dashboard"))
#expect(!titles.contains("Status Page"))
#expect(titles.contains("Refresh"))
#expect(titles.contains("Settings..."))
#expect(titles.contains("About CodexBar"))
#expect(titles.contains("Quit"))
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
#expect(refreshItem.view is PersistentRefreshMenuView)
#expect(refreshItem.keyEquivalent.isEmpty)
#expect(refreshItem.keyEquivalentModifierMask.isEmpty)
let settingsItem = menu.items.first { $0.title == "Settings..." }
#expect(settingsItem != nil)
#expect(settingsItem?.keyEquivalent == ",")
#expect(settingsItem?.keyEquivalentModifierMask == [.command])
let quitItem = menu.items.first { $0.title == "Quit" }
#expect(quitItem != nil)
#expect(quitItem?.keyEquivalent == "q")
#expect(quitItem?.keyEquivalentModifierMask == [.command])
}
}
@MainActor
extension StatusMenuTests {
@Test
func `status blurb uses wrapped view-backed menu item`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = true
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let statusText = "An SSL error has occurred and a secure connection to the server cannot be made."
store.statuses[.codex] = ProviderStatus(
indicator: .critical,
description: statusText,
updatedAt: nil)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let statusItem = menu.items.first(where: { $0.toolTip == statusText })
#expect(statusItem != nil)
#expect(statusItem?.view != nil)
#expect(statusItem?.title.isEmpty == true)
#expect(statusItem?.view?.frame.width == 310)
}
@Test
func `provider toggle updates status item visibility`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
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() }
#expect(controller.statusItems[.claude]?.isVisible == true)
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
}
controller.handleProviderConfigChange(reason: "test")
#expect(controller.statusItems[.claude] == nil)
}
@Test
func `provider config changes preserve status item instances`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
try settings.setProviderEnabled(provider: .codex, metadata: #require(registry.metadata[.codex]), enabled: true)
try settings.setProviderEnabled(
provider: .claude,
metadata: #require(registry.metadata[.claude]),
enabled: true)
try settings.setProviderEnabled(
provider: .gemini,
metadata: #require(registry.metadata[.gemini]),
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())
let codexItem = try #require(controller.statusItems[.codex])
#expect(controller.statusItem.autosaveName == "codexbar-merged")
#expect(codexItem.autosaveName == "codexbar-codex")
try settings.setProviderEnabled(
provider: .gemini,
metadata: #require(registry.metadata[.gemini]),
enabled: true)
controller.handleProviderConfigChange(reason: "test")
#expect(controller.statusItems[.codex] === codexItem)
#expect(controller.statusItems[.codex]?.autosaveName == "codexbar-codex")
#expect(controller.statusItems[.gemini]?.autosaveName == "codexbar-gemini")
}
@Test
func `hides open AI web submenus when no history`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
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 claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
}
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)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
updatedAt: Date())
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let titles = Set(menu.items.map(\.title))
#expect(!titles.contains("Credits history"))
#expect(!titles.contains("Usage breakdown"))
}
@Test
func `hides open AI web submenus when open AI web extras disabled`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.openAIWebAccessEnabled = false
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
}
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 event = CreditEvent(date: Date(), service: "CLI", creditsUsed: 1)
let breakdown = OpenAIDashboardSnapshot.makeDailyBreakdown(from: [event], maxDays: 30)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: [event],
dailyBreakdown: breakdown,
usageBreakdown: breakdown,
creditsPurchaseURL: nil,
updatedAt: Date())
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let titles = Set(menu.items.map(\.title))
#expect(!titles.contains("Credits history"))
#expect(!titles.contains("Usage breakdown"))
}
@Test
func `hosted chart submenu matches widened parent menu width`() {
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
let previousMenuRefresh = StatusItemController.menuRefreshEnabled
StatusItemController.menuCardRenderingEnabled = true
StatusItemController.setMenuRefreshEnabledForTesting(false)
defer {
StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
StatusItemController.setMenuRefreshEnabledForTesting(previousMenuRefresh)
}
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let event = CreditEvent(date: Date(), service: "CLI", creditsUsed: 1)
let breakdown = OpenAIDashboardSnapshot.makeDailyBreakdown(from: [event], maxDays: 30)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: [event],
dailyBreakdown: breakdown,
usageBreakdown: breakdown,
creditsPurchaseURL: nil,
updatedAt: Date())
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let parentMenu = NSMenu()
parentMenu.autoenablesItems = false
let wideItem = NSMenuItem(title: String(repeating: "W", count: 60), action: nil, keyEquivalent: "")
parentMenu.addItem(wideItem)
let submenu = controller.makeHostedSubviewPlaceholderMenu(chartID: StatusItemController.usageBreakdownChartID)
let submenuItem = NSMenuItem(title: "Usage breakdown", action: nil, keyEquivalent: "")
submenuItem.submenu = submenu
parentMenu.addItem(submenuItem)
let parentWidth = ceil(parentMenu.size.width)
#expect(parentWidth > 310)
controller.hydrateHostedSubviewMenuIfNeeded(submenu)
let chartItem = submenu.items.first
#expect(chartItem?.representedObject as? String == StatusItemController.usageBreakdownChartID)
#expect(chartItem?.view != nil)
#expect(abs((chartItem?.view?.frame.width ?? 0) - parentWidth) <= 0.5)
}
@Test
func `hosted storage submenu is height capped and scroll enabled`() {
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
let previousMenuRefresh = StatusItemController.menuRefreshEnabled
StatusItemController.menuCardRenderingEnabled = true
StatusItemController.setMenuRefreshEnabledForTesting(false)
defer {
StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
StatusItemController.setMenuRefreshEnabledForTesting(previousMenuRefresh)
}
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.providerStorageFootprintsEnabled = true
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let root = "/Users/test/.claude"
store.providerStorageFootprints[.claude] = ProviderStorageFootprint(
provider: .claude,
totalBytes: 1_756_000_000,
paths: [root],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "\(root)/projects", totalBytes: 1_500_000_000),
.init(path: "\(root)/file-history", totalBytes: 103_000_000),
.init(path: "\(root)/telemetry", totalBytes: 51_000_000),
.init(path: "\(root)/plugins", totalBytes: 33_000_000),
.init(path: "\(root)/history.jsonl", totalBytes: 3_800_000),
.init(path: "\(root)/shell-snapshots", totalBytes: 1_500_000),
.init(path: "\(root)/plans", totalBytes: 1_100_000),
.init(path: "\(root)/paste-cache", totalBytes: 541_000),
.init(path: "\(root)/session-env", totalBytes: 208_000),
.init(path: "\(root)/todos", totalBytes: 6700),
],
updatedAt: Date(timeIntervalSince1970: 0))
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
let submenu = NSMenu()
let didAppend = controller.appendStorageBreakdownItem(to: submenu, provider: .claude, width: 310)
#expect(didAppend)
let item = submenu.items.first
#expect(item?.isEnabled == true)
#expect((item?.view?.frame.height ?? 0) <= 620)
}
@Test
func `shows open AI web submenus when history exists`() throws {
self.disableMenuCardsForTesting()
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "StatusMenuTests-history"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.openAIWebAccessEnabled = true
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
}
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 calendar = Calendar(identifier: .gregorian)
var components = DateComponents()
components.calendar = calendar
components.timeZone = TimeZone(secondsFromGMT: 0)
components.year = 2025
components.month = 12
components.day = 18
let date = try #require(components.date)
let events = [CreditEvent(date: date, service: "CLI", creditsUsed: 1)]
let breakdown = OpenAIDashboardSnapshot.makeDailyBreakdown(from: events, maxDays: 30)
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: events,
dailyBreakdown: breakdown,
usageBreakdown: breakdown,
creditsPurchaseURL: nil,
updatedAt: Date())
store.openAIDashboardAttachmentAuthorized = true
store.openAIDashboardRequiresLogin = false
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let usageItem = menu.items.first { ($0.representedObject as? String) == "menuCardUsage" }
let creditsItem = menu.items.first { ($0.representedObject as? String) == "menuCardCredits" }
let creditsHistoryItem = menu.items.first { item in
item.submenu?.items.contains { ($0.representedObject as? String) == "creditsHistoryChart" } == true
}
#expect(
usageItem?.submenu?.items
.contains { ($0.representedObject as? String) == "usageBreakdownChart" } == true)
#expect(creditsItem == nil && creditsHistoryItem != nil)
}
@Test
func `shows open AI API usage chart submenu without codex web history`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.selectedMenuProvider = .openai
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let registry = ProviderRegistry.shared
let metadata = try #require(registry.metadata[.openai])
settings.setProviderEnabled(provider: .openai, metadata: metadata, enabled: true)
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
let now = Date(timeIntervalSince1970: 1_700_000_000)
let usage = OpenAIAPIUsageSnapshot(
daily: [
OpenAIAPIUsageSnapshot.DailyBucket(
day: "2023-11-14",
startTime: now,
endTime: now.addingTimeInterval(86400),
costUSD: 9,
requests: 12,
inputTokens: 100,
cachedInputTokens: 0,
outputTokens: 50,
totalTokens: 150,
lineItems: [],
models: []),
],
updatedAt: now)
store._setSnapshotForTesting(usage.toUsageSnapshot(), provider: .openai)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu(for: .openai)
controller.menuWillOpen(menu)
let usageItem = menu.items.first { ($0.representedObject as? String) == "menuCardUsage" }
#expect(usageItem?.submenu?.items
.contains { ($0.representedObject as? String) == StatusItemController.costHistoryChartID } == true)
#expect(menu.items.contains { ($0.representedObject as? String) == "menuCardHeader" } == false)
#expect(menu.items.contains { ($0.representedObject as? String) == "menuCardExtraUsage" } == false)
}
@Test
func `shows credits before cost in codex menu card sections`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let registry = ProviderRegistry.shared
let metadata = registry.metadata
try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true)
try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: false)
try settings.setProviderEnabled(provider: .gemini, metadata: #require(metadata[.gemini]), enabled: false)
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
store.credits = CreditsSnapshot(remaining: 100, events: [], updatedAt: Date())
store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "user@example.com",
codeReviewRemainingPercent: 100,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
updatedAt: Date())
store.openAIDashboardAttachmentAuthorized = true
store.openAIDashboardRequiresLogin = false
store._setTokenSnapshotForTesting(CostUsageTokenSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 123,
last30DaysCostUSD: 1.23,
daily: [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: nil,
outputTokens: nil,
totalTokens: 123,
costUSD: 1.23,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: Date()), provider: .codex)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = menu.items.compactMap { $0.representedObject as? String }
let creditsIndex = ids.firstIndex(of: "menuCardCredits")
let costIndex = ids.firstIndex(of: "menuCardCost")
#expect(creditsIndex != nil)
#expect(costIndex != nil)
#expect(try #require(creditsIndex) < costIndex!)
}
@Test
func `hosted cost submenu preserves provider context after empty hydration`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.costUsageEnabled = true
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 submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.costHistoryChartID,
provider: .codex)
#expect(submenu.autoenablesItems == false)
#expect(submenu.items.first?.isEnabled == true)
controller.hydrateHostedSubviewMenuIfNeeded(submenu)
#expect(submenu.items.count == 1)
#expect(submenu.items.first?.title == "No data available")
#expect(submenu.items.first?.toolTip == UsageProvider.codex.rawValue)
store._setTokenSnapshotForTesting(CostUsageTokenSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 123,
last30DaysCostUSD: 1.23,
daily: [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: nil,
outputTokens: nil,
totalTokens: 123,
costUSD: 1.23,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: Date()), provider: .codex)
controller.hydrateHostedSubviewMenuIfNeeded(submenu)
#expect(submenu.items.count == 1)
#expect(submenu.items.first?.title != "No data available")
#expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
#expect(submenu.items.first?.isEnabled == true)
}
@Test
func `shows extra usage for claude when using menu card sections`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
settings.claudeWebExtrasEnabled = true
let registry = ProviderRegistry.shared
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: false)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: true)
}
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 identity = ProviderIdentitySnapshot(
providerID: .claude,
accountEmail: "user@example.com",
accountOrganization: nil,
loginMethod: "web")
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 10, windowMinutes: nil, resetsAt: nil, resetDescription: "Resets soon"),
secondary: nil,
tertiary: nil,
providerCost: ProviderCostSnapshot(
used: 0,
limit: 2000,
currencyCode: "EUR",
period: "Monthly",
resetsAt: nil,
updatedAt: Date()),
updatedAt: Date(),
identity: identity)
store._setSnapshotForTesting(snapshot, provider: .claude)
store._setTokenSnapshotForTesting(CostUsageTokenSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 123,
last30DaysCostUSD: 1.23,
daily: [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: nil,
outputTokens: nil,
totalTokens: 123,
costUSD: 1.23,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: Date()), provider: .claude)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = menu.items.compactMap { $0.representedObject as? String }
#expect(ids.contains("menuCardExtraUsage"))
}
@Test
func `shows vertex cost when usage error present`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .vertexai
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let registry = ProviderRegistry.shared
if let vertexMeta = registry.metadata[.vertexai] {
settings.setProviderEnabled(provider: .vertexai, metadata: vertexMeta, enabled: true)
}
if let codexMeta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: false)
}
if let claudeMeta = registry.metadata[.claude] {
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
}
let fetcher = UsageFetcher()
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
store._setErrorForTesting("No Vertex AI usage data found for the current project.", provider: .vertexai)
store._setTokenSnapshotForTesting(CostUsageTokenSnapshot(
sessionTokens: 10,
sessionCostUSD: 0.01,
last30DaysTokens: 100,
last30DaysCostUSD: 1.0,
daily: [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: nil,
outputTokens: nil,
totalTokens: 100,
costUSD: 1.0,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: Date()), provider: .vertexai)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = menu.items.compactMap { $0.representedObject as? String }
#expect(ids.contains("menuCardCost"))
}
}
extension StatusMenuTests {
@Test
func `overview tab renders overview rows for all active providers when three or fewer`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.mergedMenuLastSelectedWasOverview = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude || provider == .cursor
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = self.representedIDs(in: menu)
let overviewRows = ids.filter { $0.hasPrefix("overviewRow-") }
#expect(overviewRows.count == 3)
#expect(overviewRows.contains("overviewRow-codex"))
#expect(overviewRows.contains("overviewRow-claude"))
#expect(overviewRows.contains("overviewRow-cursor"))
#expect(ids.contains("menuCard") == false)
}
@Test
func `overview tab honors stored subset when three or fewer`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.mergedMenuLastSelectedWasOverview = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude || provider == .cursor
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
_ = settings.setMergedOverviewProviderSelection(
provider: .claude,
isSelected: false,
activeProviders: [.codex, .claude, .cursor])
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = self.representedIDs(in: menu)
let overviewRows = ids.filter { $0.hasPrefix("overviewRow-") }
#expect(overviewRows.count == 2)
#expect(overviewRows.contains("overviewRow-codex"))
#expect(overviewRows.contains("overviewRow-cursor"))
#expect(overviewRows.contains("overviewRow-claude") == false)
#expect(ids.contains("menuCard") == false)
}
@Test
func `overview tab with explicit empty selection is hidden and shows provider detail`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.mergedMenuLastSelectedWasOverview = true
settings.mergedOverviewSelectedProviders = []
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex ||
provider == .claude ||
provider == .cursor ||
provider == .opencode
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let ids = self.representedIDs(in: menu)
let switcherButtons = self.switcherButtons(in: menu)
#expect(switcherButtons.count == store.enabledProvidersForDisplay().count)
#expect(switcherButtons.contains(where: { $0.title == "Overview" }) == false)
#expect(switcherButtons.contains(where: { $0.state == .on && $0.tag == 0 }))
#expect(ids.contains("menuCard"))
#expect(ids.contains(where: { $0.hasPrefix("overviewRow-") }) == false)
#expect(ids.contains("overviewEmptyState") == false)
#expect(menu.items.contains(where: { $0.title == "No providers selected for Overview." }) == false)
}
@Test
func `overview rows keep menu item action in rendered mode`() throws {
StatusItemController.menuCardRenderingEnabled = true
StatusItemController.setMenuRefreshEnabledForTesting(false)
defer { self.disableMenuCardsForTesting() }
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
settings.mergedMenuLastSelectedWasOverview = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
guard let metadata = registry.metadata[provider] else { continue }
let shouldEnable = provider == .codex || provider == .claude
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable)
}
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 menu = controller.makeMenu()
controller.menuWillOpen(menu)
let claudeRow = try #require(menu.items.first {
($0.representedObject as? String) == "overviewRow-claude"
})
#expect(claudeRow.action != nil)
#expect(claudeRow.target is StatusItemController)
}
}