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

1744 lines
70 KiB
Swift

import AppKit
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
extension StatusMenuTests {
@Test
func `store observation marks open menu stale without rebuilding during tracking`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = controller.menuVersions[key]
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
let now = Date()
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 33,
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..<20 where controller.menuContentVersion == openedVersion {
await Task.yield()
}
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
#expect(rebuildCount == 0)
}
@Test
func `closed merged menu defers rebuild until next open instead of pre-warming`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.mergedMenu = menu
controller.statusItem.menu = menu
for _ in 0..<20 {
await Task.yield()
}
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
for _ in 0..<40 {
await Task.yield()
}
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
controller.cancelAllClosedMenuRebuilds()
controller.closedMenusDeferredUntilNextOpen.removeAll(keepingCapacity: false)
let openedVersion = controller.menuVersions[key]
// Background data-refresh tick (stale allowed): closed prep is skipped entirely, so
// the closed merged menu must not be pre-warmed or marked deferred.
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
#expect(!controller.closedMenusDeferredUntilNextOpen.contains(key))
// A required (non-stale) invalidation must also leave the closed merged menu deferred.
controller.invalidateMenus()
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.menuVersions[key] == openedVersion)
#expect(controller.closedMenusDeferredUntilNextOpen.contains(key))
// The deferred merged menu is repopulated synchronously on the next open.
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
#expect(controller.menuVersions[key] == controller.menuContentVersion)
#expect(!controller.closedMenusDeferredUntilNextOpen.contains(key))
}
@Test
func `data refresh invalidation does not rebuild closed non merged attached menu`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
// Use a non-merged attached menu: stale data-refresh invalidations should not pre-warm any
// closed attached menu, while required invalidations still may prepare non-merged menus.
controller.fallbackMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
for _ in 0..<40 {
await Task.yield()
}
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
controller.cancelAllClosedMenuRebuilds()
let openedVersion = controller.menuVersions[key]
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
for _ in 0..<40 where controller.menuVersions[key] != controller.menuContentVersion {
await Task.yield()
}
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `required non merged closed menu preparation survives later data refresh invalidation`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
// Use a non-merged attached menu so this covers the delayed closed-menu rebuild path. Merged
// menus are intentionally deferred until next open on current main (#1274).
controller.fallbackMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
controller.invalidateMenus()
let requiredVersion = controller.latestRequiredMenuRebuildVersion
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
for _ in 0..<40 where controller.menuVersions[key] == openedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(requiredVersion > (openedVersion ?? -1))
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `closed attached menu preparation waits for store refresh to finish`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
// Use a non-merged attached menu: the merged menu is intentionally never pre-warmed while
// closed (#1274), so the in-flight-refresh prep machinery is exercised via the fallback menu.
controller.fallbackMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
store.isRefreshing = true
controller.invalidateMenus()
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.menuVersions[key] == openedVersion)
store.isRefreshing = false
controller.invalidateMenus()
for _ in 0..<40 where controller.menuVersions[key] == openedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `closed attached menu preparation waits for token refresh to finish`() async {
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
// Use a non-merged attached menu: the merged menu is intentionally never pre-warmed while
// closed (#1274), so the in-flight-refresh prep machinery is exercised via the fallback menu.
controller.fallbackMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
store.tokenRefreshInFlight.insert(.codex)
controller.invalidateMenus()
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.menuVersions[key] == openedVersion)
store.tokenRefreshInFlight.remove(.codex)
controller.invalidateMenus()
for _ in 0..<40 where controller.menuVersions[key] == openedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `closed menu rebuild cleanup runs when weak menu disappears`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
let key: ObjectIdentifier
do {
let menu = NSMenu()
key = ObjectIdentifier(menu)
controller.rebuildClosedMenuIfNeeded(menu)
#expect(controller.closedMenuRebuildTasks[key] != nil)
#expect(controller.closedMenuRebuildTokens[key] != nil)
}
for _ in 0..<40 where controller.closedMenuRebuildTasks[key] != nil {
await Task.yield()
}
#expect(controller.closedMenuRebuildTasks[key] == nil)
#expect(controller.closedMenuRebuildTokens[key] == nil)
}
@Test
func `merged menu close defers stale rebuild until next open`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.zero)
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.mergedMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
controller.invalidateMenus(refreshOpenMenus: false)
#expect(controller.menuNeedsRefresh(menu))
controller.menuDidClose(menu)
await self.waitUntilClosedMenuRebuildRemainsDeferred(controller, key: key, openedVersion: openedVersion)
#expect(controller.closedMenuRebuildTasks[key] == nil)
#expect(controller.menuVersions[key] == openedVersion)
controller.menuWillOpen(menu)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `menu open keeps stale nonempty content while store refresh is active`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.mergedMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
let openedItemCount = menu.items.count
store.isRefreshing = true
defer { store.isRefreshing = false }
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
#expect(controller.menuVersions[key] == openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(menu.items.count == openedItemCount)
#expect(controller.openMenus[key] === menu)
}
@Test
func `menu open rebuilds stale content after privacy setting changes during refresh`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.mergedMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
store.isRefreshing = true
defer { store.isRefreshing = false }
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
settings.hidePersonalInfo = true
controller.invalidateMenus()
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
#expect(controller.menuVersions[key] == controller.menuContentVersion)
#expect(controller.menuVersions[key] != openedVersion)
}
@Test
func `menu open keeps stale nonempty content while token refresh is active`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.mergedMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
let openedItemCount = menu.items.count
store.tokenRefreshInFlight.insert(.codex)
defer { store.tokenRefreshInFlight.remove(.codex) }
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
#expect(controller.menuVersions[key] == openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(menu.items.count == openedItemCount)
#expect(controller.openMenus[key] === menu)
}
@Test
func `explicit store actions defer visible parent menu rebuild`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = controller.menuVersions[key]
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.refreshOpenMenusAfterExplicitStoreAction()
for _ in 0..<20 {
await Task.yield()
}
#expect(controller.menuContentVersion != openedVersion)
#expect(rebuildCount == 0)
#expect(controller.menuVersions[key] == openedVersion)
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(key))
}
@Test
func `repeated explicit store actions keep parent rebuild deferred`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.refreshOpenMenusAfterExplicitStoreAction()
controller.refreshOpenMenusAfterExplicitStoreAction()
controller.refreshOpenMenusAfterExplicitStoreAction()
for _ in 0..<20 {
await Task.yield()
}
#expect(rebuildCount == 0)
#expect(controller.menuVersions[key] != controller.menuContentVersion)
#expect(controller.parentMenuRebuildsDeferredDuringTracking.contains(key))
}
@Test
func `explicit refresh rebuilds stale parent after hosted submenu closes`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let menuKey = ObjectIdentifier(menu)
controller.openMenus[menuKey] = menu
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.usageBreakdownChartID,
provider: .codex)
let submenuKey = ObjectIdentifier(submenu)
controller.openMenus[submenuKey] = submenu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = controller.menuVersions[menuKey]
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.refreshOpenMenusAfterExplicitStoreAction()
for _ in 0..<20 where controller.menuContentVersion == openedVersion {
await Task.yield()
}
#expect(controller.menuVersions[menuKey] == openedVersion)
controller.menuDidClose(submenu)
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(controller.openMenus[submenuKey] == nil)
#expect(rebuildCount == 1)
#expect(controller.menuVersions[menuKey] == controller.menuContentVersion)
#expect(!controller.parentMenuRebuildsDeferredDuringTracking.contains(menuKey))
}
@Test
func `hosted submenu close waits for active refresh before rebuilding parent`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let menuKey = ObjectIdentifier(menu)
controller.openMenus[menuKey] = menu
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.usageBreakdownChartID,
provider: .codex)
let submenuKey = ObjectIdentifier(submenu)
controller.openMenus[submenuKey] = submenu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = controller.menuVersions[menuKey]
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
store.isRefreshing = true
controller.refreshOpenMenusAfterExplicitStoreAction()
controller.menuDidClose(submenu)
for _ in 0..<20 {
await Task.yield()
}
#expect(controller.openMenus[submenuKey] == nil)
#expect(rebuildCount == 0)
#expect(controller.menuVersions[menuKey] == openedVersion)
#expect(controller.parentMenuRebuildPendingAfterHostedSubviewClose)
store.isRefreshing = false
controller.handleObservedStoreMenuChange()
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
#expect(controller.menuVersions[menuKey] == controller.menuContentVersion)
#expect(!controller.parentMenuRebuildPendingAfterHostedSubviewClose)
#expect(!controller.parentMenuRebuildsDeferredDuringTracking.contains(menuKey))
}
@Test
func `plain open menu refresh preserves pending switcher hosted submenu cleanup`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let menuKey = ObjectIdentifier(menu)
controller.openMenus[menuKey] = menu
let submenu = controller.makeHostedSubviewPlaceholderMenu(
chartID: StatusItemController.usageBreakdownChartID,
provider: .codex)
let submenuKey = ObjectIdentifier(submenu)
controller.openMenus[submenuKey] = submenu
controller.menuRefreshEnabledOverrideForTesting = true
var rootRebuildCount = 0
controller._test_openMenuRebuildObserver = { rebuiltMenu in
guard rebuiltMenu === menu else { return }
rootRebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
controller.deferSwitcherMenuRebuildIfStillVisible(menu, provider: .codex)
controller.refreshOpenMenuIfStillVisible(menu, provider: .codex)
for _ in 0..<20 where rootRebuildCount == 0 {
await Task.yield()
}
#expect(controller.openMenus[submenuKey] == nil)
#expect(rootRebuildCount == 1)
#expect(controller.menuVersions[menuKey] == controller.menuContentVersion)
}
@Test
func `rapid switcher rebuild requests coalesce before populating open menu`() 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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let menuKey = ObjectIdentifier(menu)
controller.openMenus[menuKey] = menu
controller.menuRefreshEnabledOverrideForTesting = true
controller._test_providerSwitcherMenuRebuildDebounceNanoseconds = 0
defer { controller._test_providerSwitcherMenuRebuildDebounceNanoseconds = nil }
var rebuildCount = 0
controller._test_openMenuRebuildObserver = { _ in
rebuildCount += 1
}
defer { controller._test_openMenuRebuildObserver = nil }
var refreshGateEntries = 0
var pendingRefreshGates: [CheckedContinuation<Void, Never>] = []
func resumePendingRefreshGates() {
let gates = pendingRefreshGates
pendingRefreshGates.removeAll(keepingCapacity: true)
for gate in gates {
gate.resume()
}
}
controller._test_openMenuRefreshYieldOverride = {
refreshGateEntries += 1
await withCheckedContinuation { continuation in
pendingRefreshGates.append(continuation)
}
}
defer {
resumePendingRefreshGates()
controller._test_openMenuRefreshYieldOverride = nil
}
controller.deferSwitcherMenuRebuildIfStillVisible(menu, provider: .codex)
for _ in 0..<20 where refreshGateEntries == 0 {
await Task.yield()
}
#expect(refreshGateEntries == 1)
#expect(rebuildCount == 0)
controller.deferSwitcherMenuRebuildIfStillVisible(menu, provider: .codex)
resumePendingRefreshGates()
for _ in 0..<20 where refreshGateEntries < 2 {
await Task.yield()
}
#expect(refreshGateEntries == 2)
#expect(rebuildCount == 0)
resumePendingRefreshGates()
for _ in 0..<20 where rebuildCount == 0 {
await Task.yield()
}
#expect(rebuildCount == 1)
for _ in 0..<20 {
await Task.yield()
}
#expect(rebuildCount == 1)
}
@Test
func `codex parent menu open defers stale OpenAI web refresh until tracking ends`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = nil
store.lastOpenAIDashboardSnapshot = nil
store._test_providerRefreshOverride = { _ in }
defer { store._test_providerRefreshOverride = nil }
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
defer { store._test_codexCreditsLoaderOverride = nil }
let blocker = BlockingManagedOpenAIDashboardLoader()
var refreshInteractions: [ProviderInteraction] = []
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
refreshInteractions.append(ProviderInteractionContext.current)
return try await blocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
for _ in 0..<20 {
await Task.yield()
}
#expect(await blocker.startedCount() == 0)
#expect(controller.deferredOpenAIDashboardRefreshReason != nil)
controller.menuDidClose(menu)
await blocker.waitUntilStarted(count: 1)
#expect(await blocker.startedCount() == 1)
#expect(refreshInteractions == [.background])
await blocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [],
updatedAt: Date())))
}
@Test
func `programmatic parent menu close schedules deferred OpenAI web refresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = nil
store.lastOpenAIDashboardSnapshot = nil
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 0, events: [], updatedAt: Date())
}
defer { store._test_codexCreditsLoaderOverride = nil }
let blocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await blocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
#expect(controller.deferredOpenAIDashboardRefreshReason != nil)
controller.forgetClosedMenu(menu)
await blocker.waitUntilStarted(count: 1)
#expect(await blocker.startedCount() == 1)
await blocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [],
updatedAt: Date())))
}
@Test
func `deferred OpenAI web refresh retries after active store refresh completes`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = nil
store.lastOpenAIDashboardSnapshot = nil
store.isRefreshing = true
let blocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await blocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
controller.deferOpenAIDashboardRefreshUntilMenuCloses(reason: "parent menu open")
controller.scheduleDeferredMenuInteractionRefreshIfNeeded()
try? await Task.sleep(for: .milliseconds(50))
#expect(await blocker.startedCount() == 0)
#expect(controller.deferredOpenAIDashboardRefreshReason != nil)
store.isRefreshing = false
await blocker.waitUntilStarted(count: 1)
#expect(await blocker.startedCount() == 1)
await blocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [],
updatedAt: Date())))
}
@Test
func `deferred OpenAI web refresh waits for deferred store refresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._setSnapshotForTesting(nil, provider: .codex)
store.openAIDashboard = nil
store.lastOpenAIDashboardSnapshot = nil
let providerBlocker = BlockingStatusMenuProviderRefresh()
store._test_providerRefreshOverride = { provider in
guard provider == .codex else { return }
await providerBlocker.awaitRelease()
}
defer { store._test_providerRefreshOverride = nil }
let dashboardBlocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await dashboardBlocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.menuDidClose(menu)
await providerBlocker.waitUntilStarted()
#expect(await dashboardBlocker.startedCount() == 0)
await providerBlocker.resumeNext()
await dashboardBlocker.waitUntilStarted(count: 1)
#expect(await dashboardBlocker.startedCount() == 1)
await dashboardBlocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [],
updatedAt: Date())))
}
@Test
func `reopened menu keeps dashboard refresh deferred after store refresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._setSnapshotForTesting(nil, provider: .codex)
store.openAIDashboard = nil
store.lastOpenAIDashboardSnapshot = nil
let providerBlocker = BlockingStatusMenuProviderRefresh()
store._test_providerRefreshOverride = { provider in
guard provider == .codex else { return }
await providerBlocker.awaitRelease()
}
defer { store._test_providerRefreshOverride = nil }
let dashboardBlocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await dashboardBlocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.menuDidClose(menu)
await providerBlocker.waitUntilStarted()
let reopenedMenu = controller.makeMenu()
controller.menuWillOpen(reopenedMenu)
await providerBlocker.resumeNext()
try? await Task.sleep(for: .milliseconds(50))
#expect(await dashboardBlocker.startedCount() == 0)
#expect(controller.deferredOpenAIDashboardRefreshReason != nil)
controller.menuDidClose(reopenedMenu)
await dashboardBlocker.waitUntilStarted(count: 1)
#expect(await dashboardBlocker.startedCount() == 1)
await dashboardBlocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [],
updatedAt: Date())))
}
@Test
func `codex parent menu close refreshes recent dashboard cache with no chart history`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = self.makeOpenAIDashboard(dailyBreakdown: [], updatedAt: Date())
store.lastOpenAIDashboardSnapshot = store.openAIDashboard
store._test_providerRefreshOverride = { _ in }
defer { store._test_providerRefreshOverride = nil }
let blocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await blocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero)
defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
for _ in 0..<20 {
await Task.yield()
}
#expect(await blocker.startedCount() == 0)
controller.menuDidClose(menu)
await blocker.waitUntilStarted(count: 1)
#expect(await blocker.startedCount() == 1)
await blocker.resumeNext(with: .success(self.makeOpenAIDashboard(
dailyBreakdown: [
OpenAIDashboardDailyBreakdown(day: "2026-05-24", services: [], totalCreditsUsed: 12),
],
updatedAt: Date())))
}
@Test
func `codex parent menu open throttles recent empty dashboard retry`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.openAIWebBatterySaverEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let now = Date()
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = self.makeOpenAIDashboard(dailyBreakdown: [], updatedAt: now.addingTimeInterval(-120))
store.lastOpenAIDashboardSnapshot = store.openAIDashboard
store.lastOpenAIDashboardAttemptAt = now.addingTimeInterval(-60)
store._test_providerRefreshOverride = { _ in }
defer { store._test_providerRefreshOverride = nil }
let blocker = BlockingManagedOpenAIDashboardLoader()
store._test_openAIDashboardLoaderOverride = { _, _, _, _ in
try await blocker.awaitResult()
}
defer { store._test_openAIDashboardLoaderOverride = nil }
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
controller.menuDidClose(menu)
try? await Task.sleep(for: .milliseconds(150))
#expect(await blocker.startedCount() == 0)
}
@Test
func `credits history arriving after open rebuilds parent menu after tracking ends`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.showOptionalCreditsAndExtraUsage = true
self.enableOnlyCodex(settings)
let now = Date()
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: true)
store.credits = CreditsSnapshot(remaining: 100, events: [], updatedAt: now)
store.openAIDashboard = self.makeOpenAIDashboard(dailyBreakdown: [], updatedAt: now)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
#expect(self.menuItem(in: menu, id: "menuCardCredits") == nil)
store.openAIDashboard = self.makeOpenAIDashboard(
dailyBreakdown: [
OpenAIDashboardDailyBreakdown(day: "2026-05-24", services: [], totalCreditsUsed: 12),
],
updatedAt: now.addingTimeInterval(10))
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
let creditsItem = try #require(self.menuItem(in: menu, id: "menuCardCredits"))
#expect(
creditsItem.submenu?.items.first?.representedObject as? String ==
StatusItemController.creditsHistoryChartID)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `fresh dashboard history with same day count rebuilds parent menu after tracking ends`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.showOptionalCreditsAndExtraUsage = true
self.enableOnlyCodex(settings)
let now = Date(timeIntervalSince1970: 100)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: true)
store.credits = CreditsSnapshot(remaining: 100, events: [], updatedAt: now)
store.openAIDashboard = self.makeOpenAIDashboard(
dailyBreakdown: [
OpenAIDashboardDailyBreakdown(day: "2026-05-24", services: [], totalCreditsUsed: 12),
],
updatedAt: now)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
_ = try #require(self.menuItem(in: menu, id: "menuCardCredits"))
store.openAIDashboard = self.makeOpenAIDashboard(
dailyBreakdown: [
OpenAIDashboardDailyBreakdown(day: "2026-05-24", services: [], totalCreditsUsed: 99),
],
updatedAt: now.addingTimeInterval(10))
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
let creditsItem = try #require(self.menuItem(in: menu, id: "menuCardCredits"))
#expect(creditsItem.submenu?.items.first?.representedObject as? String == StatusItemController
.creditsHistoryChartID)
}
@Test
func `token cost history arriving after open rebuilds parent menu after tracking ends`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
#expect(self.menuItem(in: menu, id: "menuCardCost") == nil)
store._setTokenSnapshotForTesting(self.makeCodexTokenCostSnapshot(), provider: .codex)
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
let costItem = try #require(self.menuItem(in: menu, id: "menuCardCost"))
#expect(costItem.submenu?.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `fresh token cost history with same day count rebuilds parent menu after tracking ends`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
self.enableOnlyCodex(settings)
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store._setTokenSnapshotForTesting(
self.makeCodexTokenCostSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 456,
last30DaysCostUSD: 1.23,
updatedAt: Date(timeIntervalSince1970: 100)),
provider: .codex)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
_ = try #require(self.menuItem(in: menu, id: "menuCardCost"))
store._setTokenSnapshotForTesting(
self.makeCodexTokenCostSnapshot(
sessionTokens: 999,
sessionCostUSD: 0.99,
last30DaysTokens: 888,
last30DaysCostUSD: 8.88,
updatedAt: Date(timeIntervalSince1970: 200)),
provider: .codex)
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
let costItem = try #require(self.menuItem(in: menu, id: "menuCardCost"))
#expect(costItem.submenu?.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
}
@Test
func `plan utilization history arriving after open rebuilds parent menu after tracking ends`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
self.enableOnlyCodex(settings)
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())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
let usageHistoryItem = try #require(self.menuItem(in: menu, id: "usageHistorySubmenu"))
#expect(usageHistoryItem.submenu?.items.first?.representedObject as? String == StatusItemController
.usageHistoryChartID)
let openedRevision = store.planUtilizationHistoryRevision
await store.recordPlanUtilizationHistorySample(
provider: .codex,
snapshot: self.makeCodexPlanUtilizationSnapshot(),
now: Date())
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(store.planUtilizationHistoryRevision > openedRevision)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
}
@Test
func `dashboard attachment authorization arriving after open rebuilds parent menu after close`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
settings.openAIWebAccessEnabled = true
settings.codexCookieSource = .auto
self.enableOnlyCodex(settings)
let now = Date()
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
store.openAIDashboard = self.makeOpenAIDashboard(
dailyBreakdown: [
OpenAIDashboardDailyBreakdown(day: "2026-05-24", services: [], totalCreditsUsed: 12),
],
updatedAt: now)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
controller.openMenus[key] = menu
controller.menuRefreshEnabledOverrideForTesting = true
let openedVersion = try #require(controller.menuVersions[key])
#expect(store.openAIDashboardAttachmentRevision == 0)
store.openAIDashboardAttachmentAuthorized = true
await self.waitUntilOpenMenuStaysStale(controller, key: key, after: openedVersion)
#expect(store.openAIDashboardAttachmentRevision == 1)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
await self.closeMenuAndWaitUntilFresh(controller, menu: menu, key: key)
}
private func enableOnlyCodex(_ settings: SettingsStore) {
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)
}
}
private func menuItem(in menu: NSMenu, id: String) -> NSMenuItem? {
menu.items.first { ($0.representedObject as? String) == id }
}
private func waitUntilMenuVersionChanges(
_ controller: StatusItemController,
from version: Int?) async
{
for _ in 0..<20 where controller.menuContentVersion == version {
await Task.yield()
}
}
private func waitUntilOpenMenuStaysStale(
_ controller: StatusItemController,
key: ObjectIdentifier,
after version: Int?) async
{
for _ in 0..<40 {
guard controller.menuContentVersion != version else {
await Task.yield()
continue
}
guard controller.menuVersions[key] == version else {
await Task.yield()
continue
}
return
}
}
private func closeMenuAndWaitUntilFresh(
_ controller: StatusItemController,
menu: NSMenu,
key: ObjectIdentifier) async
{
controller.menuDidClose(menu)
for _ in 0..<40 where controller.menuVersions[key] != controller.menuContentVersion {
await Task.yield()
}
if controller.menuVersions[key] != controller.menuContentVersion {
controller.menuWillOpen(menu)
}
for _ in 0..<40 where controller.menuVersions[key] != controller.menuContentVersion {
await Task.yield()
}
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
private func waitUntilClosedMenuRebuildRemainsDeferred(
_ controller: StatusItemController,
key: ObjectIdentifier,
openedVersion: Int?) async
{
for _ in 0..<40
where controller.closedMenuRebuildTasks[key] != nil ||
controller.menuVersions[key] != openedVersion
{
await Task.yield()
}
}
private func makeOpenAIDashboard(
dailyBreakdown: [OpenAIDashboardDailyBreakdown],
updatedAt: Date) -> OpenAIDashboardSnapshot
{
OpenAIDashboardSnapshot(
signedInEmail: "codex@example.com",
codeReviewRemainingPercent: nil,
creditEvents: [],
dailyBreakdown: dailyBreakdown,
usageBreakdown: [],
creditsPurchaseURL: nil,
updatedAt: updatedAt)
}
private func makeCodexTokenCostSnapshot(
sessionTokens: Int = 123,
sessionCostUSD: Double = 0.12,
last30DaysTokens: Int = 456,
last30DaysCostUSD: Double = 1.23,
updatedAt: Date = Date()) -> CostUsageTokenSnapshot
{
CostUsageTokenSnapshot(
sessionTokens: sessionTokens,
sessionCostUSD: sessionCostUSD,
last30DaysTokens: last30DaysTokens,
last30DaysCostUSD: last30DaysCostUSD,
daily: [
CostUsageDailyReport.Entry(
date: "2026-05-24",
inputTokens: nil,
outputTokens: nil,
totalTokens: sessionTokens,
costUSD: last30DaysCostUSD,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: updatedAt)
}
private func makeCodexPlanUtilizationSnapshot() -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: 35,
windowMinutes: 300,
resetsAt: Date().addingTimeInterval(1800),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 42,
windowMinutes: 10080,
resetsAt: Date().addingTimeInterval(86400),
resetDescription: nil),
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: "codex@example.com",
accountOrganization: nil,
loginMethod: "Plus Plan"))
}
/// The recent-interaction signal that `AdaptiveRefreshPolicy` reads has exactly one production
/// entry point: `StatusItemController.menuWillOpen(_:)` calling `store.noteMenuOpened()`. Every
/// other adaptive-refresh test drives `UsageStore` directly, so none of them would catch that
/// wiring line being deleted — this test drives the real menu-open path instead.
@Test
func `menuWillOpen records the menu-open signal on the store`() {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
#expect(store.lastMenuOpenAt == nil)
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
#expect(store.lastMenuOpenAt != nil)
}
}
private actor BlockingStatusMenuProviderRefresh {
private var continuations: [CheckedContinuation<Void, Never>] = []
private var startWaiters: [CheckedContinuation<Void, Never>] = []
private var started = 0
func awaitRelease() async {
self.started += 1
self.resumeStartWaiters()
await withCheckedContinuation { continuation in
self.continuations.append(continuation)
}
}
func waitUntilStarted() async {
if self.started > 0 { return }
await withCheckedContinuation { continuation in
self.startWaiters.append(continuation)
}
}
func resumeNext() {
guard !self.continuations.isEmpty else { return }
self.continuations.removeFirst().resume()
}
private func resumeStartWaiters() {
let waiters = self.startWaiters
self.startWaiters = []
for waiter in waiters {
waiter.resume()
}
}
}