authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 00:19:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 00:19:33-07:00
log487d1766caba52579f07a3e5ebc6f276227e4382
tree2eb6a8ff16d2e014f2cdb20df9fef1f894967283
parent3bcde91f8215ec24eeffdd4abdee8fcb8a5e94f9
signature Signed by PGP key B5690EEEBB952194

refactor(web): test the live usage column builder (#9993)


2 files changed, 10 insertions(+), 26 deletions(-)

apps/web/src/components/usage/UsageProviderChart.test.ts+9-7
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
22
3import { buildDayColumns, niceScale } from "./UsageProviderChart";3import { buildPeriodColumns, niceScale } from "./UsageProviderChart";
4import { providersWithUsage } from "./usageProviders";4import { providersWithUsage } from "./usageProviders";
55
6describe("niceScale", () => {6describe("niceScale", () => {
...@@ -41,7 +41,7 @@ describe("niceScale", () => {...@@ -41,7 +41,7 @@ describe("niceScale", () => {
41 });41 });
42});42});
4343
44describe("buildDayColumns", () => {44describe("buildPeriodColumns", () => {
45 const days = ["2026-08-01", "2026-08-02", "2026-08-03"];45 const days = ["2026-08-01", "2026-08-02", "2026-08-03"];
46 const byDay = new Map([46 const byDay = new Map([
47 [47 [
...@@ -69,11 +69,13 @@ describe("buildDayColumns", () => {...@@ -69,11 +69,13 @@ describe("buildDayColumns", () => {
69 ]);69 ]);
7070
71 it("plots each day on its own", () => {71 it("plots each day on its own", () => {
72 expect(buildDayColumns(days, byDay, "cost").map((column) => column.total)).toEqual([30, 0, 5]);72 expect(buildPeriodColumns(days, byDay, "cost").map((column) => column.total)).toEqual([
73 30, 0, 5,
74 ]);
73 });75 });
7476
75 it("reads the requested metric", () => {77 it("reads the requested metric", () => {
76 expect(buildDayColumns(days, byDay, "tokens").map((column) => column.total)).toEqual([78 expect(buildPeriodColumns(days, byDay, "tokens").map((column) => column.total)).toEqual([
77 300, 0, 50,79 300, 0, 50,
78 ]);80 ]);
79 });81 });
...@@ -81,7 +83,7 @@ describe("buildDayColumns", () => {...@@ -81,7 +83,7 @@ describe("buildDayColumns", () => {
81 it("keeps band values absolute rather than cumulative", () => {83 it("keeps band values absolute rather than cumulative", () => {
82 // Regression: the bands were once stack offsets, which drew Claude Code84 // Regression: the bands were once stack offsets, which drew Claude Code
83 // permanently above Codex regardless of which provider spent more.85 // permanently above Codex regardless of which provider spent more.
84 const [first] = buildDayColumns(days, byDay, "cost");86 const [first] = buildPeriodColumns(days, byDay, "cost");
8587
86 expect(first?.bands).toEqual([88 expect(first?.bands).toEqual([
87 { provider: "codex", value: 10 },89 { provider: "codex", value: 10 },
...@@ -91,7 +93,7 @@ describe("buildDayColumns", () => {...@@ -91,7 +93,7 @@ describe("buildDayColumns", () => {
91 });93 });
9294
93 it("reports the total as the sum of its bands", () => {95 it("reports the total as the sum of its bands", () => {
94 for (const column of buildDayColumns(days, byDay, "cost")) {96 for (const column of buildPeriodColumns(days, byDay, "cost")) {
95 const sum = column.bands.reduce((running, band) => running + band.value, 0);97 const sum = column.bands.reduce((running, band) => running + band.value, 0);
96 expect(column.total).toBeCloseTo(sum, 9);98 expect(column.total).toBeCloseTo(sum, 9);
97 }99 }
...@@ -125,7 +127,7 @@ describe("hourly chart columns", () => {...@@ -125,7 +127,7 @@ describe("hourly chart columns", () => {
125 ]);127 ]);
126128
127 expect(129 expect(
128 buildDayColumns(130 buildPeriodColumns(
129 ["2026-08-11T08:37:00.000Z", "2026-08-11T09:37:00.000Z", "2026-08-11T10:37:00.000Z"],131 ["2026-08-11T08:37:00.000Z", "2026-08-11T09:37:00.000Z", "2026-08-11T10:37:00.000Z"],
130 byHour,132 byHour,
131 "cost",133 "cost",
apps/web/src/components/usage/UsageProviderChart.tsx+1-19
...@@ -54,7 +54,7 @@ function valueFor(...@@ -54,7 +54,7 @@ function valueFor(
54 return metric === "tokens" ? entry.totalTokens : entry.costUsd;54 return metric === "tokens" ? entry.totalTokens : entry.costUsd;
55}55}
5656
57function buildPeriodColumns(57export function buildPeriodColumns(
58 periods: readonly string[],58 periods: readonly string[],
59 byPeriod: ReadonlyMap<string, DailyTotals | HourlyTotals>,59 byPeriod: ReadonlyMap<string, DailyTotals | HourlyTotals>,
60 metric: UsageChartMetric,60 metric: UsageChartMetric,
...@@ -169,24 +169,6 @@ export function niceScale(peak: number, count: number): { max: number; ticks: re...@@ -169,24 +169,6 @@ export function niceScale(peak: number, count: number): { max: number; ticks: re
169 return { max, ticks };169 return { max, ticks };
170}170}
171171
172/**
173 * Turns the merged daily totals into one column per day.
174 *
175 * Values are absolute, not cumulative: each provider is drawn from the same
176 * zero baseline so the chart never implies that one provider is always larger.
177 *
178 * The chart paths and the hover readout both consume this, so the number under
179 * the cursor is by construction the number that was plotted rather than a
180 * second derivation that can drift from it.
181 */
182export function buildDayColumns(
183 days: readonly string[],
184 byDay: ReadonlyMap<string, DailyTotals>,
185 metric: UsageChartMetric,
186): readonly DayColumn[] {
187 return buildPeriodColumns(days, byDay, metric);
188}
189
190export function UsageProviderChart({172export function UsageProviderChart({
191 providers,173 providers,
192 days,174 days,