summaryrefslogtreecommitdiffhomepage
path: root/src/core/metrics/format.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-10 10:34:37 +0900
committerAdam Malczewski <[email protected]>2026-06-10 10:34:37 +0900
commit89ca80bac1e143a4ec5ba6e2e1d4998acce2553c (patch)
tree7af49f5fd2385c02153accb5fdb0a6c43ba37229 /src/core/metrics/format.test.ts
parentf8bf715abc8a89ec0c6370b40403c509b1ce2870 (diff)
downloaddispatch-web-89ca80bac1e143a4ec5ba6e2e1d4998acce2553c.tar.gz
dispatch-web-89ca80bac1e143a4ec5ba6e2e1d4998acce2553c.zip
feat(metrics): inline cache hit-rate badges (last turn + chat total)
Derive cache hit rate (cacheReadTokens / inputTokens) from data already folded in core/metrics — no backend/contract change. - core/metrics: computeCachePct + viewCacheRate (pct + success/warning/error level by 66/33 thresholds + isHit); thread a running cumulativeUsage onto each finalized turn-metrics row for the conversation total. - ChatView: render two labelled, colour-coded percentage badges in the turn-total bubble — "Last turn:" (that turn) and "Chat Total:" (cumulative). - Honour backend caveats: absent cache fields -> 0, divide-by-zero guarded, a legitimate 0% renders plainly (not "no data").
Diffstat (limited to 'src/core/metrics/format.test.ts')
-rw-r--r--src/core/metrics/format.test.ts54
1 files changed, 53 insertions, 1 deletions
diff --git a/src/core/metrics/format.test.ts b/src/core/metrics/format.test.ts
index 9881e50..77c5204 100644
--- a/src/core/metrics/format.test.ts
+++ b/src/core/metrics/format.test.ts
@@ -1,6 +1,12 @@
import type { StepId, StepMetrics, TurnMetrics } from "@dispatch/wire";
import { describe, expect, it } from "vitest";
-import { computeTps, viewStepMetrics, viewTurnMetrics } from "./format";
+import {
+ computeCachePct,
+ computeTps,
+ viewCacheRate,
+ viewStepMetrics,
+ viewTurnMetrics,
+} from "./format";
describe("computeTps", () => {
it("null when elapsed missing", () => {
@@ -197,3 +203,49 @@ describe("viewTurnMetrics", () => {
expect(view.tps).toBe("54 tok/s");
});
});
+
+describe("computeCachePct", () => {
+ it("is cacheReadTokens / inputTokens as a rounded percentage", () => {
+ expect(computeCachePct({ inputTokens: 2737, outputTokens: 10, cacheReadTokens: 2560 })).toBe(
+ 94,
+ );
+ expect(computeCachePct({ inputTokens: 2669, outputTokens: 10, cacheReadTokens: 384 })).toBe(14);
+ });
+
+ it("is 0 when cacheReadTokens absent (legitimate miss, not missing data)", () => {
+ expect(computeCachePct({ inputTokens: 1000, outputTokens: 50 })).toBe(0);
+ });
+
+ it("is 0 when there are no input tokens (guard divide-by-zero)", () => {
+ expect(computeCachePct({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 5 })).toBe(0);
+ });
+
+ it("clamps to 100 if read somehow exceeds input", () => {
+ expect(computeCachePct({ inputTokens: 100, outputTokens: 0, cacheReadTokens: 250 })).toBe(100);
+ });
+});
+
+describe("viewCacheRate", () => {
+ it("success level for a high hit rate (>= 66)", () => {
+ const v = viewCacheRate({ inputTokens: 100, outputTokens: 0, cacheReadTokens: 93 });
+ expect(v.pct).toBe(93);
+ expect(v.level).toBe("success");
+ expect(v.isHit).toBe(true);
+ });
+
+ it("warning level for a mid hit rate (33..65)", () => {
+ const v = viewCacheRate({ inputTokens: 100, outputTokens: 0, cacheReadTokens: 54 });
+ expect(v.pct).toBe(54);
+ expect(v.level).toBe("warning");
+ });
+
+ it("error level for a low hit rate (< 33), including a legitimate 0%", () => {
+ expect(viewCacheRate({ inputTokens: 100, outputTokens: 0, cacheReadTokens: 14 }).level).toBe(
+ "error",
+ );
+ const miss = viewCacheRate({ inputTokens: 1000, outputTokens: 50 });
+ expect(miss.pct).toBe(0);
+ expect(miss.level).toBe("error");
+ expect(miss.isHit).toBe(false);
+ });
+});