Files
steipete--codexbar/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift
2026-07-13 12:22:33 +08:00

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"))
}
}