import type { SurfaceSpec } from "@dispatch/ui-contract"; import type { QueuedMessage, QueuePayload } from "@dispatch/wire"; import { describe, expect, it } from "vitest"; import { buildQueueSpec, combine } from "./pure.js"; import { createMessageQueueService, type MessageQueueDeps } from "./service.js"; interface Recorder extends MessageQueueDeps { readonly calls: { value: number }; } function makeDeps(): Recorder { let idCounter = 0; let now = 1_000; const calls = { value: 0 }; return { id: () => `q-${++idCounter}`, now: () => (now += 10), notify: () => { calls.value += 1; }, calls, }; } function queueMessages(spec: SurfaceSpec): readonly QueuedMessage[] { const field = spec.fields[0]; if (field === undefined || field.kind !== "custom") { throw new Error("expected a custom field"); } return (field.payload as QueuePayload).messages; } describe("message-queue service", () => { it("enqueue pushes a surface update (snapshot grew)", () => { const deps = makeDeps(); const svc = createMessageQueueService(deps); expect(deps.calls.value).toBe(0); const snapshot = svc.enqueue("c1", "hello"); expect(deps.calls.value).toBe(1); // surface update pushed expect(snapshot).toHaveLength(1); const first = snapshot[0]; if (first === undefined) throw new Error("expected a message"); expect(first.text).toBe("hello"); expect(typeof first.id).toBe("string"); expect(first.queuedAt).toBe(1_010); // the surface spec reflects the grown snapshot expect(queueMessages(buildQueueSpec(svc.getQueue("c1")))).toHaveLength(1); }); it("drain pushes a surface update (empty)", () => { const deps = makeDeps(); const svc = createMessageQueueService(deps); svc.enqueue("c1", "a"); svc.enqueue("c1", "b"); expect(deps.calls.value).toBe(2); // two enqueues notified const drained = svc.drain("c1"); expect(deps.calls.value).toBe(3); // drain pushed a surface update expect(drained).toHaveLength(2); expect(drained.map((m) => m.text)).toEqual(["a", "b"]); // cleared + surface now shows empty expect(svc.getQueue("c1")).toEqual([]); expect(queueMessages(buildQueueSpec(svc.getQueue("c1")))).toEqual([]); }); it("drain on empty does NOT push a surface update (already empty)", () => { const deps = makeDeps(); const svc = createMessageQueueService(deps); expect(svc.drain("c1")).toEqual([]); expect(deps.calls.value).toBe(0); // no notify — no change // after a real drain empties it, a further drain is a no-op (no notify) svc.enqueue("c1", "x"); expect(deps.calls.value).toBe(1); svc.drain("c1"); expect(deps.calls.value).toBe(2); svc.drain("c1"); expect(deps.calls.value).toBe(2); // unchanged — queue was already empty }); it("getQueue returns current snapshot; empty for unknown conversation", () => { const deps = makeDeps(); const svc = createMessageQueueService(deps); expect(svc.getQueue("nope")).toEqual([]); svc.enqueue("c1", "hi"); expect(svc.getQueue("c1")).toHaveLength(1); }); it("drain returns the raw QueuedMessage[] the orchestrator combines", () => { const deps = makeDeps(); const svc = createMessageQueueService(deps); svc.enqueue("c1", "alpha"); svc.enqueue("c1", "beta"); const drained = svc.drain("c1"); expect(combine(drained)).toBe("alpha\n\nbeta"); }); });