codeburn/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift
iamtoruk 58c6cce6db fix(menubar): the selection fallback showed another provider's totals under the selected tab
Switching providers stashed the outgoing payload and rendered it under the new
tab until that tab's own fetch landed. That is not continuity, it is a wrong
number: opening the Hermes tab printed the all-provider total, and the test
suite pinned that behaviour by asserting $12 under Hermes and $12.34 under
Cursor.

The fallback is gone. With no cached data for the selection, `payload` is
empty, `hasCachedData` is false, and the popover renders the loading state it
already has for a cold key, so no dollar figure is shown for a provider whose
data has not arrived. AgentTabStrip and MenuBarContent take the same
hasCachedData gate for the same reason.
2026-09-02 07:55:43 -07:00

829 lines
31 KiB
Swift

import Foundation
import Testing
@testable import CodeBurnMenubar
private func combinedUsage(cost: Double = 12.5) -> CombinedUsage {
CombinedUsage(
perDevice: [
CombinedDeviceUsage(
id: "local",
name: "MacBook",
local: true,
error: nil,
cost: cost,
calls: 3,
sessions: 2,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180
)
],
combined: CombinedUsageTotals(
cost: cost,
calls: 3,
sessions: 2,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180,
deviceCount: 1,
reachableCount: 1
)
)
}
private func claudeConfigSelector(selectedId: String? = nil) -> ClaudeConfigSelector {
ClaudeConfigSelector(
selectedId: selectedId,
options: [
ClaudeConfigOption(id: "claude-config:work", label: "claude-work", path: "/tmp/claude-work"),
ClaudeConfigOption(id: "claude-config:personal", label: "claude-personal", path: "/tmp/claude-personal")
]
)
}
private func menubarPayload(cost: Double,
calls: Int = 1,
sessions: Int = 1,
inputTokens: Int = 1,
outputTokens: Int = 1,
providers: [String: Double]? = nil,
providerDetails: [ProviderDetail] = [],
combined: CombinedUsage? = nil,
claudeConfigs: ClaudeConfigSelector? = nil) -> MenubarPayload {
MenubarPayload(
generated: "test",
current: CurrentBlock(
label: "Today",
cost: cost,
calls: calls,
sessions: sessions,
oneShotRate: nil,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheHitPercent: 0,
codexCredits: nil,
topActivities: [],
topModels: [],
localModelSavings: LocalModelSavings(totalUSD: 0, calls: 0, byModel: [], byProvider: []),
providers: providers ?? ["claude": cost],
providerDetails: providerDetails,
topProjects: [],
modelEfficiency: [],
topSessions: [],
retryTax: RetryTax(totalUSD: 0, retries: 0, editTurns: 0, byModel: []),
routingWaste: RoutingWaste(totalSavingsUSD: 0, baselineModel: "", baselineCostPerEdit: 0, byModel: []),
tools: [],
skills: [],
subagents: [],
mcpServers: []
),
optimize: OptimizeBlock(findingCount: 0, savingsUSD: 0, topFindings: []),
history: HistoryBlock(daily: []),
combined: combined,
claudeConfigs: claudeConfigs
)
}
@Suite("AppStore refresh recovery")
@MainActor
struct AppStoreRefreshRecoveryTests {
@Test("an active all-provider slice rejects a contradictory scoped zero")
func activeAllProviderSliceRejectsScopedZero() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 11.5, "hermes agent": 0.5],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 11.5, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0.5, calls: 7, hasUsage: true),
]
)
let falseZero = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
store.suppressRefreshesForTesting()
store.switchTo(provider: .hermes)
store.setCachedPayloadForTesting(falseZero, period: .today, provider: .hermes, fetchedAt: Date())
#expect(store.providerPayloadContradictsAllForTesting(falseZero, period: .today, provider: .hermes))
// The Hermes tab must show NO dollar figure. It used to fall back to the
// previous selection's payload and print $12, which is Claude's spend
// plus Hermes', under a tab labelled Hermes.
#expect(!store.hasCachedData)
#expect(store.payload.current.cost == 0)
#expect(store.payload.current.calls == 0)
#expect(store.needsInteractivePayloadRefresh)
}
@Test("positive provider spend cannot disappear when scoped calls survive")
func positiveSpendCannotDisappearWhenScopedCallsSurvive() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 11.5, "hermes agent": 0.5],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 11.5, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0.5, calls: 7, hasUsage: true),
]
)
let missingSpend = menubarPayload(
cost: 0,
calls: 7,
sessions: 7,
inputTokens: 100,
outputTokens: 20,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 7, hasUsage: true),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(store.providerPayloadContradictsAllForTesting(missingSpend, period: .today, provider: .hermes))
}
@Test("flat-rate activity rejects a contradictory scoped zero")
func flatRateActivityRejectsScopedZero() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 12, "hermes agent": 0],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 12, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 7, hasUsage: true),
]
)
let falseZero = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(store.providerPayloadContradictsAllForTesting(falseZero, period: .today, provider: .hermes))
}
@Test("token-only activity accepts a scoped payload with matching usage")
func tokenOnlyActivityAcceptsMatchingScopedUsage() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 12, "hermes agent": 0],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 12, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: true),
]
)
let tokenOnly = menubarPayload(
cost: 0,
calls: 0,
sessions: 1,
inputTokens: 100,
outputTokens: 20,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: true),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(!store.providerPayloadContradictsAllForTesting(tokenOnly, period: .today, provider: .hermes))
}
@Test("scoped providerDetails hasUsage remains authoritative with empty headline totals")
func scopedProviderDetailsUsageRemainsAuthoritative() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 12, "hermes agent": 0],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 12, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: true),
]
)
let providerDetailOnly = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
inputTokens: 0,
outputTokens: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: true),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(!store.providerPayloadContradictsAllForTesting(providerDetailOnly, period: .today, provider: .hermes))
}
@Test("token-only activity rejects a completely empty scoped payload")
func tokenOnlyActivityRejectsEmptyScopedPayload() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 12, "hermes agent": 0],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 12, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: true),
]
)
let empty = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
inputTokens: 0,
outputTokens: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(store.providerPayloadContradictsAllForTesting(empty, period: .today, provider: .hermes))
}
@Test("late all-provider evidence invalidates an already cached scoped zero")
func lateAllProviderEvidenceInvalidatesScopedZero() {
let store = AppStore()
let falseZero = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
inputTokens: 0,
outputTokens: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
let all = menubarPayload(
cost: 12,
providers: ["claude": 11.5, "hermes agent": 0.5],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 11.5, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0.5, calls: 7, hasUsage: true),
]
)
store.suppressRefreshesForTesting()
store.switchTo(provider: .hermes)
store.setCachedPayloadForTesting(falseZero, period: .today, provider: .hermes, fetchedAt: Date())
#expect(store.hasCachedData)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(!store.hasCachedData)
#expect(store.needsInteractivePayloadRefresh)
}
@Test("quiet refresh joins matching in-flight evidence instead of racing ahead")
func quietRefreshJoinsMatchingInFlightEvidence() async {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 11.5, "hermes agent": 0.5],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 11.5, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0.5, calls: 7, hasUsage: true),
]
)
store.setCacheDateToTodayForTesting()
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date())
let joined = Task { @MainActor in
await store.refreshQuietlyForTesting(period: .today, provider: .all)
}
while store.inFlightWaiterCountForTesting(period: .today, provider: .all) == 0 {
await Task.yield()
}
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
store.finishInFlightForTesting(period: .today, provider: .all)
#expect(await joined.value)
}
@Test("refresh pipeline reset releases matching in-flight waiters")
func refreshPipelineResetReleasesInFlightWaiters() async {
let store = AppStore()
store.setCacheDateToTodayForTesting()
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date())
let joined = Task { @MainActor in
await store.refreshQuietlyForTesting(period: .today, provider: .all)
}
while store.inFlightWaiterCountForTesting(period: .today, provider: .all) == 0 {
await Task.yield()
}
store.resetLoadingState()
#expect(!(await joined.value))
#expect(!store.isInFlightForTesting(period: .today, provider: .all))
}
@Test("selection refresh snapshots scoped and all-provider keys together")
func selectionRefreshSnapshotsMatchingKeys() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.switchTo(period: .month)
store.switchTo(provider: .hermes)
let snapshot = store.selectionRefreshKeysForTesting()
store.switchTo(period: .today)
store.switchTo(provider: .cursor)
#expect(snapshot.scoped.period == .month)
#expect(snapshot.scoped.provider == .hermes)
#expect(snapshot.all.period == .month)
#expect(snapshot.all.provider == .all)
#expect(snapshot.scoped.day == snapshot.all.day)
#expect(snapshot.scoped.days == snapshot.all.days)
#expect(snapshot.scoped.claudeConfigSourceId == snapshot.all.claudeConfigSourceId)
}
@Test("a genuine provider zero remains valid when the all-provider slice is also zero")
func genuineProviderZeroRemainsValid() {
let store = AppStore()
let all = menubarPayload(
cost: 12,
providers: ["claude": 12, "hermes agent": 0],
providerDetails: [
ProviderDetail(id: "claude", label: "Claude", cost: 12, calls: 4, hasUsage: true),
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
let zero = menubarPayload(
cost: 0,
calls: 0,
sessions: 0,
providers: ["hermes agent": 0],
providerDetails: [
ProviderDetail(id: "hermes", label: "Hermes Agent", cost: 0, calls: 0, hasUsage: false),
]
)
store.setCachedPayloadForTesting(all, period: .today, provider: .all, fetchedAt: Date())
#expect(!store.providerPayloadContradictsAllForTesting(zero, period: .today, provider: .hermes))
}
@Test("a provider switch shows no figure until the target provider loads")
func providerSwitchShowsNoStaleFigure() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.setCachedPayloadForTesting(
menubarPayload(cost: 12.34),
period: .today,
provider: .all,
fetchedAt: Date()
)
store.switchTo(provider: .cursor)
// Keeping the all-provider payload mounted under the Cursor tab is not
// continuity, it is a wrong number: $12.34 is every provider's spend.
// Empty here is what puts the popover on its loading state instead.
#expect(store.selectedProvider == .cursor)
#expect(!store.hasCachedData)
#expect(store.payload.current.cost == 0)
// The all-provider selection still reads its own cached payload.
store.switchTo(provider: .all)
#expect(store.hasCachedData)
#expect(store.payload.current.cost == 12.34)
}
@Test("stale visible payload triggers hard recovery without clearing cache")
func stalePayloadTriggersHardRecoveryWithoutClearingCache() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 92.33),
period: .today,
provider: .all,
fetchedAt: Date().addingTimeInterval(-180)
)
#expect(store.todayPayload?.current.cost == 92.33)
#expect(store.needsInteractivePayloadRefresh)
#expect(store.needsStatusPayloadRefresh)
#expect(store.hasStaleInteractivePayload)
#expect(store.shouldResetInteractiveRefreshPipeline)
store.resetRefreshState(clearCache: false)
#expect(store.todayPayload?.current.cost == 92.33)
}
@Test("fresh visible payload does not trigger hard recovery")
func freshPayloadDoesNotTriggerHardRecovery() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 164.06),
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(!store.needsInteractivePayloadRefresh)
#expect(!store.needsStatusPayloadRefresh)
#expect(!store.hasStaleInteractivePayload)
#expect(!store.shouldResetInteractiveRefreshPipeline)
}
@Test("payload cache partitions local and combined scope")
func payloadCachePartitionsByScope() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 10),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 99, combined: combinedUsage(cost: 42)),
scope: .combined,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all)?.current.cost == 10)
#expect(store.cachedPayloadForTesting(scope: .combined, period: .today, provider: .all)?.current.cost == 99)
store.selectedScope = .combined
#expect(store.payload.current.cost == 10)
#expect(store.payload.combined?.combined.cost == 42)
}
@Test("multi-day combined selection uses local cache path")
func multiDayCombinedSelectionUsesLocalCachePath() {
let store = AppStore()
let days: Set<String> = ["2026-06-01", "2026-06-02"]
store.selectedScope = .combined
store.selectedDays = days
store.setCachedPayloadForTesting(
menubarPayload(cost: 18),
scope: .local,
period: .today,
provider: .all,
days: days,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 99, combined: combinedUsage(cost: 44)),
scope: .combined,
period: .today,
provider: .all,
days: days,
fetchedAt: Date()
)
#expect(store.activeScope == .local)
#expect(store.payload.current.cost == 18)
#expect(store.payload.combined == nil)
}
@Test("combined failure state does not invalidate local badge payload")
func combinedFailureDoesNotInvalidateLocalBadgePayload() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 31),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.selectedScope = .combined
store.setLastErrorForTesting(
"timeout",
scope: .combined,
period: .today,
provider: .all
)
#expect(store.lastError == "timeout")
#expect(store.menubarPayload?.current.cost == 31)
#expect(!store.needsStatusPayloadRefresh)
#expect(store.payload.current.cost == 31)
#expect(store.payload.combined == nil)
}
@Test("menubar badge shows the combined total under combined scope")
func menubarBadgeShowsCombinedTotal() {
let store = AppStore()
store.suppressRefreshesForTesting()
let period = store.menubarPeriod
// Local badge figure and a higher cross-device combined total, both for
// the badge's period.
store.setCachedPayloadForTesting(
menubarPayload(cost: 30),
scope: .local,
period: period,
provider: .all,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 30, combined: combinedUsage(cost: 75)),
scope: .combined,
period: period,
provider: .all,
fetchedAt: Date()
)
// Local scope: no combined total, so the badge renders the local figure.
store.selectedScope = .local
#expect(store.menubarBadgeCombined == nil)
// Combined scope: the badge total is the cross-device aggregate ($75),
// not the local $30 — this is the fix for the badge trailing the popover.
store.selectedScope = .combined
#expect(store.menubarBadgeCombined?.cost == 75)
}
@Test("badge reports a device shortfall when a paired peer is unreachable")
func menubarBadgeReportsDeviceShortfall() {
let store = AppStore()
store.suppressRefreshesForTesting()
let period = store.menubarPeriod
// Combined payload where only 1 of 2 paired devices reported this cycle
// (the peer is asleep/off-network), so the aggregate is degraded to local.
let degraded = CombinedUsage(
perDevice: [],
combined: CombinedUsageTotals(
cost: 30,
calls: 3,
sessions: 2,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180,
deviceCount: 2,
reachableCount: 1
)
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 30, combined: degraded),
scope: .combined,
period: period,
provider: .all,
fetchedAt: Date()
)
store.selectedScope = .combined
let shortfall = store.menubarBadgeDeviceShortfall
#expect(shortfall?.reachable == 1)
#expect(shortfall?.total == 2)
}
@Test("badge reports no shortfall when every paired device reports")
func menubarBadgeNoShortfallWhenAllReachable() {
let store = AppStore()
store.suppressRefreshesForTesting()
let period = store.menubarPeriod
// combinedUsage() carries deviceCount == reachableCount == 1.
store.setCachedPayloadForTesting(
menubarPayload(cost: 30, combined: combinedUsage(cost: 30)),
scope: .combined,
period: period,
provider: .all,
fetchedAt: Date()
)
store.selectedScope = .combined
#expect(store.menubarBadgeDeviceShortfall == nil)
// Local scope never reports a shortfall.
store.selectedScope = .local
#expect(store.menubarBadgeDeviceShortfall == nil)
}
@Test("menubar badge falls back to local when no combined payload is cached")
func menubarBadgeFallsBackWhenCombinedMissing() {
let store = AppStore()
store.suppressRefreshesForTesting()
let period = store.menubarPeriod
store.setCachedPayloadForTesting(
menubarPayload(cost: 30),
scope: .local,
period: period,
provider: .all,
fetchedAt: Date()
)
// Combined scope selected but no combined payload cached yet (cold cache
// or an unreachable peer): the badge must fall back to the local figure.
store.selectedScope = .combined
#expect(store.menubarBadgeCombined == nil)
}
@Test("switching to combined resets selected provider to all")
func switchingToCombinedResetsSelectedProviderToAll() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.selectedScope = .local
store.selectedProvider = .claude
store.switchTo(scope: .combined)
#expect(store.selectedScope == .combined)
#expect(store.selectedProvider == .all)
}
@Test("selected Claude config partitions payload cache")
func selectedClaudeConfigPartitionsPayloadCache() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 10, claudeConfigs: claudeConfigSelector()),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 4, claudeConfigs: claudeConfigSelector(selectedId: "claude-config:work")),
scope: .local,
period: .today,
provider: .all,
claudeConfigSourceId: "claude-config:work",
fetchedAt: Date()
)
#expect(store.payload.current.cost == 10)
store.selectedClaudeConfigSourceId = "claude-config:work"
#expect(store.payload.current.cost == 4)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all)?.current.cost == 10)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all, claudeConfigSourceId: "claude-config:work")?.current.cost == 4)
}
@Test("Claude config selector is hidden until multiple configs are available")
func claudeConfigSelectorVisibilityRequiresMultipleConfigs() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 1),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(!store.shouldShowClaudeConfigSelector)
store.setCachedPayloadForTesting(
menubarPayload(cost: 2, claudeConfigs: claudeConfigSelector()),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.shouldShowClaudeConfigSelector)
#expect(store.claudeConfigOptions.map(\.label) == ["claude-work", "claude-personal"])
}
@Test("selecting Claude config resets provider and combined scope")
func selectingClaudeConfigResetsProviderAndCombinedScope() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.selectedScope = .combined
store.selectedProvider = .codex
store.switchTo(claudeConfigSourceId: "claude-config:work")
#expect(store.selectedClaudeConfigSourceId == "claude-config:work")
#expect(store.selectedScope == .local)
#expect(store.selectedProvider == .all)
}
@Test("daily budget warning is suppressed for combined scope")
func dailyBudgetWarningIsSuppressedForCombinedScope() {
let defaults = UserDefaults.standard
let previousDisplayMetric = defaults.object(forKey: "CodeBurnDisplayMetric")
let previousDailyBudget = defaults.object(forKey: "CodeBurnDailyBudget")
defer {
if let previousDisplayMetric {
defaults.set(previousDisplayMetric, forKey: "CodeBurnDisplayMetric")
} else {
defaults.removeObject(forKey: "CodeBurnDisplayMetric")
}
if let previousDailyBudget {
defaults.set(previousDailyBudget, forKey: "CodeBurnDailyBudget")
} else {
defaults.removeObject(forKey: "CodeBurnDailyBudget")
}
}
let store = AppStore()
store.selectedScope = .local
store.selectedDays = []
store.displayMetric = .cost
store.dailyBudget = 10
store.setCachedPayloadForTesting(
menubarPayload(cost: 12.5),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.isOverDailyBudget)
#expect(store.shouldShowDailyBudgetWarning)
store.selectedScope = .combined
#expect(store.isOverDailyBudget)
#expect(!store.shouldShowDailyBudgetWarning)
}
@Test("missing today status payload needs status refresh")
func missingTodayStatusPayloadNeedsStatusRefresh() {
let store = AppStore()
#expect(store.todayPayload == nil)
#expect(store.needsStatusPayloadRefresh)
}
@Test("missing unattempted payload triggers hard recovery")
func missingUnattemptedPayloadTriggersHardRecovery() {
let store = AppStore()
#expect(!store.hasCachedData)
#expect(!store.hasAttemptedCurrentKeyLoad)
#expect(store.needsInteractivePayloadRefresh)
#expect(store.hasMissingInteractivePayloadWithoutAttempt)
#expect(store.shouldResetInteractiveRefreshPipeline)
}
@Test("orphaned stale in-flight entry does not block stuck-loading recovery")
func staleInFlightDoesNotBlockRecovery() {
let store = AppStore()
// A quiet refresh torn down across sleep/wake can leave an in-flight
// entry behind for the current key with no cache and no active loading
// counter, far older than the watchdog window. Recovery must clear it
// and proceed instead of bailing on the in-flight guard forever.
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date().addingTimeInterval(-3600))
#expect(store.isInFlightForTesting(period: .today, provider: .all))
let canRecover = store.prepareStuckLoadingRecovery()
#expect(canRecover)
#expect(!store.isInFlightForTesting(period: .today, provider: .all))
}
@Test("healthy in-flight fetch is not killed by recovery")
func healthyInFlightFetchSurvivesRecovery() {
let store = AppStore()
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date())
let canRecover = store.prepareStuckLoadingRecovery()
#expect(!canRecover)
#expect(store.isInFlightForTesting(period: .today, provider: .all))
}
@Test("prepareStuckLoadingRecovery clears stale loading bookkeeping for the current key")
func popoverRecoveryClearsStuckLoading() {
let store = AppStore()
// Seed an orphaned in-flight entry older than the 60s watchdog so the
// stale-clear path runs, mimicking a fetch torn down across sleep/wake.
store.seedInFlightForTesting(
period: .today,
provider: .all,
insertedAt: Date().addingTimeInterval(-120)
)
#expect(store.isInFlightForTesting(period: .today, provider: .all))
let willFetch = store.prepareStuckLoadingRecovery()
#expect(willFetch)
#expect(!store.isInFlightForTesting(period: .today, provider: .all))
}
}