365 lines
15 KiB
Swift
365 lines
15 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@MainActor
|
|
@Suite(.serialized)
|
|
struct StatusItemIconObservationSignatureTests {
|
|
private func makeController(suiteName: String) -> (SettingsStore, UsageStore, StatusItemController) {
|
|
let settings = SettingsStore(
|
|
configStore: testConfigStore(suiteName: suiteName),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
settings.statusChecksEnabled = true
|
|
settings.refreshFrequency = .manual
|
|
settings.usageBarsShowUsed = false
|
|
settings.showOptionalCreditsAndExtraUsage = true
|
|
settings.menuBarShowsBrandIconWithPercent = false
|
|
settings.menuBarShowsHighestUsage = false
|
|
settings.mergeIcons = true
|
|
settings.mergedMenuLastSelectedWasOverview = false
|
|
settings.selectedMenuProvider = .codex
|
|
|
|
let registry = ProviderRegistry.shared
|
|
if let codexMeta = registry.metadata[.codex] {
|
|
settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true)
|
|
}
|
|
if let claudeMeta = registry.metadata[.claude] {
|
|
settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false)
|
|
}
|
|
|
|
let fetcher = UsageFetcher()
|
|
let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings)
|
|
store._setSnapshotForTesting(Self.makeSnapshot(provider: .codex, email: "icon@example.com"), provider: .codex)
|
|
let controller = StatusItemController(
|
|
store: store,
|
|
settings: settings,
|
|
account: fetcher.loadAccountInfo(),
|
|
updater: DisabledUpdaterController(),
|
|
preferencesSelection: PreferencesSelection(),
|
|
statusBar: .system)
|
|
return (settings, store, controller)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature ignores refresh and status metadata churn`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-refresh-metadata")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
store.statuses[.codex] = ProviderStatus(
|
|
indicator: .none,
|
|
description: "initial",
|
|
updatedAt: Date(timeIntervalSince1970: 10))
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store.isRefreshing = true
|
|
store.statuses[.codex] = ProviderStatus(
|
|
indicator: .none,
|
|
description: "same indicator, newer timestamp",
|
|
updatedAt: Date(timeIntervalSince1970: 20))
|
|
|
|
#expect(controller.storeIconObservationSignature() == baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature ignores non visual snapshot churn`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-snapshot-metadata")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeSnapshot(
|
|
provider: .codex,
|
|
email: "rotated-account@example.com",
|
|
updatedAt: Date(timeIntervalSince1970: 200)),
|
|
provider: .codex)
|
|
|
|
let signature = controller.storeIconObservationSignature()
|
|
|
|
#expect(signature == baseline)
|
|
#expect(!signature.contains("rotated-account@example.com"))
|
|
}
|
|
|
|
@Test
|
|
func `merged store icon observation signature ignores non primary snapshot churn`() throws {
|
|
let (settings, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-merged-secondary-snapshot")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let registry = ProviderRegistry.shared
|
|
let claudeMetadata = try #require(registry.metadata[.claude])
|
|
settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: true)
|
|
settings.selectedMenuProvider = .codex
|
|
store._setSnapshotForTesting(
|
|
Self.makeSnapshot(provider: .claude, email: "claude@example.com"),
|
|
provider: .claude)
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeSnapshot(
|
|
provider: .claude,
|
|
email: "changed@example.com",
|
|
primaryUsedPercent: 99,
|
|
secondaryUsedPercent: 88,
|
|
updatedAt: Date(timeIntervalSince1970: 300)),
|
|
provider: .claude)
|
|
|
|
#expect(controller.storeIconObservationSignature() == baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature changes when icon percentages change`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-percent-change")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeSnapshot(
|
|
provider: .codex,
|
|
email: "icon@example.com",
|
|
primaryUsedPercent: 42,
|
|
secondaryUsedPercent: 63),
|
|
provider: .codex)
|
|
|
|
#expect(controller.storeIconObservationSignature() != baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature tracks selected copilot budget`() throws {
|
|
let (settings, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-copilot-budget")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let registry = ProviderRegistry.shared
|
|
let codexMetadata = try #require(registry.metadata[.codex])
|
|
let copilotMetadata = try #require(registry.metadata[.copilot])
|
|
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: false)
|
|
settings.setProviderEnabled(provider: .copilot, metadata: copilotMetadata, enabled: true)
|
|
settings.selectedMenuProvider = .copilot
|
|
settings.copilotBudgetExtrasEnabled = true
|
|
settings.copilotIconSecondaryWindowID = "copilot-budget-agent"
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeCopilotSnapshot(budgetUsedPercent: 25),
|
|
provider: .copilot)
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeCopilotSnapshot(budgetUsedPercent: 75),
|
|
provider: .copilot)
|
|
|
|
#expect(controller.storeIconObservationSignature() != baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature changes when credit fallback changes`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-credit-fallback")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
store._setSnapshotForTesting(
|
|
Self.makeSnapshot(
|
|
provider: .codex,
|
|
email: "icon@example.com",
|
|
primaryUsedPercent: 100,
|
|
secondaryUsedPercent: 20),
|
|
provider: .codex)
|
|
store.credits = CreditsSnapshot(remaining: 80, events: [], updatedAt: Date(timeIntervalSince1970: 100))
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store.credits = CreditsSnapshot(remaining: 42, events: [], updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
#expect(controller.storeIconObservationSignature() != baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature ignores unused credit balance`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-unused-credits")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
store.credits = CreditsSnapshot(remaining: 80, events: [], updatedAt: Date(timeIntervalSince1970: 100))
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store.credits = CreditsSnapshot(remaining: 42, events: [], updatedAt: Date(timeIntervalSince1970: 200))
|
|
|
|
#expect(controller.storeIconObservationSignature() == baseline)
|
|
}
|
|
|
|
@Test
|
|
func `merged store icon observation signature ignores non primary status changes`() throws {
|
|
let (settings, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-merged-secondary-status")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let registry = ProviderRegistry.shared
|
|
let claudeMetadata = try #require(registry.metadata[.claude])
|
|
settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: true)
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store.statuses[.claude] = ProviderStatus(
|
|
indicator: .major,
|
|
description: "Claude status issue",
|
|
updatedAt: Date(timeIntervalSince1970: 20))
|
|
|
|
#expect(controller.storeIconObservationSignature() == baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature changes when status indicator changes`() {
|
|
let (_, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-status-indicator")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
store.statuses[.codex] = ProviderStatus(
|
|
indicator: .none,
|
|
description: "initial",
|
|
updatedAt: Date(timeIntervalSince1970: 10))
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
store.statuses[.codex] = ProviderStatus(
|
|
indicator: .major,
|
|
description: "major outage",
|
|
updatedAt: Date(timeIntervalSince1970: 20))
|
|
|
|
#expect(controller.storeIconObservationSignature() != baseline)
|
|
}
|
|
|
|
@Test
|
|
func `store icon observation signature changes when hide critters toggles`() {
|
|
let (settings, _, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-hide-critters")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
settings.menuBarHidesCritters = false
|
|
let baseline = controller.storeIconObservationSignature()
|
|
|
|
settings.menuBarHidesCritters = true
|
|
|
|
#expect(controller.storeIconObservationSignature() != baseline)
|
|
}
|
|
|
|
@Test
|
|
func `display settings persist cached widget snapshot`() async {
|
|
let (settings, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-widget-display")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
var widgetSnapshots: [WidgetSnapshot] = []
|
|
store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) }
|
|
defer { store._test_widgetSnapshotSaveOverride = nil }
|
|
|
|
settings.usageBarsShowUsed = true
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
await store.widgetSnapshotPersistTask?.value
|
|
|
|
#expect(widgetSnapshots.last?.usageBarsShowUsed == true)
|
|
#expect(widgetSnapshots.last?.entries.contains(where: { $0.provider == .codex }) == true)
|
|
}
|
|
|
|
@Test
|
|
func `config only settings do not persist cached widget snapshot`() async {
|
|
let (settings, store, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-widget-config-only")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
var widgetSnapshots: [WidgetSnapshot] = []
|
|
store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) }
|
|
defer { store._test_widgetSnapshotSaveOverride = nil }
|
|
|
|
settings.zaiAPIToken = "test-token"
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
await store.widgetSnapshotPersistTask?.value
|
|
|
|
#expect(widgetSnapshots.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `updateIcons reuses a precomputed store icon signature instead of recomputing it`() {
|
|
let (_, _, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-precomputed-reuse")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
let precomputed = "precomputed-store-icon-signature-sentinel"
|
|
controller.updateIcons(precomputedStoreIconSignature: precomputed)
|
|
|
|
// A supplied signature must be stored verbatim; if updateIcons recomputed it, the gate would
|
|
// never equal the sentinel value.
|
|
#expect(controller.lastObservedStoreIconWorkSignature == precomputed)
|
|
}
|
|
|
|
@Test
|
|
func `updateIcons recomputes the store icon signature when none is provided`() {
|
|
let (_, _, controller) = self.makeController(
|
|
suiteName: "StatusItemIconObservationSignatureTests-recompute-default")
|
|
defer { controller.releaseStatusItemsForTesting() }
|
|
|
|
controller.updateIcons()
|
|
|
|
#expect(controller.lastObservedStoreIconWorkSignature == controller.storeIconObservationSignature())
|
|
}
|
|
|
|
private static func makeSnapshot(
|
|
provider: UsageProvider,
|
|
email: String,
|
|
primaryUsedPercent: Double = 10,
|
|
secondaryUsedPercent: Double = 20,
|
|
updatedAt: Date = Date(timeIntervalSince1970: 100))
|
|
-> UsageSnapshot
|
|
{
|
|
UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: primaryUsedPercent,
|
|
windowMinutes: 300,
|
|
resetsAt: nil,
|
|
resetDescription: nil),
|
|
secondary: RateWindow(
|
|
usedPercent: secondaryUsedPercent,
|
|
windowMinutes: 10080,
|
|
resetsAt: nil,
|
|
resetDescription: nil),
|
|
updatedAt: updatedAt,
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: provider,
|
|
accountEmail: email,
|
|
accountOrganization: nil,
|
|
loginMethod: "plus"))
|
|
}
|
|
|
|
private static func makeCopilotSnapshot(budgetUsedPercent: Double) -> UsageSnapshot {
|
|
UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: 10,
|
|
windowMinutes: nil,
|
|
resetsAt: nil,
|
|
resetDescription: nil),
|
|
secondary: RateWindow(
|
|
usedPercent: 20,
|
|
windowMinutes: nil,
|
|
resetsAt: nil,
|
|
resetDescription: nil),
|
|
extraRateWindows: [
|
|
NamedRateWindow(
|
|
id: "copilot-budget-agent",
|
|
title: "Budget - Copilot Agent Premium Requests",
|
|
window: RateWindow(
|
|
usedPercent: budgetUsedPercent,
|
|
windowMinutes: nil,
|
|
resetsAt: nil,
|
|
resetDescription: nil)),
|
|
],
|
|
updatedAt: Date(timeIntervalSince1970: 100),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .copilot,
|
|
accountEmail: "copilot@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "individual"))
|
|
}
|
|
}
|