1744 lines
70 KiB
Swift
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()
|
|
}
|
|
}
|
|
}
|