@runtypelabs/persona 4.17.1 → 4.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/chunk-3I2S3I5P.js +1 -0
  2. package/dist/chunk-76IELU7F.js +1 -0
  3. package/dist/chunk-EFJBUH5F.js +1 -0
  4. package/dist/chunk-PDPVORUF.js +1 -0
  5. package/dist/chunk-Q735PSZL.js +1 -0
  6. package/dist/chunk-VK6JOIYJ.js +1 -0
  7. package/dist/codegen.cjs +4 -4
  8. package/dist/codegen.js +4 -4
  9. package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
  10. package/dist/context-mentions.cjs +9 -8
  11. package/dist/context-mentions.d.cts +1212 -14
  12. package/dist/context-mentions.d.ts +1212 -14
  13. package/dist/context-mentions.js +9 -8
  14. package/dist/event-stream-view-4EZ7BGFK.js +1 -0
  15. package/dist/event-stream-view.cjs +129 -0
  16. package/dist/event-stream-view.d.cts +8384 -0
  17. package/dist/event-stream-view.d.ts +8384 -0
  18. package/dist/event-stream-view.js +129 -0
  19. package/dist/history-view-BD6OXLNA.js +898 -0
  20. package/dist/history-view.cjs +898 -0
  21. package/dist/history-view.d.cts +1461 -0
  22. package/dist/history-view.d.ts +1461 -0
  23. package/dist/history-view.js +898 -0
  24. package/dist/index.cjs +77 -77
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1987 -17
  27. package/dist/index.d.ts +1987 -17
  28. package/dist/index.global.js +67 -66
  29. package/dist/index.global.js.map +1 -1
  30. package/dist/index.js +77 -77
  31. package/dist/index.js.map +1 -1
  32. package/dist/launcher.global.js +5 -4
  33. package/dist/launcher.global.js.map +1 -1
  34. package/dist/plugin-kit.cjs +1 -1
  35. package/dist/plugin-kit.d.cts +0 -8
  36. package/dist/plugin-kit.d.ts +0 -8
  37. package/dist/plugin-kit.js +1 -1
  38. package/dist/smart-dom-reader.d.cts +1212 -14
  39. package/dist/smart-dom-reader.d.ts +1212 -14
  40. package/dist/theme-editor-preview.cjs +970 -73
  41. package/dist/theme-editor-preview.d.cts +1331 -15
  42. package/dist/theme-editor-preview.d.ts +1331 -15
  43. package/dist/theme-editor-preview.js +81 -81
  44. package/dist/theme-editor.cjs +7 -7
  45. package/dist/theme-editor.d.cts +1215 -15
  46. package/dist/theme-editor.d.ts +1215 -15
  47. package/dist/theme-editor.js +9 -9
  48. package/dist/theme-reference.cjs +1 -1
  49. package/dist/theme-reference.d.cts +226 -7
  50. package/dist/theme-reference.d.ts +226 -7
  51. package/dist/theme-reference.js +1 -1
  52. package/dist/widget.css +1 -1
  53. package/package.json +16 -4
  54. package/src/client-history-rest.test.ts +1034 -0
  55. package/src/client-visitor-history.test.ts +596 -0
  56. package/src/client.ts +1040 -24
  57. package/src/components/artifact-inline.ts +8 -1
  58. package/src/components/artifact-pane.test.ts +278 -1
  59. package/src/components/artifact-pane.ts +135 -67
  60. package/src/components/artifact-preview.test.ts +14 -1
  61. package/src/components/artifact-preview.ts +9 -0
  62. package/src/components/header-builder.ts +3 -0
  63. package/src/components/header-layouts.test.ts +171 -7
  64. package/src/components/header-layouts.ts +83 -53
  65. package/src/components/header-parts.test.ts +154 -1
  66. package/src/components/header-parts.ts +266 -138
  67. package/src/components/history-confirm-dialog.ts +177 -0
  68. package/src/components/history-shell-copy.ts +99 -0
  69. package/src/components/history-view/announcer.ts +40 -0
  70. package/src/components/history-view/copy.ts +105 -0
  71. package/src/components/history-view/css.ts +1078 -0
  72. package/src/components/history-view/grouping.test.ts +142 -0
  73. package/src/components/history-view/grouping.ts +142 -0
  74. package/src/components/history-view/icons.ts +73 -0
  75. package/src/components/history-view/rows.ts +341 -0
  76. package/src/components/history-view/state.ts +309 -0
  77. package/src/components/history-view.test.ts +2660 -0
  78. package/src/components/history-view.ts +1891 -0
  79. package/src/components/launcher-surface.test.ts +133 -0
  80. package/src/components/launcher.test.ts +18 -0
  81. package/src/components/launcher.ts +7 -7
  82. package/src/components/panel.ts +17 -7
  83. package/src/components/welcome.ts +72 -2
  84. package/src/defaults.ts +36 -3
  85. package/src/event-stream-view-bundle.test.ts +69 -0
  86. package/src/event-stream-view-entry.ts +19 -0
  87. package/src/event-stream-view-loader.ts +31 -0
  88. package/src/event-stream-view.ts +19 -0
  89. package/src/generated/runtype-openapi-contract.ts +1 -0
  90. package/src/history-render.ts +325 -0
  91. package/src/history-view-bundle.test.ts +78 -0
  92. package/src/history-view-entry.ts +41 -0
  93. package/src/history-view-loader.test.ts +30 -0
  94. package/src/history-view-loader.ts +32 -0
  95. package/src/history-view.ts +28 -0
  96. package/src/index-core.ts +50 -2
  97. package/src/index-global.ts +55 -0
  98. package/src/internal/demo-history-provider.test.ts +402 -0
  99. package/src/internal/demo-history-provider.ts +689 -0
  100. package/src/internal/history-provider-registry.ts +25 -0
  101. package/src/internal/history-provider.ts +150 -0
  102. package/src/internal/runtype-history-provider.test.ts +313 -0
  103. package/src/internal/runtype-history-provider.ts +240 -0
  104. package/src/plugin-kit.test.ts +16 -0
  105. package/src/plugin-kit.ts +13 -5
  106. package/src/plugins/types.ts +177 -1
  107. package/src/session-history.test.ts +1397 -0
  108. package/src/session.ts +1200 -26
  109. package/src/styles/widget-styles.test.ts +133 -0
  110. package/src/styles/widget.css +227 -8
  111. package/src/theme-editor/preview-utils.test.ts +19 -0
  112. package/src/theme-editor/preview-utils.ts +15 -1
  113. package/src/theme-editor/preview.ts +7 -1
  114. package/src/theme-editor/role-mappings.ts +4 -1
  115. package/src/theme-editor/sections.test.ts +168 -1
  116. package/src/theme-editor/sections.ts +136 -7
  117. package/src/theme-editor/webmcp/summary.ts +16 -2
  118. package/src/theme-reference.ts +20 -6
  119. package/src/types/theme.ts +213 -7
  120. package/src/types.ts +927 -7
  121. package/src/ui.artifact-rail-resize.test.ts +235 -0
  122. package/src/ui.attachments-drop.test.ts +1 -0
  123. package/src/ui.header-update-stability.test.ts +2 -2
  124. package/src/ui.history-chunk.test.ts +72 -0
  125. package/src/ui.history-config-provider.test.ts +318 -0
  126. package/src/ui.history-controller.test.ts +711 -0
  127. package/src/ui.history-lifecycle.test.ts +456 -0
  128. package/src/ui.history-render-hooks.test.ts +904 -0
  129. package/src/ui.history-shell.test.ts +1887 -0
  130. package/src/ui.rail-overlay.test.ts +573 -0
  131. package/src/ui.rail-resize.test.ts +284 -0
  132. package/src/ui.rebuild-composer.test.ts +21 -0
  133. package/src/ui.render-welcome.test.ts +42 -0
  134. package/src/ui.shortcuts.test.ts +166 -0
  135. package/src/ui.ts +3071 -240
  136. package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
  137. package/src/utils/artifact-file.ts +15 -0
  138. package/src/utils/artifact-gate.test.ts +4 -0
  139. package/src/utils/artifact-gate.ts +13 -17
  140. package/src/utils/artifact-resize.test.ts +28 -0
  141. package/src/utils/artifact-resize.ts +9 -2
  142. package/src/utils/buttons.ts +188 -1
  143. package/src/utils/config-merge.test.ts +91 -0
  144. package/src/utils/dropdown.ts +10 -1
  145. package/src/utils/history-messages.test.ts +284 -0
  146. package/src/utils/history-messages.ts +284 -0
  147. package/src/utils/icons.ts +3 -0
  148. package/src/utils/shortcuts.test.ts +288 -0
  149. package/src/utils/shortcuts.ts +221 -0
  150. package/src/utils/theme.test.ts +501 -4
  151. package/src/utils/theme.ts +42 -3
  152. package/src/utils/tokens.ts +153 -15
  153. package/src/utils/tooltip-timing.ts +4 -0
  154. package/src/utils/tooltip.test.ts +249 -7
  155. package/src/utils/tooltip.ts +146 -4
  156. package/src/utils/visitor-store.test.ts +425 -0
  157. package/src/utils/visitor-store.ts +337 -0
  158. package/dist/chunk-QJ4LV2KW.js +0 -1
