From 6dc8776a982ffa780822472ee3fefe435d3405d1 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:51:14 +0800 Subject: [PATCH 1/4] Show project and title in spend dashboard session rows Co-authored-by: Cursor --- .../PreferencesSpendDashboardPane.swift | 8 +-- Sources/CodexBar/SpendActivityHeatmap.swift | 12 +++++ Sources/CodexBar/SpendDashboardModel.swift | 37 ++++++++++++- Sources/CodexBarCore/CostUsageModels.swift | 8 ++- .../CostUsage/CostUsageScanner+Projects.swift | 4 +- .../CodexBarTests/CostUsageFetcherTests.swift | 5 +- .../SpendDashboardModelTests.swift | 53 +++++++++++++++++++ 7 files changed, 120 insertions(+), 7 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 3c71ac51be..616f4a3d0f 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -1289,16 +1289,18 @@ private struct SpendSessionRows: View { var body: some View { VStack(alignment: .leading, spacing: 0) { ForEach(Array(self.group.sessions.enumerated()), id: \.element.id) { index, row in - let subtitle = row.modelName ?? SpendActivityDateFormatting.mediumDateString(row.lastActivity) + let subtitle = (row.contextLabels + [ + SpendActivityDateFormatting.mediumDateTimeString(row.lastActivity), + ]).joined(separator: " · ") if index > 0 { Divider() } HStack(spacing: 10) { SpendProviderIcon(provider: row.provider, sourceKind: .native) VStack(alignment: .leading, spacing: 2) { - Text(row.displayName) + Text(row.headline) .lineLimit(1) - .help(row.displayName) + .help(row.headline) Text(subtitle) .font(.caption) .foregroundStyle(.secondary) diff --git a/Sources/CodexBar/SpendActivityHeatmap.swift b/Sources/CodexBar/SpendActivityHeatmap.swift index 4f8a402902..b8ac3abdf1 100644 --- a/Sources/CodexBar/SpendActivityHeatmap.swift +++ b/Sources/CodexBar/SpendActivityHeatmap.swift @@ -337,6 +337,18 @@ enum SpendActivityDateFormatting { formatter.timeStyle = .none return formatter.string(from: date) } + + static func mediumDateTimeString(_ date: Date, calendar: Calendar? = nil, locale: Locale? = nil) -> String { + let formatter = DateFormatter() + formatter.locale = locale ?? codexBarLocalizedResourceLocale() + formatter.calendar = calendar ?? Calendar.current + if let calendar { + formatter.timeZone = calendar.timeZone + } + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter.string(from: date) + } } enum SpendActivityAccessibility { diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 769e1d0452..06d27abcde 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -330,6 +330,27 @@ struct SpendDashboardModel: Equatable, Sendable { let totalTokens: Int? let totalCost: Double? let modelName: String? + var title: String? + var projectName: String? + + /// Most specific identity available: session title, then project folder, then source name. + var headline: String { + self.title ?? self.projectName ?? self.displayName + } + + var contextLabels: [String] { + var labels: [String] = [] + if self.title != nil, let projectName = self.projectName { + labels.append(projectName) + } + if self.headline != self.displayName { + labels.append(self.displayName) + } + if let modelName = self.modelName { + labels.append(modelName) + } + return labels + } } struct HourlyPoint: Identifiable, Equatable, Sendable { @@ -1452,7 +1473,9 @@ struct SpendDashboardModel: Equatable, Sendable { lastActivity: session.lastActivity, totalTokens: session.totalTokens, totalCost: session.costUSD.map { $0 * summary.costMultiplier }, - modelName: modelName) + modelName: modelName, + title: Self.nonEmptyTrimmed(session.title), + projectName: Self.sessionProjectName(path: session.projectPath)) } } .sorted { lhs, rhs in @@ -1464,6 +1487,18 @@ struct SpendDashboardModel: Equatable, Sendable { return Array(rows.prefix(12)) } + private static func sessionProjectName(path: String?) -> String? { + guard let path = self.nonEmptyTrimmed(path) else { return nil } + return self.nonEmptyTrimmed(URL(fileURLWithPath: path, isDirectory: true).lastPathComponent) + } + + private static func nonEmptyTrimmed(_ value: String?) -> String? { + guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { + return nil + } + return trimmed + } + private static func hourlyPoints( summaries: [InputSummary], selectedDay: Date?, diff --git a/Sources/CodexBarCore/CostUsageModels.swift b/Sources/CodexBarCore/CostUsageModels.swift index 2f02b9ccc0..c6a7f1be10 100644 --- a/Sources/CodexBarCore/CostUsageModels.swift +++ b/Sources/CodexBarCore/CostUsageModels.swift @@ -125,6 +125,8 @@ public struct CostUsageSessionBreakdown: Sendable, Equatable, Identifiable { public let requestCount: Int? public let costUSD: Double? public let modelBreakdowns: [CostUsageDailyReport.ModelBreakdown] + public let title: String? + public let projectPath: String? public var id: String { self.sessionID @@ -140,9 +142,13 @@ public struct CostUsageSessionBreakdown: Sendable, Equatable, Identifiable { totalTokens: Int?, requestCount: Int?, costUSD: Double?, - modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]) + modelBreakdowns: [CostUsageDailyReport.ModelBreakdown], + title: String? = nil, + projectPath: String? = nil) { self.sessionID = sessionID + self.title = title + self.projectPath = projectPath self.lastActivity = lastActivity self.inputTokens = inputTokens self.cachedInputTokens = cachedInputTokens diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift index 592645ed65..194ea40d2e 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner+Projects.swift @@ -75,7 +75,9 @@ extension CostUsageScanner { totalTokens: summary?.totalTokens, requestCount: requestCounts.isEmpty ? nil : requestCounts.reduce(0, +), costUSD: summary?.totalCostUSD, - modelBreakdowns: Self.codexProjectModelBreakdowns(from: report.data) ?? []) + modelBreakdowns: Self.codexProjectModelBreakdowns(from: report.data) ?? [], + title: file.usage.codexSession?.title, + projectPath: file.usage.projectPath ?? file.usage.codexSession?.cwd) } .sorted { lhs, rhs in if lhs.lastActivity != rhs.lastActivity { diff --git a/Tests/CodexBarTests/CostUsageFetcherTests.swift b/Tests/CodexBarTests/CostUsageFetcherTests.swift index f7fc9f07c5..d35b6cb9b2 100644 --- a/Tests/CodexBarTests/CostUsageFetcherTests.swift +++ b/Tests/CodexBarTests/CostUsageFetcherTests.swift @@ -1207,7 +1207,7 @@ extension CostUsageFetcherTests { [ "type": "session_meta", "timestamp": env.isoString(for: day), - "payload": ["session_id": "first-session"], + "payload": ["session_id": "first-session", "cwd": "/Users/example/Projects/alpha-app"], ], [ "type": "event_msg", @@ -1284,6 +1284,9 @@ extension CostUsageFetcherTests { #expect(first.requestCount == nil) #expect(first.modelBreakdowns.map(\.modelName) == ["gpt-5.4"]) #expect(first.costUSD != nil) + #expect(first.projectPath == "/Users/example/Projects/alpha-app") + let second = try #require(snapshot.sessions.first(where: { $0.sessionID == "second-session" })) + #expect(second.projectPath == nil) let cache = CostUsageStoreAccess.read(cacheRoot: env.cacheRoot) let range = CostUsageScanner.CostUsageDayRange(since: day, until: day) diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index f9ff2869ee..dac4fa1bf5 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -1115,6 +1115,59 @@ extension SpendDashboardModelTests { #expect(group.provenance == .listPriceEstimate) } + @Test + func `session rows prefer title and project over the source name`() throws { + let now = Date(timeIntervalSince1970: 1_784_222_400) + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + func session(_ id: String, title: String?, projectPath: String?) -> CostUsageSessionBreakdown { + CostUsageSessionBreakdown( + sessionID: id, + lastActivity: now, + inputTokens: 10, + cachedInputTokens: nil, + outputTokens: 2, + totalTokens: 12, + requestCount: 1, + costUSD: 1, + modelBreakdowns: [.init(modelName: "gpt-5.4", costUSD: 1, totalTokens: 12)], + title: title, + projectPath: projectPath) + } + let input = SpendDashboardModel.ProviderInput( + id: "codex", + provider: .codex, + displayName: "Codex · #1", + snapshot: CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 36, + last30DaysCostUSD: 3, + currencyCode: "USD", + daily: [Self.entry(day: "2026-07-16", cost: 3, tokens: 36)], + sessions: [ + session("a", title: "Fix login bug", projectPath: "/Users/example/Projects/alpha-app"), + session("b", title: " ", projectPath: "/Users/example/Projects/beta-app/"), + session("c", title: nil, projectPath: nil), + ], + updatedAt: now)) + let model = SpendDashboardModel.build(inputs: [input], requestedDays: 30, now: now, calendar: calendar) + let rows = try #require(model.groups.first?.sessions) + let byID = Dictionary(uniqueKeysWithValues: rows.map { ($0.id, $0) }) + + let titled = try #require(byID["codex:a"]) + #expect(titled.headline == "Fix login bug") + #expect(titled.contextLabels == ["alpha-app", "Codex · #1", "gpt-5.4"]) + + let projectOnly = try #require(byID["codex:b"]) + #expect(projectOnly.headline == "beta-app") + #expect(projectOnly.contextLabels == ["Codex · #1", "gpt-5.4"]) + + let bare = try #require(byID["codex:c"]) + #expect(bare.headline == "Codex · #1") + #expect(bare.contextLabels == ["gpt-5.4"]) + } + @Test func `stored day keys stay put when the display timezone changes`() throws { let now = Date(timeIntervalSince1970: 1_784_222_400) // 2026-07-16 12:00:00 UTC From f2898585659fae68edb2ac035db5ab2941457a74 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:55:09 +0800 Subject: [PATCH 2/4] Keep spend dashboard hourly chart and daily counts readable Co-authored-by: Cursor --- .../PreferencesSpendDashboardPane.swift | 19 ++++++-- Sources/CodexBar/SpendDashboardModel.swift | 42 ++++++++++++++++-- .../SpendDashboardDailyLedgerTests.swift | 41 ++++++++++++++++++ .../SpendDashboardModelTests.swift | 43 +++++++++++++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBar/PreferencesSpendDashboardPane.swift b/Sources/CodexBar/PreferencesSpendDashboardPane.swift index 616f4a3d0f..a458bee4be 100644 --- a/Sources/CodexBar/PreferencesSpendDashboardPane.swift +++ b/Sources/CodexBar/PreferencesSpendDashboardPane.swift @@ -837,6 +837,11 @@ private struct SpendDashboardTrendPanel: View { HStack(alignment: .center, spacing: 12) { Text(self.activeSection.title) .font(.headline) + if self.activeSection == .hourly, let hourlyDay = self.group.hourlyDay { + Text(SpendActivityDateFormatting.mediumDateString(hourlyDay, calendar: self.group.calendar)) + .font(.caption) + .foregroundStyle(.secondary) + } Spacer() if self.availableSections.count > 1 { Picker(L("Usage & Spend"), selection: self.normalizedSelection) { @@ -1259,17 +1264,23 @@ private struct SpendDailyLedgerRow: View { } private var tokensText: String { - self.countText(self.summary.totalTokens, format: UsageFormatter.tokenCountString) + self.countText( + self.summary.totalTokens, + isPartial: self.summary.hasPartialTokens, + format: UsageFormatter.tokenCountString) } private var requestsText: String { - self.countText(self.summary.requestCount, format: codexBarLocalizedInteger) + self.countText( + self.summary.requestCount, + isPartial: self.summary.hasPartialRequests, + format: codexBarLocalizedInteger) } - private func countText(_ count: Int?, format: (Int) -> String) -> String { + private func countText(_ count: Int?, isPartial: Bool, format: (Int) -> String) -> String { guard let count else { return "—" } let text = format(count) - return self.summary.hasPartialCounts ? "≥\(text)" : text + return isPartial ? "≥\(text)" : text } private var accessibilityLabel: String { diff --git a/Sources/CodexBar/SpendDashboardModel.swift b/Sources/CodexBar/SpendDashboardModel.swift index 06d27abcde..547b700bde 100644 --- a/Sources/CodexBar/SpendDashboardModel.swift +++ b/Sources/CodexBar/SpendDashboardModel.swift @@ -201,6 +201,19 @@ struct SpendDashboardModel: Equatable, Sendable { self.incompleteRequestCount > 0 || self.providers.contains(where: \.countsAreLowerBound) } + /// Totals sum only the providers that report the metric, so a mixed day is a floor. + var hasPartialTokens: Bool { + self.hasPartialCounts || Self.mixesKnownAndUnknown(self.providers.map(\.totalTokens)) + } + + var hasPartialRequests: Bool { + self.hasPartialCounts || Self.mixesKnownAndUnknown(self.providers.map(\.requestCount)) + } + + private static func mixesKnownAndUnknown(_ values: [Int?]) -> Bool { + values.contains { $0 != nil } && values.contains { $0 == nil } + } + var id: Date { self.day } @@ -257,6 +270,8 @@ struct SpendDashboardModel: Equatable, Sendable { let overflowModelCount: Int let displayedModels: [ModelRow] let selectedDay: Date? + /// Day shown by the hourly chart: the selected day, else the newest day with hourly spend. + let hourlyDay: Date? let hourlyPoints: [HourlyPoint] let hourlyChartDomain: ClosedRange? let timeZone: TimeZone @@ -289,6 +304,7 @@ struct SpendDashboardModel: Equatable, Sendable { sessions: [SessionRow] = [], overflowModelCount: Int = 0, selectedDay: Date? = nil, + hourlyDay: Date? = nil, hourlyPoints: [HourlyPoint] = [], hourlyChartDomain: ClosedRange? = nil, timeZone: TimeZone = .current) @@ -313,6 +329,7 @@ struct SpendDashboardModel: Equatable, Sendable { self.overflowModelCount = overflowModelCount self.displayedModels = Array(models.prefix(Self.modelRowDisplayLimit)) self.selectedDay = selectedDay + self.hourlyDay = hourlyDay ?? selectedDay self.hourlyPoints = hourlyPoints self.hourlyChartDomain = hourlyChartDomain self.timeZone = timeZone @@ -648,9 +665,10 @@ struct SpendDashboardModel: Equatable, Sendable { case (false, false): .unknown } let overflowCount = max(0, modelSummary.rows.count - CurrencyGroup.modelRowDisplayLimit) + let hourlyDay = selectedDay ?? Self.latestHourlyDay(summaries: summaries, bounds: bounds, calendar: calendar) let hourlyPoints = Self.hourlyPoints( summaries: summaries, - selectedDay: selectedDay, + selectedDay: hourlyDay, bounds: bounds, calendar: calendar) return CurrencyGroup( @@ -673,10 +691,11 @@ struct SpendDashboardModel: Equatable, Sendable { sessions: Self.sessionRows(summaries: summaries, bounds: bounds, calendar: calendar), overflowModelCount: overflowCount, selectedDay: selectedDay, + hourlyDay: hourlyPoints.isEmpty ? nil : hourlyDay, hourlyPoints: hourlyPoints, hourlyChartDomain: Self.hourlyChartDomain( points: hourlyPoints, - selectedDay: selectedDay, + selectedDay: hourlyDay, calendar: calendar), timeZone: calendar.timeZone) } @@ -1049,8 +1068,8 @@ struct SpendDashboardModel: Equatable, Sendable { result.append(DailySummary( day: day, providers: sortedRows, - totalTokens: Self.completeIntSum(providerRows.map(\.totalTokens)), - requestCount: Self.completeIntSum(providerRows.map(\.requestCount)), + totalTokens: Self.knownIntSum(providerRows.map(\.totalTokens)), + requestCount: Self.knownIntSum(providerRows.map(\.requestCount)), totalCost: totalCost)) guard let nextDay = calendar.date(byAdding: .day, value: 1, to: day) else { return [] } day = calendar.startOfDay(for: nextDay) @@ -1553,6 +1572,21 @@ struct SpendDashboardModel: Equatable, Sendable { } } + /// Hourly bars stay readable only within one day, so an unselected range focuses the newest day with spend. + private static func latestHourlyDay( + summaries: [InputSummary], + bounds: ClosedRange, + calendar: Calendar) -> Date? + { + summaries + .filter { !$0.hasInvalidCostHistory } + .flatMap(\.input.snapshot.hourly) + .filter { (Self.validCost($0.costUSD) ?? 0) > 0 } + .map { calendar.startOfDay(for: $0.hour) } + .filter { bounds.contains($0) } + .max() + } + private static func hourlyChartDomain( points: [HourlyPoint], selectedDay: Date?, diff --git a/Tests/CodexBarTests/SpendDashboardDailyLedgerTests.swift b/Tests/CodexBarTests/SpendDashboardDailyLedgerTests.swift index b1978011c6..a687db745b 100644 --- a/Tests/CodexBarTests/SpendDashboardDailyLedgerTests.swift +++ b/Tests/CodexBarTests/SpendDashboardDailyLedgerTests.swift @@ -33,6 +33,47 @@ struct SpendDashboardDailyLedgerTests { #expect(rows.allSatisfy { $0.isKnownIdle == complete }) } + @Test + func `cost only providers keep known token and request counts as a floor`() throws { + let claude = Self.input( + id: "claude", + provider: .claude, + displayName: "Claude", + entries: [Self.entry(day: "2026-07-16", cost: 2, tokens: 20, requests: 2)], + totalTokens: 20) + let cursor = SpendDashboardModel.ProviderInput( + provider: .cursor, + displayName: "Cursor", + snapshot: CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: nil, + last30DaysCostUSD: 3, + currencyCode: "USD", + historyDays: 3, + daily: [CostUsageDailyReport.Entry( + date: "2026-07-16", + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + costUSD: 3, + modelsUsed: nil, + modelBreakdowns: nil)], + updatedAt: Self.now)) + + let mixed = try #require(Self.group(inputs: [claude, cursor])?.dailySummaries.last) + #expect(mixed.totalCost == 5) + #expect(mixed.totalTokens == 20) + #expect(mixed.requestCount == 2) + #expect(mixed.hasPartialTokens) + #expect(mixed.hasPartialRequests) + + let alone = try #require(Self.group(inputs: [claude])?.dailySummaries.last) + #expect(alone.totalTokens == 20) + #expect(!alone.hasPartialTokens) + #expect(!alone.hasPartialRequests) + } + @Test func `unpriced requests with zero tokens remain unknown spend`() throws { let input = Self.input( diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index dac4fa1bf5..96133426fc 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -1115,6 +1115,49 @@ extension SpendDashboardModelTests { #expect(group.provenance == .listPriceEstimate) } + @Test + func `hourly chart focuses the newest day with spend when no day is selected`() throws { + let now = Date(timeIntervalSince1970: 1_784_222_400) // 2026-07-16 12:00:00 UTC + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let today = calendar.startOfDay(for: now) + let olderHour = try #require(calendar.date(byAdding: .day, value: -3, to: now)) + let emptyLaterHour = try #require(calendar.date(byAdding: .hour, value: 1, to: now)) + let input = SpendDashboardModel.ProviderInput( + id: "codex", + provider: .codex, + displayName: "Codex", + snapshot: CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 30, + last30DaysCostUSD: 3, + currencyCode: "USD", + daily: [ + Self.entry(day: "2026-07-13", cost: 1, tokens: 10), + Self.entry(day: "2026-07-16", cost: 2, tokens: 20), + ], + hourly: [ + CostUsageHourlyEntry(hour: olderHour, totalTokens: 10, costUSD: 1), + CostUsageHourlyEntry(hour: now, totalTokens: 20, costUSD: 2), + CostUsageHourlyEntry(hour: emptyLaterHour, totalTokens: 0, costUSD: 0), + ], + updatedAt: now)) + + let group = try #require(SpendDashboardModel.build( + inputs: [input], requestedDays: 30, now: now, calendar: calendar).groups.first) + #expect(group.hourlyDay == today) + #expect(group.hourlyPoints.allSatisfy { calendar.isDate($0.hour, inSameDayAs: today) }) + #expect(group.hourlyPoints.map(\.cost).reduce(0, +) == 2) + #expect(group.hourlyChartDomain?.lowerBound == today) + + let olderDay = calendar.startOfDay(for: olderHour) + let selected = try #require(SpendDashboardModel.build( + inputs: [input], requestedDays: 30, now: now, calendar: calendar, selectedDay: olderDay).groups.first) + #expect(selected.hourlyDay == olderDay) + #expect(selected.hourlyPoints.map(\.cost) == [1]) + } + @Test func `session rows prefer title and project over the source name`() throws { let now = Date(timeIntervalSince1970: 1_784_222_400) From 059bec7b6ab2557a37f116317b2324dc87fa96c8 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:38:47 +0800 Subject: [PATCH 3/4] Refresh parser hash and gatekeeper anchor Co-authored-by: Cursor --- Sources/CodexBarCore/Generated/CodexParserHash.generated.swift | 2 +- Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index 7867cce45c..6ae5e8536c 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "86c0a89614a11da9" + static let value = "e571f6bf944e28c2" } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index f176db8c77..8d51f2b5ac 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -2491,7 +2491,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/SpendDashboardModel.swift", - line: 1361, + line: 1401, anchor: "guard provider == .mistral || provider == .openrouter || provider == .xai else { return displayCalendar }", expectedProviderIDs: ["mistral", "openrouter", "xai"], expectedReferenceCount: 3, From ede78afa034b0155101c0ad99f657f4603352429 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:28:19 +0800 Subject: [PATCH 4/4] Move spend dashboard session and hourly tests to their own suite Co-authored-by: Cursor --- .../SpendDashboardModelTests.swift | 96 --------------- .../SpendDashboardSessionAndHourlyTests.swift | 113 ++++++++++++++++++ 2 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 Tests/CodexBarTests/SpendDashboardSessionAndHourlyTests.swift diff --git a/Tests/CodexBarTests/SpendDashboardModelTests.swift b/Tests/CodexBarTests/SpendDashboardModelTests.swift index 96133426fc..f9ff2869ee 100644 --- a/Tests/CodexBarTests/SpendDashboardModelTests.swift +++ b/Tests/CodexBarTests/SpendDashboardModelTests.swift @@ -1115,102 +1115,6 @@ extension SpendDashboardModelTests { #expect(group.provenance == .listPriceEstimate) } - @Test - func `hourly chart focuses the newest day with spend when no day is selected`() throws { - let now = Date(timeIntervalSince1970: 1_784_222_400) // 2026-07-16 12:00:00 UTC - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - let today = calendar.startOfDay(for: now) - let olderHour = try #require(calendar.date(byAdding: .day, value: -3, to: now)) - let emptyLaterHour = try #require(calendar.date(byAdding: .hour, value: 1, to: now)) - let input = SpendDashboardModel.ProviderInput( - id: "codex", - provider: .codex, - displayName: "Codex", - snapshot: CostUsageTokenSnapshot( - sessionTokens: nil, - sessionCostUSD: nil, - last30DaysTokens: 30, - last30DaysCostUSD: 3, - currencyCode: "USD", - daily: [ - Self.entry(day: "2026-07-13", cost: 1, tokens: 10), - Self.entry(day: "2026-07-16", cost: 2, tokens: 20), - ], - hourly: [ - CostUsageHourlyEntry(hour: olderHour, totalTokens: 10, costUSD: 1), - CostUsageHourlyEntry(hour: now, totalTokens: 20, costUSD: 2), - CostUsageHourlyEntry(hour: emptyLaterHour, totalTokens: 0, costUSD: 0), - ], - updatedAt: now)) - - let group = try #require(SpendDashboardModel.build( - inputs: [input], requestedDays: 30, now: now, calendar: calendar).groups.first) - #expect(group.hourlyDay == today) - #expect(group.hourlyPoints.allSatisfy { calendar.isDate($0.hour, inSameDayAs: today) }) - #expect(group.hourlyPoints.map(\.cost).reduce(0, +) == 2) - #expect(group.hourlyChartDomain?.lowerBound == today) - - let olderDay = calendar.startOfDay(for: olderHour) - let selected = try #require(SpendDashboardModel.build( - inputs: [input], requestedDays: 30, now: now, calendar: calendar, selectedDay: olderDay).groups.first) - #expect(selected.hourlyDay == olderDay) - #expect(selected.hourlyPoints.map(\.cost) == [1]) - } - - @Test - func `session rows prefer title and project over the source name`() throws { - let now = Date(timeIntervalSince1970: 1_784_222_400) - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - func session(_ id: String, title: String?, projectPath: String?) -> CostUsageSessionBreakdown { - CostUsageSessionBreakdown( - sessionID: id, - lastActivity: now, - inputTokens: 10, - cachedInputTokens: nil, - outputTokens: 2, - totalTokens: 12, - requestCount: 1, - costUSD: 1, - modelBreakdowns: [.init(modelName: "gpt-5.4", costUSD: 1, totalTokens: 12)], - title: title, - projectPath: projectPath) - } - let input = SpendDashboardModel.ProviderInput( - id: "codex", - provider: .codex, - displayName: "Codex · #1", - snapshot: CostUsageTokenSnapshot( - sessionTokens: nil, - sessionCostUSD: nil, - last30DaysTokens: 36, - last30DaysCostUSD: 3, - currencyCode: "USD", - daily: [Self.entry(day: "2026-07-16", cost: 3, tokens: 36)], - sessions: [ - session("a", title: "Fix login bug", projectPath: "/Users/example/Projects/alpha-app"), - session("b", title: " ", projectPath: "/Users/example/Projects/beta-app/"), - session("c", title: nil, projectPath: nil), - ], - updatedAt: now)) - let model = SpendDashboardModel.build(inputs: [input], requestedDays: 30, now: now, calendar: calendar) - let rows = try #require(model.groups.first?.sessions) - let byID = Dictionary(uniqueKeysWithValues: rows.map { ($0.id, $0) }) - - let titled = try #require(byID["codex:a"]) - #expect(titled.headline == "Fix login bug") - #expect(titled.contextLabels == ["alpha-app", "Codex · #1", "gpt-5.4"]) - - let projectOnly = try #require(byID["codex:b"]) - #expect(projectOnly.headline == "beta-app") - #expect(projectOnly.contextLabels == ["Codex · #1", "gpt-5.4"]) - - let bare = try #require(byID["codex:c"]) - #expect(bare.headline == "Codex · #1") - #expect(bare.contextLabels == ["gpt-5.4"]) - } - @Test func `stored day keys stay put when the display timezone changes`() throws { let now = Date(timeIntervalSince1970: 1_784_222_400) // 2026-07-16 12:00:00 UTC diff --git a/Tests/CodexBarTests/SpendDashboardSessionAndHourlyTests.swift b/Tests/CodexBarTests/SpendDashboardSessionAndHourlyTests.swift new file mode 100644 index 0000000000..338e7dc9a8 --- /dev/null +++ b/Tests/CodexBarTests/SpendDashboardSessionAndHourlyTests.swift @@ -0,0 +1,113 @@ +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +struct SpendDashboardSessionAndHourlyTests { + @Test + func `hourly chart focuses the newest day with spend when no day is selected`() throws { + let now = Date(timeIntervalSince1970: 1_784_222_400) // 2026-07-16 12:00:00 UTC + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + let today = calendar.startOfDay(for: now) + let olderHour = try #require(calendar.date(byAdding: .day, value: -3, to: now)) + let emptyLaterHour = try #require(calendar.date(byAdding: .hour, value: 1, to: now)) + let input = SpendDashboardModel.ProviderInput( + id: "codex", + provider: .codex, + displayName: "Codex", + snapshot: CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 30, + last30DaysCostUSD: 3, + currencyCode: "USD", + daily: [ + Self.entry(day: "2026-07-13", cost: 1, tokens: 10), + Self.entry(day: "2026-07-16", cost: 2, tokens: 20), + ], + hourly: [ + CostUsageHourlyEntry(hour: olderHour, totalTokens: 10, costUSD: 1), + CostUsageHourlyEntry(hour: now, totalTokens: 20, costUSD: 2), + CostUsageHourlyEntry(hour: emptyLaterHour, totalTokens: 0, costUSD: 0), + ], + updatedAt: now)) + + let group = try #require(SpendDashboardModel.build( + inputs: [input], requestedDays: 30, now: now, calendar: calendar).groups.first) + #expect(group.hourlyDay == today) + #expect(group.hourlyPoints.allSatisfy { calendar.isDate($0.hour, inSameDayAs: today) }) + #expect(group.hourlyPoints.map(\.cost).reduce(0, +) == 2) + #expect(group.hourlyChartDomain?.lowerBound == today) + + let olderDay = calendar.startOfDay(for: olderHour) + let selected = try #require(SpendDashboardModel.build( + inputs: [input], requestedDays: 30, now: now, calendar: calendar, selectedDay: olderDay).groups.first) + #expect(selected.hourlyDay == olderDay) + #expect(selected.hourlyPoints.map(\.cost) == [1]) + } + + @Test + func `session rows prefer title and project over the source name`() throws { + let now = Date(timeIntervalSince1970: 1_784_222_400) + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + func session(_ id: String, title: String?, projectPath: String?) -> CostUsageSessionBreakdown { + CostUsageSessionBreakdown( + sessionID: id, + lastActivity: now, + inputTokens: 10, + cachedInputTokens: nil, + outputTokens: 2, + totalTokens: 12, + requestCount: 1, + costUSD: 1, + modelBreakdowns: [.init(modelName: "gpt-5.4", costUSD: 1, totalTokens: 12)], + title: title, + projectPath: projectPath) + } + let input = SpendDashboardModel.ProviderInput( + id: "codex", + provider: .codex, + displayName: "Codex · #1", + snapshot: CostUsageTokenSnapshot( + sessionTokens: nil, + sessionCostUSD: nil, + last30DaysTokens: 36, + last30DaysCostUSD: 3, + currencyCode: "USD", + daily: [Self.entry(day: "2026-07-16", cost: 3, tokens: 36)], + sessions: [ + session("a", title: "Fix login bug", projectPath: "/Users/example/Projects/alpha-app"), + session("b", title: " ", projectPath: "/Users/example/Projects/beta-app/"), + session("c", title: nil, projectPath: nil), + ], + updatedAt: now)) + let model = SpendDashboardModel.build(inputs: [input], requestedDays: 30, now: now, calendar: calendar) + let rows = try #require(model.groups.first?.sessions) + let byID = Dictionary(uniqueKeysWithValues: rows.map { ($0.id, $0) }) + + let titled = try #require(byID["codex:a"]) + #expect(titled.headline == "Fix login bug") + #expect(titled.contextLabels == ["alpha-app", "Codex · #1", "gpt-5.4"]) + + let projectOnly = try #require(byID["codex:b"]) + #expect(projectOnly.headline == "beta-app") + #expect(projectOnly.contextLabels == ["Codex · #1", "gpt-5.4"]) + + let bare = try #require(byID["codex:c"]) + #expect(bare.headline == "Codex · #1") + #expect(bare.contextLabels == ["gpt-5.4"]) + } + + private static func entry(day: String, cost: Double, tokens: Int) -> CostUsageDailyReport.Entry { + CostUsageDailyReport.Entry( + date: day, + inputTokens: nil, + outputTokens: nil, + totalTokens: tokens, + costUSD: cost, + modelsUsed: nil, + modelBreakdowns: [.init(modelName: "test-model", costUSD: cost, totalTokens: tokens)]) + } +}