646 lines
28 KiB
Swift
646 lines
28 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
extension StatusMenuTests {
|
|
@Test
|
|
func `reopening root menu resyncs readiness baseline so reverted store data still refreshes`() {
|
|
// Regression for the readiness-signature optimization (#1351): the baseline is no longer
|
|
// recomputed on every store change while menus are closed, so it must be re-anchored when a
|
|
// root menu opens. Otherwise a closed-then-reopened menu built from new data, followed by an
|
|
// open-menu change that reverts to the *previous* baseline value, would be treated as
|
|
// "unchanged" and skip the rebuild, leaving the visible menu showing stale content.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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()
|
|
|
|
// Root open anchors the baseline to snapshot A. Normalize via an explicit comparison so the
|
|
// assertion below is independent of whatever the controller's initial baseline happened to be.
|
|
controller.menuWillOpen(menu)
|
|
_ = controller.didMenuAdjunctReadinessChange()
|
|
controller.menuDidClose(menu)
|
|
|
|
// Closed store change to B: the optimization intentionally skips recomputing the baseline here.
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
|
|
// Reopening the root menu rebuilds from B and must re-anchor the baseline to B.
|
|
controller.menuWillOpen(menu)
|
|
controller.menuDidClose(menu)
|
|
|
|
// Reverting to A (the value the *first* baseline held) must still register as a change.
|
|
store._setTokenSnapshotForTesting(snapshotA, provider: .codex)
|
|
#expect(controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `root open during in flight refresh preserves stale content and does not resync baseline`() {
|
|
// When `refreshMenuForOpenIfNeeded` keeps existing menu content during an in-flight provider
|
|
// refresh, the readiness baseline must not be re-anchored to live store data. Otherwise the
|
|
// refresh-completion store mutation would compare equal against the prematurely resynced baseline
|
|
// and skip the rebuild, leaving stale content visible (#1351).
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
let key = ObjectIdentifier(menu)
|
|
let openedVersion = controller.menuVersions[key]
|
|
|
|
store.isRefreshing = true
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
|
|
|
|
#expect(controller.menuContentVersion != openedVersion)
|
|
#expect(controller.menuVersions[key] == openedVersion)
|
|
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
// Stale content was preserved: the menu is still behind the current content version.
|
|
#expect(controller.menuNeedsRefresh(menu))
|
|
|
|
store.isRefreshing = false
|
|
// Refresh completion must still register as a readiness change so the open menu can rebuild.
|
|
#expect(controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `native merged menu preparation during in flight refresh preserves stale menu freshness`() throws {
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = true
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
if let claudeMetadata = ProviderRegistry.shared.metadata[.claude] {
|
|
settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: true)
|
|
}
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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 = try #require(controller.statusItem.menu)
|
|
#expect(menu === controller.mergedMenu)
|
|
controller.menuNeedsUpdate(menu)
|
|
let key = ObjectIdentifier(menu)
|
|
let preparedVersion = controller.menuVersions[key]
|
|
|
|
store.isRefreshing = true
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.invalidateMenus(allowStaleContentDuringDataRefresh: true)
|
|
|
|
controller.menuNeedsUpdate(menu)
|
|
|
|
#expect(controller.menuVersions[key] == preparedVersion)
|
|
#expect(controller.menuNeedsRefresh(menu))
|
|
}
|
|
|
|
@Test
|
|
func `root open before deferred store observation rebuilds and refreshes matching observer`() {
|
|
// Store observation invalidates menus from a deferred main-actor task. If a closed menu opens after
|
|
// live data changes but before that task runs, it must rebuild from live data and let the matching
|
|
// observer invalidate any coalesced non-readiness menu state without losing the readiness baseline.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
// Simulate the live store mutation being visible before the observation task has invalidated menus.
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
let key = ObjectIdentifier(menu)
|
|
let versionAfterOpen = controller.menuContentVersion
|
|
let menuVersionAfterOpen = controller.menuVersions[key]
|
|
#expect(!controller.menuNeedsRefresh(menu))
|
|
|
|
controller.handleObservedStoreMenuChange()
|
|
|
|
#expect(controller.menuContentVersion != versionAfterOpen)
|
|
#expect(controller.menuVersions[key] == menuVersionAfterOpen)
|
|
#expect(controller.menuNeedsRefresh(menu))
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `root open before deferred store observation during refresh leaves observer pending`() {
|
|
// The pre-observer root-open repair must not bypass the in-flight refresh stale-content path.
|
|
// While data is refreshing, the deferred observer should still invalidate the open menu and defer
|
|
// parent rebuild instead of marking an intermediate snapshot fresh.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
store.isRefreshing = true
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
let versionAfterOpen = controller.menuContentVersion
|
|
#expect(!controller.menuNeedsRefresh(menu))
|
|
|
|
controller.handleObservedStoreMenuChange()
|
|
|
|
#expect(controller.menuContentVersion != versionAfterOpen)
|
|
#expect(controller.menuNeedsRefresh(menu))
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `fresh newer-version root open during unrelated refresh still reanchors baseline`() {
|
|
// An in-flight refresh elsewhere must not block re-anchoring when this menu was already rebuilt for
|
|
// a newer menuContentVersion. Otherwise the stale baseline can still hide a later reverted update.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.invalidateMenus()
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
|
|
store.isRefreshing = true
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
store._setTokenSnapshotForTesting(snapshotA, provider: .codex)
|
|
#expect(controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `equal-signature root open advances baseline version before next pre-observer change`() {
|
|
// A root open whose signature still equals the baseline can nevertheless confirm that the visible
|
|
// menu is fresh for a newer menuContentVersion. Record that version so a later live-data change before
|
|
// its deferred observer does not look like already-rendered data.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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(for: .codex)
|
|
controller.providerMenus[.codex] = menu
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
controller.invalidateMenus()
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.menuWillOpen(menu)
|
|
controller.menuDidClose(menu)
|
|
|
|
let versionBeforeChange = controller.menuContentVersion
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
#expect(controller.menuContentVersion != versionBeforeChange)
|
|
#expect(!controller.menuNeedsRefresh(menu))
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `newer-version root open rebuilds when rendered signature is older than live data`() {
|
|
// A menu can be fresh for the current menuContentVersion while still having rendered an older
|
|
// readiness signature than the current live store. Root open must rebuild in that pre-observer gap.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
let snapshotC = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 333,
|
|
sessionCostUSD: 3.33,
|
|
last30DaysTokens: 3333,
|
|
last30DaysCostUSD: 33.33,
|
|
updatedAt: Date(timeIntervalSince1970: 300))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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(for: .codex)
|
|
controller.providerMenus[.codex] = menu
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.invalidateMenus()
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.menuWillOpen(menu)
|
|
controller.menuDidClose(menu)
|
|
|
|
let versionBeforeChange = controller.menuContentVersion
|
|
store._setTokenSnapshotForTesting(snapshotC, provider: .codex)
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
#expect(controller.menuContentVersion != versionBeforeChange)
|
|
#expect(!controller.menuNeedsRefresh(menu))
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `equal-signature root open rebuilds when rendered signature reverted before observer`() {
|
|
// A closed provider menu can be rebuilt from B while the readiness baseline remains A. If live data
|
|
// reverts to A before the deferred observer runs, root open must still repair the B-rendered menu.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableOnlyCodexForReadinessBaseline(settings)
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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(for: .codex)
|
|
controller.providerMenus[.codex] = menu
|
|
let key = ObjectIdentifier(menu)
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .codex)
|
|
controller.invalidateMenus()
|
|
controller.populateMenu(menu, provider: .codex)
|
|
controller.markMenuFresh(menu)
|
|
|
|
let renderedBSignature = controller.menuReadinessSignatures[key]
|
|
store._setTokenSnapshotForTesting(snapshotA, provider: .codex)
|
|
let versionBeforeOpen = controller.menuContentVersion
|
|
controller.menuWillOpen(menu)
|
|
defer { controller.menuDidClose(menu) }
|
|
|
|
#expect(controller.menuContentVersion != versionBeforeOpen)
|
|
#expect(controller.menuReadinessSignatures[key] != renderedBSignature)
|
|
#expect(controller.menuReadinessSignatures[key] == controller.menuAdjunctReadinessSignature())
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
@Test
|
|
func `provider root open before deferred store observation leaves sibling provider menu stale`() {
|
|
// The readiness signature is global across enabled providers. In split-icon mode, opening one
|
|
// provider's menu must not consume a pending global observation while leaving sibling menus marked
|
|
// fresh even though their provider data changed.
|
|
self.disableMenuCardsForTesting()
|
|
let settings = self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = false
|
|
settings.costUsageEnabled = true
|
|
self.enableProvidersForReadinessBaseline(settings, providers: [.claude, .codex])
|
|
|
|
let snapshotA = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 111,
|
|
sessionCostUSD: 1.11,
|
|
last30DaysTokens: 1111,
|
|
last30DaysCostUSD: 11.11,
|
|
updatedAt: Date(timeIntervalSince1970: 100))
|
|
let snapshotB = self.makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: 222,
|
|
sessionCostUSD: 2.22,
|
|
last30DaysTokens: 2222,
|
|
last30DaysCostUSD: 22.22,
|
|
updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
|
|
store._setTokenSnapshotForTesting(snapshotA, provider: .claude)
|
|
store._setTokenSnapshotForTesting(snapshotA, 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 claudeMenu = controller.makeMenu(for: .claude)
|
|
controller.populateMenu(claudeMenu, provider: .claude)
|
|
controller.markMenuFresh(claudeMenu)
|
|
let codexMenu = controller.makeMenu(for: .codex)
|
|
controller.populateMenu(codexMenu, provider: .codex)
|
|
controller.markMenuFresh(codexMenu)
|
|
controller.resyncMenuAdjunctReadinessBaseline()
|
|
|
|
store._setTokenSnapshotForTesting(snapshotB, provider: .claude)
|
|
controller.menuWillOpen(codexMenu)
|
|
defer { controller.menuDidClose(codexMenu) }
|
|
|
|
let versionAfterOpen = controller.menuContentVersion
|
|
#expect(!controller.menuNeedsRefresh(codexMenu))
|
|
#expect(controller.menuNeedsRefresh(claudeMenu))
|
|
|
|
controller.handleObservedStoreMenuChange()
|
|
|
|
#expect(controller.menuContentVersion != versionAfterOpen)
|
|
#expect(controller.menuNeedsRefresh(codexMenu))
|
|
#expect(controller.menuNeedsRefresh(claudeMenu))
|
|
#expect(!controller.didMenuAdjunctReadinessChange())
|
|
}
|
|
|
|
private func enableOnlyCodexForReadinessBaseline(_ settings: SettingsStore) {
|
|
self.enableProvidersForReadinessBaseline(settings, providers: [.codex])
|
|
}
|
|
|
|
private func enableProvidersForReadinessBaseline(_ settings: SettingsStore, providers: Set<UsageProvider>) {
|
|
let registry = ProviderRegistry.shared
|
|
for provider in UsageProvider.allCases {
|
|
guard let metadata = registry.metadata[provider] else { continue }
|
|
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: providers.contains(provider))
|
|
}
|
|
}
|
|
|
|
private func makeReadinessBaselineTokenSnapshot(
|
|
sessionTokens: Int,
|
|
sessionCostUSD: Double,
|
|
last30DaysTokens: Int,
|
|
last30DaysCostUSD: Double,
|
|
updatedAt: Date) -> CostUsageTokenSnapshot
|
|
{
|
|
CostUsageTokenSnapshot(
|
|
sessionTokens: sessionTokens,
|
|
sessionCostUSD: sessionCostUSD,
|
|
last30DaysTokens: last30DaysTokens,
|
|
last30DaysCostUSD: last30DaysCostUSD,
|
|
daily: [
|
|
CostUsageDailyReport.Entry(
|
|
date: "2026-05-24",
|
|
inputTokens: nil,
|
|
outputTokens: nil,
|
|
totalTokens: sessionTokens,
|
|
costUSD: last30DaysCostUSD,
|
|
modelsUsed: nil,
|
|
modelBreakdowns: nil),
|
|
],
|
|
updatedAt: updatedAt)
|
|
}
|
|
}
|