import AppKitimport CodexBarCoreimport Foundationimport Testing@testable import CodexBar /// Regression coverage for battery drain caused by fallback-provider animation./// See GitHub issues #269, #139.@MainActor@Suite(.serialized)struct BatteryDrainDiagnosticTests { private func ensureAppKitInitialized() { _ = NSApplication.shared } private func makeStatusBarForTesting() -> NSStatusBar { // Use the real system status bar in tests. Creating standalone NSStatusBar instances // has caused AppKit teardown crashes under swiftpm-testing-helper. .system } @Test func `Fallback provider should not animate when all providers are disabled`() { self.ensureAppKitInitialized() let settings = SettingsStore( configStore: testConfigStore(suiteName: "BatteryDrain-AllDisabled"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false let registry = ProviderRegistry.shared for provider in UsageProvider.allCases { if let meta = registry.metadata[provider] { settings.setProviderEnabled(provider: provider, metadata: meta, enabled: false) } } 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()) defer { controller.releaseStatusItemsForTesting() } #expect( controller.needsMenuBarIconAnimation() == false, "Should not animate when only fallback provider is visible") #expect( controller.animationDriver == nil, "Animation driver should not start for fallback provider") } @Test func `Enabled provider with data should not animate`() { self.ensureAppKitInitialized() let settings = SettingsStore( configStore: testConfigStore(suiteName: "BatteryDrain-HasData"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = true settings.selectedMenuProvider = .codex let registry = ProviderRegistry.shared if let meta = registry.metadata[.codex] { settings.setProviderEnabled(provider: .codex, metadata: meta, enabled: true) } let fetcher = UsageFetcher() let store = UsageStore( fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) let snapshot = UsageSnapshot( primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil), secondary: RateWindow(usedPercent: 30, windowMinutes: nil, resetsAt: nil, resetDescription: nil), updatedAt: Date()) store._setSnapshotForTesting(snapshot, provider: .codex) let controller = StatusItemController( store: store, settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) defer { controller.releaseStatusItemsForTesting() } #expect( controller.needsMenuBarIconAnimation() == false, "Should not animate when provider has data") #expect( controller.animationDriver == nil, "Animation driver should be nil when data is present") } @Test func `Enabled provider without data should animate`() { self.ensureAppKitInitialized() let settings = SettingsStore( configStore: testConfigStore(suiteName: "BatteryDrain-NoData"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false let registry = ProviderRegistry.shared if let meta = registry.metadata[.codex] { settings.setProviderEnabled(provider: .codex, metadata: meta, 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()) defer { controller.releaseStatusItemsForTesting() } #expect( controller.needsMenuBarIconAnimation() == true, "Should animate when enabled provider has no data") } @Test func `Enabled provider with error should not animate`() { self.ensureAppKitInitialized() let settings = SettingsStore( configStore: testConfigStore(suiteName: "BatteryDrain-ErrorStops"), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false let registry = ProviderRegistry.shared if let meta = registry.metadata[.codex] { settings.setProviderEnabled(provider: .codex, metadata: meta, enabled: true) } let fetcher = UsageFetcher() let store = UsageStore( fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) store._setErrorForTesting("simulated Codex RPC timeout", provider: .codex) let controller = StatusItemController( store: store, settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: self.makeStatusBarForTesting()) #expect(store.isStale(provider: .codex) == true) #expect( controller.needsMenuBarIconAnimation() == false, "Should not animate when provider has recorded an error") #expect(controller.animationDriver == nil) }}
import AppKit
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
/// Regression coverage for battery drain caused by fallback-provider animation.
/// See GitHub issues #269, #139.
@MainActor
@Suite(.serialized)
struct BatteryDrainDiagnosticTests {
private func ensureAppKitInitialized() {
_ = NSApplication.shared
}
private func makeStatusBarForTesting() -> NSStatusBar {
// Use the real system status bar in tests. Creating standalone NSStatusBar instances
// has caused AppKit teardown crashes under swiftpm-testing-helper.
.system
}
@Test
func `Fallback provider should not animate when all providers are disabled`() {
self.ensureAppKitInitialized()
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "BatteryDrain-AllDisabled"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
for provider in UsageProvider.allCases {
if let meta = registry.metadata[provider] {
settings.setProviderEnabled(provider: provider, metadata: meta, enabled: false)
}
}
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())
defer { controller.releaseStatusItemsForTesting() }
#expect(
controller.needsMenuBarIconAnimation() == false,
"Should not animate when only fallback provider is visible")
#expect(
controller.animationDriver == nil,
"Animation driver should not start for fallback provider")
}
@Test
func `Enabled provider with data should not animate`() {
self.ensureAppKitInitialized()
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "BatteryDrain-HasData"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .codex
let registry = ProviderRegistry.shared
if let meta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: meta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings)
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 50, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 30, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date())
store._setSnapshotForTesting(snapshot, provider: .codex)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }
#expect(
controller.needsMenuBarIconAnimation() == false,
"Should not animate when provider has data")
#expect(
controller.animationDriver == nil,
"Animation driver should be nil when data is present")
}
@Test
func `Enabled provider without data should animate`() {
self.ensureAppKitInitialized()
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "BatteryDrain-NoData"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
if let meta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: meta, 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())
defer { controller.releaseStatusItemsForTesting() }
#expect(
controller.needsMenuBarIconAnimation() == true,
"Should animate when enabled provider has no data")
}
@Test
func `Enabled provider with error should not animate`() {
self.ensureAppKitInitialized()
let settings = SettingsStore(
configStore: testConfigStore(suiteName: "BatteryDrain-ErrorStops"),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = false
let registry = ProviderRegistry.shared
if let meta = registry.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: meta, enabled: true)
}
let fetcher = UsageFetcher()
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings)
store._setErrorForTesting("simulated Codex RPC timeout", provider: .codex)
let controller = StatusItemController(
store: store,
settings: settings,
account: fetcher.loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
#expect(store.isStale(provider: .codex) == true)
#expect(
controller.needsMenuBarIconAnimation() == false,
"Should not animate when provider has recorded an error")
#expect(controller.animationDriver == nil)
}
}