From 984f31c954b88f40bdd75bc2820e8371251eb979 Mon Sep 17 00:00:00 2001 From: bowenliang123 Date: Tue, 6 Oct 2026 21:36:06 +0800 Subject: [PATCH] refactor(stats): drop the cache-hit cell from the Context Stats card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token card's corner already carries the session's cache-hit share (ef72a7b), so the Context Stats board duplicated it. The grid is now six cells: turns, steps, human inputs, live tool calls, and the two cost cells. The removal takes its dead plumbing with it — the card drops its `usage` prop, and `cacheHitPercent` loses the `decimals` override that existed only for this cell's one-decimal display. The README rows and the card's screenshot move the figure to the token card. --- README.md | 4 +- docs/context-stats.png | Bin 4773 -> 19625 bytes src/client/components/contextView.tsx | 4 +- src/client/components/statsContext.tsx | 36 ++----- src/client/format.ts | 16 ++- src/client/services.ts | 8 +- src/client/styles/stats.css | 2 +- tests/client/components/contextView.spec.ts | 6 +- tests/client/components/statsContext.spec.ts | 104 ++++++------------- tests/client/format.spec.ts | 7 -- 10 files changed, 63 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 9f4d0bc..8bbcd6e 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ Open any session and click the **Context / 上下文** tab: | Card | The question it answers | | --- | --- | -| **Context Stats** | Turns, steps, human inputs, live tool calls, the session's cache-hit rate — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). | -| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring. | +| **Context Stats** | Turns, steps, human inputs, live tool calls — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). | +| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring, and the session's cache-hit rate in the corner. | | **Timing Stats** | How active time split across model calls, tool runs, and overhead. | | **Current Context** | What's in the window *right now*. | | **Context Trend** | Every request's size — and its story. | diff --git a/docs/context-stats.png b/docs/context-stats.png index 60c4186d13550e5903794e20336d7bad16dadd56..1e91c990b526c1f51ca1c328a68d4b739f1258d5 100644 GIT binary patch literal 19625 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

; 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 literal 4773 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

