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

854 lines
34 KiB
Swift

import CodexBarCore
import SwiftUI
import Testing
@testable import CodexBar
@MainActor
struct CostHistoryChartMenuViewTests {
@Test
@MainActor
func `model breakdown keeps every item behind a bounded scrolling viewport`() {
let breakdown = (1...6).map { index in
CostUsageDailyReport.ModelBreakdown(
modelName: "model-\(index)",
costUSD: Double(index),
totalTokens: index * 100)
}
let ordered = CostHistoryChartMenuView.orderedBreakdownItems(breakdown)
#expect(ordered.map(\.modelName) == [
"model-6",
"model-5",
"model-4",
"model-3",
"model-2",
"model-1",
])
#expect(CostHistoryChartMenuView.detailViewportRowCount(itemCount: ordered.count) == 4)
#expect(CostHistoryChartMenuView.detailRowsNeedScrolling(itemCount: ordered.count))
#expect(CostHistoryChartMenuView.detailOverflowHint(itemCount: ordered.count) == "Scroll to see more models")
#expect(CostHistoryChartMenuView.detailOverflowHint(itemCount: 4) == nil)
}
@Test
@MainActor
func `menu hosting view publishes measured height through intrinsic size`() {
let hosting = MenuHostingView(rootView: EmptyView())
hosting.frame = CGRect(x: 0, y: 0, width: 320, height: 1)
hosting.applyMeasuredHeight(width: 320, height: 123.2)
#expect(hosting.frame.size == CGSize(width: 320, height: 124))
#expect(hosting.intrinsicContentSize.height == 124)
}
@Test
@MainActor
func `cost history defaults selection to latest day`() {
let daily = [
CostUsageDailyReport.Entry(
date: "2026-06-07",
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
costUSD: 1.25,
modelsUsed: ["gpt-5.5"],
modelBreakdowns: nil),
CostUsageDailyReport.Entry(
date: "2026-06-09",
inputTokens: 200,
outputTokens: 75,
totalTokens: 275,
costUSD: 2.5,
modelsUsed: ["gpt-5.5"],
modelBreakdowns: nil),
]
#expect(
CostHistoryChartMenuView._defaultSelectedDateKeyForTesting(
provider: .codex,
daily: daily) == "2026-06-09")
}
@Test
@MainActor
func `cost history sizes its viewport to the largest breakdown in the range`() {
let threeRows = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [Self.entry(date: "2026-06-07", modelCount: 1), Self.entry(date: "2026-06-08", modelCount: 3)])
let cappedRows = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [Self.entry(date: "2026-06-07", modelCount: 6)])
let mixedRows = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [Self.entry(date: "2026-06-07", modelCount: 6), Self.entry(date: "2026-06-08", modelCount: 1)])
let noRows = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [Self.entry(date: "2026-06-07", modelCount: 0)])
#expect(threeRows.rowCount == 3)
#expect(!threeRows.hasOverflow)
#expect(threeRows.rowHeight == 36)
#expect(cappedRows.rowCount == 4)
#expect(cappedRows.hasOverflow)
#expect(mixedRows.rowCount == 4)
#expect(mixedRows.hasOverflow)
#expect(noRows.rowCount == 0)
#expect(!noRows.hasOverflow)
}
@Test
@MainActor
func `cost history expands every row only when the range contains mode details`() {
let compact = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [Self.entry(date: "2026-06-07", modelCount: 2)])
let expanded = CostHistoryChartMenuView._detailViewportConfigurationForTesting(
provider: .codex,
daily: [
Self.entry(date: "2026-06-07", modelCount: 2),
Self.entry(date: "2026-06-08", modelCount: 1, hasModeDetails: true),
])
#expect(compact.rowHeight == 36)
#expect(expanded.rowHeight == 44)
#expect(compact.rowCount == expanded.rowCount)
}
@Test
@MainActor
func `axis dates span first to last for multi-day data`() {
let daily = [
CostUsageDailyReport.Entry(
date: "2026-05-21",
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: 1.0,
modelsUsed: nil,
modelBreakdowns: nil),
CostUsageDailyReport.Entry(
date: "2026-06-17",
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: 2.0,
modelsUsed: nil,
modelBreakdowns: nil),
]
let dates = CostHistoryChartMenuView._axisDatesForTesting(provider: .codex, daily: daily)
let cal = Calendar.current
#expect(dates.count == 2)
#expect(cal.component(.month, from: dates[0]) == 5)
#expect(cal.component(.day, from: dates[0]) == 21)
#expect(cal.component(.month, from: dates[1]) == 6)
#expect(cal.component(.day, from: dates[1]) == 17)
#expect(
CostHistoryChartMenuView._axisLabelPlacementForTesting(
provider: .codex,
daily: daily) == .edges)
}
@Test
@MainActor
func `axis dates collapse to one for single-day data`() {
let daily = [
CostUsageDailyReport.Entry(
date: "2026-06-17",
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: 1.0,
modelsUsed: nil,
modelBreakdowns: nil),
]
let dates = CostHistoryChartMenuView._axisDatesForTesting(provider: .codex, daily: daily)
#expect(dates.count == 1)
#expect(
CostHistoryChartMenuView._axisLabelPlacementForTesting(
provider: .codex,
daily: daily) == .centered)
}
@Test
@MainActor
func `axis dates are empty when there is no cost data`() {
let daily = [
CostUsageDailyReport.Entry(
date: "2026-06-17",
inputTokens: nil,
outputTokens: nil,
totalTokens: nil,
costUSD: nil,
modelsUsed: nil,
modelBreakdowns: nil),
]
let dates = CostHistoryChartMenuView._axisDatesForTesting(provider: .codex, daily: daily)
#expect(dates.isEmpty)
#expect(
CostHistoryChartMenuView._axisLabelPlacementForTesting(
provider: .codex,
daily: daily) == .hidden)
}
@Test
@MainActor
func `y-axis tick values are empty for flat or no data`() {
#expect(CostHistoryChartMenuView._yAxisTickValuesForTesting(maxCostUSD: 0).isEmpty)
#expect(CostHistoryChartMenuView._yAxisTickValuesForTesting(maxCostUSD: -1).isEmpty)
}
@Test
@MainActor
func `y-axis tick values use two ticks for small ranges`() {
let ticks = CostHistoryChartMenuView._yAxisTickValuesForTesting(maxCostUSD: 0.50)
#expect(ticks == [0, 0.50])
}
@Test
@MainActor
func `y-axis tick values use three ticks for ranges at or above one dollar`() {
let ticks = CostHistoryChartMenuView._yAxisTickValuesForTesting(maxCostUSD: 12.0)
#expect(ticks == [0, 6.0, 12.0])
let large = CostHistoryChartMenuView._yAxisTickValuesForTesting(maxCostUSD: 1000.0)
#expect(large == [0, 500.0, 1000.0])
}
@Test(arguments: [
(0.0, "$0"),
(12.56, "$13"),
(0.50, "$0.50"),
])
@MainActor
func `y-axis cost labels preserve cents only for nonzero sub-dollar values`(
value: Double,
expected: String)
{
#expect(CostHistoryChartMenuView._yAxisCostStringForTesting(value) == expected)
}
@Test
@MainActor
func `cost history fitting height stays stable across compact overflow and mode selections`() {
let compactLatestHasOneModel = [
Self.entry(date: "2026-06-07", modelCount: 3),
Self.entry(date: "2026-06-08", modelCount: 1),
]
let compactLatestHasThreeModels = [
Self.entry(date: "2026-06-07", modelCount: 1),
Self.entry(date: "2026-06-08", modelCount: 3),
]
let overflowLatestHasFourModels = [
Self.entry(date: "2026-06-07", modelCount: 6),
Self.entry(date: "2026-06-08", modelCount: 4),
]
let overflowLatestHasSixModels = [
Self.entry(date: "2026-06-07", modelCount: 4),
Self.entry(date: "2026-06-08", modelCount: 6),
]
let modeLatestHasOneModel = [
Self.entry(date: "2026-06-07", modelCount: 6),
Self.entry(date: "2026-06-08", modelCount: 1, hasModeDetails: true),
]
let modeLatestHasSixModels = [
Self.entry(date: "2026-06-07", modelCount: 1, hasModeDetails: true),
Self.entry(date: "2026-06-08", modelCount: 6),
]
let compactHeight = Self.renderedHeight(daily: compactLatestHasOneModel)
let overflowHeight = Self.renderedHeight(daily: overflowLatestHasFourModels)
let modeHeight = Self.renderedHeight(daily: modeLatestHasOneModel)
#expect(compactHeight == Self.renderedHeight(daily: compactLatestHasThreeModels))
#expect(overflowHeight == Self.renderedHeight(daily: overflowLatestHasSixModels))
#expect(modeHeight == Self.renderedHeight(daily: modeLatestHasSixModels))
#expect(compactHeight < overflowHeight)
#expect(overflowHeight < modeHeight)
}
@Test
@MainActor
func `cost history without model breakdown stays compact`() {
let noBreakdown = [Self.entry(date: "2026-06-07", modelCount: 0)]
let withBreakdown = [Self.entry(date: "2026-06-07", modelCount: 1)]
#expect(Self.renderedHeight(daily: noBreakdown) < Self.renderedHeight(daily: withBreakdown))
}
@Test
@MainActor
func `single differing project source remains visible`() {
let matching = Self.project(path: "/tmp/main", sourcePath: "/tmp/main")
let differing = Self.project(path: "/tmp/main", sourcePath: "/tmp/worktree")
#expect(CostHistoryChartMenuView.visibleProjectSources(matching).isEmpty)
#expect(CostHistoryChartMenuView.visibleProjectSources(differing).compactMap(\.path) == ["/tmp/worktree"])
}
@Test
@MainActor
func `render fingerprint is stable for identical snapshots`() {
let snapshot = Self.makeSnapshot(dailyCost: 1.23, projectCount: 5)
let first = CostHistoryChartMenuView.renderFingerprint(from: snapshot, provider: .codex)
let second = CostHistoryChartMenuView.renderFingerprint(from: snapshot, provider: .codex)
#expect(first == second)
#expect(first.projects.count == 5)
#expect(first.projects.allSatisfy { $0.sources.count <= 2 })
}
@Test
@MainActor
func `render fingerprint changes when daily cost changes`() {
let before = CostHistoryChartMenuView.renderFingerprint(
from: Self.makeSnapshot(dailyCost: 1.0),
provider: .codex)
let after = CostHistoryChartMenuView.renderFingerprint(
from: Self.makeSnapshot(dailyCost: 2.0),
provider: .codex)
#expect(before != after)
}
@Test
@MainActor
func `render fingerprint changes for total currency history window and label`() {
let base = Self.makeSnapshot(dailyCost: 1.0)
#expect(
CostHistoryChartMenuView.renderFingerprint(from: base, provider: .codex)
!= CostHistoryChartMenuView.renderFingerprint(from: Self.makeSnapshot(
dailyCost: 1.0,
totalCostUSD: 9.99), provider: .codex))
#expect(
CostHistoryChartMenuView.renderFingerprint(from: base, provider: .codex)
!= CostHistoryChartMenuView.renderFingerprint(from: Self.makeSnapshot(
dailyCost: 1.0,
currencyCode: "EUR"), provider: .codex))
#expect(
CostHistoryChartMenuView.renderFingerprint(from: base, provider: .codex)
!= CostHistoryChartMenuView.renderFingerprint(from: Self.makeSnapshot(
dailyCost: 1.0,
historyDays: 7), provider: .codex))
#expect(
CostHistoryChartMenuView.renderFingerprint(from: base, provider: .codex)
!= CostHistoryChartMenuView.renderFingerprint(from: Self.makeSnapshot(
dailyCost: 1.0,
historyLabel: "Last week"), provider: .codex))
}
@Test
@MainActor
func `render fingerprint tracks daily token request and model breakdown fields`() {
let baseDaily = [Self.entry(date: "2026-06-07", modelCount: 1)]
let base = Self.fingerprint(dailyCost: 1.0, daily: baseDaily, projects: [])
var changedTokens = baseDaily
changedTokens[0] = CostUsageDailyReport.Entry(
date: "2026-06-07",
inputTokens: 100,
outputTokens: 50,
totalTokens: 999,
costUSD: 1,
modelsUsed: ["model-0"],
modelBreakdowns: changedTokens[0].modelBreakdowns)
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: changedTokens, projects: []))
var changedRequests = baseDaily
changedRequests[0] = CostUsageDailyReport.Entry(
date: "2026-06-07",
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
requestCount: 42,
costUSD: 1,
modelsUsed: ["model-0"],
modelBreakdowns: changedRequests[0].modelBreakdowns)
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: changedRequests, projects: []))
let changedModel = [
Self.entry(date: "2026-06-07", modelCount: 1, modelNamePrefix: "other"),
]
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: changedModel, projects: []))
let changedMode = [
Self.entry(date: "2026-06-07", modelCount: 1, hasModeDetails: true),
]
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: changedMode, projects: []))
let reorderedDaily = [
Self.entry(date: "2026-06-08", modelCount: 1),
Self.entry(date: "2026-06-07", modelCount: 1),
]
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: reorderedDaily, projects: []))
}
@Test
@MainActor
func `render fingerprint ignores hidden daily accounting fields and source order`() {
let visibleModel = CostUsageDailyReport.ModelBreakdown(
modelName: "model-visible",
costUSD: 0.75,
totalTokens: 120,
requestCount: 1,
standardCostUSD: 0.5,
priorityCostUSD: 0.25,
standardTokens: 80,
priorityTokens: 40)
let base = CostUsageDailyReport.Entry(
date: "2026-06-07",
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 20,
cacheCreationTokens: 10,
totalTokens: 150,
requestCount: 2,
costUSD: 1,
modelsUsed: ["model-visible"],
modelBreakdowns: [visibleModel])
let hiddenFieldsChanged = CostUsageDailyReport.Entry(
date: base.date,
inputTokens: 999,
outputTokens: 888,
cacheReadTokens: 777,
cacheCreationTokens: 666,
totalTokens: base.totalTokens,
requestCount: base.requestCount,
costUSD: base.costUSD,
modelsUsed: ["unused-model-name"],
modelBreakdowns: [CostUsageDailyReport.ModelBreakdown(
modelName: visibleModel.modelName,
costUSD: visibleModel.costUSD,
totalTokens: visibleModel.totalTokens,
requestCount: 999,
standardCostUSD: visibleModel.standardCostUSD,
priorityCostUSD: visibleModel.priorityCostUSD,
standardTokens: visibleModel.standardTokens,
priorityTokens: visibleModel.priorityTokens)])
#expect(Self.fingerprint(daily: [base]) == Self.fingerprint(daily: [hiddenFieldsChanged]))
let secondDay = Self.entry(date: "2026-06-08", modelCount: 1)
#expect(
Self.fingerprint(daily: [base, secondDay])
== Self.fingerprint(daily: [secondDay, base]))
let hiddenModeTokens = CostUsageDailyReport.ModelBreakdown(
modelName: visibleModel.modelName,
costUSD: visibleModel.costUSD,
totalTokens: visibleModel.totalTokens,
standardTokens: 1,
priorityTokens: 2)
let changedHiddenModeTokens = CostUsageDailyReport.ModelBreakdown(
modelName: visibleModel.modelName,
costUSD: visibleModel.costUSD,
totalTokens: visibleModel.totalTokens,
standardTokens: 999,
priorityTokens: 888)
#expect(
Self.fingerprint(daily: [Self.entry(modelBreakdowns: [hiddenModeTokens])])
== Self.fingerprint(daily: [Self.entry(modelBreakdowns: [changedHiddenModeTokens])]))
}
@Test
@MainActor
func `render fingerprint excludes invalid daily rows that the chart drops`() {
let invalidRows = [
Self.dailyEntry(date: "2026-06-07", costUSD: nil),
Self.dailyEntry(date: "2026-06-08", costUSD: -1),
Self.dailyEntry(date: "not-a-date", costUSD: 1),
]
let differentInvalidRows = [
Self.dailyEntry(date: "2026-06-09", costUSD: nil),
Self.dailyEntry(date: "2026-06-10", costUSD: -99),
Self.dailyEntry(date: "still-not-a-date", costUSD: 99),
]
let empty = Self.fingerprint(daily: [])
#expect(Self.fingerprint(daily: invalidRows) == Self.fingerprint(daily: differentInvalidRows))
#expect(Self.fingerprint(daily: invalidRows) != empty)
#expect(Self.fingerprint(daily: [Self.dailyEntry(date: "2026-06-07", costUSD: 1)]) != empty)
}
@Test
@MainActor
func `render fingerprint tracks every visible model breakdown field`() {
let base = CostUsageDailyReport.ModelBreakdown(
modelName: "model-visible",
costUSD: 1,
totalTokens: 100,
standardCostUSD: 0.75,
priorityCostUSD: 0.25,
standardTokens: 75,
priorityTokens: 25)
let variants = [
CostUsageDailyReport.ModelBreakdown(
modelName: "model-renamed",
costUSD: base.costUSD,
totalTokens: base.totalTokens,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: base.priorityCostUSD,
standardTokens: base.standardTokens,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: 2,
totalTokens: base.totalTokens,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: base.priorityCostUSD,
standardTokens: base.standardTokens,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: base.costUSD,
totalTokens: 200,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: base.priorityCostUSD,
standardTokens: base.standardTokens,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: base.costUSD,
totalTokens: base.totalTokens,
standardCostUSD: 0.5,
priorityCostUSD: base.priorityCostUSD,
standardTokens: base.standardTokens,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: base.costUSD,
totalTokens: base.totalTokens,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: 0.5,
standardTokens: base.standardTokens,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: base.costUSD,
totalTokens: base.totalTokens,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: base.priorityCostUSD,
standardTokens: 50,
priorityTokens: base.priorityTokens),
CostUsageDailyReport.ModelBreakdown(
modelName: base.modelName,
costUSD: base.costUSD,
totalTokens: base.totalTokens,
standardCostUSD: base.standardCostUSD,
priorityCostUSD: base.priorityCostUSD,
standardTokens: base.standardTokens,
priorityTokens: 50),
]
let baseFingerprint = Self.fingerprint(daily: [Self.entry(modelBreakdowns: [base])])
for variant in variants {
#expect(baseFingerprint != Self.fingerprint(daily: [Self.entry(modelBreakdowns: [variant])]))
}
}
@Test
@MainActor
func `render fingerprint excludes projects hidden for non-codex providers`() {
let first = Self.fingerprint(
projects: [Self.makeProject(index: 0, sourceCount: 2)],
provider: .claude)
let changed = Self.fingerprint(
projects: [Self.makeProject(index: 0, sourceCount: 2, totalCostUSD: 99)],
provider: .claude)
#expect(first.projects.isEmpty)
#expect(first == changed)
}
@Test
@MainActor
func `render fingerprint tracks visible project and source fields only`() {
let daily = [Self.entry(date: "2026-06-07", modelCount: 1)]
let projects = Self.makeProjects(count: 6, sourcesPerProject: 3)
let base = Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: projects)
var sixthProjectNestedDaily = projects
sixthProjectNestedDaily[5] = Self.makeProject(
index: 5,
sourceCount: 3,
nestedDailyCost: 99.0)
#expect(base == Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: sixthProjectNestedDaily))
var topProjectNestedDaily = projects
topProjectNestedDaily[0] = Self.makeProject(
index: 0,
sourceCount: 3,
nestedDailyCost: 99.0)
#expect(base == Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: topProjectNestedDaily))
var renamedTopProject = projects
renamedTopProject[0] = Self.makeProject(index: 0, sourceCount: 3, nameSuffix: "-renamed")
#expect(base != Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: renamedTopProject))
var changedTopProjectPath = projects
changedTopProjectPath[0] = Self.makeProject(index: 0, sourceCount: 3, pathSuffix: "-renamed")
#expect(base != Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: changedTopProjectPath))
var changedTopProjectTotals = projects
changedTopProjectTotals[0] = Self.makeProject(index: 0, sourceCount: 3, totalCostUSD: 42.0, totalTokens: 9999)
#expect(base != Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: changedTopProjectTotals))
let reorderedProjects = Array(projects.reversed())
#expect(base != Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: reorderedProjects))
var promotedHiddenProject = projects
let promoted = Self.makeProject(
index: 5,
sourceCount: 3,
totalCostUSD: 1000.0)
promotedHiddenProject.remove(at: 5)
promotedHiddenProject.insert(promoted, at: 0)
#expect(base != Self.fingerprint(totalCostUSD: 6.0, daily: daily, projects: promotedHiddenProject))
}
@Test
@MainActor
func `render fingerprint tracks source visibility and overflow count`() {
let daily = [Self.entry(date: "2026-06-07", modelCount: 1)]
let twoSources = [
Self.makeProject(index: 0, sourceCount: 2),
]
let threeSources = [
Self.makeProject(index: 0, sourceCount: 3),
]
let base = Self.fingerprint(dailyCost: 1.0, daily: daily, projects: twoSources)
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: daily, projects: threeSources))
var thirdSourceRenamed = threeSources
thirdSourceRenamed[0] = Self.makeProject(index: 0, sourceCount: 3, renameThirdSource: true)
#expect(Self.fingerprint(dailyCost: 1.0, daily: daily, projects: threeSources)
== Self.fingerprint(dailyCost: 1.0, daily: daily, projects: thirdSourceRenamed))
var firstSourceRenamed = twoSources
firstSourceRenamed[0] = Self.makeProject(index: 0, sourceCount: 2, renameFirstSource: true)
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: daily, projects: firstSourceRenamed))
var firstSourceTotals = twoSources
firstSourceTotals[0] = Self.makeProject(
index: 0,
sourceCount: 2,
firstSourceCostUSD: 9.99,
firstSourceTokens: 8888)
#expect(base != Self.fingerprint(dailyCost: 1.0, daily: daily, projects: firstSourceTotals))
let hiddenSingleSource = [
Self.project(path: "/tmp/main", sourcePath: "/tmp/main"),
]
let visibleSingleSource = [
Self.project(path: "/tmp/main", sourcePath: "/tmp/worktree"),
]
#expect(
Self.fingerprint(dailyCost: 1.0, daily: daily, projects: hiddenSingleSource)
!= Self.fingerprint(dailyCost: 1.0, daily: daily, projects: visibleSingleSource))
}
private static func project(path: String, sourcePath: String) -> CostUsageProjectBreakdown {
CostUsageProjectBreakdown(
name: "Project",
path: path,
totalTokens: 10,
totalCostUSD: 0.1,
daily: [],
modelBreakdowns: nil,
sources: [
CostUsageProjectSourceBreakdown(
name: "Source",
path: sourcePath,
totalTokens: 10,
totalCostUSD: 0.1,
daily: [],
modelBreakdowns: nil),
])
}
@MainActor
private static func renderedHeight(daily: [CostUsageDailyReport.Entry]) -> CGFloat {
let hosting = MenuHostingView(rootView: CostHistoryChartMenuView(
provider: .codex,
daily: daily,
totalCostUSD: nil,
width: 320))
hosting.frame = CGRect(x: 0, y: 0, width: 320, height: 1)
hosting.layoutSubtreeIfNeeded()
return ceil(hosting.fittingSize.height)
}
private static func entry(
date: String,
modelCount: Int,
hasModeDetails: Bool = false,
modelNamePrefix: String = "model") -> CostUsageDailyReport.Entry
{
CostUsageDailyReport.Entry(
date: date,
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
costUSD: 1,
modelsUsed: modelCount > 0 ? (0..<modelCount).map { "\(modelNamePrefix)-\($0)" } : nil,
modelBreakdowns: modelCount > 0
? (0..<modelCount).map {
CostUsageDailyReport.ModelBreakdown(
modelName: "\(modelNamePrefix)-\($0)",
costUSD: Double($0 + 1),
totalTokens: ($0 + 1) * 100,
standardCostUSD: hasModeDetails ? Double($0 + 1) * 0.75 : nil)
}
: nil)
}
private static func entry(
modelBreakdowns: [CostUsageDailyReport.ModelBreakdown]) -> CostUsageDailyReport.Entry
{
CostUsageDailyReport.Entry(
date: "2026-06-07",
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
costUSD: 1,
modelsUsed: modelBreakdowns.map(\.modelName),
modelBreakdowns: modelBreakdowns)
}
private static func dailyEntry(date: String, costUSD: Double?) -> CostUsageDailyReport.Entry {
CostUsageDailyReport.Entry(
date: date,
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
costUSD: costUSD,
modelsUsed: nil,
modelBreakdowns: nil)
}
private static func makeSnapshot(
dailyCost: Double = 1.0,
projectCount: Int = 0,
totalCostUSD: Double? = nil,
currencyCode: String = "USD",
historyDays: Int = 30,
historyLabel: String? = nil,
daily: [CostUsageDailyReport.Entry]? = nil,
projects: [CostUsageProjectBreakdown]? = nil) -> CostUsageTokenSnapshot
{
CostUsageTokenSnapshot(
sessionTokens: 123,
sessionCostUSD: 0.12,
last30DaysTokens: 123,
last30DaysCostUSD: totalCostUSD ?? dailyCost,
currencyCode: currencyCode,
historyDays: historyDays,
historyLabel: historyLabel,
daily: daily ?? [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: nil,
outputTokens: nil,
totalTokens: 123,
costUSD: dailyCost,
modelsUsed: nil,
modelBreakdowns: nil),
],
projects: projects ?? self.makeProjects(count: projectCount, sourcesPerProject: 1),
updatedAt: Date())
}
private static func fingerprint(
dailyCost: Double = 1.0,
totalCostUSD: Double? = nil,
currencyCode: String = "USD",
historyDays: Int = 30,
historyLabel: String? = nil,
daily: [CostUsageDailyReport.Entry]? = nil,
projects: [CostUsageProjectBreakdown]? = nil,
provider: UsageProvider = .codex) -> CostHistoryChartMenuView.RenderFingerprint
{
CostHistoryChartMenuView.renderFingerprint(from: self.makeSnapshot(
dailyCost: dailyCost,
totalCostUSD: totalCostUSD,
currencyCode: currencyCode,
historyDays: historyDays,
historyLabel: historyLabel,
daily: daily,
projects: projects), provider: provider)
}
private static func makeProjects(count: Int, sourcesPerProject: Int) -> [CostUsageProjectBreakdown] {
(0..<count).map { Self.makeProject(index: $0, sourceCount: sourcesPerProject) }
}
private static func makeProject(
index: Int,
sourceCount: Int,
totalCostUSD: Double = 1.0,
totalTokens: Int? = nil,
nestedDailyCost: Double = 0.01,
nameSuffix: String = "",
pathSuffix: String = "",
renameThirdSource: Bool = false,
renameFirstSource: Bool = false,
firstSourceCostUSD: Double? = nil,
firstSourceTokens: Int? = nil) -> CostUsageProjectBreakdown
{
let nestedDaily = [
CostUsageDailyReport.Entry(
date: "2025-12-23",
inputTokens: 1,
outputTokens: 1,
totalTokens: 10,
costUSD: nestedDailyCost,
modelsUsed: ["nested"],
modelBreakdowns: [
CostUsageDailyReport.ModelBreakdown(
modelName: "nested-model",
costUSD: nestedDailyCost,
totalTokens: 10),
]),
]
let sources = (0..<sourceCount).map { sourceIndex in
CostUsageProjectSourceBreakdown(
name: {
if renameThirdSource, sourceIndex == 2 {
return "Renamed Source"
}
if renameFirstSource, sourceIndex == 0 {
return "Renamed Source"
}
return "Source-\(sourceIndex)"
}(),
path: "/tmp/project-\(index)\(pathSuffix)/source-\(sourceIndex)",
totalTokens: sourceIndex == 0 ? (firstSourceTokens ?? 10 + sourceIndex) : 10 + sourceIndex,
totalCostUSD: sourceIndex == 0
? (firstSourceCostUSD ?? totalCostUSD / Double(sourceCount))
: totalCostUSD / Double(sourceCount),
daily: nestedDaily,
modelBreakdowns: [
CostUsageDailyReport.ModelBreakdown(
modelName: "source-model",
costUSD: nestedDailyCost,
totalTokens: 10),
])
}
return CostUsageProjectBreakdown(
name: "Project-\(index)\(nameSuffix)",
path: "/tmp/project-\(index)\(pathSuffix)",
totalTokens: totalTokens ?? 100 + index,
totalCostUSD: totalCostUSD + Double(index),
daily: nestedDaily,
modelBreakdowns: [
CostUsageDailyReport.ModelBreakdown(
modelName: "project-model",
costUSD: nestedDailyCost,
totalTokens: 10),
],
sources: sources)
}
}