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

308 lines
12 KiB
Swift

import AppKit
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@MainActor
private final class ClosedMenuManualRefreshGate {
private var continuation: CheckedContinuation<Void, Never>?
private var isOpen = false
func wait() async {
if self.isOpen {
self.isOpen = false
return
}
await withCheckedContinuation { continuation in
self.continuation = continuation
}
}
func resume() {
if let continuation = self.continuation {
continuation.resume()
self.continuation = nil
} else {
self.isOpen = true
}
}
}
extension StatusMenuTests {
@Test
func `stale data refresh suppresses icon attached closed menu preparation`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
if let metadata = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
}
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
for _ in 0..<20 {
await Task.yield()
}
let menu = controller.makeMenu()
// Simulate a closed menu that was attached by an icon update but has never been opened.
controller.fallbackMenu = menu
controller.statusItem.menu = menu
let key = ObjectIdentifier(menu)
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
controller.prepareAttachedClosedMenusIfNeeded()
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == nil)
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `stale refresh completion requeues required closed menu preparation blocked by refresh`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
if let metadata = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
}
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
for _ in 0..<20 {
await Task.yield()
}
let menu = controller.makeMenu()
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
store.isRefreshing = true
for _ in 0..<40 where controller.closedMenuRebuildTasks[key] != nil {
await Task.yield()
}
#expect(requiredVersion > (openedVersion ?? -1))
#expect(controller.closedMenuRebuildTasks[key] == nil)
#expect(controller.menuVersions[key] == openedVersion)
store.isRefreshing = false
controller.fallbackMenu = menu
controller.statusItem.menu = menu
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
for _ in 0..<40 where controller.menuVersions[key] == openedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `manual refresh completion requeues required closed menu preparation`() async throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
if let metadata = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
}
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.fallbackMenu = menu
controller.statusItem.menu = menu
controller.populateMenu(menu, provider: nil)
controller.markMenuFresh(menu)
let key = ObjectIdentifier(menu)
let initialVersion = controller.menuVersions[key]
let gate = ClosedMenuManualRefreshGate()
controller._test_manualRefreshOperation = { await gate.wait() }
defer {
gate.resume()
controller._test_manualRefreshOperation = nil
}
controller.refreshNow()
let task = try #require(controller.manualRefreshTasks[.global])
controller.invalidateMenus()
for _ in 0..<40 {
await Task.yield()
}
#expect(controller.menuVersions[key] == initialVersion)
gate.resume()
await task.value
for _ in 0..<40 where controller.menuVersions[key] == initialVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
}
@Test
func `closed menu prewarm waits for other menu tracking to end`() 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() }
StatusItemController.setClosedMenuPreparationDelayForTesting(.milliseconds(50))
defer { StatusItemController.resetClosedMenuPreparationDelayForTesting() }
controller.menuRefreshEnabledOverrideForTesting = true
let closedMenu = controller.makeMenu(for: .claude)
controller.providerMenus[.claude] = closedMenu
controller.populateMenu(closedMenu, provider: .claude)
controller.markMenuFresh(closedMenu)
let closedKey = ObjectIdentifier(closedMenu)
let closedVersion = controller.menuVersions[closedKey]
controller.invalidateMenus()
controller.rebuildClosedMenuIfNeeded(closedMenu)
#expect(controller.closedMenuRebuildTasks[closedKey] != nil)
let visibleMenu = controller.makeMenu(for: .codex)
controller.menuWillOpen(visibleMenu)
try? await Task.sleep(for: .milliseconds(80))
for _ in 0..<20 where controller.closedMenuRebuildTasks[closedKey] != nil {
await Task.yield()
}
#expect(controller.menuVersions[closedKey] == closedVersion)
#expect(controller.openMenus[ObjectIdentifier(visibleMenu)] != nil)
controller.menuDidClose(visibleMenu)
try? await Task.sleep(for: .milliseconds(80))
for _ in 0..<20 where controller.menuVersions[closedKey] == closedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[closedKey] == controller.menuContentVersion)
}
@Test
func `data refresh while persistent menu is open rebuilds on close`() async {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
if let metadata = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex)
}
}
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.fallbackMenu = menu
controller.statusItem.menu = menu
controller.menuWillOpen(menu)
let key = ObjectIdentifier(menu)
let openedVersion = controller.menuVersions[key]
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
#expect(controller.menuContentVersion != openedVersion)
#expect(controller.menuVersions[key] == openedVersion)
controller.menuDidClose(menu)
for _ in 0..<40 where controller.menuVersions[key] == openedVersion {
await Task.yield()
}
#expect(controller.openMenus.isEmpty)
#expect(controller.menuVersions[key] == controller.menuContentVersion)
#expect(controller.menuVersions[key] != openedVersion)
}
}