summaryrefslogtreecommitdiffhomepage
path: root/packages/message-queue/src/pure.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 02:08:44 +0900
committerAdam Malczewski <[email protected]>2026-06-21 02:08:44 +0900
commitba47df37f0c89bff4f0c3dd7d0bc2ef6c8062b92 (patch)
tree21d87eb847cd526a506cf274467fd1359f349705 /packages/message-queue/src/pure.test.ts
parent75032313a96856a932c109efbbe6b6a7eb782222 (diff)
downloaddispatch-ba47df37f0c89bff4f0c3dd7d0bc2ef6c8062b92.tar.gz
dispatch-ba47df37f0c89bff4f0c3dd7d0bc2ef6c8062b92.zip
feat(message-queue): per-conversation queue + steering injection
A per-conversation message queue (new message-queue extension) holds user messages enqueued while a turn generates; delivered mid-turn as steering at the tool-result boundary (or carried to a new turn if no tool call fires). - kernel: RunTurnInput.drainSteering callback (generic; kernel stays pure) - wire 0.7.0->0.8.0: QueuedMessage, QueuePayload, TurnSteeringEvent (additive) - transport-contract 0.11.0->0.12.0: POST /conversations/:id/queue + chat.queue WS op - message-queue ext: queue state + per-conversation custom surface (rendererId message-queue) - session-orchestrator: enqueue facade + drainSteering wiring + post-seal carry - transport-http/ws: queue endpoint + chat.queue op (fixes WsClientMessage exhaustive switch) - host-bin: register message-queue 1043 vitest + 199 transport bun pass; tsc/biome clean; boot smoke clean. FE courier: frontend-message-queue-handoff.md.
Diffstat (limited to 'packages/message-queue/src/pure.test.ts')
-rw-r--r--packages/message-queue/src/pure.test.ts144
1 files changed, 144 insertions, 0 deletions
diff --git a/packages/message-queue/src/pure.test.ts b/packages/message-queue/src/pure.test.ts
new file mode 100644
index 0000000..ff1a08f
--- /dev/null
+++ b/packages/message-queue/src/pure.test.ts
@@ -0,0 +1,144 @@
+import type { QueuedMessage } from "@dispatch/wire";
+import { describe, expect, it } from "vitest";
+import {
+ buildQueueSpec,
+ combine,
+ drain,
+ enqueue,
+ getQueue,
+ MESSAGE_QUEUE_RENDERER_ID,
+ MESSAGE_QUEUE_SURFACE_ID,
+ type MessageQueueState,
+ type QueueDeps,
+} from "./pure.js";
+
+function makeDeps(): QueueDeps {
+ let idCounter = 0;
+ let now = 1_000;
+ return {
+ id: () => `id-${++idCounter}`,
+ now: () => (now += 10),
+ };
+}
+
+describe("enqueue", () => {
+ it("enqueue appends + returns snapshot with the new message (id unique, queuedAt set)", () => {
+ const state: MessageQueueState = new Map();
+ const deps = makeDeps();
+
+ const first = enqueue(state, "c1", "first", deps);
+ expect(first).toHaveLength(1);
+ const firstMsg = first[0];
+ if (firstMsg === undefined) throw new Error("expected a message");
+ expect(firstMsg.id).toBe("id-1");
+ expect(firstMsg.text).toBe("first");
+ expect(firstMsg.queuedAt).toBe(1_010);
+
+ const second = enqueue(state, "c1", "second", deps);
+ expect(second).toHaveLength(2);
+ const secondMsg = second[1];
+ if (secondMsg === undefined) throw new Error("expected a message");
+ expect(secondMsg.id).toBe("id-2"); // unique per message
+ expect(secondMsg.text).toBe("second");
+ expect(secondMsg.queuedAt).toBe(1_020);
+
+ // a separate conversation does not share state
+ const other = enqueue(state, "c2", "other", deps);
+ expect(other).toHaveLength(1);
+ expect(getQueue(state, "c2")).toHaveLength(1);
+ });
+});
+
+describe("getQueue", () => {
+ it("getQueue returns current snapshot; empty array when none", () => {
+ const state: MessageQueueState = new Map();
+ const deps = makeDeps();
+
+ expect(getQueue(state, "unknown")).toEqual([]);
+
+ enqueue(state, "c1", "x", deps);
+ enqueue(state, "c1", "y", deps);
+ const snap = getQueue(state, "c1");
+ expect(snap.map((m) => m.text)).toEqual(["x", "y"]);
+
+ // returns a COPY — mutating it does not affect live state
+ snap.push({ id: "evil", text: "mutate", queuedAt: 0 });
+ expect(getQueue(state, "c1")).toHaveLength(2);
+ });
+});
+
+describe("drain", () => {
+ it("drain returns all + clears; second drain returns []", () => {
+ const state: MessageQueueState = new Map();
+ const deps = makeDeps();
+ enqueue(state, "c1", "a", deps);
+ enqueue(state, "c1", "b", deps);
+
+ const drained = drain(state, "c1");
+ expect(drained.map((m) => m.text)).toEqual(["a", "b"]);
+
+ // cleared
+ expect(getQueue(state, "c1")).toEqual([]);
+ expect(drain(state, "c1")).toEqual([]);
+ });
+
+ it("drain on an empty queue returns [] and is a no-op", () => {
+ const state: MessageQueueState = new Map();
+
+ // never had a queue
+ expect(drain(state, "c1")).toEqual([]);
+ expect(getQueue(state, "c1")).toEqual([]);
+
+ // and after a drain that emptied it, draining again is a no-op
+ const deps = makeDeps();
+ enqueue(state, "c1", "once", deps);
+ drain(state, "c1");
+ expect(drain(state, "c1")).toEqual([]);
+ expect(getQueue(state, "c1")).toEqual([]);
+ });
+});
+
+describe("combine", () => {
+ it("combine joins texts with blank-line separator", () => {
+ const msgs: QueuedMessage[] = [
+ { id: "1", text: "alpha", queuedAt: 1 },
+ { id: "2", text: "beta", queuedAt: 2 },
+ ];
+ expect(combine(msgs)).toBe("alpha\n\nbeta");
+ expect(combine([])).toBe("");
+ expect(combine([{ id: "1", text: "solo", queuedAt: 1 }])).toBe("solo");
+ });
+});
+
+describe("buildQueueSpec", () => {
+ it("renders a custom field with the queue snapshot", () => {
+ const msgs: QueuedMessage[] = [
+ { id: "1", text: "a", queuedAt: 1 },
+ { id: "2", text: "b", queuedAt: 2 },
+ ];
+ const spec = buildQueueSpec(msgs);
+ expect(spec.id).toBe(MESSAGE_QUEUE_SURFACE_ID);
+ expect(spec.region).toBe("side");
+ expect(spec.title).toBe("Message Queue");
+ expect(spec.fields).toHaveLength(1);
+
+ const field = spec.fields[0];
+ if (field === undefined || field.kind !== "custom") {
+ throw new Error("expected a custom field");
+ }
+ expect(field.rendererId).toBe(MESSAGE_QUEUE_RENDERER_ID);
+ const payload = field.payload as { messages: readonly QueuedMessage[] };
+ expect(payload.messages).toHaveLength(2);
+ expect(payload.messages.map((m) => m.text)).toEqual(["a", "b"]);
+ });
+
+ it("renders an empty list for an empty queue", () => {
+ const spec = buildQueueSpec([]);
+ const field = spec.fields[0];
+ if (field === undefined || field.kind !== "custom") {
+ throw new Error("expected a custom field");
+ }
+ const payload = field.payload as { messages: readonly QueuedMessage[] };
+ expect(payload.messages).toEqual([]);
+ });
+});