summaryrefslogtreecommitdiffhomepage
path: root/src/core/chunks/reducer.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 02:41:37 +0900
committerAdam Malczewski <[email protected]>2026-06-07 02:41:37 +0900
commit1e936915b2c3725ca0a6a6e2fd0116ab551a71b6 (patch)
treea5b1134786c90a145daea8b8438525565d1439fc /src/core/chunks/reducer.test.ts
parent6a890e9be8ea4057e5d512f50e37da11d0a8aedf (diff)
downloaddispatch-web-1e936915b2c3725ca0a6a6e2fd0116ab551a71b6.tar.gz
dispatch-web-1e936915b2c3725ca0a6a6e2fd0116ab551a71b6.zip
fix: optimistic user message echo + tabs persistence
Bug 1 (sent message didn't appear until turn end): the transcript only folded assistant AgentEvents, so the user's own message showed only after turn-sealed resync. Add core/chunks appendUserMessage() (provisional user chunk, superseded on history sync) and call it in chat send() — the message now renders instantly. Bug 2 (tabs didn't persist on refresh): the app passed { storage: undefined } to createLocalStore, which the adapter treats as a no-op store, so nothing was saved. Default to globalThis.localStorage. Regression test exercises the non-injected path. Also updated app store tests for the echo (assistant-vs-user chunk filtering). Verified: svelte-check 0/0, vitest 288 (stable x2), biome clean, build ok.
Diffstat (limited to 'src/core/chunks/reducer.test.ts')
-rw-r--r--src/core/chunks/reducer.test.ts63
1 files changed, 62 insertions, 1 deletions
diff --git a/src/core/chunks/reducer.test.ts b/src/core/chunks/reducer.test.ts
index f83edb4..b7165e4 100644
--- a/src/core/chunks/reducer.test.ts
+++ b/src/core/chunks/reducer.test.ts
@@ -11,7 +11,7 @@ import type {
TurnUsageEvent,
} from "@dispatch/wire";
import { describe, expect, it } from "vitest";
-import { applyHistory, foldEvent, initialState } from "./reducer";
+import { appendUserMessage, applyHistory, foldEvent, initialState } from "./reducer";
import { selectChunks, selectMessages } from "./selectors";
const turnStart = (turnId: string): TurnStartEvent => ({
@@ -404,3 +404,64 @@ describe("selectMessages", () => {
expect(msgs[1]?.chunks[0]).toEqual({ type: "text", text: "a1a2" });
});
});
+
+describe("appendUserMessage", () => {
+ it("adds a provisional user text chunk", () => {
+ let s = initialState();
+ s = appendUserMessage(s, "hello from user");
+ const chunks = selectChunks(s);
+ expect(chunks).toHaveLength(1);
+ expect(chunks[0]?.seq).toBeNull();
+ expect(chunks[0]?.role).toBe("user");
+ expect(chunks[0]?.chunk).toEqual({ type: "text", text: "hello from user" });
+ expect(chunks[0]?.provisional).toBe(true);
+ });
+
+ it("selectMessages includes the optimistic user message", () => {
+ let s = initialState();
+ s = appendUserMessage(s, "what is 2+2?");
+ const msgs = selectMessages(s);
+ expect(msgs).toHaveLength(1);
+ expect(msgs[0]?.role).toBe("user");
+ expect(msgs[0]?.chunks).toHaveLength(1);
+ expect(msgs[0]?.chunks[0]).toEqual({ type: "text", text: "what is 2+2?" });
+ });
+
+ it("user echo then turn-sealed + applyHistory supersedes the provisional user chunk", () => {
+ let s = initialState();
+ s = appendUserMessage(s, "hi");
+ expect(selectChunks(s)).toHaveLength(1);
+
+ s = foldEvent(s, turnStart("t1"));
+ s = foldEvent(s, textDelta("t1", "hello back"));
+ s = foldEvent(s, turnSealed("t1"));
+ s = applyHistory(s, [
+ storedChunk(1, "user", { type: "text", text: "hi" }),
+ storedChunk(2, "assistant", { type: "text", text: "hello back" }),
+ ]);
+ const chunks = selectChunks(s);
+ expect(chunks).toHaveLength(2);
+ expect(chunks[0]?.seq).toBe(1);
+ expect(chunks[0]?.role).toBe("user");
+ expect(chunks[0]?.chunk).toEqual({ type: "text", text: "hi" });
+ expect(chunks[0]?.provisional).toBe(false);
+ expect(chunks[1]?.seq).toBe(2);
+ expect(chunks[1]?.role).toBe("assistant");
+ expect(chunks[1]?.provisional).toBe(false);
+ });
+
+ it("flushes accumulating chunk before appending user message", () => {
+ let s = initialState();
+ s = foldEvent(s, turnStart("t1"));
+ s = foldEvent(s, textDelta("t1", "partial"));
+ expect(s.accumulating).toEqual({ kind: "text", text: "partial" });
+
+ s = appendUserMessage(s, "user msg");
+ expect(s.accumulating).toBeNull();
+ expect(s.provisional).toHaveLength(2);
+ expect(s.provisional[0]?.role).toBe("assistant");
+ expect(s.provisional[0]?.chunk).toEqual({ type: "text", text: "partial" });
+ expect(s.provisional[1]?.role).toBe("user");
+ expect(s.provisional[1]?.chunk).toEqual({ type: "text", text: "user msg" });
+ });
+});