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

1576 lines
67 KiB
Swift

import AppKit
import CodexBarCore
import SwiftUI
import Testing
@testable import CodexBar
private final class RefreshShortcutRecorder: StatusItemMenuPersistentActionDelegate {
var refreshCount = 0
var refreshMenuIDs: [ObjectIdentifier] = []
var refreshMenuInteractionGenerations: [Int] = []
var settingsCount = 0
var quitCount = 0
var navigationDirections: [StatusItemMenuProviderNavigationDirection] = []
func performPersistentRefreshAction(
in menuID: ObjectIdentifier,
menuInteractionGeneration: Int)
{
self.refreshCount += 1
self.refreshMenuIDs.append(menuID)
self.refreshMenuInteractionGenerations.append(menuInteractionGeneration)
}
func performPersistentSettingsAction() {
self.settingsCount += 1
}
func performPersistentQuitAction() {
self.quitCount += 1
}
func performProviderNavigation(_ direction: StatusItemMenuProviderNavigationDirection) {
self.navigationDirections.append(direction)
}
}
@MainActor
private final class UpdateReadyUpdater: UpdaterProviding {
var automaticallyChecksForUpdates = false
var automaticallyDownloadsUpdates = false
let isAvailable = true
let unavailableReason: String? = nil
let updateStatus = UpdateStatus(isUpdateReady: true)
func checkForUpdates(_: Any?) {}
func installUpdate() {}
}
@MainActor
private final class ManualRefreshGate {
private var continuation: CheckedContinuation<Void, Never>?
private var isOpen = false
func wait() async {
if self.isOpen {
self.isOpen = false
return
}
await withCheckedContinuation { continuation in
self.continuation = continuation
}
}
func resume() {
if let continuation = self.continuation {
continuation.resume()
self.continuation = nil
} else {
self.isOpen = true
}
}
func waitUntilSignaled(timeout: Duration = .seconds(5)) async -> Bool {
let deadline = ContinuousClock.now + timeout
while !self.isOpen {
if ContinuousClock.now >= deadline {
return false
}
try? await Task.sleep(for: .milliseconds(10))
}
self.isOpen = false
return true
}
}
enum BlockingEnrichmentStage: Sendable {
case credits
case dashboard
}
@MainActor
@Suite(.serialized)
struct StatusMenuPersistentRefreshTests {
private func makeSettings() -> SettingsStore {
testSettingsStore(suiteName: "StatusMenuPersistentRefreshTests")
}
private func makeController(
settings: SettingsStore,
updater: UpdaterProviding = DisabledUpdaterController(),
account: AccountInfo? = nil) -> StatusItemController
{
let environment = Self.isolatedEnvironment()
let fetcher = UsageFetcher(environment: environment)
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
startupBehavior: .testing,
environmentBase: environment)
if let account {
store.accountInfoCache[.codex] = UsageStore.AccountInfoCacheEntry(
account: account,
configRevision: settings.configRevision,
expiresAt: .distantFuture)
}
return StatusItemController(
store: store,
settings: settings,
account: account ?? AccountInfo(email: nil, plan: nil),
updater: updater,
preferencesSelection: PreferencesSelection(),
statusBar: .system)
}
private static func isolatedEnvironment() -> [String: String] {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-tests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
return [
"HOME": root.path,
"CODEX_HOME": root.appendingPathComponent(".codex", isDirectory: true).path,
"XDG_CONFIG_HOME": root.appendingPathComponent(".config", isDirectory: true).path,
]
}
private func enableOnly(_ providers: Set<UsageProvider>, settings: SettingsStore) {
for provider in UsageProvider.allCases {
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: providers.contains(provider))
}
}
private static func makeTokenSnapshot() -> CostUsageTokenSnapshot {
CostUsageTokenSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 456,
last30DaysCostUSD: 1.23,
daily: [],
updatedAt: Date())
}
@Test
func `refresh row is custom and appears above settings`() throws {
let previousRendering = StatusItemController.menuCardRenderingEnabled
StatusItemController.menuCardRenderingEnabled = true
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
let settingsItem = try #require(menu.items.first { $0.title == "Settings..." })
let refreshIndex = try #require(menu.items.firstIndex(where: { $0 === refreshItem }))
let settingsIndex = try #require(menu.items.firstIndex(where: { $0 === settingsItem }))
#expect(refreshItem.action == nil)
#expect(refreshItem.target == nil)
let refreshView = try #require(refreshItem.view)
#expect(refreshView is any MenuCardHighlighting)
#expect(refreshView.fittingSize.height > 0)
#expect(controller.isPersistentRefreshItem(refreshItem))
#expect(refreshItem.keyEquivalent.isEmpty)
#expect(refreshItem.keyEquivalentModifierMask.isEmpty)
#expect(refreshIndex < settingsIndex)
}
@Test
func `persistent refresh installs tracking monitor and handles command R without native shortcut`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
#expect(controller.providerSwitcherShortcutEventMonitor != nil)
#expect(controller.providerSwitcherShortcutMenuID == ObjectIdentifier(menu))
#expect(refreshItem.keyEquivalent.isEmpty)
let gate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await gate.wait() }
#expect(try controller.handleMenuTrackingShortcutEvent(self.keyEvent("r", keyCode: 15), menu: menu))
for _ in 0..<20 where controller.manualRefreshTasks[.provider(.codex)] == nil {
await Task.yield()
}
let task = try #require(controller.manualRefreshTasks[.provider(.codex)])
#expect(!refreshItem.isEnabled)
gate.resume()
await task.value
#expect(controller.manualRefreshTasks[.provider(.codex)] == nil)
#expect(refreshItem.isEnabled)
}
@Test
func `only refresh uses a custom row while standard actions stay native`() throws {
let previousRendering = StatusItemController.menuCardRenderingEnabled
StatusItemController.menuCardRenderingEnabled = true
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings, updater: UpdateReadyUpdater())
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let updateItem = try #require(menu.items.first { $0.title == "Update ready, restart now?" })
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(MenuDescriptor.MenuAction.installUpdate.systemImageName == "arrow.down.circle")
#expect(MenuDescriptor.MenuAction.dashboard.systemImageName == "chart.xyaxis.line")
#expect(updateItem.image != nil)
#expect(refreshItem.view is any MenuCardHighlighting)
#expect(refreshItem.action == nil)
#expect(controller.isPersistentRefreshItem(refreshItem))
#expect(refreshItem.keyEquivalent.isEmpty)
#expect(refreshItem.keyEquivalentModifierMask.isEmpty)
#expect(updateItem.view == nil)
#expect(updateItem.action != nil)
#expect(updateItem.target === controller)
for (title, key) in [("Settings...", ","), ("About CodexBar", ""), ("Quit", "q")] {
let item = try #require(menu.items.first { $0.title == title })
#expect(item.view == nil)
#expect(item.action != nil)
#expect(item.target === controller)
#expect(item.keyEquivalent == key)
if !key.isEmpty {
#expect(item.keyEquivalentModifierMask == [.command])
}
}
}
@Test
func `persistent refresh row reflects scoped global and manual refresh state`() throws {
let previousRendering = StatusItemController.menuCardRenderingEnabled
StatusItemController.menuCardRenderingEnabled = true
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
#expect(controller.persistentRefreshItems.allObjects.contains { $0 === refreshItem })
#expect(refreshItem.isEnabled)
controller.store.refreshingProviders.insert(.claude)
controller.updatePersistentRefreshItemsEnabled()
#expect(refreshItem.isEnabled)
controller.store.refreshingProviders.insert(.codex)
controller.updatePersistentRefreshItemsEnabled()
#expect(!refreshItem.isEnabled)
controller.store.refreshingProviders.removeAll()
controller.store.isRefreshing = true
controller.updatePersistentRefreshItemsEnabled()
#expect(!refreshItem.isEnabled)
controller.store.isRefreshing = false
// A manual refresh scoped to another provider must not grey out this provider's row.
controller.manualRefreshTasks[.provider(.claude)] = Task {}
controller.updatePersistentRefreshItemsEnabled()
#expect(refreshItem.isEnabled)
// This provider's own manual refresh does disable its row.
controller.manualRefreshTasks[.provider(.codex)] = Task {}
controller.updatePersistentRefreshItemsEnabled()
#expect(!refreshItem.isEnabled)
controller.manualRefreshTasks[.provider(.codex)] = nil
controller.manualRefreshTasks[.provider(.claude)] = nil
controller.updatePersistentRefreshItemsEnabled()
#expect(refreshItem.isEnabled)
// An all-providers refresh greys every row.
controller.manualRefreshTasks[.global] = Task {}
controller.updatePersistentRefreshItemsEnabled()
#expect(!refreshItem.isEnabled)
controller.manualRefreshTasks[.global] = nil
controller.updatePersistentRefreshItemsEnabled()
#expect(refreshItem.isEnabled)
refreshItem.representedObject = "notRefresh"
controller.manualRefreshTasks[.global] = Task {}
controller.updatePersistentRefreshItemsEnabled()
#expect(refreshItem.isEnabled)
#expect(!controller.persistentRefreshItems.allObjects.contains { $0 === refreshItem })
controller.manualRefreshTasks[.global] = nil
}
@Test
func `refresh monitor follows refresh success and failure`() {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let monitor = controller.menuCardRefreshMonitor
let fallback = MenuCardLiveSubtitle(text: "Fallback", style: .info)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
controller.store.refreshingProviders.insert(.codex)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .loading)
controller.store.refreshingProviders.remove(.codex)
monitor.beginManualRefresh(frozenModels: [:], provider: nil)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
controller.store.refreshingProviders.insert(.codex)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .loading)
monitor.endManualRefresh()
controller.store.refreshingProviders.remove(.codex)
let now = Date()
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 10,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let success = monitor.subtitle(for: .codex, fallback: fallback)
#expect(success.style == .info)
#expect(success.text == UsageFormatter.updatedString(from: now, now: Date()))
controller.store.errors[.codex] = "Refresh failed"
let failure = monitor.subtitle(for: .codex, fallback: fallback)
#expect(failure.style == .error)
#expect(failure.text == "Refresh failed")
monitor.beginManualRefresh(frozenModels: [:], provider: nil)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .error)
controller.store.refreshingProviders.insert(.codex)
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .loading)
controller.store.refreshingProviders.remove(.codex)
}
@Test
func `scoped refresh monitor leaves unrelated providers unchanged`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let monitor = controller.menuCardRefreshMonitor
let codexModel = try #require(controller.menuCardModel(for: .codex))
let fallback = MenuCardLiveSubtitle(text: "Claude idle", style: .info)
let expectedClaude = monitor.subtitle(for: .claude, fallback: fallback)
monitor.beginManualRefresh(frozenModels: [.codex: codexModel], provider: .codex)
defer { monitor.endManualRefresh(for: .codex) }
#expect(monitor.isManualRefreshInFlight(for: .codex))
#expect(!monitor.isManualRefreshInFlight(for: .claude))
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .loading)
let actualClaude = monitor.subtitle(for: .claude, fallback: fallback)
#expect(actualClaude.text == expectedClaude.text)
#expect(actualClaude.style == expectedClaude.style)
}
@Test
func `refresh monitor updates compatible usage values after manual refresh completes`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let now = Date()
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 10,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 20,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7200),
resetDescription: nil),
updatedAt: now)
let fallback = try #require(controller.menuCardModel(for: .claude))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [:], provider: .claude)
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 65,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 75,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7200),
resetDescription: nil),
updatedAt: now.addingTimeInterval(1))
let inFlight = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
#expect(inFlight.metrics.map(\.percent) == fallback.metrics.map(\.percent))
controller.menuCardRefreshMonitor.endManualRefresh(for: .claude)
let refreshed = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
let expected = try #require(controller.menuCardModel(for: .claude))
#expect(refreshed.metrics.map(\.percent) == expected.metrics.map(\.percent))
#expect(refreshed.metrics.map(\.percent) != fallback.metrics.map(\.percent))
}
@Test
func `manual refresh keeps frozen quota even if menu rebuilds before completion`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let now = Date()
for provider in [UsageProvider.claude, .codex] {
controller.store.snapshots[provider] = UsageSnapshot(
primary: RateWindow(
usedPercent: 21,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: provider))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [provider: frozen])
controller.store.refreshingProviders.insert(provider)
controller.store.snapshots[provider] = UsageSnapshot(
primary: RateWindow(
usedPercent: 18,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now.addingTimeInterval(1))
let rebuiltFallback = try #require(controller.menuCardModel(for: provider))
let inFlight = controller.menuCardRefreshMonitor.model(for: provider, fallback: rebuiltFallback)
#expect(frozen.metrics.first?.percentLabel == "79% left")
#expect(rebuiltFallback.metrics.first?.percentLabel == "82% left")
#expect(inFlight.metrics.first?.percentLabel == "79% left")
controller.menuCardRefreshMonitor.endManualRefresh()
controller.store.refreshingProviders.remove(provider)
let completed = controller.menuCardRefreshMonitor.model(for: provider, fallback: frozen)
#expect(completed.metrics.first?.percentLabel == "82% left")
}
}
@Test
func `manual refresh uses fallback when frozen quota layout is incompatible`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let now = Date()
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 21,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .claude))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [.claude: frozen])
controller.store.refreshingProviders.insert(.claude)
defer { controller.store.refreshingProviders.remove(.claude) }
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 18,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 12,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7 * 24 * 60 * 60),
resetDescription: nil),
updatedAt: now.addingTimeInterval(1))
let rebuiltFallback = try #require(controller.menuCardModel(for: .claude))
let inFlight = controller.menuCardRefreshMonitor.model(for: .claude, fallback: rebuiltFallback)
#expect(frozen.metrics.count == 1)
#expect(rebuiltFallback.metrics.count == 2)
#expect(inFlight.metrics.count == 2)
#expect(inFlight.metrics.map(\.id) == rebuiltFallback.metrics.map(\.id))
}
@Test
func `manual refresh preserves frozen quota when supplemental metric remains`() throws {
let settings = self.makeSettings()
let controller = self.makeController(
settings: settings,
account: AccountInfo(email: "test@example.com", plan: "pro"))
let now = Date()
controller.store.openAIDashboard = OpenAIDashboardSnapshot(
signedInEmail: "test@example.com",
codeReviewRemainingPercent: 88,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
updatedAt: now)
controller.store.openAIDashboardAttachmentAuthorized = true
controller.store.openAIDashboardRequiresLogin = false
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 21,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 12,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7 * 24 * 60 * 60),
resetDescription: nil),
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .codex))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [.codex: frozen])
controller.store.refreshingProviders.insert(.codex)
defer { controller.store.refreshingProviders.remove(.codex) }
controller.store.snapshots[.codex] = UsageSnapshot(
primary: nil,
secondary: nil,
updatedAt: now.addingTimeInterval(1))
let fallback = try #require(controller.menuCardModel(for: .codex))
let inFlight = controller.menuCardRefreshMonitor.model(for: .codex, fallback: fallback)
#expect(frozen.metrics.count == 3)
#expect(fallback.metrics.map(\.id) == ["code-review"])
#expect(inFlight.metrics.map(\.id) == frozen.metrics.map(\.id))
#expect(inFlight.metrics.first?.percentLabel == "79% left")
}
@Test
func `manual refresh uses fallback when empty quota gains credit content`() throws {
let settings = self.makeSettings()
let controller = self.makeController(
settings: settings,
account: AccountInfo(email: "test@example.com", plan: "pro"))
let now = Date()
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 21,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .codex))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [.codex: frozen])
controller.store.refreshingProviders.insert(.codex)
defer { controller.store.refreshingProviders.remove(.codex) }
controller.store.snapshots[.codex] = UsageSnapshot(
primary: nil,
secondary: nil,
updatedAt: now.addingTimeInterval(1))
controller.store.credits = CreditsSnapshot(
remaining: 42,
events: [],
updatedAt: now.addingTimeInterval(1))
let fallback = try #require(controller.menuCardModel(for: .codex))
let inFlight = controller.menuCardRefreshMonitor.model(for: .codex, fallback: fallback)
#expect(frozen.metrics.count == 1)
#expect(fallback.metrics.isEmpty)
#expect(fallback.creditsText != nil)
#expect(inFlight.metrics.isEmpty)
#expect(inFlight.creditsText == fallback.creditsText)
}
}
extension StatusMenuPersistentRefreshTests {
@Test
func `manual refresh uses fallback when empty quota gains a placeholder`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let now = Date()
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 21,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .claude))
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [.claude: frozen])
controller.store.snapshots.removeValue(forKey: .claude)
let fallback = try #require(controller.menuCardModel(for: .claude))
controller.store.refreshingProviders.insert(.claude)
defer { controller.store.refreshingProviders.remove(.claude) }
let inFlight = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
#expect(frozen.metrics.count == 1)
#expect(fallback.metrics.isEmpty)
#expect(fallback.placeholder != nil)
#expect(inFlight.metrics.isEmpty)
#expect(inFlight.placeholder == fallback.placeholder)
}
@Test
func `refresh monitor updates single line credit balances`() throws {
let settings = self.makeSettings()
let controller = self.makeController(
settings: settings,
account: AccountInfo(email: "test@example.com", plan: "pro"))
let now = Date()
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 10,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
controller.store.credits = CreditsSnapshot(remaining: 80, events: [], updatedAt: now)
let fallback = try #require(controller.menuCardModel(for: .codex))
controller.store.credits = CreditsSnapshot(
remaining: 42,
events: [],
updatedAt: now.addingTimeInterval(1))
let refreshed = controller.menuCardRefreshMonitor.model(for: .codex, fallback: fallback)
#expect(refreshed.creditsRemaining == 42)
#expect(refreshed.creditsText != fallback.creditsText)
}
@Test
func `refresh monitor preserves multiline workspace credit text`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
controller.store.snapshots[.amp] = UsageSnapshot(
primary: nil,
secondary: nil,
ampUsage: AmpUsageDetails(
individualCredits: 12,
workspaceBalances: [AmpWorkspaceBalance(name: "Team", remaining: 7)]),
updatedAt: Date())
let fallback = try #require(controller.menuCardModel(for: .amp))
controller.store.snapshots[.amp] = UsageSnapshot(
primary: nil,
secondary: nil,
ampUsage: AmpUsageDetails(
individualCredits: 10,
workspaceBalances: [AmpWorkspaceBalance(name: "Team", remaining: 3)]),
updatedAt: Date())
let refreshed = controller.menuCardRefreshMonitor.model(for: .amp, fallback: fallback)
#expect(refreshed.creditsText == fallback.creditsText)
}
@Test
func `refresh monitor preserves tracked layout when refresh adds usage sections`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let fallback = try #require(controller.menuCardModel(for: .claude))
#expect(fallback.metrics.isEmpty)
controller.store.snapshots[.claude] = UsageSnapshot(
primary: RateWindow(
usedPercent: 25,
windowMinutes: 300,
resetsAt: Date().addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: Date())
let refreshed = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
#expect(refreshed.metrics.isEmpty)
#expect(refreshed.placeholder == fallback.placeholder)
}
@Test
func `refresh monitor preserves tracked layout when token error appears`() throws {
let settings = self.makeSettings()
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let controller = self.makeController(settings: settings)
controller.store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
let fallback = try #require(controller.menuCardModel(for: .claude))
#expect(fallback.tokenUsage?.errorLine == nil)
controller.store._setTokenErrorForTesting("New token usage error", provider: .claude)
let refreshed = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
#expect(refreshed.tokenUsage?.errorLine == nil)
}
@Test
func `refresh monitor preserves tracked layout when token error text changes`() throws {
let settings = self.makeSettings()
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let controller = self.makeController(settings: settings)
controller.store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
controller.store._setTokenErrorForTesting("Old token usage error", provider: .claude)
let fallback = try #require(controller.menuCardModel(for: .claude))
controller.store._setTokenErrorForTesting(
"A longer replacement error that could occupy more lines",
provider: .claude)
let refreshed = controller.menuCardRefreshMonitor.model(for: .claude, fallback: fallback)
#expect(refreshed.tokenUsage?.errorLine == "Old token usage error")
}
@Test
func `live subtitle preserves canonical model error filtering`() throws {
let settings = self.makeSettings()
let controller = self.makeController(
settings: settings,
account: AccountInfo(email: "test@example.com", plan: "pro"))
controller.store.errors[.codex] = UsageError.noRateLimitsFound.errorDescription
let model = try #require(controller.menuCardModel(for: .codex))
let fallback = MenuCardLiveSubtitle(text: "Fallback", style: .error)
let liveSubtitle = controller.menuCardRefreshMonitor.subtitle(for: .codex, fallback: fallback)
#expect(liveSubtitle.text == model.subtitleText)
#expect(liveSubtitle.style == model.subtitleStyle)
#expect(liveSubtitle.text != UsageError.noRateLimitsFound.errorDescription)
#expect(liveSubtitle.style != .error)
}
@Test
func `override cards keep their own subtitle`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let liveModel = try #require(controller.menuCardModel(for: .codex))
let overrideModel = try #require(controller.menuCardModel(
for: .codex,
errorOverride: "Account unavailable",
forceOverrideCard: true))
#expect(liveModel.usesLiveSubtitle)
#expect(!overrideModel.usesLiveSubtitle)
#expect(overrideModel.subtitleText == "Account unavailable")
}
@Test
func `live failure keeps the measured card height`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
func fittingHeight(for model: UsageMenuCardView.Model) -> CGFloat {
NSHostingView(rootView: UsageMenuCardView(model: model, width: 320)
.environment(\.menuCardRefreshMonitor, controller.menuCardRefreshMonitor))
.fittingSize.height
}
let idleModel = try #require(controller.menuCardModel(for: .codex))
let idleHeight = fittingHeight(for: idleModel)
controller.store.errors[.codex] = "Short error"
let failureHeight = fittingHeight(for: idleModel)
#expect(failureHeight == idleHeight)
let errorModel = try #require(controller.menuCardModel(for: .codex))
let errorHeight = fittingHeight(for: errorModel)
controller.store.errors[.codex] =
"Refresh failed with a much longer replacement message that must not resize the tracked menu"
let replacementErrorHeight = fittingHeight(for: errorModel)
controller.menuCardRefreshMonitor.beginManualRefresh(frozenModels: [:], provider: nil)
let retryHeight = fittingHeight(for: errorModel)
#expect(replacementErrorHeight == errorHeight)
let fallback = MenuCardLiveSubtitle(text: errorModel.subtitleText, style: errorModel.subtitleStyle)
#expect(controller.menuCardRefreshMonitor.subtitle(for: .codex, fallback: fallback).style == .error)
controller.store.refreshingProviders.insert(.codex)
#expect(controller.menuCardRefreshMonitor.subtitle(for: .codex, fallback: fallback).style == .loading)
controller.store.refreshingProviders.remove(.codex)
#expect(retryHeight == errorHeight)
}
@Test
func `manual refresh is suppressed after shutdown preparation`() {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
var requestCount = 0
controller._test_manualRefreshOperation = {
requestCount += 1
}
controller.prepareForAppShutdown()
controller.refreshNow()
#expect(requestCount == 0)
#expect(controller.manualRefreshTasks.isEmpty)
#expect(!controller.menuCardRefreshMonitor.isManualRefreshInFlight)
}
@Test
func `repeated manual refresh clicks share one lifecycle`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
let gate = ManualRefreshGate()
var requestCount = 0
controller._test_manualRefreshOperation = {
requestCount += 1
await gate.wait()
}
controller.refreshNow()
let task = try #require(controller.manualRefreshTasks[.global])
controller.refreshNow()
controller.refreshNow()
await Task.yield()
#expect(requestCount == 1)
#expect(controller.menuCardRefreshMonitor.isManualRefreshInFlight)
gate.resume()
await task.value
#expect(controller.manualRefreshTasks[.global] == nil)
#expect(!controller.menuCardRefreshMonitor.isManualRefreshInFlight)
}
@Test
func `provider menu persistent refresh row and command R refresh only that provider`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
self.enableOnly([.claude, .codex], settings: settings)
let controller = self.makeController(settings: settings)
let menu = try #require(controller.makeMenu(for: .claude) as? StatusItemMenu)
let codexMenu = try #require(controller.makeMenu(for: .codex) as? StatusItemMenu)
controller.menuWillOpen(menu)
controller.menuWillOpen(codexMenu)
defer {
controller.menuDidClose(codexMenu)
controller.menuDidClose(menu)
}
let mouseGate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await mouseGate.wait() }
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
let refreshView = try #require(refreshItem.view as? PersistentRefreshMenuView)
#expect(refreshView.accessibilityPerformPress())
for _ in 0..<20 where controller.manualRefreshTasks[.provider(.claude)] == nil {
await Task.yield()
}
let mouseTask = try #require(controller.manualRefreshTasks[.provider(.claude)])
// Refreshing Claude greys the Claude row but must leave the Codex row available.
#expect(controller.isRefreshActionInFlight(for: menu))
#expect(!controller.isRefreshActionInFlight(for: codexMenu))
mouseGate.resume()
await mouseTask.value
let keyboardGate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await keyboardGate.wait() }
#expect(try menu.performKeyEquivalent(with: self.keyEvent("r", keyCode: 15)))
for _ in 0..<20 where controller.manualRefreshTasks[.provider(.claude)] == nil {
await Task.yield()
}
let keyboardTask = try #require(controller.manualRefreshTasks[.provider(.claude)])
keyboardGate.resume()
await keyboardTask.value
}
@Test
func `provider menu does not replace matching scoped refresh`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
self.enableOnly([.claude, .codex], settings: settings)
let controller = self.makeController(settings: settings)
let menu = try #require(controller.makeMenu(for: .claude) as? StatusItemMenu)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
controller.store.refreshingProviders.insert(.claude)
var requestCount = 0
controller._test_manualRefreshOperation = { requestCount += 1 }
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
controller.performPersistentRefreshAction(in: ObjectIdentifier(menu))
#expect(try menu.performKeyEquivalent(with: self.keyEvent("r", keyCode: 15)))
for _ in 0..<20 {
await Task.yield()
}
#expect(requestCount == 0)
#expect(controller.manualRefreshTasks.isEmpty)
}
@Test
func `merged overview refreshes globally while selected provider stays scoped`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnly([.claude, .codex], settings: settings)
settings.mergedMenuLastSelectedWasOverview = true
let controller = self.makeController(settings: settings)
let menu = try #require(controller.makeMenu() as? StatusItemMenu)
controller.mergedMenu = menu
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let overviewGate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await overviewGate.wait() }
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
#expect(controller.isPersistentRefreshItem(refreshItem))
controller.performPersistentRefreshAction(in: ObjectIdentifier(menu))
for _ in 0..<20 where controller.manualRefreshTasks[.global] == nil {
await Task.yield()
}
let overviewTask = try #require(controller.manualRefreshTasks[.global])
overviewGate.resume()
await overviewTask.value
settings.mergedMenuLastSelectedWasOverview = false
controller.selectedMenuProvider = .claude
let providerGate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await providerGate.wait() }
#expect(try menu.performKeyEquivalent(with: self.keyEvent("r", keyCode: 15)))
for _ in 0..<20 where controller.manualRefreshTasks[.provider(.claude)] == nil {
await Task.yield()
}
let providerTask = try #require(controller.manualRefreshTasks[.provider(.claude)])
providerGate.resume()
await providerTask.value
}
@Test
func `provider scoped refresh updates status and widget snapshot`() async {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.statusChecksEnabled = true
self.enableOnly([.synthetic], settings: settings)
let controller = self.makeController(settings: settings)
controller.store._test_providerRefreshOverride = { _ in }
controller.store._test_providerStatusFetchOverride = { provider in
#expect(provider == .synthetic)
return ProviderStatus(indicator: .none, description: "Operational", updatedAt: Date())
}
var savedSnapshots = 0
controller.store._test_widgetSnapshotSaveOverride = { _ in
savedSnapshots += 1
}
await controller.performStoreRefresh(
for: .synthetic,
refreshOpenMenusWhenComplete: false,
interaction: .userInitiated)
_ = await controller.store.widgetSnapshotPersistTask?.result
#expect(controller.store.statuses[.synthetic]?.description == "Operational")
#expect(savedSnapshots == 1)
}
@Test
func `failed manual refresh returns persistent item to enabled and surfaces error`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
let gate = ManualRefreshGate()
controller._test_manualRefreshOperation = {
await gate.wait()
controller.store.errors[.codex] = "Refresh failed"
}
controller.refreshNow()
let task = try #require(controller.manualRefreshTasks[.global])
#expect(!refreshItem.isEnabled)
gate.resume()
await task.value
#expect(controller.manualRefreshTasks[.global] == nil)
#expect(refreshItem.isEnabled)
let fallback = MenuCardLiveSubtitle(text: "Fallback", style: .info)
#expect(controller.menuCardRefreshMonitor.subtitle(for: .codex, fallback: fallback).style == .error)
}
@Test
func `status item menu intercepts persistent shortcuts without native item selection`() throws {
let menu = StatusItemMenu()
let recorder = RefreshShortcutRecorder()
menu.persistentActionDelegate = recorder
menu.menuInteractionGeneration = 42
#expect(try menu.performKeyEquivalent(with: self.keyEvent("r", keyCode: 15)) == true)
#expect(try menu.performKeyEquivalent(with: self.keyEvent(",", keyCode: 43)) == true)
#expect(try menu.performKeyEquivalent(with: self.keyEvent("q", keyCode: 12)) == true)
#expect(recorder.refreshCount == 1)
#expect(recorder.refreshMenuIDs == [ObjectIdentifier(menu)])
#expect(recorder.refreshMenuInteractionGenerations == [42])
#expect(recorder.settingsCount == 1)
#expect(recorder.quitCount == 1)
}
}
extension StatusMenuPersistentRefreshTests {
private func keyEvent(_ characters: String, keyCode: UInt16) throws -> NSEvent {
try #require(NSEvent.keyEvent(
with: .keyDown,
location: .zero,
modifierFlags: [.command],
timestamp: 0,
windowNumber: 0,
context: nil,
characters: characters,
charactersIgnoringModifiers: characters,
isARepeat: false,
keyCode: keyCode))
}
@Test
func `refresh row metrics match tuned native-style values`() {
let metrics = PersistentRefreshRowMetrics.defaults
#expect(metrics.rowHeight == 24)
#expect(metrics.selectionHorizontalInset == 5)
#expect(metrics.selectionVerticalInset == 0)
#expect(metrics.selectionCornerRadius == 7)
#expect(metrics.leadingPadding == 15)
#expect(metrics.trailingPadding == 8)
#expect(metrics.iconWidth == 16)
#expect(metrics.iconSymbolPointSize == 16)
#expect(metrics.iconSymbolWeight == .regular)
#expect(metrics.iconTitleSpacing == 4.5)
#expect(metrics.shortcutFontSize == 13)
#expect(metrics.shortcutXOffset == -9.5)
#expect(metrics.shortcutYOffset == 0)
}
@Test
func `refresh shortcut display has stable native-style column`() throws {
let previousRendering = StatusItemController.menuCardRenderingEnabled
StatusItemController.menuCardRenderingEnabled = true
defer { StatusItemController.menuCardRenderingEnabled = previousRendering }
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
let controller = self.makeController(settings: settings)
let menu = controller.makeMenu(for: .codex)
controller.menuWillOpen(menu)
let refreshItem = try #require(menu.items.first { $0.title == "Refresh" })
let refreshView = try #require(refreshItem.view as? PersistentRefreshMenuView)
refreshView.applySize(width: 320, height: PersistentRefreshRowMetrics.defaults.rowHeight)
refreshView.layoutSubtreeIfNeeded()
let shortcutField = try #require(
refreshView.subviews.compactMap { $0 as? NSTextField }.first { $0.stringValue == "⌘ R" })
#expect(shortcutField.alignment == .left)
#expect(shortcutField.lineBreakMode == .byClipping)
#expect(shortcutField.frame.width >= 40)
let shortcutFont = try #require(shortcutField.font)
#expect(abs(shortcutFont.pointSize - PersistentRefreshRowMetrics.defaults.shortcutFontSize) < 0.001)
let iconView = try #require(refreshView.subviews.compactMap { $0 as? NSImageView }.first)
let titleField = try #require(
refreshView.subviews.compactMap { $0 as? NSTextField }.first { $0.stringValue == "Refresh" })
#expect(iconView.frame.minX == PersistentRefreshRowMetrics.defaults.leadingPadding)
#expect(titleField.frame.minX == PersistentRefreshRowMetrics.defaults.leadingPadding
+ PersistentRefreshRowMetrics.defaults.iconWidth
+ PersistentRefreshRowMetrics.defaults.iconTitleSpacing)
#expect(iconView.frame.width == PersistentRefreshRowMetrics.defaults.iconWidth)
#expect(iconView.frame.height == PersistentRefreshRowMetrics.defaults.iconWidth)
}
@Test
func `refresh row width follows final rendered menu width`() {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let metrics = PersistentRefreshRowMetrics.defaults
let refreshView = PersistentRefreshMenuView(
title: "Refresh",
systemImageName: "arrow.clockwise",
shortcutText: "⌘ R")
refreshView.applySize(width: StatusItemController.menuCardBaseWidth, height: metrics.rowHeight)
refreshView.frame.origin.x = 4
let refreshItem = NSMenuItem()
refreshItem.title = "Refresh"
refreshItem.view = refreshView
let wideNativeItem = NSMenuItem(
title: String(repeating: "W", count: 60),
action: nil,
keyEquivalent: "")
let menu = NSMenu()
menu.addItem(refreshItem)
menu.addItem(wideNativeItem)
let expectedWidth = controller.renderedMenuWidth(for: menu)
#expect(expectedWidth > StatusItemController.menuCardBaseWidth)
controller.refreshMenuCardHeights(in: menu)
#expect(abs(refreshView.frame.width - expectedWidth) <= 0.5)
#expect(refreshView.frame.origin == .zero)
#expect(refreshView.frame.height == metrics.rowHeight)
}
}
extension StatusMenuPersistentRefreshTests {
@Test
func `global manual refresh only marks active provider cards as refreshing`() {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let monitor = controller.menuCardRefreshMonitor
let fallback = MenuCardLiveSubtitle(text: "Idle", style: .info)
monitor.beginManualRefresh(frozenModels: [:], provider: nil)
defer { monitor.endManualRefresh() }
controller.store.refreshingProviders.insert(.claude)
#expect(monitor.isManualRefreshInFlight)
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.isManualRefreshInFlight(for: .claude))
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
#expect(monitor.subtitle(for: .claude, fallback: fallback).style == .loading)
controller.store.refreshingProviders.remove(.claude)
#expect(monitor.isManualRefreshInFlight)
#expect(!monitor.isManualRefreshInFlight(for: .claude))
#expect(monitor.subtitle(for: .claude, fallback: fallback).style == .info)
}
@Test
func `completed provider cards stop refreshing while another provider is still running`() async {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
settings.openAIWebAccessEnabled = false
settings.codexCookieSource = .off
self.enableOnly([.claude, .codex], settings: settings)
let controller = self.makeController(settings: settings)
let claudeStarted = ManualRefreshGate()
let releaseClaude = ManualRefreshGate()
let monitor = controller.menuCardRefreshMonitor
let fallback = MenuCardLiveSubtitle(text: "Idle", style: .info)
controller.store._test_providerRefreshOverride = { provider in
guard provider == .claude else { return }
claudeStarted.resume()
await releaseClaude.wait()
}
defer { controller.store._test_providerRefreshOverride = nil }
controller.refreshNow()
await claudeStarted.wait()
for _ in 0..<20 where controller.store.refreshingProviders != [.claude] {
await Task.yield()
}
#expect(controller.store.refreshingProviders == [.claude])
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.isManualRefreshInFlight(for: .claude))
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
#expect(monitor.subtitle(for: .claude, fallback: fallback).style == .loading)
releaseClaude.resume()
await controller.manualRefreshTasks[.global]?.value
}
@Test
func `token-cost tail does not keep completed provider card refreshing`() async {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.openAIWebAccessEnabled = false
settings.codexCookieSource = .off
self.enableOnly([.codex], settings: settings)
let controller = self.makeController(settings: settings)
let tokenRefreshStarted = ManualRefreshGate()
let releaseTokenRefresh = ManualRefreshGate()
defer { releaseTokenRefresh.resume() }
let monitor = controller.menuCardRefreshMonitor
let fallback = MenuCardLiveSubtitle(text: "Idle", style: .info)
let menu = controller.makeMenu()
var tokenRefreshWasForced: Bool?
controller.store._test_providerRefreshOverride = { _ in }
controller.store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
controller.store._test_tokenUsageRefreshOverride = { _, force in
tokenRefreshWasForced = force
tokenRefreshStarted.resume()
await releaseTokenRefresh.wait()
}
defer {
controller.store._test_providerRefreshOverride = nil
controller.store._test_codexCreditsLoaderOverride = nil
controller.store._test_tokenUsageRefreshOverride = nil
}
controller.refreshNow()
let manualTask = controller.manualRefreshTasks[.global]
#expect(await tokenRefreshStarted.waitUntilSignaled())
let manualCompletion = RefreshCompletionProbe()
let manualCompletionTask = Task {
await manualTask?.value
await manualCompletion.markCompleted()
}
#expect(await manualCompletion.waitUntilCompleted())
let tailTask = controller.store.forcedRefreshEnrichmentTask
#expect(!controller.store.isRefreshing)
#expect(controller.store.hasForcedRefreshEnrichmentInFlight)
#expect(controller.store.refreshingProviders.isEmpty)
#expect(tokenRefreshWasForced == true)
#expect(controller.isRefreshActionInFlight(for: menu))
#expect(!monitor.isManualRefreshInFlight)
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
releaseTokenRefresh.resume()
await manualCompletionTask.value
await tailTask?.value
#expect(!controller.store.hasForcedRefreshEnrichmentInFlight)
#expect(!controller.isRefreshActionInFlight(for: menu))
}
@Test
func `credit tail does not keep completed provider card refreshing`() async {
await self.verifyCompletedProviderCardStopsRefreshing(whileBlocking: .credits)
}
@Test
func `dashboard tail does not keep completed provider card refreshing`() async {
await self.verifyCompletedProviderCardStopsRefreshing(whileBlocking: .dashboard)
}
private func verifyCompletedProviderCardStopsRefreshing(whileBlocking stage: BlockingEnrichmentStage) async {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
settings.openAIWebAccessEnabled = stage == .dashboard
settings.codexCookieSource = stage == .dashboard ? .auto : .off
settings._test_liveSystemCodexAccount = ObservedSystemCodexAccount(
email: "fixture@example.com",
codexHomePath: "/Users/test/.codex",
observedAt: Date(),
identity: .emailOnly(normalizedEmail: "fixture@example.com"))
settings.codexActiveSource = .liveSystem
self.enableOnly([.codex], settings: settings)
let account = AccountInfo(email: "fixture@example.com", plan: "pro")
let controller = self.makeController(settings: settings, account: account)
controller.store.accountInfoCache[.codex] = UsageStore.AccountInfoCacheEntry(
account: account,
configRevision: settings.configRevision,
expiresAt: .distantFuture)
let stageStarted = ManualRefreshGate()
let releaseStage = ManualRefreshGate()
defer {
releaseStage.resume()
controller.prepareForAppShutdown()
settings._test_liveSystemCodexAccount = nil
}
let monitor = controller.menuCardRefreshMonitor
let fallback = MenuCardLiveSubtitle(text: "Idle", style: .info)
let menu = controller.makeMenu()
var creditsLoaderCalls = 0
var dashboardLoaderCalls = 0
controller.store._test_providerRefreshOverride = { _ in }
controller.store._test_codexCreditsLoaderOverride = {
creditsLoaderCalls += 1
if stage == .credits, creditsLoaderCalls == 1 {
stageStarted.resume()
await releaseStage.wait()
}
return CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
controller.store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
dashboardLoaderCalls += 1
if stage == .dashboard, dashboardLoaderCalls == 1 {
stageStarted.resume()
await releaseStage.wait()
}
return OpenAIDashboardSnapshot(
signedInEmail: account.email,
codeReviewRemainingPercent: 95,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
creditsRemaining: 25,
accountPlan: "Pro",
updatedAt: Date())
}
defer {
controller.store._test_providerRefreshOverride = nil
controller.store._test_codexCreditsLoaderOverride = nil
controller.store._test_openAIDashboardLoaderOverride = nil
}
controller.refreshNow()
let manualTask = controller.manualRefreshTasks[.global]
let didStartStage = await stageStarted.waitUntilSignaled()
#expect(didStartStage)
guard didStartStage else { return }
let manualCompletion = RefreshCompletionProbe()
let manualCompletionTask = Task {
await manualTask?.value
await manualCompletion.markCompleted()
}
let didCompleteManualRefresh = await manualCompletion.waitUntilCompleted()
#expect(didCompleteManualRefresh)
guard didCompleteManualRefresh else { return }
let tailTask = controller.store.forcedRefreshEnrichmentTask
#expect(!controller.store.isRefreshing)
#expect(controller.store.hasForcedRefreshEnrichmentInFlight)
#expect(controller.store.refreshingProviders.isEmpty)
#expect(controller.isRefreshActionInFlight(for: menu))
#expect(!monitor.isManualRefreshInFlight)
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.subtitle(for: .codex, fallback: fallback).style == .info)
releaseStage.resume()
await manualCompletionTask.value
let tailCompletion = RefreshCompletionProbe()
let tailCompletionTask = Task {
await tailTask?.value
await tailCompletion.markCompleted()
}
let didCompleteTail = await tailCompletion.waitUntilCompleted()
#expect(didCompleteTail)
guard didCompleteTail else { return }
await tailCompletionTask.value
#expect(creditsLoaderCalls >= 1)
#expect(dashboardLoaderCalls == (stage == .dashboard ? 1 : 0))
#expect(!controller.store.hasForcedRefreshEnrichmentInFlight)
#expect(!controller.isRefreshActionInFlight(for: menu))
}
@Test
func `provider scoped refresh waits for global forced enrichment`() async {
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.openAIWebAccessEnabled = false
settings.codexCookieSource = .off
self.enableOnly([.codex], settings: settings)
let controller = self.makeController(settings: settings)
let tokenRefreshStarted = ManualRefreshGate()
let releaseTokenRefresh = ManualRefreshGate()
let scopedWaitStarted = ManualRefreshGate()
defer { releaseTokenRefresh.resume() }
var providerRefreshCalls = 0
var tokenRefreshCalls = 0
controller.store._test_providerRefreshOverride = { _ in
providerRefreshCalls += 1
}
controller.store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
controller.store._test_tokenUsageRefreshOverride = { _, _ in
tokenRefreshCalls += 1
if tokenRefreshCalls == 1 {
tokenRefreshStarted.resume()
await releaseTokenRefresh.wait()
}
}
controller.store._test_forcedRefreshEnrichmentWaitObserver = {
scopedWaitStarted.resume()
}
defer {
controller.store._test_providerRefreshOverride = nil
controller.store._test_codexCreditsLoaderOverride = nil
controller.store._test_tokenUsageRefreshOverride = nil
controller.store._test_forcedRefreshEnrichmentWaitObserver = nil
}
controller.refreshNow()
let globalTask = controller.manualRefreshTasks[.global]
#expect(await tokenRefreshStarted.waitUntilSignaled())
let globalCompletion = RefreshCompletionProbe()
let globalCompletionTask = Task {
await globalTask?.value
await globalCompletion.markCompleted()
}
#expect(await globalCompletion.waitUntilCompleted())
#expect(providerRefreshCalls == 1)
let scopedTask = Task { @MainActor in
await controller.performStoreRefresh(
for: .codex,
refreshOpenMenusWhenComplete: false,
interaction: .userInitiated)
}
#expect(await scopedWaitStarted.waitUntilSignaled())
#expect(providerRefreshCalls == 1)
releaseTokenRefresh.resume()
await globalCompletionTask.value
await scopedTask.value
#expect(providerRefreshCalls == 2)
#expect(tokenRefreshCalls == 2)
#expect(!controller.store.hasForcedRefreshEnrichmentInFlight)
}
@Test
func `concurrent manual refreshes keep each provider's frozen card`() throws {
let settings = self.makeSettings()
let controller = self.makeController(settings: settings)
let monitor = controller.menuCardRefreshMonitor
let now = Date()
func quotaSnapshot(usedPercent: Double, updatedAt: Date) -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: usedPercent,
windowMinutes: 300,
resetsAt: updatedAt.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: updatedAt)
}
// The snapshot helper supplies every enabled provider even for a provider-scoped refresh.
controller.store.snapshots[.claude] = quotaSnapshot(usedPercent: 21, updatedAt: now)
controller.store.snapshots[.codex] = quotaSnapshot(usedPercent: 15, updatedAt: now)
let claudeFrozen = try #require(controller.menuCardModel(for: .claude))
let codexBeforeItsRefresh = try #require(controller.menuCardModel(for: .codex))
monitor.beginManualRefresh(
frozenModels: [.claude: claudeFrozen, .codex: codexBeforeItsRefresh],
provider: .claude)
// Each provider must freeze the card visible when its own refresh starts.
controller.store.snapshots[.claude] = quotaSnapshot(usedPercent: 65, updatedAt: now.addingTimeInterval(1))
controller.store.snapshots[.codex] = quotaSnapshot(usedPercent: 42, updatedAt: now.addingTimeInterval(1))
let claudeMidRefresh = try #require(controller.menuCardModel(for: .claude))
let codexFrozen = try #require(controller.menuCardModel(for: .codex))
monitor.beginManualRefresh(
frozenModels: [.claude: claudeMidRefresh, .codex: codexFrozen],
provider: .codex)
let shownClaude = monitor.model(for: .claude, fallback: claudeMidRefresh)
let shownCodex = monitor.model(for: .codex, fallback: codexFrozen)
#expect(shownClaude.metrics.first?.percentLabel == "79% left")
#expect(shownCodex.metrics.first?.percentLabel == "58% left")
// Ending Codex leaves Claude frozen; ending Claude clears it.
monitor.endManualRefresh(for: .codex)
#expect(monitor.isManualRefreshInFlight(for: .claude))
#expect(!monitor.isManualRefreshInFlight(for: .codex))
monitor.endManualRefresh(for: .claude)
#expect(!monitor.isManualRefreshInFlight(for: .claude))
}
@Test
func `refreshing one provider does not block refreshing another`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = false
self.enableOnly([.claude, .codex], settings: settings)
let controller = self.makeController(settings: settings)
let codexMenu = try #require(controller.makeMenu(for: .codex) as? StatusItemMenu)
controller.menuWillOpen(codexMenu)
defer { controller.menuDidClose(codexMenu) }
// Simulate a Claude manual refresh already in flight.
controller.manualRefreshTasks[.provider(.claude)] = Task {}
defer {
controller.manualRefreshTasks[.provider(.claude)]?.cancel()
controller.manualRefreshTasks[.provider(.claude)] = nil
}
let gate = ManualRefreshGate()
controller._test_manualRefreshOperation = { await gate.wait() }
// A Codex refresh must still start rather than being blocked by the in-flight Claude one.
controller.performPersistentRefreshAction(in: ObjectIdentifier(codexMenu))
for _ in 0..<20 where controller.manualRefreshTasks[.provider(.codex)] == nil {
await Task.yield()
}
let codexTask = try #require(controller.manualRefreshTasks[.provider(.codex)])
#expect(controller.isRefreshActionInFlight(for: codexMenu))
gate.resume()
await codexTask.value
}
@Test
func `overview stays busy through a provider refresh tail and blocks a global refresh`() async throws {
let settings = self.makeSettings()
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnly([.claude, .codex], settings: settings)
settings.mergedMenuLastSelectedWasOverview = true
let controller = self.makeController(settings: settings)
let menu = try #require(controller.makeMenu() as? StatusItemMenu)
controller.mergedMenu = menu
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
// A per-provider Claude refresh whose task outlives the store's `refreshingProviders` window
// (the status/token/credits tail runs after the provider is removed from that set).
controller.manualRefreshTasks[.provider(.claude)] = Task {}
defer {
controller.manualRefreshTasks[.provider(.claude)]?.cancel()
controller.manualRefreshTasks[.provider(.claude)] = nil
}
// Overview stands for every provider, so its row stays greyed even with `refreshingProviders` empty.
#expect(controller.store.refreshingProviders.isEmpty)
#expect(controller.isRefreshActionInFlight(for: menu))
// And a global overview refresh must not start on top of the in-flight provider refresh.
var requestCount = 0
controller._test_manualRefreshOperation = { requestCount += 1 }
controller.performPersistentRefreshAction(in: ObjectIdentifier(menu))
for _ in 0..<20 {
await Task.yield()
}
#expect(requestCount == 0)
#expect(controller.manualRefreshTasks[.global] == nil)
}
}