701 lines
29 KiB
Swift
701 lines
29 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@MainActor
|
|
@Suite(.serialized)
|
|
struct StatusMenuHostedSubmenuRefreshTests {
|
|
@Test
|
|
func `claude swap completion changes open menu readiness`() {
|
|
let settings = Self.makeSettings()
|
|
settings.setProviderEnabled(
|
|
provider: .claude,
|
|
metadata: ProviderDescriptorRegistry.descriptor(for: .claude).metadata,
|
|
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: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let before = controller.menuAdjunctReadinessSignature()
|
|
store.claudeSwapRevision &+= 1
|
|
|
|
#expect(controller.menuAdjunctReadinessSignature() != before)
|
|
}
|
|
|
|
@Test
|
|
func `status components change open menu readiness`() {
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = true
|
|
settings.setProviderEnabled(
|
|
provider: .claude,
|
|
metadata: ProviderDescriptorRegistry.descriptor(for: .claude).metadata,
|
|
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: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let before = controller.menuAdjunctReadinessSignature()
|
|
store.statusComponents[.claude] = [
|
|
ProviderStatusComponent(
|
|
id: "api",
|
|
name: "API",
|
|
indicator: .none,
|
|
status: "operational"),
|
|
]
|
|
|
|
#expect(controller.menuAdjunctReadinessSignature() != before)
|
|
}
|
|
|
|
@Test
|
|
func `project source changes open menu readiness`() {
|
|
let settings = Self.makeSettings()
|
|
settings.costUsageEnabled = true
|
|
Self.enableOnly(settings, provider: .codex)
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(projectSourcePath: "/tmp/main"), provider: .codex)
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let before = controller.menuAdjunctReadinessSignature()
|
|
store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(projectSourcePath: "/tmp/worktree"), provider: .codex)
|
|
|
|
#expect(controller.menuAdjunctReadinessSignature() != before)
|
|
}
|
|
|
|
@Test
|
|
func `status submenu link stays scoped to its provider`() throws {
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = 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: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let submenu = NSMenu()
|
|
#expect(controller.appendStatusComponentsItem(
|
|
to: submenu,
|
|
provider: .claude,
|
|
width: StatusItemController.menuCardBaseWidth))
|
|
#expect(controller.hydrateHostedSubviewMenuIfNeeded(submenu))
|
|
|
|
let link = try #require(submenu.items.last)
|
|
#expect(link.action == #selector(StatusItemController.openStatusPageFromMenuItem(_:)))
|
|
#expect(link.identifier?.rawValue == UsageProvider.claude.rawValue)
|
|
#expect(link.target === controller)
|
|
}
|
|
|
|
@Test
|
|
func `storage native row preserves its plain menu title`() throws {
|
|
let settings = Self.makeSettings()
|
|
settings.providerStorageFootprintsEnabled = true
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
Self.seedStorageFootprint(in: store)
|
|
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let menu = NSMenu()
|
|
#expect(controller.addStorageMenuCardSection(
|
|
to: menu,
|
|
provider: .claude,
|
|
width: StatusItemController.menuCardBaseWidth))
|
|
let item = try #require(menu.items.first)
|
|
#expect(item.title.hasPrefix(L("Storage")))
|
|
#expect(item.title == item.attributedTitle?.string)
|
|
#expect(item.view == nil)
|
|
#expect(item.isEnabled)
|
|
#expect(item.submenu != nil)
|
|
}
|
|
|
|
@Test
|
|
func `open parent menu defers data rebuild until parent tracking ends`() async throws {
|
|
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
|
|
StatusItemController.menuCardRenderingEnabled = true
|
|
defer {
|
|
StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
|
|
}
|
|
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = true
|
|
settings.selectedMenuProvider = .claude
|
|
settings.costUsageEnabled = true
|
|
settings.costSummaryDisplayStyle = .both
|
|
Self.enableOnlyClaude(settings)
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
Self.seedClaudeSnapshots(in: store)
|
|
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
controller.menuRefreshEnabledOverrideForTesting = false
|
|
|
|
let menu = controller.makeMenu()
|
|
controller.menuWillOpen(menu)
|
|
let parentKey = ObjectIdentifier(menu)
|
|
controller.openMenus[parentKey] = menu
|
|
controller.menuVersions[parentKey] = controller.menuContentVersion
|
|
|
|
let costItem = try #require(menu.items.first { ($0.representedObject as? String) == "menuCardCost" })
|
|
#expect(costItem.view == nil)
|
|
#expect(costItem.title == StatusItemController.costMenuTitle)
|
|
#expect(costItem.isEnabled)
|
|
let submenu = try #require(costItem.submenu)
|
|
#expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
|
|
#expect(submenu.minimumWidth >= StatusItemController.menuCardBaseWidth)
|
|
#expect(submenu.items.first?.view == nil)
|
|
|
|
controller.menuRefreshEnabledOverrideForTesting = true
|
|
controller.menuWillOpen(submenu)
|
|
let submenuKey = ObjectIdentifier(submenu)
|
|
#expect(controller.openMenus[submenuKey] === submenu)
|
|
#expect(submenu.items.first?.view != nil)
|
|
|
|
let oldParentVersion = try #require(controller.menuVersions[parentKey])
|
|
controller.invalidateMenus(
|
|
refreshOpenMenus: true,
|
|
deferOpenParentMenuRebuild: true)
|
|
#expect(controller.menuVersions[parentKey] == oldParentVersion)
|
|
controller.invalidateMenus(
|
|
refreshOpenMenus: true,
|
|
deferOpenParentMenuRebuild: true)
|
|
#expect(controller.menuVersions[parentKey] == oldParentVersion)
|
|
|
|
controller.menuDidClose(submenu)
|
|
#expect(controller.openMenus[submenuKey] == nil)
|
|
|
|
for _ in 0..<40 where controller.menuVersions[parentKey] != oldParentVersion {
|
|
await Task.yield()
|
|
}
|
|
#expect(controller.menuVersions[parentKey] == oldParentVersion)
|
|
|
|
controller.menuDidClose(menu)
|
|
for _ in 0..<40 where controller.menuVersions[parentKey] != controller.menuContentVersion {
|
|
await Task.yield()
|
|
}
|
|
if controller.menuVersions[parentKey] != controller.menuContentVersion {
|
|
controller.menuWillOpen(menu)
|
|
}
|
|
for _ in 0..<40 where controller.menuVersions[parentKey] != controller.menuContentVersion {
|
|
await Task.yield()
|
|
}
|
|
#expect(controller.menuVersions[parentKey] == controller.menuContentVersion)
|
|
}
|
|
|
|
@Test
|
|
func `open hosted submenu rebuilds from unavailable placeholder when data arrives`() async {
|
|
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
|
|
StatusItemController.menuCardRenderingEnabled = true
|
|
defer {
|
|
StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
|
|
}
|
|
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = true
|
|
settings.selectedMenuProvider = .claude
|
|
settings.costUsageEnabled = true
|
|
Self.enableOnlyClaude(settings)
|
|
|
|
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: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
controller.menuRefreshEnabledOverrideForTesting = true
|
|
|
|
let submenu = controller.makeHostedSubviewPlaceholderMenu(
|
|
chartID: StatusItemController.costHistoryChartID,
|
|
provider: .claude,
|
|
width: StatusItemController.menuCardBaseWidth)
|
|
controller.menuWillOpen(submenu)
|
|
let submenuKey = ObjectIdentifier(submenu)
|
|
#expect(controller.openMenus[submenuKey] === submenu)
|
|
#expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
|
|
#expect(submenu.items.first?.view == nil)
|
|
#expect(submenu.items.first?.title == "No data available")
|
|
|
|
let openedVersion = controller.menuContentVersion
|
|
store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
|
|
controller.invalidateMenus(refreshOpenMenus: true)
|
|
|
|
for _ in 0..<40 {
|
|
if controller.menuContentVersion != openedVersion,
|
|
submenu.items.first?.view != nil
|
|
{
|
|
break
|
|
}
|
|
await Task.yield()
|
|
}
|
|
|
|
#expect(controller.menuContentVersion != openedVersion)
|
|
#expect(submenu.items.first?.representedObject as? String == StatusItemController.costHistoryChartID)
|
|
#expect(submenu.items.first?.view != nil)
|
|
#expect(submenu.items.first?.title != "No data available")
|
|
}
|
|
|
|
@Test
|
|
func `open hydrated provider submenu preserves identity across refresh`() throws {
|
|
try self.assertHostedSubmenuPreservesIdentity(
|
|
chartID: StatusItemController.costHistoryChartID,
|
|
provider: .claude,
|
|
seed: Self.seedClaudeSnapshots)
|
|
try self.assertHostedSubmenuPreservesIdentity(
|
|
chartID: StatusItemController.costHistoryChartID,
|
|
provider: .openai,
|
|
seed: Self.seedOpenAICostSnapshot)
|
|
try self.assertHostedSubmenuPreservesIdentity(
|
|
chartID: StatusItemController.usageHistoryChartID,
|
|
provider: .claude,
|
|
seed: Self.seedPlanUtilizationHistory)
|
|
try self.assertHostedSubmenuPreservesIdentity(
|
|
chartID: StatusItemController.storageBreakdownID,
|
|
provider: .claude,
|
|
seed: Self.seedStorageFootprint)
|
|
try self.assertHostedSubmenuPreservesIdentity(
|
|
chartID: StatusItemController.zaiHourlyUsageChartID,
|
|
provider: .zai,
|
|
seed: Self.seedZaiHourlyUsage)
|
|
}
|
|
|
|
@Test
|
|
func `hosted chart items size to the displayed view without a throwaway controller`() throws {
|
|
try self.assertHostedChartItemHeightMatchesRefresh(
|
|
chartID: StatusItemController.costHistoryChartID,
|
|
provider: .claude,
|
|
seed: Self.seedClaudeSnapshots)
|
|
{ controller, submenu, width in
|
|
controller.appendCostHistoryChartItem(to: submenu, provider: .claude, width: width)
|
|
}
|
|
try self.assertHostedChartItemHeightMatchesRefresh(
|
|
chartID: StatusItemController.usageHistoryChartID,
|
|
provider: .claude,
|
|
seed: Self.seedPlanUtilizationHistory)
|
|
{ controller, submenu, width in
|
|
controller.appendUsageHistoryChartItem(to: submenu, provider: .claude, width: width)
|
|
}
|
|
try self.assertHostedChartItemHeightMatchesRefresh(
|
|
chartID: StatusItemController.storageBreakdownID,
|
|
provider: .claude,
|
|
seed: Self.seedStorageFootprint)
|
|
{ controller, submenu, width in
|
|
controller.appendStorageBreakdownItem(to: submenu, provider: .claude, width: width)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `zai chart render signature follows time range boundaries`() throws {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
let beforeMidnight = try #require(formatter.date(from: "2026-01-01 23:30"))
|
|
let afterMidnight = try #require(formatter.date(from: "2026-01-02 00:30"))
|
|
let modelUsage = ZaiModelUsageData(
|
|
xTime: ["2026-01-01 23:00"],
|
|
modelDataList: [
|
|
ZaiModelDataItem(modelName: "glm-4.5", tokensUsage: [100]),
|
|
])
|
|
|
|
let before = StatusItemController.zaiHourlyUsageRenderSignature(
|
|
modelUsage: modelUsage,
|
|
now: beforeMidnight)
|
|
let after = StatusItemController.zaiHourlyUsageRenderSignature(
|
|
modelUsage: modelUsage,
|
|
now: afterMidnight)
|
|
|
|
#expect(before != after)
|
|
}
|
|
|
|
@Test
|
|
func `utilization chart invalidates when active account changes`() throws {
|
|
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
|
|
StatusItemController.menuCardRenderingEnabled = true
|
|
defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering }
|
|
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
Self.enableOnlyClaude(settings)
|
|
settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token")
|
|
settings.addTokenAccount(provider: .claude, label: "Bob", token: "bob-token")
|
|
let accounts = settings.tokenAccounts(for: .claude)
|
|
let alice = try #require(accounts.first)
|
|
let bob = try #require(accounts.last)
|
|
let aliceKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: alice))
|
|
let bobKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: bob))
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
Self.seedClaudeSnapshots(in: store)
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(accounts: [
|
|
aliceKey: [Self.makePlanHistory(usedPercent: 20)],
|
|
bobKey: [Self.makePlanHistory(usedPercent: 50)],
|
|
])
|
|
settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let submenu = controller.makeHostedSubviewPlaceholderMenu(
|
|
chartID: StatusItemController.usageHistoryChartID,
|
|
provider: .claude,
|
|
width: StatusItemController.menuCardBaseWidth)
|
|
controller.menuWillOpen(submenu)
|
|
let aliceView = try #require(submenu.items.first?.view)
|
|
|
|
settings.setActiveTokenAccountIndex(1, for: .claude)
|
|
controller.refreshHostedSubviewMenu(submenu)
|
|
|
|
let bobView = try #require(submenu.items.first?.view)
|
|
#expect(bobView !== aliceView)
|
|
}
|
|
|
|
private func assertHostedChartItemHeightMatchesRefresh(
|
|
chartID: String,
|
|
provider: UsageProvider,
|
|
seed: (UsageStore) -> Void,
|
|
append: (StatusItemController, NSMenu, CGFloat) -> Bool) throws
|
|
{
|
|
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
|
|
StatusItemController.menuCardRenderingEnabled = true
|
|
defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering }
|
|
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.costUsageEnabled = true
|
|
settings.providerStorageFootprintsEnabled = true
|
|
Self.enableOnly(settings, provider: provider)
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
seed(store)
|
|
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let width = StatusItemController.menuCardBaseWidth
|
|
let submenu = NSMenu()
|
|
submenu.minimumWidth = width
|
|
#expect(append(controller, submenu, width))
|
|
|
|
let item = try #require(submenu.items.first)
|
|
let view = try #require(item.view)
|
|
let heightFromAppend = view.frame.height
|
|
// The height the append path assigns must match the authoritative re-measure pass; otherwise
|
|
// dropping the throwaway NSHostingController would have changed sizing behavior.
|
|
controller.refreshHostedSubviewHeights(in: submenu)
|
|
#expect(view.frame.height == heightFromAppend)
|
|
#expect(heightFromAppend > 1)
|
|
}
|
|
|
|
private func assertHostedSubmenuPreservesIdentity(
|
|
chartID: String,
|
|
provider: UsageProvider,
|
|
seed: (UsageStore) -> Void) throws
|
|
{
|
|
let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled
|
|
StatusItemController.menuCardRenderingEnabled = true
|
|
defer {
|
|
StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering
|
|
}
|
|
|
|
let settings = Self.makeSettings()
|
|
settings.statusChecksEnabled = false
|
|
settings.refreshFrequency = .manual
|
|
settings.mergeIcons = true
|
|
settings.selectedMenuProvider = provider
|
|
settings.costUsageEnabled = true
|
|
settings.providerStorageFootprintsEnabled = true
|
|
Self.enableOnly(settings, provider: provider)
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
seed(store)
|
|
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
controller.menuRefreshEnabledOverrideForTesting = true
|
|
|
|
let submenu = controller.makeHostedSubviewPlaceholderMenu(
|
|
chartID: chartID,
|
|
provider: provider,
|
|
width: StatusItemController.menuCardBaseWidth)
|
|
controller.menuWillOpen(submenu)
|
|
|
|
let hydratedItem = try #require(submenu.items.first)
|
|
#expect(hydratedItem.representedObject as? String == chartID)
|
|
#expect(hydratedItem.toolTip == provider.rawValue)
|
|
#expect(hydratedItem.view != nil)
|
|
#expect(hydratedItem.title != "No data available")
|
|
let hydratedView = hydratedItem.view
|
|
let inflatedHeight = hydratedView.map { view -> CGFloat in
|
|
let inflatedHeight = view.frame.height + 100
|
|
if chartID == StatusItemController.zaiHourlyUsageChartID {
|
|
view.frame.size.height = inflatedHeight
|
|
}
|
|
return inflatedHeight
|
|
}
|
|
|
|
controller.refreshHostedSubviewMenu(submenu)
|
|
|
|
let refreshedItem = try #require(submenu.items.first)
|
|
#expect(refreshedItem.representedObject as? String == chartID)
|
|
#expect(refreshedItem.toolTip == provider.rawValue)
|
|
#expect(refreshedItem.view != nil)
|
|
#expect(refreshedItem.title != "No data available")
|
|
#expect(refreshedItem.view === hydratedView)
|
|
if chartID == StatusItemController.zaiHourlyUsageChartID {
|
|
#expect(refreshedItem.view?.frame.height != inflatedHeight)
|
|
}
|
|
|
|
if chartID == StatusItemController.costHistoryChartID, provider == .claude {
|
|
store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(dailyCost: 2.34), provider: .claude)
|
|
controller.refreshHostedSubviewMenu(submenu)
|
|
|
|
let changedItem = try #require(submenu.items.first)
|
|
#expect(changedItem.view != nil)
|
|
#expect(changedItem.view !== hydratedView)
|
|
}
|
|
}
|
|
|
|
private static func makeSettings() -> SettingsStore {
|
|
let suite = "StatusMenuHostedSubmenuRefreshTests-\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defaults.removePersistentDomain(forName: suite)
|
|
return SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suite),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
}
|
|
|
|
private static func enableOnlyClaude(_ settings: SettingsStore) {
|
|
self.enableOnly(settings, provider: .claude)
|
|
}
|
|
|
|
private static func enableOnly(_ settings: SettingsStore, provider enabledProvider: 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: provider == enabledProvider)
|
|
}
|
|
}
|
|
|
|
private static func seedClaudeSnapshots(in store: UsageStore) {
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "user@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "Team"))
|
|
store._setSnapshotForTesting(snapshot, provider: .claude)
|
|
store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(), provider: .claude)
|
|
}
|
|
|
|
private static func seedOpenAICostSnapshot(in store: UsageStore) {
|
|
let day = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let apiUsage = OpenAIAPIUsageSnapshot(
|
|
daily: [
|
|
OpenAIAPIUsageSnapshot.DailyBucket(
|
|
day: "2025-12-23",
|
|
startTime: day,
|
|
endTime: day.addingTimeInterval(86400),
|
|
costUSD: 1.23,
|
|
requests: 12,
|
|
inputTokens: 100,
|
|
cachedInputTokens: 20,
|
|
outputTokens: 40,
|
|
totalTokens: 160,
|
|
lineItems: [],
|
|
models: []),
|
|
],
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_086_400))
|
|
let snapshot = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
openAIAPIUsage: apiUsage,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_086_400),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .openai,
|
|
accountEmail: "openai@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "API"))
|
|
store._setSnapshotForTesting(snapshot, provider: .openai)
|
|
}
|
|
|
|
private static func seedPlanUtilizationHistory(in store: UsageStore) {
|
|
self.seedClaudeSnapshots(in: store)
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(
|
|
unscoped: [
|
|
self.makePlanHistory(usedPercent: 24),
|
|
])
|
|
}
|
|
|
|
private static func makePlanHistory(usedPercent: Double) -> PlanUtilizationSeriesHistory {
|
|
PlanUtilizationSeriesHistory(
|
|
name: .session,
|
|
windowMinutes: 300,
|
|
entries: [
|
|
PlanUtilizationHistoryEntry(
|
|
capturedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
usedPercent: usedPercent,
|
|
resetsAt: Date(timeIntervalSince1970: 1_700_018_000)),
|
|
])
|
|
}
|
|
|
|
private static func seedStorageFootprint(in store: UsageStore) {
|
|
let root = "/Users/test/.claude"
|
|
store.providerStorageFootprints[.claude] = ProviderStorageFootprint(
|
|
provider: .claude,
|
|
totalBytes: 1024,
|
|
paths: [root],
|
|
missingPaths: [],
|
|
unreadablePaths: [],
|
|
components: [.init(path: "\(root)/projects", totalBytes: 1024)],
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
|
|
}
|
|
|
|
private static func seedZaiHourlyUsage(in store: UsageStore) {
|
|
let modelUsage = ZaiModelUsageData(
|
|
xTime: ["2026-05-26 00:00"],
|
|
modelDataList: [
|
|
ZaiModelDataItem(modelName: "glm-4.5", tokensUsage: [512]),
|
|
])
|
|
let snapshot = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
tertiary: nil,
|
|
zaiUsage: ZaiUsageSnapshot(
|
|
tokenLimit: nil,
|
|
timeLimit: nil,
|
|
planName: "Pro",
|
|
modelUsage: modelUsage,
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000)),
|
|
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .zai,
|
|
accountEmail: "zai@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "OAuth"))
|
|
store._setSnapshotForTesting(snapshot, provider: .zai)
|
|
}
|
|
|
|
private static func makeTokenSnapshot(
|
|
dailyCost: Double = 1.23,
|
|
projectSourcePath: String? = nil) -> CostUsageTokenSnapshot
|
|
{
|
|
let projects = projectSourcePath.map { sourcePath in
|
|
[
|
|
CostUsageProjectBreakdown(
|
|
name: "Project",
|
|
path: "/tmp/main",
|
|
totalTokens: 123,
|
|
totalCostUSD: dailyCost,
|
|
daily: [],
|
|
modelBreakdowns: nil,
|
|
sources: [
|
|
CostUsageProjectSourceBreakdown(
|
|
name: "Source",
|
|
path: sourcePath,
|
|
totalTokens: 123,
|
|
totalCostUSD: dailyCost,
|
|
daily: [],
|
|
modelBreakdowns: nil),
|
|
]),
|
|
]
|
|
} ?? []
|
|
return CostUsageTokenSnapshot(
|
|
sessionTokens: 123,
|
|
sessionCostUSD: 0.12,
|
|
last30DaysTokens: 123,
|
|
last30DaysCostUSD: dailyCost,
|
|
daily: [
|
|
CostUsageDailyReport.Entry(
|
|
date: "2025-12-23",
|
|
inputTokens: nil,
|
|
outputTokens: nil,
|
|
totalTokens: 123,
|
|
costUSD: dailyCost,
|
|
modelsUsed: nil,
|
|
modelBreakdowns: nil),
|
|
],
|
|
projects: projects,
|
|
updatedAt: Date())
|
|
}
|
|
}
|