summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/store.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 18:41:27 +0900
committerAdam Malczewski <[email protected]>2026-06-07 18:41:27 +0900
commit9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2 (patch)
tree1fd5b4081736cf48c7b916abcdbeab267b4322d6 /src/features/chat/store.test.ts
parentdcf5a2993c9cbeb266f2629cf33fbc43f52c7ca8 (diff)
downloaddispatch-web-9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2.tar.gz
dispatch-web-9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2.zip
feat(chat): live turn metrics — telemetry reducer + rendering
Consume wire/transport-contract 0.3.0 (step-complete event + timing fields on usage/tool-result/done). Pure core/telemetry module: foldMetricEvent (reducer) + derived selectors (stepTps, turnTps, etc). TelemetryState is pure data, no active-turn tracking — consumers pass turnId to selectors. ChatStore wires foldMetricEvent into handleDelta and exposes telemetry + currentTurnId. ChatView shows step-metrics footer (time/TPS/tokens) on assistant text bubbles and durationMs badge on tool cards. New TurnSummary component renders turn-level stats (wall-clock, tokens, steps, TPS) in a DaisyUI stats block. Extended live-probe to verify telemetry events against bin/up (pending backend restart). 336 tests, typecheck 0, biome clean, build ok.
Diffstat (limited to 'src/features/chat/store.test.ts')
-rw-r--r--src/features/chat/store.test.ts46
1 files changed, 46 insertions, 0 deletions
diff --git a/src/features/chat/store.test.ts b/src/features/chat/store.test.ts
index 71781ac..347cdd7 100644
--- a/src/features/chat/store.test.ts
+++ b/src/features/chat/store.test.ts
@@ -393,6 +393,52 @@ describe("createChatStore", () => {
store.dispose();
});
+ it("folding step-complete and usage events populates telemetry", () => {
+ const transport = createFakeTransport();
+ const historySync = createFakeHistorySync();
+ const cache = createFakeCache();
+ const store = createChatStore({
+ conversationId: CONV_ID,
+ transport: transport.impl,
+ historySync: historySync.impl,
+ cache: cache.impl,
+ });
+
+ store.handleDelta(deltaEvent({ type: "turn-start", conversationId: CONV_ID, turnId: "t1" }));
+ store.handleDelta(
+ deltaEvent({
+ type: "step-complete",
+ conversationId: CONV_ID,
+ turnId: "t1",
+ stepId: "t1#0" as StepId,
+ ttftMs: 300,
+ decodeMs: 700,
+ genTotalMs: 1000,
+ }),
+ );
+ store.handleDelta(
+ deltaEvent({
+ type: "usage",
+ conversationId: CONV_ID,
+ turnId: "t1",
+ stepId: "t1#0" as StepId,
+ usage: { inputTokens: 50, outputTokens: 20 },
+ }),
+ );
+
+ const turn = store.telemetry.turns.get("t1");
+ expect(turn).toBeDefined();
+ expect(turn?.steps).toHaveLength(1);
+ const step = turn?.steps.find((s) => s.stepId === ("t1#0" as StepId));
+ expect(step).toBeDefined();
+ expect(step?.ttftMs).toBe(300);
+ expect(step?.decodeMs).toBe(700);
+ expect(step?.usage?.inputTokens).toBe(50);
+ expect(step?.usage?.outputTokens).toBe(20);
+
+ store.dispose();
+ });
+
it("handleDelta ignores a chat.delta for a different conversationId", () => {
const transport = createFakeTransport();
const historySync = createFakeHistorySync();