From 8e7340b2b1134c6720c72b2b7053e5433dc706d5 Mon Sep 17 00:00:00 2001 From: Alec Gutman <44984861+Chipagosfinest@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:29:48 -0700 Subject: [PATCH 1/2] Show selected-provider spend in Overview --- .github/pr-proof/overview-spend-summary.png | Bin 0 -> 20483 bytes .../CodexBar/StatusItemController+Menu.swift | 22 ++++ .../StatusItemController+MenuTypes.swift | 98 ++++++++++++++++++ .../StatusItemController+OverviewSpend.swift | 27 +++++ .../OverviewSpendSummaryTests.swift | 86 +++++++++++++++ .../SpendDashboardScreenshotRenderTests.swift | 9 ++ 6 files changed, 242 insertions(+) create mode 100644 .github/pr-proof/overview-spend-summary.png create mode 100644 Sources/CodexBar/StatusItemController+OverviewSpend.swift create mode 100644 Tests/CodexBarTests/OverviewSpendSummaryTests.swift diff --git a/.github/pr-proof/overview-spend-summary.png b/.github/pr-proof/overview-spend-summary.png new file mode 100644 index 0000000000000000000000000000000000000000..3f4fe69970646b8f2a15f64de1c479c2e7e7ebdf GIT binary patch literal 20483 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 String? in + guard let cost = group.totalCost ?? Self.safeCostSum(group.providers.compactMap(\.totalCost)) else { + return nil + } + let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) + let groupIsPartial = group.totalCost == nil || knownCostCount < providerCount + return groupIsPartial ? "~\(formatted)" : formatted + } + self.primarySpendText = spendTexts.isEmpty ? L("Spend unavailable") : spendTexts.joined(separator: " · ") + self.coverageText = "\(codexBarLocalizedInteger(knownCostCount)) / " + + "\(codexBarLocalizedInteger(providerCount)) \(L("Providers"))" + self.tokenText = knownTokens.map { + let formatted = ShareStatsFormatting.compactCount($0) + let value = tokenCoverageIsComplete ? formatted : "~\(formatted)" + return L("%@ tokens", value) + } + } + + private static func safeTokenSum(_ values: [Int]) -> Int? { + var total = 0 + for value in values { + let result = total.addingReportingOverflow(value) + guard !result.overflow else { return nil } + total = result.partialValue + } + return values.isEmpty ? nil : total + } + + private static func safeCostSum(_ values: [Double]) -> Double? { + guard !values.isEmpty else { return nil } + var total = 0.0 + for value in values { + guard value.isFinite else { return nil } + let result = total + value + guard result.isFinite else { return nil } + total = result + } + return total + } +} + +struct OverviewSpendSummaryCardView: View { + let summary: OverviewSpendSummary + let days: Int + let width: CGFloat + + var body: some View { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 6) { + Text(L("Usage & Spend")) + .font(.headline.weight(.semibold)) + Text("·") + Text(spendDashboardDayRangeText(self.days)) + } + .foregroundStyle(.secondary) + + Text(self.summary.primarySpendText) + .font(.system(.title2, design: .rounded, weight: .bold)) + .monospacedDigit() + .lineLimit(2) + + HStack(spacing: 8) { + Text(self.summary.coverageText) + if let tokenText = self.summary.tokenText { + Text("·") + Text(tokenText) + } + } + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) + .padding(.vertical, 10) + .frame(width: self.width, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.accentColor.opacity(0.08)) + .padding(.horizontal, 6) + } + } +} + struct OverviewMenuCardRowView: View { static let showsSectionDividers = false diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift new file mode 100644 index 0000000000..aee63de202 --- /dev/null +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -0,0 +1,27 @@ +import CodexBarCore +import Foundation + +extension StatusItemController { + func overviewSpendDashboardModel( + providers: [UsageProvider], + now: Date = Date()) -> SpendDashboardModel + { + guard self.settings.costUsageEnabled else { + return SpendDashboardModel(requestedDays: self.settings.costUsageHistoryDays, groups: []) + } + let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in + guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { + return nil + } + return SpendDashboardModel.ProviderInput( + provider: provider, + displayName: self.store.metadata(for: provider).displayName, + snapshot: snapshot) + } + return SpendDashboardModel.build( + inputs: inputs, + requestedDays: self.settings.costUsageHistoryDays, + now: now, + preferredCurrencyCode: self.settings.preferredCurrencyCode) + } +} diff --git a/Tests/CodexBarTests/OverviewSpendSummaryTests.swift b/Tests/CodexBarTests/OverviewSpendSummaryTests.swift new file mode 100644 index 0000000000..eaa94ac0d0 --- /dev/null +++ b/Tests/CodexBarTests/OverviewSpendSummaryTests.swift @@ -0,0 +1,86 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct OverviewSpendSummaryTests { + @Test + func `summary marks incomplete provider coverage as partial`() { + let group = self.group( + providers: [ + self.provider(.codex, tokens: 4_800_000, cost: 412.64), + self.provider(.claude, tokens: nil, cost: nil), + self.provider(.openrouter, tokens: 9_640_000, cost: 282.74), + self.provider(.cursor, tokens: 1_250_000, cost: 64.18), + ], + totalTokens: nil, + totalCost: nil) + + let summary = OverviewSpendSummary( + model: SpendDashboardModel(requestedDays: 30, groups: [group]), + providerCount: 4) + + #expect(summary.primarySpendText == "~$759.56") + #expect(summary.coverageText == "3 / 4 Providers") + #expect(summary.tokenText == "~15.7M tokens") + #expect(summary.isPartial) + } + + @Test + func `summary keeps distinct currencies separate`() { + let usd = self.group( + currencyCode: "USD", + providers: [self.provider(.codex, tokens: 1000, cost: 12)], + totalTokens: 1000, + totalCost: 12) + let eur = self.group( + currencyCode: "EUR", + providers: [self.provider(.claude, tokens: 2000, cost: 8)], + totalTokens: 2000, + totalCost: 8) + + let summary = OverviewSpendSummary( + model: SpendDashboardModel(requestedDays: 7, groups: [eur, usd]), + providerCount: 2) + + #expect(summary.primarySpendText.contains("$12.00")) + #expect(summary.primarySpendText.contains("€8.00")) + #expect(summary.coverageText == "2 / 2 Providers") + #expect(summary.tokenText == "3K tokens") + #expect(!summary.isPartial) + } + + private func provider( + _ provider: UsageProvider, + tokens: Int?, + cost: Double?) -> SpendDashboardModel.ProviderRow + { + SpendDashboardModel.ProviderRow( + id: provider.rawValue, + rank: 1, + provider: provider, + displayName: provider.rawValue, + totalTokens: tokens, + totalCost: cost, + coveredDayCount: 30) + } + + private func group( + currencyCode: String = "USD", + providers: [SpendDashboardModel.ProviderRow], + totalTokens: Int?, + totalCost: Double?) -> SpendDashboardModel.CurrencyGroup + { + SpendDashboardModel.CurrencyGroup( + currencyCode: currencyCode, + providers: providers, + models: [], + projects: [], + dailyPoints: [], + totalTokens: totalTokens, + totalCost: totalCost, + coveredDayCount: 30, + chartDomain: Date(timeIntervalSince1970: 0)...Date(timeIntervalSince1970: 86400), + modelHistoryCompleteness: totalCost == nil ? .incomplete : .complete) + } +} diff --git a/Tests/CodexBarTests/SpendDashboardScreenshotRenderTests.swift b/Tests/CodexBarTests/SpendDashboardScreenshotRenderTests.swift index 918f0d72bb..f3815b797d 100644 --- a/Tests/CodexBarTests/SpendDashboardScreenshotRenderTests.swift +++ b/Tests/CodexBarTests/SpendDashboardScreenshotRenderTests.swift @@ -61,6 +61,15 @@ final class SpendDashboardScreenshotRenderTests: XCTestCase { let renders: [(String, AnyView)] = [ ("usage-spend-30d", AnyView(Self.chrome(selectedDays: 30, group: thirtyGroup))), ("usage-spend-all", AnyView(Self.chrome(selectedDays: SpendDashboardSource.scanDays, group: allGroup))), + ( + "overview-spend-summary", + AnyView( + OverviewSpendSummaryCardView( + summary: OverviewSpendSummary(model: thirty, providerCount: 2), + days: 30, + width: 320) + .padding(.vertical, 8) + .background(Color(nsColor: .windowBackgroundColor)))), ] for (name, view) in renders { let data = try XCTUnwrap(Self.pngData(for: view), "render failed for \(name)") From 0828161bd164b27b1acaa5ba02960a833f593394 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 17 Aug 2026 19:04:13 -0700 Subject: [PATCH 2/2] fix: align Overview spend with spend dashboard --- .github/pr-proof/overview-spend-summary.png | Bin 20483 -> 31242 bytes CHANGELOG.md | 1 + .../CodexBar/StatusItemController+Menu.swift | 10 +- .../StatusItemController+MenuTypes.swift | 98 -------------- .../StatusItemController+OverviewSpend.swift | 124 +++++++++++++++++- .../OverviewSpendSummaryTests.swift | 70 ++++++++-- .../ProviderArchitectureGatekeeperTests.swift | 6 +- .../StatusMenuOverviewSpendTests.swift | 81 ++++++++++++ 8 files changed, 274 insertions(+), 116 deletions(-) create mode 100644 Tests/CodexBarTests/StatusMenuOverviewSpendTests.swift diff --git a/.github/pr-proof/overview-spend-summary.png b/.github/pr-proof/overview-spend-summary.png index 3f4fe69970646b8f2a15f64de1c479c2e7e7ebdf..4a4ea5511818821acd0c534d51abb8116f790df5 100644 GIT binary patch literal 31242 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 literal 20483 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 String? in - guard let cost = group.totalCost ?? Self.safeCostSum(group.providers.compactMap(\.totalCost)) else { - return nil - } - let formatted = UsageFormatter.currencyString(cost, currencyCode: group.currencyCode) - let groupIsPartial = group.totalCost == nil || knownCostCount < providerCount - return groupIsPartial ? "~\(formatted)" : formatted - } - self.primarySpendText = spendTexts.isEmpty ? L("Spend unavailable") : spendTexts.joined(separator: " · ") - self.coverageText = "\(codexBarLocalizedInteger(knownCostCount)) / " + - "\(codexBarLocalizedInteger(providerCount)) \(L("Providers"))" - self.tokenText = knownTokens.map { - let formatted = ShareStatsFormatting.compactCount($0) - let value = tokenCoverageIsComplete ? formatted : "~\(formatted)" - return L("%@ tokens", value) - } - } - - private static func safeTokenSum(_ values: [Int]) -> Int? { - var total = 0 - for value in values { - let result = total.addingReportingOverflow(value) - guard !result.overflow else { return nil } - total = result.partialValue - } - return values.isEmpty ? nil : total - } - - private static func safeCostSum(_ values: [Double]) -> Double? { - guard !values.isEmpty else { return nil } - var total = 0.0 - for value in values { - guard value.isFinite else { return nil } - let result = total + value - guard result.isFinite else { return nil } - total = result - } - return total - } -} - -struct OverviewSpendSummaryCardView: View { - let summary: OverviewSpendSummary - let days: Int - let width: CGFloat - - var body: some View { - VStack(alignment: .leading, spacing: 7) { - HStack(spacing: 6) { - Text(L("Usage & Spend")) - .font(.headline.weight(.semibold)) - Text("·") - Text(spendDashboardDayRangeText(self.days)) - } - .foregroundStyle(.secondary) - - Text(self.summary.primarySpendText) - .font(.system(.title2, design: .rounded, weight: .bold)) - .monospacedDigit() - .lineLimit(2) - - HStack(spacing: 8) { - Text(self.summary.coverageText) - if let tokenText = self.summary.tokenText { - Text("·") - Text(tokenText) - } - } - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - } - .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) - .padding(.vertical, 10) - .frame(width: self.width, alignment: .leading) - .background { - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color.accentColor.opacity(0.08)) - .padding(.horizontal, 6) - } - } -} - struct OverviewMenuCardRowView: View { static let showsSectionDividers = false diff --git a/Sources/CodexBar/StatusItemController+OverviewSpend.swift b/Sources/CodexBar/StatusItemController+OverviewSpend.swift index aee63de202..9819fff1b0 100644 --- a/Sources/CodexBar/StatusItemController+OverviewSpend.swift +++ b/Sources/CodexBar/StatusItemController+OverviewSpend.swift @@ -1,14 +1,132 @@ import CodexBarCore import Foundation +import SwiftUI + +struct OverviewSpendSummary: Equatable { + let primarySpendText: String + let providerCoverageText: String + let tokenText: String? + let historyCoverageText: String + let pricingCoverageText: String + let provenanceText: String + let isPartial: Bool + + init(model: SpendDashboardModel, providerCount: Int) { + let includedProviders = model.groups.flatMap(\.providers) + let providerCount = max(max(0, providerCount), includedProviders.count) + let pricedProviderCount = includedProviders.count { $0.totalCost != nil } + let tokenProviderCount = includedProviders.count { $0.totalTokens != nil } + let isPartial = pricedProviderCount > 0 && pricedProviderCount < providerCount + self.isPartial = isPartial + + self.primarySpendText = model.groups.isEmpty + ? L("Spend unavailable") + : model.groups.map { group in + let text = spendDashboardGroupCostText(group) + guard isPartial, group.totalCost != nil, !text.hasPrefix("~") else { return text } + return "~\(text)" + }.joined(separator: " · ") + self.providerCoverageText = L( + "%d of %d subscriptions have spend", + pricedProviderCount, + providerCount) + + let tokens = Self.safeTokenSum(model.groups.compactMap(\.totalTokens)) + self.tokenText = tokens.map { + let value = ShareStatsFormatting.compactCount($0) + let isPartial = tokenProviderCount < providerCount + return L("%@ tokens", isPartial ? "~\(value)" : value) + } + + let coveredDays = includedProviders.count < providerCount + ? 0 + : model.groups.map(\.coveredDayCount).min() ?? 0 + self.historyCoverageText = spendDashboardCoverageText( + covered: coveredDays, + requested: model.requestedDays) + + let coverage = model.groups.reduce(into: CostUsageCoverageCounts()) { result, group in + result.merge(group.coverage) + } + self.pricingCoverageText = spendDashboardCoverageChipText(coverage) + self.provenanceText = model.groups + .map(\.provenance) + .reduce(into: [CostProvenance]()) { values, provenance in + guard !values.contains(provenance) else { return } + values.append(provenance) + } + .map(spendDashboardProvenanceText) + .joined(separator: " · ") + } + + private static func safeTokenSum(_ values: [Int]) -> Int? { + guard !values.isEmpty else { return nil } + var total = 0 + for value in values { + let result = total.addingReportingOverflow(value) + guard !result.overflow else { return nil } + total = result.partialValue + } + return total + } +} + +struct OverviewSpendSummaryCardView: View { + let summary: OverviewSpendSummary + let days: Int + let width: CGFloat + + var body: some View { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 6) { + Text(L("Usage & Spend")) + .font(.headline.weight(.semibold)) + Text("·") + Text(spendDashboardDayRangeText(self.days)) + } + .foregroundStyle(.secondary) + + Text(self.summary.primarySpendText) + .font(.system(.title2, design: .rounded, weight: .bold)) + .monospacedDigit() + .lineLimit(2) + + HStack(spacing: 8) { + Text(self.summary.providerCoverageText) + if let tokenText = self.summary.tokenText { + Text("·") + Text(tokenText) + } + } + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + Text("\(self.summary.historyCoverageText) · \(self.summary.provenanceText)") + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + Text(self.summary.pricingCoverageText) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) + .padding(.vertical, 10) + .frame(width: self.width, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.accentColor.opacity(0.08)) + .padding(.horizontal, 6) + } + } +} extension StatusItemController { func overviewSpendDashboardModel( providers: [UsageProvider], now: Date = Date()) -> SpendDashboardModel { - guard self.settings.costUsageEnabled else { - return SpendDashboardModel(requestedDays: self.settings.costUsageHistoryDays, groups: []) - } let inputs = providers.compactMap { provider -> SpendDashboardModel.ProviderInput? in guard let snapshot = self.store.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot else { return nil diff --git a/Tests/CodexBarTests/OverviewSpendSummaryTests.swift b/Tests/CodexBarTests/OverviewSpendSummaryTests.swift index eaa94ac0d0..bc14360371 100644 --- a/Tests/CodexBarTests/OverviewSpendSummaryTests.swift +++ b/Tests/CodexBarTests/OverviewSpendSummaryTests.swift @@ -13,16 +13,20 @@ struct OverviewSpendSummaryTests { self.provider(.openrouter, tokens: 9_640_000, cost: 282.74), self.provider(.cursor, tokens: 1_250_000, cost: 64.18), ], - totalTokens: nil, - totalCost: nil) + totalTokens: 15_690_000, + totalCost: 759.56, + coverage: CostUsageCoverageCounts(priced: 3, unpriced: 1)) let summary = OverviewSpendSummary( model: SpendDashboardModel(requestedDays: 30, groups: [group]), providerCount: 4) #expect(summary.primarySpendText == "~$759.56") - #expect(summary.coverageText == "3 / 4 Providers") + #expect(summary.providerCoverageText == "3 of 4 subscriptions have spend") #expect(summary.tokenText == "~15.7M tokens") + #expect(summary.historyCoverageText == "Coverage: 30 / 30") + #expect(summary.pricingCoverageText == "Priced 3 · Unpriced 1 · Unmetered 0 · Estimated 0") + #expect(summary.provenanceText == "List-price equivalent") #expect(summary.isPartial) } @@ -32,12 +36,15 @@ struct OverviewSpendSummaryTests { currencyCode: "USD", providers: [self.provider(.codex, tokens: 1000, cost: 12)], totalTokens: 1000, - totalCost: 12) + totalCost: 12, + coveredDayCount: 7) let eur = self.group( currencyCode: "EUR", providers: [self.provider(.claude, tokens: 2000, cost: 8)], totalTokens: 2000, - totalCost: 8) + totalCost: 8, + coveredDayCount: 7, + provenance: .vendorMetered) let summary = OverviewSpendSummary( model: SpendDashboardModel(requestedDays: 7, groups: [eur, usd]), @@ -45,11 +52,51 @@ struct OverviewSpendSummaryTests { #expect(summary.primarySpendText.contains("$12.00")) #expect(summary.primarySpendText.contains("€8.00")) - #expect(summary.coverageText == "2 / 2 Providers") + #expect(summary.providerCoverageText == "2 of 2 subscriptions have spend") #expect(summary.tokenText == "3K tokens") + #expect(summary.historyCoverageText == "Coverage: 7 / 7") + #expect(summary.provenanceText == "Plan metered · List-price equivalent") + #expect(!summary.isPartial) + } + + @Test + func `summary keeps wholly unpriced spend unavailable`() { + let group = self.group( + providers: [self.provider(.claude, tokens: 2000, cost: nil)], + totalTokens: 2000, + totalCost: nil, + coverage: CostUsageCoverageCounts(unpriced: 1), + provenance: .unknown) + + let summary = OverviewSpendSummary( + model: SpendDashboardModel(requestedDays: 30, groups: [group]), + providerCount: 1) + + #expect(summary.primarySpendText == "Spend unavailable") + #expect(summary.providerCoverageText == "0 of 1 subscriptions have spend") + #expect(summary.pricingCoverageText == "Priced 0 · Unpriced 1 · Unmetered 0 · Estimated 0") + #expect(summary.provenanceText == "Spend unavailable") #expect(!summary.isPartial) } + @Test + func `summary marks a missing selected provider as partial`() { + let group = self.group( + providers: [self.provider(.codex, tokens: 1000, cost: 12)], + totalTokens: 1000, + totalCost: 12) + + let summary = OverviewSpendSummary( + model: SpendDashboardModel(requestedDays: 30, groups: [group]), + providerCount: 2) + + #expect(summary.primarySpendText == "~$12.00") + #expect(summary.providerCoverageText == "1 of 2 subscriptions have spend") + #expect(summary.tokenText == "~1K tokens") + #expect(summary.historyCoverageText == "Coverage: 0 / 30") + #expect(summary.isPartial) + } + private func provider( _ provider: UsageProvider, tokens: Int?, @@ -69,7 +116,10 @@ struct OverviewSpendSummaryTests { currencyCode: String = "USD", providers: [SpendDashboardModel.ProviderRow], totalTokens: Int?, - totalCost: Double?) -> SpendDashboardModel.CurrencyGroup + totalCost: Double?, + coveredDayCount: Int = 30, + coverage: CostUsageCoverageCounts? = nil, + provenance: CostProvenance = .listPriceEstimate) -> SpendDashboardModel.CurrencyGroup { SpendDashboardModel.CurrencyGroup( currencyCode: currencyCode, @@ -79,8 +129,10 @@ struct OverviewSpendSummaryTests { dailyPoints: [], totalTokens: totalTokens, totalCost: totalCost, - coveredDayCount: 30, + coveredDayCount: coveredDayCount, chartDomain: Date(timeIntervalSince1970: 0)...Date(timeIntervalSince1970: 86400), - modelHistoryCompleteness: totalCost == nil ? .incomplete : .complete) + modelHistoryCompleteness: totalCost == nil ? .incomplete : .complete, + coverage: coverage ?? CostUsageCoverageCounts(priced: providers.count), + provenance: provenance) } } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index d70f7bb89f..8e5c710179 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -962,7 +962,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The memory-pressure debug fixture installs its synthetic entry in the Codex cache slot."), SuppressedProviderReference( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1087, + line: 1113, anchor: "controller.refreshOpenMenuIfStillVisible(menu, provider: .codex)", expectedProviderIDs: ["codex"], reason: "This provider-specific app branch passes its already-selected identity to a shared helper."), @@ -2482,7 +2482,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1120, + line: 1146, anchor: "return .provider((self.resolvedMenuProvider(enabledProviders: enabledProviders) ?? .codex).instanceID)", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2490,7 +2490,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/StatusItemController+Menu.swift", - line: 1133, + line: 1159, anchor: "return self.store.enabledFirstPartyProvidersForDisplay().first ?? .codex", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, diff --git a/Tests/CodexBarTests/StatusMenuOverviewSpendTests.swift b/Tests/CodexBarTests/StatusMenuOverviewSpendTests.swift new file mode 100644 index 0000000000..9a6a3eb1ef --- /dev/null +++ b/Tests/CodexBarTests/StatusMenuOverviewSpendTests.swift @@ -0,0 +1,81 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +extension StatusMenuTests { + @Test + func `overview spend follows the inline display preference`() throws { + for (style, enabled) in [ + (CostSummaryDisplayStyle.inlineSummary, true), + (.both, true), + (.costSubmenu, false), + ] { + let result = try self.overviewSpendSummaryIsPresent(style: style, costUsageEnabled: true) + #expect(result == enabled, "Unexpected Overview spend visibility for \(style.rawValue)") + } + + #expect(try !self.overviewSpendSummaryIsPresent(style: .both, costUsageEnabled: false)) + } + + private func overviewSpendSummaryIsPresent( + style: CostSummaryDisplayStyle, + costUsageEnabled: Bool) throws -> Bool + { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.mergedMenuLastSelectedWasOverview = true + settings.costSummaryDisplayStyle = style + settings.costUsageEnabled = costUsageEnabled + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + let shouldEnable = provider == .codex || provider == .claude + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable) + } + + let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false) + let now = Date() + let components = Calendar.current.dateComponents([.year, .month, .day], from: now) + let year = try #require(components.year) + let month = try #require(components.month) + let dayOfMonth = try #require(components.day) + let day = String(format: "%04d-%02d-%02d", year, month, dayOfMonth) + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( + sessionTokens: 100, + sessionCostUSD: 1, + last30DaysTokens: 100, + last30DaysCostUSD: 1, + costProvenance: .listPriceEstimate, + daily: [ + CostUsageDailyReport.Entry( + date: day, + inputTokens: 60, + outputTokens: 40, + totalTokens: 100, + requestCount: 1, + costUSD: 1, + modelsUsed: ["test-model"], + modelBreakdowns: nil), + ], + updatedAt: now), provider: .codex) + + let controller = StatusItemController( + store: store, + settings: settings, + account: UsageFetcher().loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = controller.makeMenu() + controller.menuWillOpen(menu) + return menu.items.contains { ($0.representedObject as? String) == "overviewSpendSummary" } + } +}