Files
dsh-context/tests/client/components/statsContext.spec.ts
T

130 lines
5.7 KiB
TypeScript

// StatsContext (src/client/components/statsContext.tsx) rendered with real
// React: the six-cell grid — session shape with the whole-session human-input
// tally, the chat-line cache-hit cell, and the priced cost cell with its rate
// tooltip — in both locales. The context-event tallies live on the events
// card's kind filters (contextView.spec.ts); `countsOfRecords` still derives
// every count the split generation's wire head carries, pinned here.
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { countsOfRecords, makeStatsContext } from '../../../src/client/components/statsContext'
import type { ContextEventRecord, RequestRecord, SessionCostUsage, TokenUsage } from '../../../src/shared/types'
import { makeKit, mount, queryAll, text } from '../helpers/kit'
const kit = makeKit()
const kitZh = makeKit('zh')
const StatsContext = makeStatsContext(kit)
const StatsContextZh = makeStatsContext(kitZh)
const COST: SessionCostUsage = { flash: { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } }
// Prompt-side billed input 300 (100 uncached + 200 read) → hit 66.6% truncated.
const USAGE: TokenUsage = { uncachedInputTokens: 100, outputTokens: 50, cacheReadTokens: 200, cacheWriteTokens: 0 }
function req(turn?: number): RequestRecord {
return {
time: 0, seq: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0,
...(turn !== undefined ? { turn } : {}),
}
}
function ev(kind: ContextEventRecord['kind']): ContextEventRecord {
return { seq: 0, time: 0, kind }
}
function cells(container: HTMLElement): { labels: string[]; values: string[] } {
const grid = queryAll(container, '.lc-stat')
return {
labels: grid.map(el => el.querySelector('.lc-stat-label')?.textContent ?? ''),
values: grid.map(el => el.querySelector('.lc-stat-value')?.textContent ?? ''),
}
}
describe('countsOfRecords (the inline generation derivation)', () => {
test('tallies distinct turns, records, and the three priced event kinds', () => {
// Two steps in turn 1, one in turn 2, one without a turn (folds as turn 0).
const counts = countsOfRecords(
[req(1), req(1), req(2), req()],
[ev('inject'), ev('inject'), ev('inject'), ev('compaction'), ev('compaction'), ev('prune'), ev('model'), ev('mode')],
)
// model/mode events do not appear (only the three priced kinds do).
assert.deepEqual(counts, { turns: 3, steps: 4, injects: 3, compactions: 2, prunes: 1 })
})
test('empty collections tally zero', () => {
assert.deepEqual(countsOfRecords([], []), { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 })
})
})
describe('StatsContext', () => {
test('folds the six-cell grid: shape stats, the cache-hit cell, and cost', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 3, steps: 4, injects: 3, compactions: 2, prunes: 1 },
humanInputs: 7,
toolCalls: 3,
usage: USAGE,
cost: COST,
locale: 'en',
}))
assert.ok(text(m.container).includes('Context Stats'))
const { labels, values } = cells(m.container)
assert.equal(labels.length, 6)
assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cache Hit', 'Cost?'])
assert.deepEqual(values, ['3', '4', '7', '3', '66.6%', '$0.30'])
await m.unmount()
})
test('absent counters, usage, and cost degrade to zeros and the dash', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
usage: null,
locale: 'en',
}))
assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—'])
await m.unmount()
// A usage report with nothing billed prompt-side dashes the hit too.
const zero: TokenUsage = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
const m2 = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
usage: zero,
locale: 'en',
}))
assert.deepEqual(cells(m2.container).values, ['0', '0', '0', '0', '—', '—'])
await m2.unmount()
})
test('the human-inputs and cost cells are tipped; the cost bubble lists both families with peak/off rates', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
usage: null,
cost: COST,
locale: 'en',
}))
assert.equal(queryAll(m.container, '.lc-stat-tip').length, 2)
assert.equal(queryAll(m.container, '.lc-stat-q').length, 2)
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
assert.ok(tips[0].includes('question answerings'), 'the human-inputs tip explains its tally')
const costTip = tips[1]
assert.ok(costTip.includes('Per-1M-token rates'))
assert.ok(costTip.includes('deepseek-flash / deepseek-v4-flash'))
assert.ok(costTip.includes('deepseek-v4-pro'))
assert.ok(costTip.includes('miss $0.3/$0.15'))
assert.ok(costTip.includes('output $1.2/$0.6'))
await m.unmount()
})
test('the zh locale localizes labels and prices the cost in CNY', async () => {
const m = await mount(h(StatsContextZh, {
counts: { turns: 1, steps: 1, injects: 0, compactions: 1, prunes: 0 },
usage: USAGE,
cost: COST,
locale: 'zh',
}))
assert.ok(text(m.container).includes('上下文统计'))
const { labels, values } = cells(m.container)
assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '缓存命中', '预估费用?'])
assert.deepEqual(values, ['1', '1', '0', '0', '66.6%', '¥2.00'])
await m.unmount()
})
})