daili-usage-keeper / web /src /components /usage /OverviewCharts.logic.test.ts
pjpjq's picture
fix: build usage keeper from source
b034029 verified
import { describe, expect, it } from 'vitest';
import { buildOverviewCostTrendSeries } from './CostTrendChart';
import { buildTokenBreakdownChartSeries } from './TokenBreakdownChart';
import { buildHourlyTokenBreakdown } from '@/utils/usage';
import { buildChartData, filterUsageByWindow } from '@/utils/usage';
import type { UsageOverviewResponse, UsageEvent, UsageSnapshot } from '@/lib/types';
const overviewUsage: UsageOverviewResponse = {
usage: {
total_requests: 2,
success_count: 2,
failure_count: 0,
total_tokens: 300,
requests_by_day: {},
requests_by_hour: {},
tokens_by_day: {},
tokens_by_hour: {},
apis: {
'provider-a': {
display_name: 'Provider A',
total_requests: 2,
success_count: 2,
failure_count: 0,
total_tokens: 300,
models: {
'claude-sonnet': {
total_requests: 2,
success_count: 2,
failure_count: 0,
total_tokens: 300,
},
},
},
},
},
summary: {
request_count: 2,
token_count: 300,
window_minutes: 180,
rpm: 2 / 180,
tpm: 300 / 180,
total_cost: 1.23,
cost_available: true,
cached_tokens: 30,
reasoning_tokens: 30,
},
series: {
requests: {
'2026-04-23': 2,
},
tokens: {
'2026-04-23': 300,
},
rpm: {
'2026-04-23': 2 / 1440,
},
tpm: {
'2026-04-23': 300 / 1440,
},
cost: {
'2026-04-23': 1.23,
},
},
hourly_series: {
requests: {
'2026-04-23T00:00:00Z': 1,
'2026-04-23T02:00:00Z': 1,
},
tokens: {
'2026-04-23T00:00:00Z': 100,
'2026-04-23T02:00:00Z': 200,
},
rpm: {
'2026-04-23T00:00:00Z': 1 / 60,
'2026-04-23T02:00:00Z': 1 / 60,
},
tpm: {
'2026-04-23T00:00:00Z': 100 / 60,
'2026-04-23T02:00:00Z': 200 / 60,
},
cost: {
'2026-04-23T00:00:00Z': 0.45,
'2026-04-23T02:00:00Z': 0.78,
},
input_tokens: {
'2026-04-23T00:00:00Z': 60,
'2026-04-23T02:00:00Z': 140,
},
output_tokens: {
'2026-04-23T00:00:00Z': 40,
'2026-04-23T02:00:00Z': 60,
},
cached_tokens: {
'2026-04-23T00:00:00Z': 10,
'2026-04-23T02:00:00Z': 20,
},
reasoning_tokens: {
'2026-04-23T00:00:00Z': 10,
'2026-04-23T02:00:00Z': 20,
},
},
daily_series: {
requests: {
'2026-04-23': 2,
},
tokens: {
'2026-04-23': 300,
},
rpm: {
'2026-04-23': 2 / 1440,
},
tpm: {
'2026-04-23': 300 / 1440,
},
cost: {
'2026-04-23': 1.23,
},
input_tokens: {
'2026-04-23': 200,
},
output_tokens: {
'2026-04-23': 100,
},
cached_tokens: {
'2026-04-23': 30,
},
reasoning_tokens: {
'2026-04-23': 30,
},
},
};
const asyncEvents: UsageEvent[] = [
{
timestamp: '2026-04-23T02:00:00.000Z',
model: 'claude-sonnet',
source: 'source-a',
auth_index: '1',
failed: false,
latency_ms: 120,
tokens: {
input_tokens: 100,
output_tokens: 60,
reasoning_tokens: 20,
cached_tokens: 20,
total_tokens: 200,
},
},
];
describe('overview chart data flow', () => {
it('requests and tokens charts need the full overview payload to read explicit hourly and daily series', () => {
const filterWindow = {
startMs: Date.parse('2026-04-23T01:00:00.000Z'),
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
windowMinutes: 120,
};
const filteredUsage = filterUsageByWindow(overviewUsage.usage as UsageSnapshot, filterWindow);
const wrongRequests = buildChartData(filteredUsage, 'hour', 'requests', ['all'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
const correctRequests = buildChartData({
...overviewUsage.usage,
requests_by_hour: overviewUsage.hourly_series?.requests ?? {},
requests_by_day: overviewUsage.daily_series?.requests ?? {},
tokens_by_hour: overviewUsage.hourly_series?.tokens ?? {},
tokens_by_day: overviewUsage.daily_series?.tokens ?? {},
}, 'hour', 'requests', ['all'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
const correctTokens = buildChartData({
...overviewUsage.usage,
requests_by_hour: overviewUsage.hourly_series?.requests ?? {},
requests_by_day: overviewUsage.daily_series?.requests ?? {},
tokens_by_hour: overviewUsage.hourly_series?.tokens ?? {},
tokens_by_day: overviewUsage.daily_series?.tokens ?? {},
}, 'day', 'tokens', ['all'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
expect(wrongRequests.labels).toEqual([]);
expect(correctRequests.labels).toHaveLength(24);
expect(correctRequests.datasets[0]?.data.filter((value) => value > 0)).toEqual([1, 1]);
expect(correctTokens.labels).toEqual(['2026-04-23']);
expect(correctTokens.datasets[0]?.data).toEqual([300]);
});
it('keeps aggregate overview charts visible when all traffic is selected with extra model lines', () => {
const chartUsage = {
...overviewUsage.usage,
requests_by_hour: overviewUsage.hourly_series?.requests ?? {},
requests_by_day: overviewUsage.daily_series?.requests ?? {},
tokens_by_hour: overviewUsage.hourly_series?.tokens ?? {},
tokens_by_day: overviewUsage.daily_series?.tokens ?? {},
};
const requests = buildChartData(chartUsage, 'hour', 'requests', ['all', 'claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
const tokens = buildChartData(chartUsage, 'day', 'tokens', ['all', 'claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
expect(requests.labels).toHaveLength(24);
expect(requests.datasets[0]?.label).toBe('All');
expect(requests.datasets[0]?.data.filter((value) => value > 0)).toEqual([1, 1]);
expect(tokens.labels).toEqual(['2026-04-23']);
expect(tokens.datasets[0]?.label).toBe('All');
expect(tokens.datasets[0]?.data).toEqual([300]);
});
it('renders selected model lines from backend overview model series', () => {
const chartUsage = {
...overviewUsage.usage,
requests_by_hour: overviewUsage.hourly_series?.requests ?? {},
requests_by_day: overviewUsage.daily_series?.requests ?? {},
tokens_by_hour: overviewUsage.hourly_series?.tokens ?? {},
tokens_by_day: overviewUsage.daily_series?.tokens ?? {},
model_series: {
'claude-sonnet': {
requests_by_hour: {
'2026-04-23T00:00:00Z': 1,
'2026-04-23T02:00:00Z': 1,
},
requests_by_day: {
'2026-04-23': 2,
},
tokens_by_hour: {
'2026-04-23T00:00:00Z': 100,
'2026-04-23T02:00:00Z': 200,
},
tokens_by_day: {
'2026-04-23': 300,
},
},
},
};
const requests = buildChartData(chartUsage, 'hour', 'requests', ['all', 'claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
const tokens = buildChartData(chartUsage, 'day', 'tokens', ['all', 'claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
expect(requests.datasets.map((dataset) => dataset.label)).toEqual(['All', 'claude-sonnet']);
expect(requests.datasets[1]?.data.filter((value) => value > 0)).toEqual([1, 1]);
expect(tokens.datasets.map((dataset) => dataset.label)).toEqual(['All', 'claude-sonnet']);
expect(tokens.datasets[1]?.data).toEqual([300]);
});
it('renders a single selected model line without all traffic selected', () => {
const chartUsage = {
...overviewUsage.usage,
requests_by_hour: overviewUsage.hourly_series?.requests ?? {},
requests_by_day: overviewUsage.daily_series?.requests ?? {},
tokens_by_hour: overviewUsage.hourly_series?.tokens ?? {},
tokens_by_day: overviewUsage.daily_series?.tokens ?? {},
model_series: {
'claude-sonnet': {
requests_by_hour: {
'2026-04-23T00:00:00Z': 1,
'2026-04-23T02:00:00Z': 1,
},
requests_by_day: {
'2026-04-23': 2,
},
tokens_by_hour: {
'2026-04-23T00:00:00Z': 100,
'2026-04-23T02:00:00Z': 200,
},
tokens_by_day: {
'2026-04-23': 300,
},
},
},
};
const requests = buildChartData(chartUsage, 'hour', 'requests', ['claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
const tokens = buildChartData(chartUsage, 'day', 'tokens', ['claude-sonnet'], {
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T03:00:00.000Z'),
});
expect(requests.datasets.map((dataset) => dataset.label)).toEqual(['claude-sonnet']);
expect(requests.datasets[0]?.data.filter((value) => value > 0)).toEqual([1, 1]);
expect(tokens.datasets.map((dataset) => dataset.label)).toEqual(['claude-sonnet']);
expect(tokens.datasets[0]?.data).toEqual([300]);
});
it('token breakdown needs the async event-derived usage shape to show data on first render', () => {
const withoutEvents = buildHourlyTokenBreakdown(overviewUsage.usage, 24, Date.parse('2026-04-23T03:00:00.000Z'));
const usageWithAsyncEvents = {
...(overviewUsage.usage ?? {}),
apis: {
__overview__: {
total_requests: asyncEvents.length,
success_count: asyncEvents.length,
failure_count: 0,
total_tokens: 200,
models: {
__overview__: {
total_requests: asyncEvents.length,
success_count: asyncEvents.length,
failure_count: 0,
total_tokens: 200,
details: [
{
timestamp: asyncEvents[0].timestamp,
latency_ms: asyncEvents[0].latency_ms,
source: asyncEvents[0].source,
auth_index: asyncEvents[0].auth_index ?? '',
failed: false,
tokens: asyncEvents[0].tokens,
},
],
},
},
},
},
};
const withEvents = buildHourlyTokenBreakdown(usageWithAsyncEvents, 24, Date.parse('2026-04-23T03:00:00.000Z'));
expect(withoutEvents.labels).toEqual([]);
expect(withEvents.labels.length).toBeGreaterThan(0);
expect(withEvents.dataByCategory.input.some((value) => value > 0)).toBe(true);
});
it('keeps short-range overview hour charts aligned to backend partial-hour buckets', () => {
const chartUsage = {
...overviewUsage.usage,
requests_by_hour: {
'2026-04-24T02:00:00Z': 34,
'2026-04-24T03:00:00Z': 41,
'2026-04-24T04:00:00Z': 9,
'2026-04-24T05:00:00Z': 16,
'2026-04-24T06:00:00Z': 93,
},
tokens_by_hour: {
'2026-04-24T02:00:00Z': 3664982,
'2026-04-24T03:00:00Z': 5003310,
'2026-04-24T04:00:00Z': 1362696,
'2026-04-24T05:00:00Z': 2583370,
'2026-04-24T06:00:00Z': 6477989,
},
};
const requests = buildChartData(chartUsage, 'hour', 'requests', ['all'], {
hourWindowHours: 4,
endMs: Date.parse('2026-04-24T06:16:00Z'),
});
const tokens = buildChartData(chartUsage, 'hour', 'tokens', ['all'], {
hourWindowHours: 4,
endMs: Date.parse('2026-04-24T06:16:00Z'),
});
expect(requests.labels).toHaveLength(5);
expect(requests.datasets[0]?.data).toEqual([34, 41, 9, 16, 93]);
expect(tokens.labels).toHaveLength(5);
expect(tokens.datasets[0]?.data[0]).toBe(3664982);
});
it('fills empty hourly buckets between backend overview points', () => {
const chartUsage = {
...overviewUsage.usage,
requests_by_hour: {
'2026-04-24T02:00:00Z': 1,
'2026-04-24T04:00:00Z': 2,
},
tokens_by_hour: {
'2026-04-24T02:00:00Z': 100,
'2026-04-24T04:00:00Z': 200,
},
};
const requests = buildChartData(chartUsage, 'hour', 'requests', ['all'], {
hourWindowHours: 4,
endMs: Date.parse('2026-04-24T04:16:00Z'),
});
const tokens = buildChartData(chartUsage, 'hour', 'tokens', ['all'], {
hourWindowHours: 4,
endMs: Date.parse('2026-04-24T04:16:00Z'),
});
expect(requests.labels).toHaveLength(5);
expect(requests.datasets[0]?.data).toEqual([0, 0, 1, 0, 2]);
expect(tokens.datasets[0]?.data).toEqual([0, 0, 100, 0, 200]);
});
it('fills token breakdown hour buckets across the latest 24 hours when only one bucket has data', () => {
const series = buildTokenBreakdownChartSeries({
usage: {
...overviewUsage,
hourly_series: {
...overviewUsage.hourly_series!,
input_tokens: {
'2026-04-23T23:00:00Z': 100,
},
output_tokens: {
'2026-04-23T23:00:00Z': 50,
},
cached_tokens: {},
reasoning_tokens: {},
},
},
period: 'hour',
hourWindowHours: 24,
endMs: Date.parse('2026-04-23T23:16:00Z'),
});
expect(series.labels).toHaveLength(24);
expect(series.dataByCategory.input.slice(0, 23)).toEqual(Array(23).fill(0));
expect(series.dataByCategory.input[23]).toBe(100);
expect(series.dataByCategory.output[23]).toBe(50);
});
it('keeps overview hour charts capped to the latest 24 hours even when the query range is 7d', () => {
const usageWithSevenDaysOfDetails = {
...overviewUsage.usage,
apis: {
__overview__: {
total_requests: 48,
success_count: 48,
failure_count: 0,
total_tokens: 4800,
models: {
__overview__: {
total_requests: 48,
success_count: 48,
failure_count: 0,
total_tokens: 4800,
details: Array.from({ length: 48 }, (_, index) => ({
timestamp: `2026-04-${index < 24 ? '22' : '23'}T${String(index % 24).padStart(2, '0')}:00:00.000Z`,
latency_ms: 100,
source: 'source-a',
auth_index: '1',
failed: false,
tokens: {
input_tokens: 50,
output_tokens: 50,
reasoning_tokens: 0,
cached_tokens: 0,
total_tokens: 100,
},
})),
},
},
},
},
};
const requestsByHour = buildChartData(usageWithSevenDaysOfDetails, 'hour', 'requests', ['all'], {
hourWindowHours: 168,
endMs: Date.parse('2026-04-23T23:59:59Z'),
});
const tokenBreakdownByHour = buildHourlyTokenBreakdown(
usageWithSevenDaysOfDetails,
168,
Date.parse('2026-04-23T23:59:59Z'),
);
const costTrendByHour = buildOverviewCostTrendSeries({
usage: {
...overviewUsage,
summary: {
...overviewUsage.summary!,
window_minutes: 7 * 24 * 60,
},
series: {
...overviewUsage.series!,
cost: Object.fromEntries(
Array.from({ length: 48 }, (_, index) => [
`2026-04-${index < 24 ? '22' : '23'}T${String(index % 24).padStart(2, '0')}:00:00Z`,
index + 1,
]),
),
},
},
period: 'hour',
hourWindowHours: 168,
endMs: Date.parse('2026-04-23T23:59:59Z'),
});
expect(requestsByHour.labels).toHaveLength(24);
expect(tokenBreakdownByHour.labels).toHaveLength(24);
expect(costTrendByHour.labels).toHaveLength(24);
});
});