566 lines
27 KiB
Swift
566 lines
27 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@MainActor
|
|
@Suite(.serialized)
|
|
struct StatusItemControllerSplitLifecycleTests {
|
|
private func disableMenuCardsForTesting() {
|
|
StatusItemController.menuCardRenderingEnabled = false
|
|
StatusItemController.setMenuRefreshEnabledForTesting(false)
|
|
}
|
|
|
|
private func makeStatusBarForTesting() -> NSStatusBar {
|
|
.system
|
|
}
|
|
|
|
private func makeSettings() -> SettingsStore {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defaults.removePersistentDomain(forName: suite)
|
|
return SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suite),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
}
|
|
|
|
private func containsHostingView(_ view: NSView) -> Bool {
|
|
if String(describing: type(of: view)).contains("NSHostingView") {
|
|
return true
|
|
}
|
|
return view.subviews.contains { self.containsHostingView($0) }
|
|
}
|
|
|
|
private func makeSplitController() throws -> (SettingsStore, StatusItemController) {
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.providerDetectionCompleted = true
|
|
|
|
let registry = ProviderRegistry.shared
|
|
for provider in UsageProvider.allCases {
|
|
if let metadata = registry.metadata[provider] {
|
|
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: false)
|
|
}
|
|
}
|
|
try settings.setProviderEnabled(provider: .codex, metadata: #require(registry.metadata[.codex]), enabled: true)
|
|
try settings.setProviderEnabled(
|
|
provider: .claude,
|
|
metadata: #require(registry.metadata[.claude]),
|
|
enabled: true)
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: self.makeStatusBarForTesting())
|
|
return (settings, controller)
|
|
}
|
|
|
|
@Test
|
|
func `provider config notifications relay background work impact between settings stores`() {
|
|
self.disableMenuCardsForTesting()
|
|
let sourceSettings = self.makeSettings()
|
|
let controllerSettings = self.makeSettings()
|
|
controllerSettings.statusChecksEnabled = false
|
|
controllerSettings.refreshFrequency = .manual
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(
|
|
fetcher: fetcher,
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: controllerSettings)
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: controllerSettings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: self.makeStatusBarForTesting(),
|
|
observeProviderConfigNotifications: true)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let initialBackgroundRevision = controllerSettings.backgroundWorkSettingsRevision
|
|
let reorderedProviders = Array(sourceSettings.orderedProviders().reversed())
|
|
sourceSettings.setProviderOrder(reorderedProviders)
|
|
|
|
#expect(controllerSettings.orderedProviders() == reorderedProviders)
|
|
#expect(controllerSettings.backgroundWorkSettingsRevision == initialBackgroundRevision)
|
|
|
|
sourceSettings.codexUsageDataSource = .cli
|
|
|
|
#expect(controllerSettings.codexUsageDataSource == .cli)
|
|
#expect(controllerSettings.backgroundWorkSettingsRevision == initialBackgroundRevision + 1)
|
|
}
|
|
|
|
@Test
|
|
func `merged mode removes split provider status items`() throws {
|
|
let (settings, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
#expect(controller.statusItems[.codex] != nil)
|
|
#expect(controller.statusItems[.claude] != nil)
|
|
#expect(controller.expectedVisibleStatusItemAutosaveNames == ["codexbar-codex", "codexbar-claude"])
|
|
|
|
settings.mergeIcons = true
|
|
controller.handleProviderConfigChange(reason: "test")
|
|
|
|
#expect(controller.statusItem.isVisible == true)
|
|
#expect(controller.statusItems.isEmpty)
|
|
#expect(controller.expectedVisibleStatusItemAutosaveNames == ["codexbar-merged"])
|
|
}
|
|
|
|
@Test
|
|
func `removing split provider status items clears all menu lifecycle state`() throws {
|
|
let (settings, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let menus = try [UsageProvider.codex, .claude].map { provider in
|
|
try #require(controller.providerMenus[provider])
|
|
}
|
|
let keys = menus.map(ObjectIdentifier.init)
|
|
for (menu, key) in zip(menus, keys) {
|
|
controller.menuProviders[key] = .codex
|
|
controller.menuReadinessSignatures[key] = "readiness"
|
|
controller.menuIdentitySignatures[key] = "identity"
|
|
controller.menuSession.markFresh(key)
|
|
controller.menuSession.deferUntilNextOpen(key)
|
|
controller.menuSession.deferParentRebuild(key)
|
|
controller.openMenus[key] = menu
|
|
controller.menuRefreshTasks[key] = Task {
|
|
try? await Task.sleep(for: .seconds(60))
|
|
}
|
|
controller.closedMenuRebuildTasks[key] = Task {
|
|
try? await Task.sleep(for: .seconds(60))
|
|
}
|
|
controller.openMenuRebuildTasks[key] = Task {
|
|
try? await Task.sleep(for: .seconds(60))
|
|
}
|
|
_ = controller.closedMenuRebuildRequests.replaceRequest(for: key)
|
|
_ = controller.openMenuRebuildRequests.replaceRequest(for: key)
|
|
controller.openMenuRebuildsClosingHostedSubviewMenus.insert(key)
|
|
controller.highlightedMenuItems[key] = NSMenuItem(title: "Highlighted", action: nil, keyEquivalent: "")
|
|
controller.nativeHighlightDeferredMenuRebuilds[key] = .init(provider: .codex)
|
|
controller.pendingMenuBaselineResyncs.insert(key)
|
|
}
|
|
|
|
settings.mergeIcons = true
|
|
controller.handleProviderConfigChange(reason: "test")
|
|
|
|
for key in keys {
|
|
#expect(controller.menuProviders[key] == nil)
|
|
#expect(controller.menuReadinessSignatures[key] == nil)
|
|
#expect(controller.menuIdentitySignatures[key] == nil)
|
|
#expect(controller.menuSession.renderedVersion(for: key) == nil)
|
|
#expect(!controller.menuSession.isDeferredUntilNextOpen(key))
|
|
#expect(!controller.menuSession.isParentRebuildDeferred(key))
|
|
#expect(controller.openMenus[key] == nil)
|
|
#expect(controller.menuRefreshTasks[key] == nil)
|
|
#expect(controller.closedMenuRebuildTasks[key] == nil)
|
|
#expect(controller.openMenuRebuildTasks[key] == nil)
|
|
#expect(controller.closedMenuRebuildRequests.tokens[key] == nil)
|
|
#expect(controller.openMenuRebuildRequests.tokens[key] == nil)
|
|
#expect(!controller.openMenuRebuildsClosingHostedSubviewMenus.contains(key))
|
|
#expect(controller.highlightedMenuItems[key] == nil)
|
|
#expect(controller.nativeHighlightDeferredMenuRebuilds[key] == nil)
|
|
#expect(!controller.pendingMenuBaselineResyncs.contains(key))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `menu bar icons stay appkit hosted`() throws {
|
|
let (settings, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let codexButton = try #require(controller.statusItems[.codex]?.button)
|
|
#expect(codexButton.image != nil)
|
|
#expect(!self.containsHostingView(codexButton))
|
|
|
|
settings.mergeIcons = true
|
|
controller.handleProviderConfigChange(reason: "test")
|
|
|
|
let mergedButton = try #require(controller.statusItem.button)
|
|
#expect(mergedButton.image != nil)
|
|
#expect(!self.containsHostingView(mergedButton))
|
|
}
|
|
|
|
@Test
|
|
func `status items publish stable manager identity`() throws {
|
|
let (_, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let codexButton = try #require(controller.statusItems[.codex]?.button)
|
|
let claudeButton = try #require(controller.statusItems[.claude]?.button)
|
|
|
|
#expect(controller.statusItem.autosaveName == "codexbar-merged")
|
|
#expect(controller.statusItems[.codex]?.autosaveName == "codexbar-codex")
|
|
#expect(controller.statusItems[.claude]?.autosaveName == "codexbar-claude")
|
|
#expect(controller.statusItem.button?.accessibilityIdentifier() == "CodexBar.StatusItem")
|
|
#expect(codexButton.accessibilityIdentifier() == "CodexBar.StatusItem.codex")
|
|
#expect(claudeButton.accessibilityIdentifier() == "CodexBar.StatusItem.claude")
|
|
#expect(controller.statusItem.button?.accessibilityTitle() == "CodexBar")
|
|
#expect(codexButton.accessibilityTitle() == "CodexBar")
|
|
#expect(claudeButton.accessibilityTitle() == "CodexBar")
|
|
}
|
|
|
|
@Test
|
|
func `status item identity returns stable autosave names`() {
|
|
#expect(StatusItemController.StatusItemIdentity.merged.autosaveName == "codexbar-merged")
|
|
#expect(StatusItemController.StatusItemIdentity.provider(.codex).autosaveName == "codexbar-codex")
|
|
#expect(StatusItemController.StatusItemIdentity.provider(.claude).autosaveName == "codexbar-claude")
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight leaves fresh install placement unset`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-missing-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(defaults: defaults, autosaveName: "codexbar-merged"))
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight preserves missing new key when legacy item placement exists`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-legacy-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(42, forKey: "NSStatusItem Preferred Position Item-0")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged",
|
|
legacyDefaultItemIndex: 0))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-0") == 42)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight clears suspicious matching legacy placement`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-legacy-high-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(11298, forKey: "NSStatusItem Preferred Position Item-0")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
|
|
#expect(MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged",
|
|
legacyDefaultItemIndex: 0,
|
|
maximumPreferredPosition: 3000))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.object(forKey: "NSStatusItem Preferred Position Item-0") == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight preserves missing new key when mixed legacy placements exist`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-legacy-mixed-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(42, forKey: "NSStatusItem Preferred Position Item-0")
|
|
defaults.set(11298, forKey: "NSStatusItem Preferred Position Item-1")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged",
|
|
legacyDefaultItemIndex: 0))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-0") == 42)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-1") == 11298)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight clears provider matching suspicious legacy placement`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-provider-mixed-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(42, forKey: "NSStatusItem Preferred Position Item-0")
|
|
defaults.set(11298, forKey: "NSStatusItem Preferred Position Item-1")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-codex")
|
|
|
|
#expect(MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-codex",
|
|
legacyDefaultItemIndex: 1,
|
|
maximumPreferredPosition: 3000))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-0") == 42)
|
|
#expect(defaults.object(forKey: "NSStatusItem Preferred Position Item-1") == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight leaves provider key unset when only merged legacy placement exists`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-provider-single-legacy-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(42, forKey: "NSStatusItem Preferred Position Item-0")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-codex")
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-codex",
|
|
legacyDefaultItemIndex: 1))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-0") == 42)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight preserves provider key with matching legacy placement`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-provider-matching-legacy-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
defaults.set(42, forKey: "NSStatusItem Preferred Position Item-0")
|
|
defaults.set(58, forKey: "NSStatusItem Preferred Position Item-1")
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-codex")
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-codex",
|
|
legacyDefaultItemIndex: 1))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-0") == 42)
|
|
#expect(defaults.double(forKey: "NSStatusItem Preferred Position Item-1") == 58)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight clears suspicious high position`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-high-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
defaults.set(11298, forKey: key)
|
|
|
|
#expect(MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged",
|
|
maximumPreferredPosition: 3000))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight clears old forced zero position`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-zero-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
defaults.set(0, forKey: key)
|
|
|
|
#expect(MenuBarStatusItemPlacementPreflight.prepare(defaults: defaults, autosaveName: "codexbar-merged"))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight clears malformed position`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-malformed-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
defaults.set("not-a-position", forKey: key)
|
|
|
|
#expect(MenuBarStatusItemPlacementPreflight.prepare(defaults: defaults, autosaveName: "codexbar-merged"))
|
|
|
|
#expect(defaults.object(forKey: key) == nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight preserves reasonable position`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-preserve-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
defaults.set(42, forKey: key)
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(defaults: defaults, autosaveName: "codexbar-merged"))
|
|
|
|
#expect(defaults.double(forKey: key) == 42)
|
|
}
|
|
|
|
@Test
|
|
func `status item placement preflight preserves large display position`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-placement-preserve-large-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
let key = MenuBarStatusItemPlacementPreflight.preferredPositionKey(autosaveName: "codexbar-merged")
|
|
defaults.set(2500, forKey: key)
|
|
|
|
#expect(!MenuBarStatusItemPlacementPreflight.prepare(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged",
|
|
maximumPreferredPosition: 2560))
|
|
|
|
#expect(defaults.double(forKey: key) == 2500)
|
|
}
|
|
|
|
@Test
|
|
func `status item defaults repair removes stale hidden Control Center keys once`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-repair-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(false, forKey: "NSStatusItem VisibleCC Item-0")
|
|
defaults.set(0, forKey: "NSStatusItem VisibleCC Item-12")
|
|
defaults.set(false, forKey: "NSStatusItem VisibleCC codexbar-merged")
|
|
defaults.set(true, forKey: "NSStatusItem VisibleCC Item-1")
|
|
defaults.set(false, forKey: "NSStatusItem VisibleCC com.apple.clock")
|
|
defer {
|
|
defaults.removePersistentDomain(forName: suite)
|
|
}
|
|
|
|
let repairedKeys = MenuBarStatusItemDefaultsRepair.repairHiddenVisibilityDefaultsIfNeeded(defaults: defaults)
|
|
|
|
#expect(repairedKeys == [
|
|
"NSStatusItem VisibleCC Item-0",
|
|
"NSStatusItem VisibleCC Item-12",
|
|
"NSStatusItem VisibleCC codexbar-merged",
|
|
])
|
|
#expect(defaults.object(forKey: "NSStatusItem VisibleCC Item-0") == nil)
|
|
#expect(defaults.object(forKey: "NSStatusItem VisibleCC Item-12") == nil)
|
|
#expect(defaults.object(forKey: "NSStatusItem VisibleCC codexbar-merged") == nil)
|
|
#expect(defaults.bool(forKey: "NSStatusItem VisibleCC Item-1"))
|
|
#expect(defaults.object(forKey: "NSStatusItem VisibleCC com.apple.clock") != nil)
|
|
|
|
defaults.set(false, forKey: "NSStatusItem VisibleCC Item-2")
|
|
#expect(MenuBarStatusItemDefaultsRepair.repairHiddenVisibilityDefaultsIfNeeded(defaults: defaults).isEmpty)
|
|
#expect(defaults.object(forKey: "NSStatusItem VisibleCC Item-2") != nil)
|
|
}
|
|
|
|
@Test
|
|
func `status item visibility default distinguishes enabled disabled and unset`() throws {
|
|
let suite = "StatusItemControllerSplitLifecycleTests-visibility-default-\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(true, forKey: "NSStatusItem VisibleCC codexbar-merged")
|
|
defaults.set(false, forKey: "NSStatusItem VisibleCC codexbar-claude")
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
|
|
#expect(MenuBarStatusItemDefaultsRepair.visibilityDefault(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-merged") == true)
|
|
#expect(MenuBarStatusItemDefaultsRepair.visibilityDefault(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-claude") == false)
|
|
#expect(MenuBarStatusItemDefaultsRepair.visibilityDefault(
|
|
defaults: defaults,
|
|
autosaveName: "codexbar-codex") == nil)
|
|
}
|
|
|
|
@Test
|
|
func `non destructive visibility refresh preserves split provider status items`() throws {
|
|
let (_, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let oldCodexItem = try #require(controller.statusItems[.codex])
|
|
let oldClaudeItem = try #require(controller.statusItems[.claude])
|
|
let oldCodexButton = try #require(oldCodexItem.button)
|
|
|
|
controller.refreshExistingStatusItemsForVisibilityRecovery()
|
|
|
|
let newCodexItem = try #require(controller.statusItems[.codex])
|
|
let newClaudeItem = try #require(controller.statusItems[.claude])
|
|
#expect(newCodexItem === oldCodexItem)
|
|
#expect(newClaudeItem === oldClaudeItem)
|
|
#expect(newCodexItem.button === oldCodexButton)
|
|
#expect(newCodexItem.autosaveName == "codexbar-codex")
|
|
#expect(newCodexItem.button?.accessibilityIdentifier() == "CodexBar.StatusItem.codex")
|
|
}
|
|
|
|
@Test
|
|
func `non destructive visibility refresh preserves merged status item`() throws {
|
|
let (settings, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
settings.mergeIcons = true
|
|
controller.handleProviderConfigChange(reason: "test")
|
|
let oldMergedItem = controller.statusItem
|
|
let oldMergedButton = try #require(controller.statusItem.button)
|
|
|
|
controller.refreshExistingStatusItemsForVisibilityRecovery()
|
|
|
|
#expect(controller.statusItem === oldMergedItem)
|
|
#expect(controller.statusItem.button === oldMergedButton)
|
|
#expect(controller.statusItem.autosaveName == "codexbar-merged")
|
|
#expect(controller.statusItem.button?.accessibilityIdentifier() == "CodexBar.StatusItem")
|
|
}
|
|
|
|
@Test
|
|
func `recreation produces immediately healthy snapshots for synchronous guidance check`() throws {
|
|
// verifyScreenChangeRecoveryIfNeeded does a synchronous re-check immediately after
|
|
// the single recreation to decide whether to show macOS 26 Allow-in-Menu-Bar guidance.
|
|
// AppKit must materialise the button and window before returning from
|
|
// recreateStatusItemsForVisibilityRecovery, so the item must not appear blocked at
|
|
// that point. Only a genuine system-level block would leave it blocked — which is
|
|
// exactly the case where guidance is useful.
|
|
let (_, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
controller.recreateStatusItemsForVisibilityRecovery()
|
|
|
|
let allItems = [controller.statusItem] + Array(controller.statusItems.values)
|
|
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(allItems)
|
|
#expect(!MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(snapshots))
|
|
}
|
|
|
|
@Test
|
|
func `visibility recovery recreates split provider status items`() throws {
|
|
let (_, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let oldCodexItem = try #require(controller.statusItems[.codex])
|
|
controller.recreateStatusItemsForVisibilityRecovery()
|
|
|
|
let newCodexItem = try #require(controller.statusItems[.codex])
|
|
#expect(newCodexItem !== oldCodexItem)
|
|
#expect(newCodexItem.autosaveName == "codexbar-codex")
|
|
#expect(newCodexItem.button?.accessibilityIdentifier() == "CodexBar.StatusItem.codex")
|
|
}
|
|
|
|
@Test
|
|
func `visibility recovery renders replacement merged status item`() throws {
|
|
let (settings, controller) = try self.makeSplitController()
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
settings.mergeIcons = true
|
|
controller.handleProviderConfigChange(reason: "test")
|
|
let renderedSignature = try #require(controller.lastAppliedMergedIconRenderSignature)
|
|
|
|
controller.lastAppliedMergedIconRenderSignature = renderedSignature
|
|
controller.recreateStatusItemsForVisibilityRecovery()
|
|
|
|
let mergedButton = try #require(controller.statusItem.button)
|
|
#expect(mergedButton.image != nil)
|
|
#expect(controller.statusItem.autosaveName == "codexbar-merged")
|
|
#expect(mergedButton.accessibilityIdentifier() == "CodexBar.StatusItem")
|
|
}
|
|
}
|