summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-01 08:03:57 +0900
committerAdam Malczewski <[email protected]>2026-06-01 08:03:57 +0900
commit97c1b40ead19cdfe54b9a7aeb2c0fdcc1c9653b1 (patch)
treeca9cf8d2969a8d93aa161b64886b7d277c6900f2
parentdbc3b36f6d94d719cb1a07074e5d74ce22d2fad3 (diff)
downloaddispatch-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.ts47
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?" }),