diff options
| author | Adam Malczewski <[email protected]> | 2026-06-01 08:03:57 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-01 08:03:57 +0900 |
| commit | 97c1b40ead19cdfe54b9a7aeb2c0fdcc1c9653b1 (patch) | |
| tree | ca9cf8d2969a8d93aa161b64886b7d277c6900f2 | |
| parent | dbc3b36f6d94d719cb1a07074e5d74ce22d2fad3 (diff) | |
| download | dispatch-97c1b40ead19cdfe54b9a7aeb2c0fdcc1c9653b1.tar.gz dispatch-97c1b40ead19cdfe54b9a7aeb2c0fdcc1c9653b1.zip | |
test(queue): cover multi-message continuation collapse
Add a frontend store test (flagged by a Gemini review) that queues TWO
messages mid-turn and asserts they collapse into a single untagged
initiator row joined with "\n---\n" — matching the backend's joined user
turn — and that the next turn-start tags that single row. The prior test
only covered the single-message case, leaving the join logic structurally
correct but untested.
| -rw-r--r-- | packages/frontend/tests/chat-store.test.ts | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/packages/frontend/tests/chat-store.test.ts b/packages/frontend/tests/chat-store.test.ts index ea718eb..33a9f69 100644 --- a/packages/frontend/tests/chat-store.test.ts +++ b/packages/frontend/tests/chat-store.test.ts @@ -1542,6 +1542,53 @@ describe("tabStore — chunk-native eviction / pagination / reconcile", () => { ]); }); + it("collapses MULTIPLE continuation-consumed queued messages into one initiator row", async () => { + // The backend joins several drained queued messages into a SINGLE user + // turn (with a "\n---\n" separator). The frontend must mirror that: N + // optimistic `queued-` bubbles collapse into exactly ONE untagged user + // row carrying the joined text, which the next turn-start then tags. + const store = createTabStore(); + store.handleEvent({ + type: "tab-created", + id: "mc", + title: "MC", + keyId: null, + modelId: null, + parentTabId: null, + }); + store.handleEvent({ type: "turn-start", turnId: "turn-a", tabId: "mc" }); + store.handleEvent({ type: "text-delta", delta: "answer", tabId: "mc" }); + // Two follow-ups queued while turn-a streams. + store.handleEvent({ type: "message-queued", tabId: "mc", messageId: "q1", message: "first" }); + store.handleEvent({ type: "message-queued", tabId: "mc", messageId: "q2", message: "second" }); + let tab = store.tabs.find((t) => t.id === "mc"); + expect(tab?.live.filter((m) => m.id.startsWith("queued-")).length).toBe(2); + + // Turn ends; backend drains BOTH as one continuation. + store.handleEvent({ + type: "message-consumed", + tabId: "mc", + messageIds: ["q1", "q2"], + reason: "continuation", + }); + tab = store.tabs.find((t) => t.id === "mc"); + // Exactly one user row, untagged, with the joined text — no queued- bubbles left. + const userRows = tab?.live.filter((m) => m.role === "user") ?? []; + expect(userRows.length).toBe(1); + expect(userRows[0]?.id.startsWith("queued-")).toBe(false); + expect(userRows[0]?.turnId).toBeUndefined(); + const textChunk = userRows[0]?.chunks.find((c) => c.type === "text"); + expect(textChunk && textChunk.type === "text" ? textChunk.text : "").toBe("first\n---\nsecond"); + expect(tab?.queuedMessages.length).toBe(0); + + // The next turn-start tags that single collapsed row as its initiator. + store.handleEvent({ type: "turn-sealed", turnId: "turn-a", tabId: "mc" }); + store.handleEvent({ type: "turn-start", turnId: "turn-b", tabId: "mc" }); + tab = store.tabs.find((t) => t.id === "mc"); + const tagged = tab?.live.filter((m) => m.role === "user" && m.turnId === "turn-b") ?? []; + expect(tagged.length).toBe(1); + }); + it("preserves a concurrent newer turn when an earlier deferred reconcile flushes", async () => { const sealedA = [ chunkRow("ua", "c", 0, "turn-a", "user", "text", { text: "A?" }), |
