summaryrefslogtreecommitdiffhomepage
path: root/src/features/surface-host/logic/message-queue.test.ts
blob: 93256b4a9403bf347d934dff760c222c5c598123 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import type { SurfaceSpec } from "@dispatch/ui-contract";
import type { QueuedMessage } from "@dispatch/wire";
import { describe, expect, it } from "vitest";
import {
  messageQueueCount,
  parseMessageQueuePayload,
  reconcileCancelledIds,
  selectVisibleMessages,
} from "./message-queue";

const msg = (id: string, text: string, queuedAt = 1_700_000_000_000): QueuedMessage => ({
  id,
  text,
  queuedAt,
});

describe("parseMessageQueuePayload", () => {
  it("parses a well-formed payload with messages", () => {
    const data = parseMessageQueuePayload({
      messages: [msg("m1", "steer left"), msg("m2", "actually, go right")],
    });
    expect(data).toEqual({
      messages: [msg("m1", "steer left"), msg("m2", "actually, go right")],
    });
  });

  it("parses an empty-messages payload (queue is empty)", () => {
    expect(parseMessageQueuePayload({ messages: [] })).toEqual({ messages: [] });
  });

  it("preserves message order", () => {
    const data = parseMessageQueuePayload({
      messages: [msg("a", "first"), msg("b", "second"), msg("c", "third")],
    });
    expect(data?.messages.map((m) => m.id)).toEqual(["a", "b", "c"]);
  });

  it.each([
    ["null", null],
    ["a number", 7],
    ["a string", "nope"],
    ["missing messages key", { foo: [] }],
    ["messages not an array", { messages: "x" }],
    ["entry not an object", { messages: ["x"] }],
    ["entry missing id", { messages: [{ text: "x", queuedAt: 1 }] }],
    ["entry with non-string id", { messages: [{ id: 1, text: "x", queuedAt: 1 }] }],
    ["entry missing text", { messages: [{ id: "m1", queuedAt: 1 }] }],
    ["entry with non-string text", { messages: [{ id: "m1", text: 1, queuedAt: 1 }] }],
    ["entry missing queuedAt", { messages: [{ id: "m1", text: "x" }] }],
    ["entry with non-finite queuedAt", { messages: [msg("m1", "x", Number.NaN)] }],
  ])("returns null for invalid payload: %s", (_label, payload) => {
    expect(parseMessageQueuePayload(payload)).toBeNull();
  });
});

describe("selectVisibleMessages", () => {
  it("returns the snapshot unchanged when nothing is cancelled", () => {
    const messages = [msg("m1", "a"), msg("m2", "b")];
    expect(selectVisibleMessages(messages, new Set())).toBe(messages);
  });

  it("hides the optimistically-cancelled row", () => {
    const messages = [msg("m1", "a"), msg("m2", "b"), msg("m3", "c")];
    expect(selectVisibleMessages(messages, new Set(["m2"])).map((m) => m.id)).toEqual(["m1", "m3"]);
  });

  it("hides multiple cancelled rows", () => {
    const messages = [msg("m1", "a"), msg("m2", "b"), msg("m3", "c")];
    expect(selectVisibleMessages(messages, new Set(["m1", "m3"])).map((m) => m.id)).toEqual(["m2"]);
  });

  it("tolerates a cancelled id not present in the snapshot (no-op)", () => {
    const messages = [msg("m1", "a")];
    expect(selectVisibleMessages(messages, new Set(["ghost"])).map((m) => m.id)).toEqual(["m1"]);
  });
});

describe("reconcileCancelledIds", () => {
  it("returns an empty set when nothing was cancelled", () => {
    const result = reconcileCancelledIds([msg("m1", "a")], new Set());
    expect(result.size).toBe(0);
  });

  it("drops ids the surface confirmed gone (no longer queued)", () => {
    // m1 still queued (cancel pending), m2 confirmed gone (left the snapshot).
    const messages = [msg("m1", "a")];
    const result = reconcileCancelledIds(messages, new Set(["m1", "m2"]));
    expect([...result]).toEqual(["m1"]);
  });

  it("returns the SAME set identity when nothing changed (no spurious cycle)", () => {
    const messages = [msg("m1", "a"), msg("m2", "b")];
    const cancelled = new Set(["m1", "m2"]);
    expect(reconcileCancelledIds(messages, cancelled)).toBe(cancelled);
  });

  it("returns an empty set when all cancels were confirmed", () => {
    const messages = [msg("m1", "a")];
    expect(reconcileCancelledIds(messages, new Set(["m2", "m3"])).size).toBe(0);
  });

  it("keeps a still-queued cancelled id (cancel still pending)", () => {
    const messages = [msg("m1", "a"), msg("m2", "b")];
    const result = reconcileCancelledIds(messages, new Set(["m2"]));
    expect([...result]).toEqual(["m2"]);
  });
});

describe("messageQueueCount", () => {
  const spec = (payload: unknown): SurfaceSpec => ({
    id: "message-queue",
    region: "inline",
    title: "Message queue",
    fields: [{ kind: "custom", rendererId: "message-queue", payload }],
  });

  it("counts messages, including image-only input", () => {
    expect(messageQueueCount(spec({ messages: [msg("m1", ""), msg("m2", "next")] }))).toBe(2);
  });

  it("distinguishes an empty queue from an unavailable snapshot", () => {
    expect(messageQueueCount(spec({ messages: [] }))).toBe(0);
    expect(messageQueueCount(null)).toBeUndefined();
  });

  it("returns unknown for missing fields or a different renderer", () => {
    expect(messageQueueCount({ ...spec(null), fields: [] })).toBeUndefined();
    expect(
      messageQueueCount({
        ...spec(null),
        fields: [{ kind: "custom", rendererId: "other", payload: { messages: [msg("m1", "x")] } }],
      }),
    ).toBeUndefined();
  });

  it("returns unknown for malformed payloads", () => {
    expect(messageQueueCount(spec({ messages: "nope" }))).toBeUndefined();
    expect(messageQueueCount(spec({ messages: [{ id: "m1" }] }))).toBeUndefined();
  });
});