import AppKit import CodexBarCore import Foundation import Testing @testable import CodexBar extension StatusMenuTests { @Test func `opening fresh menu does not schedule deferred refresh`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) var providerRefreshCount = 0 var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } refreshInteractions.append(ProviderInteractionContext.current) providerRefreshCount += 1 } defer { store._test_providerRefreshOverride = nil } 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 StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero) defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) for _ in 0..<20 { await Task.yield() } #expect(providerRefreshCount == 0) #expect(!controller.deferredMenuInteractionRefreshPending) controller.menuDidClose(menu) for _ in 0..<40 { await Task.yield() } #expect(providerRefreshCount == 0) #expect(refreshInteractions.isEmpty) } @Test func `non codex menu refresh all also defers codex dashboard refresh`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false settings.refreshAllProvidersOnMenuOpen = true // Enable several providers; only the available ones land in background work. The // assertion below compares against that resolved set, so it stays robust regardless. self.enableProvidersForInstantOpenTesting([.codex, .claude, .factory], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) // Give every enabled provider a fresh, non-stale snapshot so the ONLY reason to refresh on // open is the new setting — not a stale/missing retry (which is the pre-existing behavior). let now = Date() for provider in store.enabledProvidersForBackgroundWork() { store._setSnapshotForTesting( UsageSnapshot( primary: RateWindow( usedPercent: 20, windowMinutes: 300, resetsAt: now.addingTimeInterval(1800), resetDescription: nil), secondary: nil, updatedAt: now), provider: provider) } var refreshedProviders: Set = [] var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in refreshedProviders.insert(provider) refreshInteractions.append(ProviderInteractionContext.current) } defer { store._test_providerRefreshOverride = nil } 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 StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero) defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let expectedProviders = Set(store.enabledProvidersForBackgroundWork()) let menu = controller.makeMenu(for: .claude) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } for _ in 0..<80 where refreshedProviders != expectedProviders { await Task.yield() } // Every enabled provider is refreshed on open even though all snapshots were fresh. #expect(refreshedProviders == expectedProviders && expectedProviders.contains(.codex)) #expect(!refreshInteractions.isEmpty) #expect(refreshInteractions.allSatisfy { $0 == .background }) #expect(controller.deferredOpenAIDashboardRefreshReason != nil) } @Test func `menu open leaves fresh provider untouched when refresh-all-on-open is disabled`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false settings.refreshAllProvidersOnMenuOpen = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) var providerRefreshCount = 0 store._test_providerRefreshOverride = { _ in providerRefreshCount += 1 } defer { store._test_providerRefreshOverride = nil } 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 StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero) defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } // Let the delayed open-refresh task actually fire; with the setting off and fresh data, // it must still skip the refresh (today's stale/missing-only behavior). StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } for _ in 0..<40 { await Task.yield() } #expect(providerRefreshCount == 0) } @Test func `delayed open refresh does not rebuild fresh menu after unrelated data invalidation`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(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 StatusItemController.setMenuOpenRefreshDelayForTesting(.milliseconds(50)) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } let key = ObjectIdentifier(menu) let openedVersion = controller.menuVersions[key] var rebuildCount = 0 controller._test_openMenuRebuildObserver = { _ in rebuildCount += 1 } defer { controller._test_openMenuRebuildObserver = nil } // Models first-open storage-footprint publication: menu content becomes stale, but no // displayed provider was missing or failed when this menu opened. controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) try? await Task.sleep(for: .milliseconds(150)) #expect(controller.menuContentVersion != openedVersion) #expect(controller.menuVersions[key] == openedVersion) #expect(rebuildCount == 0) } @Test func `menu open with missing data refreshes asynchronously while tracking`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) var providerRefreshCount = 0 var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } refreshInteractions.append(ProviderInteractionContext.current) providerRefreshCount += 1 store._setSnapshotForTesting( UsageSnapshot( primary: RateWindow( usedPercent: 25, windowMinutes: 300, resetsAt: Date().addingTimeInterval(3600), resetDescription: nil), secondary: nil, updatedAt: Date()), provider: .codex) } defer { store._test_providerRefreshOverride = nil } 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 StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.zero) defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) #expect(controller.deferredMenuInteractionRefreshPending) for _ in 0..<40 where providerRefreshCount == 0 { await Task.yield() } #expect(providerRefreshCount == 1) #expect(refreshInteractions == [.background]) for _ in 0..<40 where controller.deferredMenuInteractionRefreshPending { await Task.yield() } #expect(!controller.deferredMenuInteractionRefreshPending) controller.menuDidClose(menu) #expect(!controller.deferredMenuInteractionRefreshPending) } @Test func `menu open renders cached data immediately after data only invalidation`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) var providerRefreshCount = 0 store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } providerRefreshCount += 1 } defer { store._test_providerRefreshOverride = 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.mergedMenu = menu controller.statusItem.menu = menu controller.populateMenu(menu, provider: nil) controller.markMenuFresh(menu) let key = ObjectIdentifier(menu) let cachedItemCount = menu.items.count let cachedVersion = controller.menuVersions[key] controller.lastMenuAdjunctReadinessSignature = "stale-baseline" controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) let dataOnlyVersion = controller.menuContentVersion var asyncRebuilds = 0 controller._test_openMenuRebuildObserver = { _ in asyncRebuilds += 1 } defer { controller._test_openMenuRebuildObserver = nil } controller.menuWillOpen(menu) #expect(cachedVersion != dataOnlyVersion) #expect(menu.items.count == cachedItemCount) #expect(controller.menuVersions[key] == cachedVersion) #expect(asyncRebuilds == 0) #expect(!controller.deferredMenuInteractionRefreshPending) for _ in 0..<40 where asyncRebuilds == 0 { await Task.yield() } #expect(asyncRebuilds == 1) #expect(controller.menuVersions[key] == controller.menuContentVersion) #expect(!controller.didMenuAdjunctReadinessChange()) controller.menuDidClose(menu) for _ in 0..<40 { await Task.yield() } #expect(providerRefreshCount == 0) } @Test func `closing before cached menu rebuild keeps next open stale`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(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 cachedVersion = controller.menuVersions[key] controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) var rebuildGateEntries = 0 var rebuildGate: CheckedContinuation? controller._test_openMenuRefreshYieldOverride = { rebuildGateEntries += 1 await withCheckedContinuation { continuation in rebuildGate = continuation } } defer { rebuildGate?.resume() controller._test_openMenuRefreshYieldOverride = nil } controller.menuWillOpen(menu) for _ in 0..<40 where rebuildGateEntries == 0 { await Task.yield() } #expect(rebuildGateEntries == 1) #expect(controller.menuVersions[key] == cachedVersion) controller.menuDidClose(menu) #expect(controller.menuNeedsRefresh(menu)) rebuildGate?.resume() rebuildGate = nil controller._test_openMenuRefreshYieldOverride = nil for _ in 0..<20 { await Task.yield() } controller.menuWillOpen(menu) for _ in 0..<40 where controller.menuNeedsRefresh(menu) { await Task.yield() } #expect(!controller.menuNeedsRefresh(menu)) controller.menuDidClose(menu) } @Test func `menu open rebuilds synchronously after provider identity changes`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "old@example.com"), provider: .codex) 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 cachedVersion = controller.menuVersions[ObjectIdentifier(menu)] store._setSnapshotForTesting( self.instantOpenSnapshot(email: "new@example.com"), provider: .codex) controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuVersions[ObjectIdentifier(menu)] != cachedVersion) #expect(controller.menuVersions[ObjectIdentifier(menu)] == controller.menuContentVersion) } @Test func `overview menu rebuilds synchronously after secondary provider identity changes`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.mergedMenuLastSelectedWasOverview = true self.enableProvidersForInstantOpenTesting([.codex, .claude], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "codex@example.com"), provider: .codex) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "old@example.com", provider: .claude), provider: .claude) let controller = StatusItemController( store: store, settings: settings, account: UsageFetcher().loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } controller.menuRefreshEnabledOverrideForTesting = true controller.selectedMenuProvider = .codex let menu = controller.makeMenu() controller.mergedMenu = menu controller.statusItem.menu = menu controller.populateMenu(menu, provider: nil) controller.markMenuFresh(menu) let cachedVersion = controller.menuVersions[ObjectIdentifier(menu)] store._setSnapshotForTesting( self.instantOpenSnapshot(email: "new@example.com", provider: .claude), provider: .claude) controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuVersions[ObjectIdentifier(menu)] != cachedVersion) #expect(controller.menuVersions[ObjectIdentifier(menu)] == controller.menuContentVersion) } @Test func `stacked Codex menu rebuilds synchronously after secondary account identity changes`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.multiAccountMenuLayout = .stacked self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "selected@example.com"), provider: .codex) let selectedAccount = CodexVisibleAccount( id: "selected", email: "selected@example.com", workspaceLabel: nil, workspaceAccountID: nil, authFingerprint: nil, storedAccountID: nil, selectionSource: .liveSystem, isActive: true, isLive: true, canReauthenticate: false, canRemove: false) let secondaryAccount = CodexVisibleAccount( id: "secondary", email: "secondary@example.com", workspaceLabel: nil, workspaceAccountID: nil, authFingerprint: nil, storedAccountID: nil, selectionSource: .liveSystem, isActive: false, isLive: false, canReauthenticate: false, canRemove: false) store.codexAccountSnapshots = [ CodexAccountUsageSnapshot( account: selectedAccount, snapshot: self.instantOpenSnapshot(email: "selected@example.com"), error: nil, sourceLabel: "test"), CodexAccountUsageSnapshot( account: secondaryAccount, snapshot: self.instantOpenSnapshot(email: "old@example.com"), error: nil, sourceLabel: "test"), ] 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 cachedVersion = controller.menuVersions[ObjectIdentifier(menu)] store.codexAccountSnapshots[1] = CodexAccountUsageSnapshot( account: secondaryAccount, snapshot: self.instantOpenSnapshot(email: "new@example.com"), error: nil, sourceLabel: "test") controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuVersions[ObjectIdentifier(menu)] != cachedVersion) #expect(controller.menuVersions[ObjectIdentifier(menu)] == controller.menuContentVersion) } @Test func `cache preserving structural invalidation rebuilds synchronously on open`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(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 cachedVersion = controller.menuVersions[key] controller.preservingMergedSwitcherContentCachesDuringInvalidation { controller.invalidateMenus() } #expect(controller.menuVersions[key] == cachedVersion) #expect(controller.menuContentVersion != controller.latestDataOnlyMenuContentVersion) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuVersions[key] == controller.menuContentVersion) #expect(controller.openMenuRebuildTasks[key] == nil) } @Test func `data invalidation after cache preserving structural invalidation still rebuilds synchronously`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(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 cachedVersion = controller.menuVersions[key] controller.preservingMergedSwitcherContentCachesDuringInvalidation { controller.invalidateMenus() } let structuralVersion = controller.menuContentVersion controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) #expect(controller.menuVersions[key] == cachedVersion) #expect(controller.latestStructuralMenuContentVersion == structuralVersion) #expect(controller.menuContentVersion == controller.latestDataOnlyMenuContentVersion) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuVersions[key] == controller.menuContentVersion) #expect(controller.openMenuRebuildTasks[key] == nil) } @Test func `menu open does not overlap provider specific refresh`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) let refreshGate = BlockingInstantOpenProviderRefresh() store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() store._setSnapshotForTesting(self.instantOpenSnapshot(email: "refreshed@example.com"), provider: .codex) } defer { store._test_providerRefreshOverride = nil } let existingRefreshTask = Task { await store.refreshProvider(.codex) } await refreshGate.waitUntilStarted(count: 1) let controller = StatusItemController( store: store, settings: settings, account: UsageFetcher().loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } controller.menuRefreshEnabledOverrideForTesting = true StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } for _ in 0..<40 { await Task.yield() } #expect(await refreshGate.startCount == 1) #expect(store.refreshingProviders.contains(.codex)) await refreshGate.releaseFirst() await existingRefreshTask.value for _ in 0..<40 where controller.deferredMenuInteractionRefreshPending { await Task.yield() } #expect(!store.isRefreshing) #expect(!controller.deferredMenuInteractionRefreshPending) } @Test func `cached menu rebuilds after active provider refresh completes`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) let refreshGate = BlockingInstantOpenProviderRefresh() store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() store._setSnapshotForTesting(self.instantOpenSnapshot(email: "refreshed@example.com"), provider: .codex) } defer { store._test_providerRefreshOverride = 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.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.mergedMenu = menu controller.statusItem.menu = menu controller.populateMenu(menu, provider: nil) controller.markMenuFresh(menu) controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) let existingRefreshTask = Task { await store.refreshProvider(.codex) } await refreshGate.waitUntilStarted(count: 1) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.menuNeedsRefresh(menu)) await refreshGate.releaseFirst() await existingRefreshTask.value for _ in 0..<80 where controller.menuNeedsRefresh(menu) { await Task.yield() } #expect(await refreshGate.startCount == 1) #expect(!controller.menuNeedsRefresh(menu)) } @Test func `menu rebuilds after displayed provider completes while another provider refreshes`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableProvidersForInstantOpenTesting([.claude, .codex], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "old@example.com"), provider: .codex) store.refreshingProviders = [.claude] defer { store.refreshingProviders = [] } let controller = StatusItemController( store: store, settings: settings, account: UsageFetcher().loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } controller.menuRefreshEnabledOverrideForTesting = true StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu(for: .codex) controller.populateMenu(menu, provider: .codex) controller.markMenuFresh(menu) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "new@example.com"), provider: .codex) controller.invalidateMenus(allowStaleContentDuringDataRefresh: true) controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } for _ in 0..<80 where controller.menuNeedsRefresh(menu) { await Task.yield() } #expect(!controller.menuNeedsRefresh(menu)) } @Test func `user refresh supersedes background provider refresh`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshGate = BlockingInstantOpenProviderRefresh() var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } refreshInteractions.append(ProviderInteractionContext.current) await refreshGate.run() } defer { store._test_providerRefreshOverride = nil } let backgroundRefreshTask = Task { await ProviderInteractionContext.$current.withValue(.background) { await store.refreshProvider(.codex, coalesceIfRefreshing: true) } } await refreshGate.waitUntilStarted(count: 1) let userRefreshTask = Task { await ProviderInteractionContext.$current.withValue(.userInitiated) { await store.refresh() } } for _ in 0..<40 { await Task.yield() } #expect(await refreshGate.startCount == 1) await refreshGate.releaseFirst() await refreshGate.waitUntilStarted(count: 2) await userRefreshTask.value await backgroundRefreshTask.value #expect(await refreshGate.startCount == 2) #expect(refreshInteractions == [.background, .userInitiated]) } @Test func `settings refresh supersedes background provider refresh without becoming user initiated`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshGate = BlockingInstantOpenProviderRefresh() var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } refreshInteractions.append(ProviderInteractionContext.current) await refreshGate.run() } defer { store._test_providerRefreshOverride = nil } let backgroundRefreshTask = Task { await store.refreshProvider(.codex, coalesceIfRefreshing: true) } await refreshGate.waitUntilStarted(count: 1) let settingsRefreshTask = Task { await store.refreshForSettingsChange() } for _ in 0..<40 { await Task.yield() } #expect(await refreshGate.startCount == 1) await refreshGate.releaseFirst() await refreshGate.waitUntilStarted(count: 2) await settingsRefreshTask.value await backgroundRefreshTask.value #expect(await refreshGate.startCount == 2) #expect(refreshInteractions == [.background, .background]) } @Test func `superseded provider refresh drains before newer result`() async throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableProvidersForInstantOpenTesting([.claude], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshes = OrderedInstantOpenProviderRefresh() let baseSpec = try #require(store.providerSpecs[.claude]) let baseDescriptor = baseSpec.descriptor let strategy = InstantOpenProviderFetchStrategy { await refreshes.awaitSnapshot() } store.providerSpecs[.claude] = ProviderSpec( style: baseSpec.style, isEnabled: baseSpec.isEnabled, descriptor: ProviderDescriptor( id: .claude, metadata: baseDescriptor.metadata, branding: baseDescriptor.branding, tokenCost: baseDescriptor.tokenCost, fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .cli, .oauth], pipeline: ProviderFetchPipeline { _ in [strategy] }), cli: baseDescriptor.cli), makeFetchContext: baseSpec.makeFetchContext) let olderTask = Task { await store.refreshProvider(.claude) } await refreshes.waitUntilStarted(count: 1) let newerTask = Task { await store.refreshProvider(.claude) } for _ in 0..<40 { await Task.yield() } #expect(await refreshes.startCount == 1) await refreshes.resume( call: 1, snapshot: self.instantOpenSnapshot( email: "old@example.com", provider: .claude, percent: 10)) await refreshes.waitUntilStarted(count: 2) await refreshes.resume( call: 2, snapshot: self.instantOpenSnapshot( email: "new@example.com", provider: .claude, percent: 80)) await newerTask.value await olderTask.value #expect(store.snapshot(for: .claude)?.primary?.usedPercent == 80) #expect(store.snapshot(for: .claude)?.accountEmail(for: .claude) == "new@example.com") } @Test func `superseded provider refresh cannot overwrite manually changed token`() async throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableProvidersForInstantOpenTesting([.stepfun], settings: settings) settings.stepfunToken = "initial-token" let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshes = OrderedInstantOpenProviderMutation() let baseSpec = try #require(store.providerSpecs[.stepfun]) let baseDescriptor = baseSpec.descriptor let strategy = InstantOpenProviderMutationFetchStrategy(mutations: refreshes) store.providerSpecs[.stepfun] = ProviderSpec( style: baseSpec.style, isEnabled: baseSpec.isEnabled, descriptor: ProviderDescriptor( id: .stepfun, metadata: baseDescriptor.metadata, branding: baseDescriptor.branding, tokenCost: baseDescriptor.tokenCost, fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .web], pipeline: ProviderFetchPipeline { _ in [strategy] }), cli: baseDescriptor.cli), makeFetchContext: baseSpec.makeFetchContext) let olderTask = Task { await store.refreshProvider(.stepfun) } await refreshes.waitUntilStarted(count: 1) let newerTask = Task { await store.refreshProvider(.stepfun) } for _ in 0..<40 { await Task.yield() } #expect(await refreshes.startCount == 1) settings.stepfunToken = "user-token" await refreshes.resume(call: 1, token: "old-token") await refreshes.waitUntilStarted(count: 2) #expect(settings.stepfunToken == "user-token") await refreshes.resume(call: 2, token: "new-token") await newerTask.value await olderTask.value #expect(settings.stepfunToken == "new-token") } @Test func `superseded provider refresh preserves rotated token when credential is unchanged`() async throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableProvidersForInstantOpenTesting([.stepfun], settings: settings) settings.stepfunToken = "initial-token" let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshes = OrderedInstantOpenProviderMutation() let baseSpec = try #require(store.providerSpecs[.stepfun]) let baseDescriptor = baseSpec.descriptor let strategy = InstantOpenProviderMutationFetchStrategy(mutations: refreshes) store.providerSpecs[.stepfun] = ProviderSpec( style: baseSpec.style, isEnabled: baseSpec.isEnabled, descriptor: ProviderDescriptor( id: .stepfun, metadata: baseDescriptor.metadata, branding: baseDescriptor.branding, tokenCost: baseDescriptor.tokenCost, fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .web], pipeline: ProviderFetchPipeline { _ in [strategy] }), cli: baseDescriptor.cli), makeFetchContext: baseSpec.makeFetchContext) let olderTask = Task { await store.refreshProvider(.stepfun) } await refreshes.waitUntilStarted(count: 1) let newerTask = Task { await store.refreshProvider(.stepfun) } await refreshes.resume(call: 1, token: "rotated-token") await refreshes.waitUntilStarted(count: 2) #expect(settings.stepfunToken == "rotated-token") await refreshes.resume(call: 2, token: "newer-token") await newerTask.value await olderTask.value #expect(settings.stepfunToken == "newer-token") } @Test func `canceling provider refresh cancels its owned probe task`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshGate = BlockingInstantOpenProviderRefresh() var refreshWasCancelled = false store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() refreshWasCancelled = Task.isCancelled } defer { store._test_providerRefreshOverride = nil } let refreshTask = Task { await store.refreshProvider(.codex) } await refreshGate.waitUntilStarted(count: 1) refreshTask.cancel() await refreshGate.releaseFirst() await refreshTask.value #expect(refreshWasCancelled) } @Test func `canceling refresh owner keeps shared provider probe alive`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) let refreshGate = BlockingInstantOpenProviderRefresh() var refreshWasCancelled = false store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() refreshWasCancelled = Task.isCancelled } defer { store._test_providerRefreshOverride = nil } let ownerTask = Task { await store.refreshProvider(.codex) } await refreshGate.waitUntilStarted(count: 1) let sharedWaiterTask = Task { await store.refreshProvider(.codex, coalesceIfRefreshing: true) } for _ in 0..<40 { await Task.yield() } ownerTask.cancel() await refreshGate.releaseFirst() await ownerTask.value await sharedWaiterTask.value #expect(!refreshWasCancelled) #expect(await refreshGate.startCount == 1) } @Test func `background refresh retries canceled provider probe with cached data`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "cached@example.com"), provider: .codex) let refreshGate = BlockingInstantOpenProviderRefresh() store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() } defer { store._test_providerRefreshOverride = nil } let ownerTask = Task { await store.refreshProvider(.codex) } await refreshGate.waitUntilStarted(count: 1) ownerTask.cancel() let backgroundTask = Task { await store.refreshProvider(.codex, coalesceIfRefreshing: true) } for _ in 0..<40 { await Task.yield() } await refreshGate.releaseFirst() await ownerTask.value await backgroundTask.value #expect(await refreshGate.startCount == 2) } @Test func `menu open refresh only retries the displayed provider`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableProvidersForInstantOpenTesting([.claude, .codex], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) store.refreshingProviders.insert(.claude) defer { store.refreshingProviders.remove(.claude) } var refreshedProviders: [UsageProvider] = [] store._test_providerRefreshOverride = { provider in refreshedProviders.append(provider) } defer { store._test_providerRefreshOverride = 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.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuProviders[ObjectIdentifier(menu)] = .codex controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } for _ in 0..<40 where refreshedProviders.isEmpty { await Task.yield() } #expect(refreshedProviders == [.codex]) } @Test func `opening fresh split menu preserves another provider deferred retry`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableProvidersForInstantOpenTesting([.claude, .codex], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "claude@example.com", provider: .claude), provider: .claude) let controller = StatusItemController( store: store, settings: settings, account: UsageFetcher().loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } controller.menuRefreshEnabledOverrideForTesting = true StatusItemController.setMenuOpenRefreshDelayForTesting(.seconds(60)) StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.seconds(60)) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } let codexMenu = controller.makeMenu(for: .codex) controller.menuWillOpen(codexMenu) controller.menuDidClose(codexMenu) #expect(controller.deferredMenuInteractionRefreshProviders == [.codex]) StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) let claudeMenu = controller.makeMenu(for: .claude) controller.menuWillOpen(claudeMenu) defer { controller.menuDidClose(claudeMenu) } for _ in 0..<40 { await Task.yield() } #expect(controller.deferredMenuInteractionRefreshProviders == [.codex]) #expect(controller.deferredMenuInteractionRefreshPending) } @Test func `overview defers only providers that need retry`() { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.mergedMenuLastSelectedWasOverview = true self.enableProvidersForInstantOpenTesting([.claude, .codex], settings: settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) store._setSnapshotForTesting( self.instantOpenSnapshot(email: "claude@example.com", provider: .claude), provider: .claude) let controller = StatusItemController( store: store, settings: settings, account: UsageFetcher().loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } controller.menuRefreshEnabledOverrideForTesting = true StatusItemController.setMenuOpenRefreshDelayForTesting(.seconds(60)) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) defer { controller.menuDidClose(menu) } #expect(controller.deferredMenuInteractionRefreshProviders == [.codex]) } @Test func `closing overview menu stops before refreshing another provider`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.mergedMenuLastSelectedWasOverview = true self.enableProvidersForInstantOpenTesting([.codex, .openai], settings: settings) settings.updateProviderConfig(provider: .openai) { config in config.apiKey = "test-openai-key" } let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) store._setSnapshotForTesting(nil, provider: .openai) let refreshGate = BlockingInstantOpenProviderRefresh() store._test_providerRefreshOverride = { _ in await refreshGate.run() } defer { store._test_providerRefreshOverride = 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.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } StatusItemController.setDeferredMenuInteractionRefreshDelayForTesting(.seconds(60)) defer { StatusItemController.resetDeferredMenuInteractionRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) await refreshGate.waitUntilStarted(count: 1) controller.menuDidClose(menu) await refreshGate.releaseFirst() for _ in 0..<80 { await Task.yield() } #expect(await refreshGate.startCount == 1) #expect(controller.deferredMenuInteractionRefreshPending) } @Test func `closing menu during missing data refresh preserves deferred retry`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) let refreshGate = BlockingInstantOpenProviderRefresh() var refreshInteractions: [ProviderInteraction] = [] store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } refreshInteractions.append(ProviderInteractionContext.current) await refreshGate.run() } defer { store._test_providerRefreshOverride = 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() } StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } let menu = controller.makeMenu() controller.menuWillOpen(menu) await refreshGate.waitUntilStarted(count: 1) #expect(controller.deferredMenuInteractionRefreshPending) #expect(store.refreshingProviders.contains(.codex)) let periodicRefreshTask = Task { await store.refresh() } for _ in 0..<40 { await Task.yield() } #expect(await refreshGate.startCount == 1) controller.menuDidClose(menu) #expect(controller.deferredMenuInteractionRefreshPending) await refreshGate.releaseFirst() await periodicRefreshTask.value for _ in 0..<40 where store.isRefreshing { await Task.yield() } for _ in 0..<40 { await Task.yield() } #expect(controller.deferredMenuInteractionRefreshPending) controller.scheduleDeferredMenuInteractionRefreshIfNeeded(delay: .zero) await refreshGate.waitUntilStarted(count: 2) for _ in 0..<40 where controller.deferredMenuInteractionRefreshPending { await Task.yield() } #expect(await refreshGate.startCount == 2) #expect(refreshInteractions == [.background, .background]) #expect(!controller.deferredMenuInteractionRefreshPending) } @Test func `closing menu during successful missing data refresh clears deferred retry`() async { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false self.enableOnlyCodexForInstantOpenTesting(settings) let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) store._setSnapshotForTesting(nil, provider: .codex) let refreshGate = BlockingInstantOpenProviderRefresh() store._test_providerRefreshOverride = { provider in guard provider == .codex else { return } await refreshGate.run() store._setSnapshotForTesting( UsageSnapshot( primary: RateWindow( usedPercent: 25, windowMinutes: 300, resetsAt: Date().addingTimeInterval(3600), resetDescription: nil), secondary: nil, updatedAt: Date()), provider: .codex) } defer { store._test_providerRefreshOverride = 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() } StatusItemController.setMenuOpenRefreshDelayForTesting(.zero) defer { StatusItemController.resetMenuOpenRefreshDelayForTesting() } var deferredRefreshCount = 0 controller.onDeferredMenuInteractionRefreshForTesting = { deferredRefreshCount += 1 } let menu = controller.makeMenu() controller.menuWillOpen(menu) await refreshGate.waitUntilStarted(count: 1) controller.menuDidClose(menu) for _ in 0..<80 { await Task.yield() } #expect(deferredRefreshCount == 0) await refreshGate.releaseFirst() for _ in 0..<80 where controller.deferredMenuInteractionRefreshPending { await Task.yield() } for _ in 0..<40 { await Task.yield() } #expect(await refreshGate.startCount == 1) #expect(deferredRefreshCount == 0) #expect(!controller.deferredMenuInteractionRefreshPending) } private func enableOnlyCodexForInstantOpenTesting(_ settings: SettingsStore) { self.enableProvidersForInstantOpenTesting([.codex], settings: settings) } private func instantOpenSnapshot( email: String, provider: UsageProvider = .codex, percent: Double = 25) -> UsageSnapshot { UsageSnapshot( primary: RateWindow( usedPercent: percent, windowMinutes: 300, resetsAt: Date().addingTimeInterval(3600), resetDescription: nil), secondary: nil, updatedAt: Date(), identity: ProviderIdentitySnapshot( providerID: provider, accountEmail: email, accountOrganization: nil, loginMethod: "ChatGPT")) } private func enableProvidersForInstantOpenTesting( _ enabledProviders: Set, 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: enabledProviders.contains(provider)) } } } private struct InstantOpenProviderFetchStrategy: ProviderFetchStrategy { let loader: @Sendable () async -> UsageSnapshot var id: String { "instant-open-provider-refresh-test" } var kind: ProviderFetchKind { .cli } func isAvailable(_: ProviderFetchContext) async -> Bool { true } func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult { let usage = await self.loader() return self.makeResult(usage: usage, sourceLabel: self.id) } func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { false } } private struct InstantOpenProviderMutationFetchStrategy: ProviderFetchStrategy { let mutations: OrderedInstantOpenProviderMutation let id = "instant-open-provider-mutation-test" let kind: ProviderFetchKind = .web func isAvailable(_: ProviderFetchContext) async -> Bool { true } func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { let token = await self.mutations.awaitToken() await context.providerManualTokenUpdater?(.stepfun, token) let usage = UsageSnapshot( primary: RateWindow( usedPercent: 25, windowMinutes: 300, resetsAt: Date().addingTimeInterval(3600), resetDescription: nil), secondary: nil, updatedAt: Date()) return self.makeResult(usage: usage, sourceLabel: self.id) } func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { false } } private actor OrderedInstantOpenProviderRefresh { private var started = 0 private var startWaiters: [(count: Int, continuation: CheckedContinuation)] = [] private var continuations: [Int: CheckedContinuation] = [:] var startCount: Int { self.started } func awaitSnapshot() async -> UsageSnapshot { self.started += 1 let call = self.started self.resumeReadyStartWaiters() return await withCheckedContinuation { continuation in self.continuations[call] = continuation } } func waitUntilStarted(count: Int) async { if self.started >= count { return } await withCheckedContinuation { continuation in self.startWaiters.append((count: count, continuation: continuation)) } } func resume(call: Int, snapshot: UsageSnapshot) { self.continuations.removeValue(forKey: call)?.resume(returning: snapshot) } private func resumeReadyStartWaiters() { var remaining: [(count: Int, continuation: CheckedContinuation)] = [] for waiter in self.startWaiters { if self.started >= waiter.count { waiter.continuation.resume() } else { remaining.append(waiter) } } self.startWaiters = remaining } } private actor OrderedInstantOpenProviderMutation { private var started = 0 private var startWaiters: [(count: Int, continuation: CheckedContinuation)] = [] private var continuations: [Int: CheckedContinuation] = [:] var startCount: Int { self.started } func awaitToken() async -> String { self.started += 1 let call = self.started self.resumeReadyStartWaiters() return await withCheckedContinuation { continuation in self.continuations[call] = continuation } } func waitUntilStarted(count: Int) async { if self.started >= count { return } await withCheckedContinuation { continuation in self.startWaiters.append((count: count, continuation: continuation)) } } func resume(call: Int, token: String) { self.continuations.removeValue(forKey: call)?.resume(returning: token) } private func resumeReadyStartWaiters() { var remaining: [(count: Int, continuation: CheckedContinuation)] = [] for waiter in self.startWaiters { if self.started >= waiter.count { waiter.continuation.resume() } else { remaining.append(waiter) } } self.startWaiters = remaining } } private actor BlockingInstantOpenProviderRefresh { private var started = 0 private var startWaiters: [(count: Int, continuation: CheckedContinuation)] = [] private var firstReleaseWaiters: [CheckedContinuation] = [] private var firstReleased = false var startCount: Int { self.started } func run() async { self.started += 1 self.resumeReadyStartWaiters() guard self.started == 1, !self.firstReleased else { return } await withCheckedContinuation { continuation in self.firstReleaseWaiters.append(continuation) } } func waitUntilStarted(count: Int) async { if self.started >= count { return } await withCheckedContinuation { continuation in self.startWaiters.append((count: count, continuation: continuation)) } } func releaseFirst() { self.firstReleased = true let waiters = self.firstReleaseWaiters self.firstReleaseWaiters.removeAll() waiters.forEach { $0.resume() } } private func resumeReadyStartWaiters() { var remaining: [(count: Int, continuation: CheckedContinuation)] = [] for waiter in self.startWaiters { if self.started >= waiter.count { waiter.continuation.resume() } else { remaining.append(waiter) } } self.startWaiters = remaining } }