@@ -0,0 +1,402 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createDemoHistoryProvider,
4
+ type DemoHistoryConversationSeed,
5
+ type DemoHistoryProvider,
6
+ } from "./demo-history-provider";
7
+ import {
8
+ HistoryProviderError,
9
+ isHistoryProviderError,
10
+ type HistoryOperationContext,
11
+ } from "./history-provider";
12
+ import { isHistoryDisplayUnavailable } from "../utils/history-messages";
13
+
14
+ const FIXED_NOW = Date.parse("2026-08-10T12:00:00.000Z");
15
+ const now = () => FIXED_NOW;
16
+ const browser: HistoryOperationContext = { scope: "browser" };
17
+ const verified: HistoryOperationContext = { scope: "verified-user" };
18
+
19
+ function provider(
20
+ options: Parameters<typeof createDemoHistoryProvider>[0] = {}
21
+ ): DemoHistoryProvider {
22
+ return createDemoHistoryProvider({ now, ...options });
23
+ }
24
+
25
+ function seedWith(messageCount: number): DemoHistoryConversationSeed[] {
26
+ return [
27
+ {
28
+ id: "c1",
29
+ title: "Paging",
30
+ targetId: "t1",
31
+ messages: Array.from({ length: messageCount }, (_, index) => ({
32
+ role: index % 2 === 0 ? ("user" as const) : ("assistant" as const),
33
+ content: `m${index + 1}`,
34
+ createdAt: new Date(FIXED_NOW - (messageCount - index) * 1000).toISOString(),
35
+ })),
36
+ },
37
+ ];
38
+ }
39
+
40
+ afterEach(() => {
41
+ vi.useRealTimers();
42
+ });
43
+
44
+ describe("capabilities", () => {
45
+ it("advertises browser scope only and omits the optional capabilities", () => {
46
+ const demo = provider();
47
+ expect(demo.capabilities.scopes).toEqual(["browser"]);
48
+ expect(demo.resetDevice).toBeUndefined();
49
+ expect(demo.subscribeAvailability).toBeUndefined();
50
+ });
51
+
52
+ it("rejects an unsupported scope locally", async () => {
53
+ const demo = provider();
54
+ await expect(demo.list({ context: verified })).rejects.toMatchObject({
55
+ code: "unsupported_scope",
56
+ });
57
+ });
58
+ });
59
+
60
+ describe("transcript paging", () => {
61
+ it("returns 25 / 25 / rest newest-page-first with oldest-first pages", async () => {
62
+ const demo = provider({ conversations: seedWith(52) });
63
+
64
+ const first = await demo.getPage("c1", { context: browser });
65
+ expect(first.messages).toHaveLength(25);
66
+ expect(first.messages[0].content).toBe("m28");
67
+ expect(first.messages[24].content).toBe("m52");
68
+ expect(first.nextCursor).not.toBeNull();
69
+ expect(first.summary.messageCount).toBe(52);
70
+ expect(first.conversationRevision).toBe(demo.getConversationRevision("c1"));
71
+
72
+ const second = await demo.getPage("c1", {
73
+ context: browser,
74
+ cursor: first.nextCursor as string,
75
+ });
76
+ expect(second.messages).toHaveLength(25);
77
+ expect(second.messages[0].content).toBe("m3");
78
+ expect(second.messages[24].content).toBe("m27");
79
+ expect(second.nextCursor).not.toBeNull();
80
+
81
+ const third = await demo.getPage("c1", {
82
+ context: browser,
83
+ cursor: second.nextCursor as string,
84
+ });
85
+ expect(third.messages.map((m) => m.content)).toEqual(["m1", "m2"]);
86
+ expect(third.nextCursor).toBeNull();
87
+ });
88
+
89
+ it("keeps a single page when the transcript fits and is stable across calls", async () => {
90
+ const demo = provider({ conversations: seedWith(7) });
91
+ const first = await demo.getPage("c1", { context: browser });
92
+ const again = await demo.getPage("c1", { context: browser });
93
+ expect(first.nextCursor).toBeNull();
94
+ expect(first.messages.map((m) => m.id)).toEqual(again.messages.map((m) => m.id));
95
+ expect(first.messages.map((m) => m.content)).toEqual([
96
+ "m1",
97
+ "m2",
98
+ "m3",
99
+ "m4",
100
+ "m5",
101
+ "m6",
102
+ "m7",
103
+ ]);
104
+ });
105
+
106
+ it("rejects an unknown conversation with not_found", async () => {
107
+ const demo = provider();
108
+ const error = await demo
109
+ .getPage("nope", { context: browser })
110
+ .catch((err: unknown) => err);
111
+ expect(isHistoryProviderError(error)).toBe(true);
112
+ expect((error as HistoryProviderError).code).toBe("not_found");
113
+ });
114
+
115
+ it("returns already-mapped messages including parts and the unavailable marker", async () => {
116
+ const demo = provider();
117
+ const refund = await demo.getPage("demo-conv-damaged-mug", { context: browser });
118
+ const oldest = await demo.getPage("demo-conv-damaged-mug", {
119
+ context: browser,
120
+ cursor: refund.nextCursor as string,
121
+ });
122
+ expect(oldest.messages[0].contentParts?.[1]?.type).toBe("image");
123
+ expect(oldest.messages[1].contentParts?.[1]?.type).toBe("file");
124
+
125
+ const bulk = await demo.getPage("demo-conv-bulk-order", { context: browser });
126
+ const withheld = bulk.messages.filter(isHistoryDisplayUnavailable);
127
+ expect(withheld).toHaveLength(1);
128
+ expect(withheld[0].content).toBe("");
129
+ });
130
+ });
131
+
132
+ describe("list paging", () => {
133
+ it("chains cursors in updatedAt-descending order", async () => {
134
+ const demo = provider();
135
+ const all = demo.getConversationIds();
136
+
137
+ const first = await demo.list({ context: browser, limit: 2 });
138
+ expect(first.items.map((item) => item.id)).toEqual(all.slice(0, 2));
139
+ expect(first.nextCursor).not.toBeNull();
140
+
141
+ const second = await demo.list({
142
+ context: browser,
143
+ limit: 2,
144
+ cursor: first.nextCursor as string,
145
+ });
146
+ expect(second.items.map((item) => item.id)).toEqual(all.slice(2, 4));
147
+
148
+ const third = await demo.list({
149
+ context: browser,
150
+ limit: 2,
151
+ cursor: second.nextCursor as string,
152
+ });
153
+ expect(third.items.map((item) => item.id)).toEqual(all.slice(4));
154
+ expect(third.nextCursor).toBeNull();
155
+ });
156
+
157
+ it("filters by targetId and carries previews plus message counts", async () => {
158
+ const demo = provider();
159
+ const filtered = await demo.list({ context: browser, targetId: "demo-support-agent" });
160
+ expect(filtered.items.map((item) => item.id)).toEqual(["demo-conv-gift-wrap"]);
161
+ expect(filtered.items[0].messageCount).toBe(2);
162
+ expect(filtered.items[0].preview).toBeTruthy();
163
+ expect(filtered.items[0].preview?.length).toBeLessThanOrEqual(140);
164
+ });
165
+ });
166
+
167
+ describe("delete semantics", () => {
168
+ it("delete removes exactly one conversation", async () => {
169
+ const demo = provider();
170
+ const before = demo.getConversationIds();
171
+ await demo.delete("demo-conv-bulk-order", { context: browser });
172
+ expect(demo.getConversationIds()).toEqual(
173
+ before.filter((id) => id !== "demo-conv-bulk-order")
174
+ );
175
+ await expect(
176
+ demo.delete("demo-conv-bulk-order", { context: browser })
177
+ ).rejects.toMatchObject({ code: "not_found" });
178
+ });
179
+
180
+ it("deleteAll respects the targetId filter", async () => {
181
+ const demo = provider();
182
+ const result = await demo.deleteAll({ context: browser, targetId: "demo-flow" });
183
+ expect(result.deleted).toBe(4);
184
+ expect(demo.getConversationIds()).toEqual(["demo-conv-gift-wrap"]);
185
+ });
186
+
187
+ it("deleteAll without a targetId clears the whole scope", async () => {
188
+ const demo = provider();
189
+ const result = await demo.deleteAll({ context: browser });
190
+ expect(result.deleted).toBe(5);
191
+ expect(demo.getConversationIds()).toEqual([]);
192
+ });
193
+
194
+ it("deleting the active conversation clears the active id", async () => {
195
+ const demo = provider();
196
+ const prepared = await demo.prepareOpen("demo-conv-order-status", { context: browser });
197
+ await prepared.commit();
198
+ await demo.delete("demo-conv-order-status", { context: browser });
199
+ expect(demo.getActiveConversationId()).toBeNull();
200
+ });
201
+ });
202
+
203
+ describe("prepared activations", () => {
204
+ it("prepareOpen changes nothing until commit", async () => {
205
+ const demo = provider();
206
+ const prepared = await demo.prepareOpen("demo-conv-bulk-order", { context: browser });
207
+ expect(prepared.conversationId).toBe("demo-conv-bulk-order");
208
+ expect(prepared.conversationRevision).toBe(
209
+ demo.getConversationRevision("demo-conv-bulk-order")
210
+ );
211
+ expect(demo.getActiveConversationId()).toBeNull();
212
+
213
+ await prepared.commit();
214
+ expect(demo.getActiveConversationId()).toBe("demo-conv-bulk-order");
215
+ });
216
+
217
+ it("discard is inert and wins over a later commit", async () => {
218
+ const demo = provider();
219
+ const first = await demo.prepareOpen("demo-conv-bulk-order", { context: browser });
220
+ const second = await demo.prepareOpen("demo-conv-gift-wrap", { context: browser });
221
+
222
+ first.discard();
223
+ first.discard();
224
+ expect(demo.getActiveConversationId()).toBeNull();
225
+
226
+ await first.commit();
227
+ expect(demo.getActiveConversationId()).toBeNull();
228
+
229
+ await second.commit();
230
+ await second.commit();
231
+ expect(demo.getActiveConversationId()).toBe("demo-conv-gift-wrap");
232
+ });
233
+
234
+ it("prepareStartNew creates the conversation only on commit", async () => {
235
+ const demo = provider();
236
+ const discarded = await demo.prepareStartNew({ context: browser });
237
+ discarded.discard();
238
+ await discarded.commit();
239
+ expect(demo.getConversationIds()).toHaveLength(5);
240
+ expect(demo.getActiveConversationId()).toBeNull();
241
+
242
+ const prepared = await demo.prepareStartNew({ context: browser });
243
+ await prepared.commit();
244
+ expect(demo.getConversationIds()).toHaveLength(6);
245
+ expect(demo.getActiveConversationId()).toBe(prepared.conversationId);
246
+ // Empty conversations are never listed, matching the API.
247
+ const listed = await demo.list({ context: browser });
248
+ expect(listed.items.map((item) => item.id)).not.toContain(prepared.conversationId);
249
+ });
250
+ });
251
+
252
+ describe("revisions", () => {
253
+ it("advances on every mutation and refreshes the summary", async () => {
254
+ const demo = provider();
255
+ const before = demo.getConversationRevision("demo-conv-order-status");
256
+ demo.appendMessage("demo-conv-order-status", {
257
+ role: "assistant",
258
+ content: "One more thing about your delivery window.",
259
+ });
260
+ const after = demo.getConversationRevision("demo-conv-order-status");
261
+ expect(after).not.toBe(before);
262
+
263
+ demo.appendMessage("demo-conv-order-status", {
264
+ role: "user",
265
+ content: "Understood, thanks.",
266
+ });
267
+ expect(demo.getConversationRevision("demo-conv-order-status")).not.toBe(after);
268
+
269
+ const page = await demo.getPage("demo-conv-order-status", { context: browser });
270
+ expect(page.summary.messageCount).toBe(6);
271
+ expect(page.summary.preview).toBe("Understood, thanks.");
272
+ expect(page.conversationRevision).toBe(
273
+ demo.getConversationRevision("demo-conv-order-status")
274
+ );
275
+ });
276
+
277
+ it("mints distinct revisions per conversation", () => {
278
+ const demo = provider();
279
+ const revisions = demo
280
+ .getConversationIds()
281
+ .map((id) => demo.getConversationRevision(id));
282
+ expect(new Set(revisions).size).toBe(revisions.length);
283
+ });
284
+ });
285
+
286
+ describe("identity status injection", () => {
287
+ it("defaults to browser_only / no_identity_provider", () => {
288
+ expect(provider().getIdentityStatus()).toEqual({
289
+ state: "browser_only",
290
+ reason: "no_identity_provider",
291
+ });
292
+ });
293
+
294
+ it("notifies subscribers on change and dedupes identical states", () => {
295
+ const demo = provider();
296
+ const seen: unknown[] = [];
297
+ const unsubscribe = demo.subscribeIdentityStatus((status) => seen.push(status));
298
+
299
+ demo.setIdentityStatus({ state: "verifying" });
300
+ demo.setIdentityStatus({ state: "verifying" });
301
+ demo.setIdentityStatus({ state: "verified" });
302
+ demo.setIdentityStatus({
303
+ state: "authentication_required",
304
+ reason: "invalid_identity_proof",
305
+ });
306
+ demo.setIdentityStatus({
307
+ state: "authentication_required",
308
+ reason: "proof_unavailable_after_binding",
309
+ });
310
+
311
+ expect(seen).toEqual([
312
+ { state: "verifying" },
313
+ { state: "verified" },
314
+ { state: "authentication_required", reason: "invalid_identity_proof" },
315
+ { state: "authentication_required", reason: "proof_unavailable_after_binding" },
316
+ ]);
317
+ expect(demo.getIdentityStatus()).toEqual({
318
+ state: "authentication_required",
319
+ reason: "proof_unavailable_after_binding",
320
+ });
321
+
322
+ unsubscribe();
323
+ demo.setIdentityStatus({ state: "identity_provider_failed" });
324
+ expect(seen).toHaveLength(4);
325
+ expect(demo.getIdentityStatus()).toEqual({ state: "identity_provider_failed" });
326
+ });
327
+
328
+ it("accepts an injected initial status", () => {
329
+ const demo = provider({ identityStatus: { state: "verified" } });
330
+ expect(demo.getIdentityStatus()).toEqual({ state: "verified" });
331
+ });
332
+ });
333
+
334
+ describe("failure injection", () => {
335
+ it("failNext fails exactly once per queued entry", async () => {
336
+ const demo = provider();
337
+ demo.failNext("list", { code: "rate_limited", retryAfterSeconds: 30 });
338
+ const error = await demo.list({ context: browser }).catch((err: unknown) => err);
339
+ expect((error as HistoryProviderError).code).toBe("rate_limited");
340
+ expect((error as HistoryProviderError).retryAfterSeconds).toBe(30);
341
+
342
+ const recovered = await demo.list({ context: browser });
343
+ expect(recovered.items).toHaveLength(5);
344
+ });
345
+
346
+ it("persistent failures apply until cleared, per operation", async () => {
347
+ const demo = provider({ failures: { getPage: { code: "unavailable" } } });
348
+ await expect(
349
+ demo.getPage("demo-conv-order-status", { context: browser })
350
+ ).rejects.toMatchObject({ code: "unavailable" });
351
+ await expect(
352
+ demo.getPage("demo-conv-order-status", { context: browser })
353
+ ).rejects.toMatchObject({ code: "unavailable" });
354
+ await expect(demo.list({ context: browser })).resolves.toBeTruthy();
355
+
356
+ demo.setFailure("deleteAll", { code: "authentication_required" });
357
+ await expect(demo.deleteAll({ context: browser })).rejects.toMatchObject({
358
+ code: "authentication_required",
359
+ });
360
+
361
+ demo.clearFailures();
362
+ await expect(
363
+ demo.getPage("demo-conv-order-status", { context: browser })
364
+ ).resolves.toBeTruthy();
365
+ await expect(demo.deleteAll({ context: browser })).resolves.toEqual({ deleted: 5 });
366
+ });
367
+
368
+ it("a failed mutation leaves the store untouched", async () => {
369
+ const demo = provider();
370
+ demo.failNext("delete", { code: "authentication_failed" });
371
+ await expect(
372
+ demo.delete("demo-conv-order-status", { context: browser })
373
+ ).rejects.toMatchObject({ code: "authentication_failed" });
374
+ expect(demo.getConversationIds()).toHaveLength(5);
375
+ });
376
+ });
377
+
378
+ describe("latency injection", () => {
379
+ it("delays operations by the configured latency", async () => {
380
+ vi.useFakeTimers();
381
+ const demo = provider({ latencyMs: 250 });
382
+ let settled = false;
383
+ const pending = demo.list({ context: browser }).then((result) => {
384
+ settled = true;
385
+ return result;
386
+ });
387
+
388
+ await vi.advanceTimersByTimeAsync(100);
389
+ expect(settled).toBe(false);
390
+
391
+ await vi.advanceTimersByTimeAsync(200);
392
+ await pending;
393
+ expect(settled).toBe(true);
394
+
395
+ demo.setLatency(0);
396
+ settled = false;
397
+ await demo.list({ context: browser }).then(() => {
398
+ settled = true;
399
+ });
400
+ expect(settled).toBe(true);
401
+ });
402
+ });