64YiOsNy4-ZWNJ)7u?J_{XoDDy|V*z zLEr1v7#jFab7#2hPaI&K^3h!Bs-W+? zF?1D3W7GQag$TpQ$bNi|x;_A+$3Vx>#v^W=Ew3-JwTpQ!xOCl6;d{-o(b#tp1+?SX zogxDS@A|oAw(a`U zlAZb#0AvchCA%;O@Y)9JmY&Mt_0}zl35~~sB`8;knZLKowPT*oX_W3vd2@X6SZDk{BG|c?o zsdA>ydd~}){dujG!!YR;nRT)FT`m9s0Dvq$+J1(!fZaY)00000NkvXXu0mjfY3*#X diff --git a/src/client/components/contextView.tsx b/src/client/components/contextView.tsx index bc6118b..7398ad5 100644 --- a/src/client/components/contextView.tsx +++ b/src/client/components/contextView.tsx @@ -94,7 +94,7 @@ export function makeContextView( // no longer mirrors it. Absent → derived fallback. const pressure = projectionOf(props, 'contextPressure', contextPressureOf) // Official token-meter `tokenUsage` projection — the same data the chat stats line below the input box reads for its '缓存命中' figure, so - // the stats board's cache-hit cell reuses it verbatim; absent → the cell drops to a dash instead of estimating. + // the Token card's figures reuse it verbatim; absent → the card drops them instead of estimating. const usage = projectionOf(props, 'tokenUsage', tokenUsageOf) // Official token-meter `contextBreakdown` projection — the exact rows the chat ring's click-open panel shows, so the overview legend // reads identically by construction; absent → the fold's own same-estimator sums inside headlineOf. @@ -591,7 +591,7 @@ export function makeContextView( pair in a narrow pane at the cards' min-width floors. */} {inSidebar ? null : (

-
diff --git a/src/client/components/statsContext.tsx b/src/client/components/statsContext.tsx index 03eaf14..223ed96 100644 --- a/src/client/components/statsContext.tsx +++ b/src/client/components/statsContext.tsx @@ -1,16 +1,15 @@ /** * The Context card: what the session's context IS and how it evolved — a - * seven-cell grid of the session's shape (turns / steps / human inputs / - * live tool calls), the whole-session cache-hit rate, and the cost estimate - * at two scopes: the family total (the current agent plus every subagent - * session) and the subagents' own share. + * six-cell grid of the session's shape (turns / steps / human inputs / + * live tool calls) and the cost estimate at two scopes: the family total + * (the current agent plus every subagent session) and the subagents' own + * share. * Count figures only: nothing here is part of a spendable whole, so no pie — - * proportions live in the composition card, and the context-event tallies - * live on the events card's kind filters (contextView.tsx). The cache-hit - * cell reads the official `tokenUsage` projection — the same source and - * formula as the harness chat stats line under the composer, shown with one - * decimal — and dashes until a provider reports usage. The cost cells price - * the host-folded cumulative billed totals (complete session logs, never + * proportions live in the composition card, the context-event tallies live + * on the events card's kind filters (contextView.tsx), and the token figures + * (the session's cache-hit share among them) live on the Token card. The + * cost cells price the host-folded cumulative billed totals (complete + * session logs, never * trimmed; the subagents' usage folds out of the session-list snapshot, * `makeSubagentCost` below) from the models.dev price book (modelPrices.ts) * in the locale's currency; their hover bubbles (a '?' marker + styled DOM @@ -26,15 +25,14 @@ */ import { useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react' -import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts, TokenUsage } from '../../shared/types' +import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts } from '../../shared/types' import { estimateSessionCost, formatCost, formatPriceRate, mergeCostUsage, priceFaceOf, toCurrency } from '../cost' import type { CostCurrency, ModelBook, PriceFace } from '../cost' import { sessionsFaceOf, subagentCostFoldOf } from '../agentTree' import type { AgentHeads } from '../agentHeads' import { useSessionsSnapshot } from '../agentHeads' -import { cacheHitPercent } from '../format' import { useModelPrices } from '../modelPrices' -import { asRecord, numOf, type ClientCtx } from '../services' +import { asRecord, type ClientCtx } from '../services' import { isDeepSeekProvider } from '../../shared/providers' import type { ViewKit } from '../viewkit' @@ -139,8 +137,6 @@ export function makeStatsContext( humanInputs?: number /** Tool calls with a result live in the current context (absent on older hosts). */ toolCalls?: number - /** The official tokenUsage projection — the cache-hit cell's source (null until a provider reports). */ - usage: TokenUsage | null cost?: SessionCostUsage locale: string /** The current session id, anchoring the subagent-cost fold (absent = nothing to fold). */ @@ -151,7 +147,6 @@ export function makeStatsContext( counts: TimelineCounts humanInputs?: number toolCalls?: number - usage: TokenUsage | null cost?: SessionCostUsage locale: string sessionId?: string @@ -223,14 +218,6 @@ export function makeStatsContext( ...notes(subDeepseek), subUnpriced ? {t('stats.costUnavailable')} : null, ] - // The harness chat stats line's own formula, shown two decimals deep: - // prompt-side cache reads over the whole billed input (output excluded), - // dashed until reported. - const hit = props.usage === null ? null - : cacheHitPercent( - numOf(props.usage.cacheReadTokens), - numOf(props.usage.uncachedInputTokens) + numOf(props.usage.cacheReadTokens) + numOf(props.usage.cacheWriteTokens), - ) const cell = (label: string, value: string | number, tip?: ReactNode, href?: string): ReactElement => { // The framed cell body, as a div — or as an anchor opening the models.dev // provider listing in a new tab when the caller hands a destination (the @@ -269,7 +256,6 @@ export function makeStatsContext( {cell(t('stats.steps'), props.counts.steps)} {cell(t('stats.humanInputs'), props.humanInputs ?? 0, t('stats.humanInputsTip'))} {cell(t('stats.toolCalls'), props.toolCalls ?? 0)} - {cell(t('stats.cacheHit'), hit === null ? '—' : `${hit}%`, t('stats.cacheHitTip'))} {cell(t('stats.cost'), cost === null ? '—' : formatCost(cost, currency), costTip, costHref)} {cell(t('stats.subCost'), subCost === null ? '—' : formatCost(subCost, currency), subTip)} diff --git a/src/client/format.ts b/src/client/format.ts index f0846c7..cfa5fa1 100644 --- a/src/client/format.ts +++ b/src/client/format.ts @@ -20,17 +20,15 @@ export function fmtBytes(n: number | null | undefined): string { /** * Cache-hit share of billed prompt-side input (`reads` over `billed`), - * TRUNCATED to `decimals` places (cut, not round) — same formula as the - * harness chat stats line's '缓存命中' figure and the stats board's cell - * (which shows two decimals). Null when nothing was billed. The 1e-9 epsilon - * absorbs only float noise (integer token counts never sit that close to a - * boundary). + * TRUNCATED to two decimals (cut, not round) — same formula as the harness + * chat stats line's '缓存命中' figure and the token card's corner. Null when + * nothing was billed. The 1e-9 epsilon absorbs only float noise (integer + * token counts never sit that close to a boundary). */ -export function cacheHitPercent(reads: number, billed: number, decimals = 2): string | null { +export function cacheHitPercent(reads: number, billed: number): string | null { if (!(billed > 0)) return null - const factor = 10 ** decimals - const scaled = Math.trunc((reads / billed) * 100 * factor + 1e-9) - return `${Math.floor(scaled / factor)}.${String(scaled % factor).padStart(decimals, '0')}` + const scaled = Math.trunc((reads / billed) * 100 * 100 + 1e-9) + return `${Math.floor(scaled / 100)}.${String(scaled % 100).padStart(2, '0')}` } /** One shared formatter: building an Intl formatter per call costs ~50x the format itself (issue #116). */ diff --git a/src/client/services.ts b/src/client/services.ts index 7916c92..30e5336 100644 --- a/src/client/services.ts +++ b/src/client/services.ts @@ -545,10 +545,10 @@ export function contextBreakdownOf(value: unknown): ContextBreakdown | null { * Narrow a delivered projection value to the official token-meter * `tokenUsage` projection (durable cumulative provider usage). Absent key or * value = the meter's projection is not composed (or no request has reported - * usage yet) — callers drop the cache-hit cell to a dash. The wire schema is - * strict with all four buckets REQUIRED (dsh token-meter's projectionSchema), - * so a partial/corrupt value degrades the whole value to null instead of - * undercounting the billed total. + * usage yet) — callers drop the affected figures instead of estimating. The + * wire schema is strict with all four buckets REQUIRED (dsh token-meter's + * projectionSchema), so a partial/corrupt value degrades the whole value to + * null instead of undercounting the billed total. */ export function tokenUsageOf(value: unknown): TokenUsage | null { const data = asRecord(value) diff --git a/src/client/styles/stats.css b/src/client/styles/stats.css index 754a4e4..cef0389 100644 --- a/src/client/styles/stats.css +++ b/src/client/styles/stats.css @@ -10,7 +10,7 @@ card it visually is, with the pointer as the only affordance. */ a.lc-stat { color: inherit; text-decoration: none; cursor: pointer; } a.lc-stat:hover { border-color: var(--dsw-alias-label-secondary); } -/* The 5-cell stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */ +/* The stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */ .lc-stats .lc-stat { padding: 6px 3px; } .lc-stats .lc-stat-q { position: absolute; top: 3px; right: 4px; margin: 0; } .lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } diff --git a/tests/client/components/contextView.spec.ts b/tests/client/components/contextView.spec.ts index 9715d84..d603a75 100644 --- a/tests/client/components/contextView.spec.ts +++ b/tests/client/components/contextView.spec.ts @@ -195,11 +195,11 @@ describe('ContextView — projection guards', () => { assert.ok(text(m.container).includes(DICT_EN['overview.used'])) // The Token card's center is the chat stats line's whole-session billed // total off the official tokenUsage projection (100 + 200 + 0 + 50), and - // the Context card's cache-hit cell shows the line's own rate two decimals - // deep (200 / 300, truncated). + // its corner shows the line's own cache-hit rate two decimals deep + // (200 / 300, truncated). const tokensCard = queryAll(m.container, '.lc-head > .lc-col-donut')[0] assert.equal(query(tokensCard, '.lc-donut-center b').textContent, '350') - assert.ok(text(m.container).includes('66.66%')) + assert.equal(query(tokensCard, '.lc-card-rate').textContent, 'Cache Hit 66.66%') assert.ok(text(m.container).includes('m-only')) await m.unmount() }) diff --git a/tests/client/components/statsContext.spec.ts b/tests/client/components/statsContext.spec.ts index 4333b7f..118fd7b 100644 --- a/tests/client/components/statsContext.spec.ts +++ b/tests/client/components/statsContext.spec.ts @@ -1,9 +1,8 @@ // StatsContext (src/client/components/statsContext.tsx) rendered with real -// React: the seven-cell grid — session shape with the whole-session -// human-input tally, the chat-line cache-hit cell with its whole-session -// share tip, the family-scope priced cost cell (current agent + subagents) -// with its per-model rate tooltip, and the subagents' own share — in both -// locales, against an injected model-price +// React: the six-cell grid — session shape with the whole-session +// human-input tally, the family-scope priced cost cell (current agent + +// subagents) with its per-model rate tooltip, and the subagents' own share — +// in both locales, against an injected model-price // book (the store never reaches the network). 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 @@ -15,7 +14,7 @@ import { afterEach, describe, test, vi, beforeEach } from 'vitest' import { countsOfRecords, makeStatsContext, makeSubagentCost } from '../../../src/client/components/statsContext' import { makeAgentHeads } from '../../../src/client/agentHeads' import { resetModelPrices, setModelPricesLoader } from '../../../src/client/modelPrices' -import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TokenUsage } from '../../../src/shared/types' +import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage } from '../../../src/shared/types' import { TestClientCtx, asClientCtx } from '../helpers/harness' import { flush, makeKit, mount, queryAll, text } from '../helpers/kit' @@ -34,8 +33,6 @@ const PROVIDERS = { const COST: SessionCostUsage = { 'deepseek-official': { 'deepseek-v4-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } }, } -// Prompt-side billed input 300 (100 uncached + 200 read) → hit 66.66% truncated. -const USAGE: TokenUsage = { uncachedInputTokens: 100, outputTokens: 50, cacheReadTokens: 200, cacheWriteTokens: 0 } function req(turn?: number): RequestRecord { return { @@ -82,60 +79,46 @@ describe('countsOfRecords (the inline generation derivation)', () => { }) describe('StatsContext', () => { - test('folds the seven-cell grid: shape stats, the cache-hit cell, and the two cost cells', async () => { + test('folds the six-cell grid: the shape stats and the two cost cells', 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', })) await flush() assert.ok(text(m.container).includes('Context Stats')) const { labels, values } = cells(m.container) - assert.equal(labels.length, 7) - assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cache Hit?', 'Cost?', 'Subagent Cost?']) + assert.equal(labels.length, 6) + assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cost?', 'Subagent Cost?']) // 1M uncached input at the doubled peak miss rate (2 × $0.15); no subagent usage → the sub cell dashes. - assert.deepEqual(values, ['3', '4', '7', '3', '66.66%', '$0.30', '—']) + assert.deepEqual(values, ['3', '4', '7', '3', '$0.30', '—']) await m.unmount() }) - test('absent counters, usage, and cost degrade to zeros and the dash', async () => { + test('absent counters 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', })) await flush() - assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—', '—']) + 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', - })) - await flush() - assert.deepEqual(cells(m2.container).values, ['0', '0', '0', '0', '—', '—', '—']) - await m2.unmount() }) test('the cost bubble lists the billed models with their billed 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', })) await flush() - assert.equal(queryAll(m.container, '.lc-stat-tip').length, 4) - assert.equal(queryAll(m.container, '.lc-stat-q').length, 4) + assert.equal(queryAll(m.container, '.lc-stat-tip').length, 3) + assert.equal(queryAll(m.container, '.lc-stat-q').length, 3) 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') - assert.ok(tips[1].includes('Cumulative cache-read'), 'the cache-hit tip names the whole-session share') - const costTip = tips[2] + const costTip = tips[1] assert.ok(costTip.includes('this agent and all its subagents'), 'the cost tip names the family scope') assert.ok(costTip.includes('Per-1M-token rates:')) assert.ok(costTip.includes('Priced as listed on models.dev for deepseek · deepseek-v4-flash.'), 'the listing line names the resolved registry face') @@ -146,19 +129,18 @@ describe('StatsContext', () => { assert.ok(costTip.includes('Output $0.60')) assert.ok(costTip.includes('peak windows'), 'a DeepSeek session explains the peak/off-peak scheme') assert.ok(!costTip.includes('|'), 'no peak|off pairs — the footnotes carry the scheme') - assert.ok(tips[3].includes('every subagent session'), 'the sub cell explains its own scope') + assert.ok(tips[2].includes('every subagent session'), 'the sub cell explains its own scope') await m.unmount() }) test('a non-DeepSeek session never sees DeepSeek-specific notes', async () => { const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: { 'zai-coding-cn': { 'glm-5.3-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, locale: 'en', })) await flush() - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('Priced as listed on models.dev for zhipuai · glm-5.3-flash.')) assert.ok(!costTip.includes('DeepSeek'), 'the DeepSeek scheme note stays out of other providers’ bubbles') // glm-5.3-flash lists cache_write at 0 — the zero band drops from the table. @@ -170,7 +152,6 @@ describe('StatsContext', () => { test('the cost cell links to the models.dev provider listing when one provider priced the scope', async () => { const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: COST, locale: 'en', })) @@ -191,7 +172,6 @@ describe('StatsContext', () => { } const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: two, locale: 'en', })) @@ -207,12 +187,11 @@ describe('StatsContext', () => { } const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: two, locale: 'en', })) await flush() - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('for deepseek · deepseek-v4-flash.')) assert.ok(costTip.includes('for zhipuai · glm-5.3-flash.')) // 1M × $0.15 × 2 (the DeepSeek peak) + 2M × $0.075 = $0.45. @@ -223,18 +202,16 @@ describe('StatsContext', () => { test('the zh locale localizes labels and prices the cost in CNY at 1 CNY = 0.15 USD', async () => { const m = await mount(h(StatsContextZh, { counts: { turns: 1, steps: 1, injects: 0, compactions: 1, prunes: 0 }, - usage: USAGE, cost: COST, locale: 'zh', })) await flush() assert.ok(text(m.container).includes('上下文统计')) const { labels, values } = cells(m.container) - assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '缓存命中?', '费用?', '子 Agent 费用?']) + assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '费用?', '子 Agent 费用?']) // $0.30 / 0.15 = ¥2; the rates convert through the same fixed rate. - assert.deepEqual(values, ['1', '1', '0', '0', '66.66%', '¥2.00', '—']) - assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('整个会话累计'), 'the cache-hit tip localizes too') - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + assert.deepEqual(values, ['1', '1', '0', '0', '¥2.00', '—']) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('含当前智能体及所有子智能体'), 'the cost tip names the family scope too') assert.ok(costTip.includes('每百万 Token 价格')) assert.ok(costTip.includes('按 deepseek · deepseek-v4-flash 在 models.dev 的刊登价格如上。')) @@ -244,15 +221,14 @@ describe('StatsContext', () => { assert.ok(costTip.includes('未缓存输入 ¥1.00')) assert.ok(costTip.includes('缓存写入 ¥1.00')) assert.ok(costTip.includes('输出 ¥4.00')) - assert.ok(text(queryAll(m.container, '.lc-stat-tip')[3]).includes('子 Agent 会话'), 'the sub tip localizes too') + assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('子 Agent 会话'), 'the sub tip localizes too') await m.unmount() }) - test('usage with no book yet stays a dash until the fetch lands', async () => { + test('a book that has not landed yet keeps the cost cell dashed', async () => { setModelPricesLoader(() => new Promise(() => {})) const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: USAGE, cost: COST, locale: 'en', })) @@ -266,13 +242,12 @@ describe('StatsContext', () => { setModelPricesLoader(() => Promise.reject(new Error('down'))) const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: USAGE, cost: COST, locale: 'en', })) await flush() assert.ok(cells(m.container).values.at(-2) === '—') - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('unavailable')) assert.ok(!costTip.includes('Per-1M-token rates')) await m.unmount() @@ -289,14 +264,13 @@ describe('StatsContext', () => { } const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: split, locale: 'en', })) await flush() // 1M at the doubled $0.3 peak miss rate + 2M at the $0.15 off-peak (book) rate. assert.ok(cells(m.container).values.at(-2) === '$0.60') - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('Cache input $0.00'), 'the table lists the book rates once, whatever the billed buckets') assert.ok(costTip.includes('Uncached input $0.15')) assert.ok(costTip.includes('peak windows'), 'the peak-window footnote explains the bucket split') @@ -307,13 +281,12 @@ describe('StatsContext', () => { test('a session whose models the book cannot price notes the outage too', async () => { const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: USAGE, cost: { 'future-provider': { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } }, locale: 'en', })) await flush() assert.ok(cells(m.container).values.at(-2) === '—') - assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('unavailable')) + assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('unavailable')) await m.unmount() }) @@ -329,12 +302,11 @@ describe('StatsContext', () => { } as unknown as SessionCostUsage const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: USAGE, cost: hostile, locale: 'en', })) await flush() - const costTip = text(queryAll(m.container, '.lc-stat-tip')[2]) + const costTip = text(queryAll(m.container, '.lc-stat-tip')[1]) assert.ok(costTip.includes('glm-5.3-flash')) assert.ok(!costTip.includes('junk')) // No row may show the peak|off-peak pair — that is DeepSeek's alone. @@ -355,7 +327,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { const Stats = makeStatsContext(kit, () => SUB) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: COST, locale: 'en', })) @@ -364,10 +335,10 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { assert.deepEqual(cells(m.container).values.slice(-2), ['$0.45', '$0.15']) const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el)) // The rate table covers BOTH sides' models in either tip. - assert.ok(tips[2].includes('for deepseek · deepseek-v4-flash.')) - assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.')) - assert.ok(tips[3].includes('every subagent session')) - assert.ok(tips[3].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table') + assert.ok(tips[1].includes('for deepseek · deepseek-v4-flash.')) + assert.ok(tips[1].includes('for zhipuai · glm-5.3-flash.')) + assert.ok(tips[2].includes('every subagent session')) + assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table') await m.unmount() }) @@ -375,7 +346,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { const Stats = makeStatsContext(kit, () => SUB) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', })) await flush() @@ -390,15 +360,14 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { const Stats = makeStatsContext(kit, () => deepseekSub) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', })) await flush() // The off bucket bills at book: 1M × $0.15. assert.deepEqual(cells(m.container).values.slice(-2), ['$0.15', '$0.15']) const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el)) - assert.ok(tips[2].includes('peak windows')) - assert.ok(tips[3].includes('peak windows'), 'the sub tip explains the scheme its own figure rides') + assert.ok(tips[1].includes('peak windows')) + assert.ok(tips[2].includes('peak windows'), 'the sub tip explains the scheme its own figure rides') await m.unmount() }) @@ -406,7 +375,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { const Stats = makeStatsContext(kit, () => ({ future: { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } })) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, cost: COST, locale: 'en', })) @@ -416,14 +384,13 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => { // total — the sub cell dashes. assert.deepEqual(cells(m.container).values.slice(-2), ['$0.30', '—']) const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el)) - assert.ok(tips[3].includes('unavailable')) + assert.ok(tips[2].includes('unavailable')) await m.unmount() }) test('a null seat keeps both cells dashed with no outage notes', async () => { const m = await mount(h(StatsContext, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', })) await flush() @@ -498,7 +465,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () = const Stats = makeStatsContext(kit, seat) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', sessionId: 'root', })) @@ -552,7 +518,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () = const Stats = makeStatsContext(kit, seat) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', sessionId: 'root', })) @@ -588,7 +553,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () = const Stats = makeStatsContext(kit, seat) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', sessionId: 'root', })) @@ -614,7 +578,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () = const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx)))) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', sessionId: 'root', })) @@ -634,7 +597,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () = const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx)))) const m = await mount(h(Stats, { counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 }, - usage: null, locale: 'en', })) await flush() diff --git a/tests/client/format.spec.ts b/tests/client/format.spec.ts index a1e0746..fe68330 100644 --- a/tests/client/format.spec.ts +++ b/tests/client/format.spec.ts @@ -60,13 +60,6 @@ describe('cacheHitPercent', () => { assert.equal(cacheHitPercent(82499, 83000), '99.39') assert.equal(cacheHitPercent(83000, 83000), '100.00') }) - - test('the stats board cell truncates to one decimal by the same formula', () => { - assert.equal(cacheHitPercent(2, 3, 1), '66.6') - assert.equal(cacheHitPercent(1, 3, 1), '33.3') - assert.equal(cacheHitPercent(82499, 83000, 1), '99.3') - assert.equal(cacheHitPercent(83000, 83000, 1), '100.0') - }) }) describe('fmtTime', () => {