diff options
| author | Adam Malczewski <[email protected]> | 2026-10-19 17:16:48 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-19 17:16:48 +0900 |
| commit | 6122a90cf0c9f938945e684e19e74153b38e7b4b (patch) | |
| tree | e97523e61581de17b7cb8705f2913b4913c28aeb /src/features/chat/ui.test.ts | |
| parent | 01f9fb2a4ad6110def531ead16b370c818e8ee81 (diff) | |
| download | dispatch-web-6122a90cf0c9f938945e684e19e74153b38e7b4b.tar.gz dispatch-web-6122a90cf0c9f938945e684e19e74153b38e7b4b.zip | |
fix(chat): cache timer no longer stalls transcript rendering
The live CacheTimerBadge effect read the `current` state it wrote, so on a
real (advancing) clock it re-ran itself until Svelte threw
effect_update_depth_exceeded, aborting the reactive flush — turn data
rendered late. The effect now uses locals and never reads its own write.
Also: drop the timer icon (time only), and size step-line timers xs to
match the step cache badge (turn lines stay sm).
Diffstat (limited to 'src/features/chat/ui.test.ts')
| -rw-r--r-- | src/features/chat/ui.test.ts | 33 |
1 files changed, 33 insertions, 0 deletions
diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts index 6b7176d..10af76c 100644 --- a/src/features/chat/ui.test.ts +++ b/src/features/chat/ui.test.ts @@ -9,6 +9,7 @@ import { type TurnMetricsEntry, } from "../../core/metrics"; import type { ConvertedJpeg } from "./adapter/image-encode"; +import CacheTimerBadge from "./ui/CacheTimerBadge.svelte"; import ChatView from "./ui/ChatView.svelte"; import Composer from "./ui/Composer.svelte"; import ModelSelector from "./ui/ModelSelector.svelte"; @@ -1664,6 +1665,9 @@ describe("ChatView — cache timer", () => { }); const stamp = screen.getByTestId("cache-timer-stamp"); expect(stamp).toHaveTextContent("1:05"); + // Sized to match the step's (xs) cache badge. + expect(stamp).toHaveClass("badge-xs"); + expect(stamp).not.toHaveClass("badge-sm"); // Same line as the step's cache badge. expect(stamp.parentElement).toContainElement(screen.getByTestId("step-cache-badge")); expect(screen.queryByTestId("cache-timer-tools")).toBeNull(); @@ -1682,11 +1686,40 @@ describe("ChatView — cache timer", () => { }); const live = screen.getByTestId("cache-timer-live"); expect(live.closest(".chat-bubble")).toHaveTextContent(/turn 2/); + expect(live).toHaveClass("badge-sm"); const stamp = screen.getByTestId("cache-timer-stamp"); expect(stamp).toHaveTextContent("10+ min"); expect(stamp.closest(".chat-bubble")).toHaveTextContent(/turn 1/); }); + it("a live timer mounts without re-running its effect on a moving clock (no render stall)", async () => { + // Regression: the badge's effect once READ the `current` state it wrote, so + // with a real (always-advancing) clock it re-ran itself until Svelte threw + // `effect_update_depth_exceeded` — aborting the flush, so turn data showed late. + let calls = 0; + const now = () => { + calls++; + return 1_000_000 + calls; + }; + render(CacheTimerBadge, { props: { since: 1_000_000, now } }); + await new Promise((r) => setTimeout(r, 20)); + expect(calls).toBeLessThan(5); + expect(screen.getByTestId("cache-timer-live")).toHaveTextContent("0:00"); + }); + + it("renders the turn's metrics alongside a live turn timer", () => { + render(ChatView, { + props: { + chunks: exchange("t1", 1), + turnMetrics: [turn("t1")], + cacheTimer: state({ running: { kind: "turn", turnId: "t1", startedAt: Date.now() } }), + }, + }); + expect(screen.getByText(/turn 1 · 10 tok out/)).toBeInTheDocument(); + expect(screen.getByText("a-t1")).toBeInTheDocument(); + expect(screen.getByTestId("cache-timer-live")).toHaveTextContent(/^0:00$/); + }); + it("puts a SEEDED turn gap (no turnId) on the most recent turn's row", () => { render(ChatView, { props: { |
