@assistant-ui/react 0.8.15 → 0.8.16

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 (70) hide show
  1. package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.js +2 -2
  2. package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.js.map +1 -1
  3. package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.mjs +1 -1
  4. package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.mjs.map +1 -1
  5. package/dist/runtimes/edge/{EdgeChatAdapter.d.ts → EdgeModelAdapter.d.ts} +4 -4
  6. package/dist/runtimes/edge/EdgeModelAdapter.d.ts.map +1 -0
  7. package/dist/runtimes/edge/{EdgeChatAdapter.js → EdgeModelAdapter.js} +8 -8
  8. package/dist/runtimes/edge/EdgeModelAdapter.js.map +1 -0
  9. package/dist/runtimes/edge/{EdgeChatAdapter.mjs → EdgeModelAdapter.mjs} +4 -4
  10. package/dist/runtimes/edge/EdgeModelAdapter.mjs.map +1 -0
  11. package/dist/runtimes/edge/converters/toLanguageModelMessages.d.ts.map +1 -1
  12. package/dist/runtimes/edge/converters/toLanguageModelMessages.js +1 -0
  13. package/dist/runtimes/edge/converters/toLanguageModelMessages.js.map +1 -1
  14. package/dist/runtimes/edge/converters/toLanguageModelMessages.mjs +1 -0
  15. package/dist/runtimes/edge/converters/toLanguageModelMessages.mjs.map +1 -1
  16. package/dist/runtimes/edge/index.d.ts +2 -1
  17. package/dist/runtimes/edge/index.d.ts.map +1 -1
  18. package/dist/runtimes/edge/index.js +5 -2
  19. package/dist/runtimes/edge/index.js.map +1 -1
  20. package/dist/runtimes/edge/index.mjs +4 -2
  21. package/dist/runtimes/edge/index.mjs.map +1 -1
  22. package/dist/runtimes/edge/streams/toolResultStream.d.ts +2 -1
  23. package/dist/runtimes/edge/streams/toolResultStream.d.ts.map +1 -1
  24. package/dist/runtimes/edge/streams/toolResultStream.js +60 -21
  25. package/dist/runtimes/edge/streams/toolResultStream.js.map +1 -1
  26. package/dist/runtimes/edge/streams/toolResultStream.mjs +58 -20
  27. package/dist/runtimes/edge/streams/toolResultStream.mjs.map +1 -1
  28. package/dist/runtimes/edge/useEdgeRuntime.d.ts +2 -2
  29. package/dist/runtimes/edge/useEdgeRuntime.d.ts.map +1 -1
  30. package/dist/runtimes/edge/useEdgeRuntime.js +2 -2
  31. package/dist/runtimes/edge/useEdgeRuntime.js.map +1 -1
  32. package/dist/runtimes/edge/useEdgeRuntime.mjs +2 -2
  33. package/dist/runtimes/edge/useEdgeRuntime.mjs.map +1 -1
  34. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.d.ts.map +1 -1
  35. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js +3 -0
  36. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js.map +1 -1
  37. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.mjs +3 -0
  38. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.mjs.map +1 -1
  39. package/dist/runtimes/utils/MessageRepository.d.ts +112 -0
  40. package/dist/runtimes/utils/MessageRepository.d.ts.map +1 -1
  41. package/dist/runtimes/utils/MessageRepository.js +103 -1
  42. package/dist/runtimes/utils/MessageRepository.js.map +1 -1
  43. package/dist/runtimes/utils/MessageRepository.mjs +103 -1
  44. package/dist/runtimes/utils/MessageRepository.mjs.map +1 -1
  45. package/dist/tests/MessageRepository.test.d.ts +2 -0
  46. package/dist/tests/MessageRepository.test.d.ts.map +1 -0
  47. package/dist/tests/setup.d.ts +2 -0
  48. package/dist/tests/setup.d.ts.map +1 -0
  49. package/dist/tests/setup.js +2656 -0
  50. package/dist/tests/setup.js.map +1 -0
  51. package/dist/tests/setup.mjs +2632 -0
  52. package/dist/tests/setup.mjs.map +1 -0
  53. package/dist/types/AssistantTypes.d.ts +1 -1
  54. package/dist/types/AssistantTypes.d.ts.map +1 -1
  55. package/dist/types/AssistantTypes.js.map +1 -1
  56. package/package.json +10 -4
  57. package/src/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.ts +1 -1
  58. package/src/runtimes/edge/{EdgeChatAdapter.ts → EdgeModelAdapter.ts} +3 -3
  59. package/src/runtimes/edge/converters/toLanguageModelMessages.ts +3 -1
  60. package/src/runtimes/edge/index.ts +3 -1
  61. package/src/runtimes/edge/streams/toolResultStream.ts +76 -27
  62. package/src/runtimes/edge/useEdgeRuntime.ts +3 -3
  63. package/src/runtimes/remote-thread-list/EMPTY_THREAD_CORE.tsx +4 -0
  64. package/src/runtimes/utils/MessageRepository.tsx +142 -1
  65. package/src/tests/MessageRepository.test.ts +690 -0
  66. package/src/tests/setup.ts +11 -0
  67. package/src/types/AssistantTypes.ts +1 -1
  68. package/dist/runtimes/edge/EdgeChatAdapter.d.ts.map +0 -1
  69. package/dist/runtimes/edge/EdgeChatAdapter.js.map +0 -1
  70. package/dist/runtimes/edge/EdgeChatAdapter.mjs.map +0 -1
@@ -0,0 +1,690 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2
+ import {
3
+ MessageRepository,
4
+ ExportedMessageRepository,
5
+ } from "../runtimes/utils/MessageRepository";
6
+ import type {
7
+ CoreMessage,
8
+ ThreadMessage,
9
+ TextContentPart,
10
+ } from "../types/AssistantTypes";
11
+
12
+ // Mock generateId and generateOptimisticId to make tests deterministic
13
+ const mockGenerateId = vi.fn();
14
+ const mockGenerateOptimisticId = vi.fn();
15
+ const mockIsOptimisticId = vi.fn((id: string) =>
16
+ id.startsWith("__optimistic__"),
17
+ );
18
+
19
+ vi.mock("../utils/idUtils", () => ({
20
+ generateId: () => mockGenerateId(),
21
+ generateOptimisticId: () => mockGenerateOptimisticId(),
22
+ isOptimisticId: (id: string) => mockIsOptimisticId(id),
23
+ }));
24
+
25
+ /**
26
+ * Tests for the MessageRepository class, which manages message threads with branching capabilities.
27
+ *
28
+ * This suite verifies that the repository:
29
+ * - Correctly manages message additions, updates, and deletions
30
+ * - Properly maintains parent-child relationships between messages
31
+ * - Handles branch creation and switching between branches
32
+ * - Successfully imports and exports repository state
33
+ * - Correctly manages optimistic messages in the thread
34
+ * - Handles edge cases and error conditions gracefully
35
+ */
36
+ describe("MessageRepository", () => {
37
+ let repository: MessageRepository;
38
+ let nextMockId = 1;
39
+
40
+ /**
41
+ * Creates a test ThreadMessage with the given overrides.
42
+ */
43
+ const createTestMessage = (overrides = {}): ThreadMessage => ({
44
+ id: "test-id",
45
+ role: "assistant",
46
+ createdAt: new Date(),
47
+ content: [{ type: "text", text: "Test message" }],
48
+ status: { type: "complete", reason: "stop" },
49
+ metadata: {
50
+ unstable_annotations: [],
51
+ unstable_data: [],
52
+ steps: [],
53
+ custom: {},
54
+ },
55
+ ...overrides,
56
+ });
57
+
58
+ /**
59
+ * Creates a test CoreMessage with the given overrides.
60
+ */
61
+ const createTestCoreMessage = (overrides = {}): CoreMessage => ({
62
+ role: "assistant",
63
+ content: [{ type: "text", text: "Test message" }],
64
+ ...overrides,
65
+ });
66
+
67
+ beforeEach(() => {
68
+ repository = new MessageRepository();
69
+ // Reset mocks with predictable counter-based values
70
+ nextMockId = 1;
71
+ mockGenerateId.mockImplementation(() => `mock-id-${nextMockId++}`);
72
+ mockGenerateOptimisticId.mockImplementation(
73
+ () => `__optimistic__mock-id-${nextMockId++}`,
74
+ );
75
+ });
76
+
77
+ afterEach(() => {
78
+ vi.clearAllMocks();
79
+ });
80
+
81
+ // Core functionality tests - these test the public contract
82
+ describe("Basic CRUD operations", () => {
83
+ /**
84
+ * Tests the ability to add a new message to the repository.
85
+ * The message should be retrievable from the repository.
86
+ */
87
+ it("should add a new message to the repository", () => {
88
+ const message = createTestMessage({ id: "message-id" });
89
+ repository.addOrUpdateMessage(null, message);
90
+
91
+ const messages = repository.getMessages();
92
+ expect(messages).toContain(message);
93
+ });
94
+
95
+ /**
96
+ * Tests the ability to update an existing message in the repository.
97
+ * The update should replace the message content while maintaining its position.
98
+ */
99
+ it("should update an existing message", () => {
100
+ const message = createTestMessage({ id: "message-id" });
101
+ repository.addOrUpdateMessage(null, message);
102
+
103
+ const updatedContent = [
104
+ { type: "text", text: "Updated message" },
105
+ ] as const;
106
+ const updatedMessage = createTestMessage({
107
+ id: "message-id",
108
+ content: updatedContent,
109
+ });
110
+
111
+ repository.addOrUpdateMessage(null, updatedMessage);
112
+
113
+ const retrievedMessage = repository.getMessage("message-id").message;
114
+ expect(retrievedMessage.content).toEqual(updatedContent);
115
+ });
116
+
117
+ /**
118
+ * Tests that the repository correctly establishes parent-child relationships.
119
+ * The child message should reference its parent properly.
120
+ */
121
+ it("should establish parent-child relationships between messages", () => {
122
+ const parent = createTestMessage({ id: "parent-id" });
123
+ const child = createTestMessage({ id: "child-id" });
124
+
125
+ repository.addOrUpdateMessage(null, parent);
126
+ repository.addOrUpdateMessage("parent-id", child);
127
+
128
+ const childWithParent = repository.getMessage("child-id");
129
+ expect(childWithParent.parentId).toBe("parent-id");
130
+ });
131
+
132
+ /**
133
+ * Tests that adding a message with a non-existent parent ID throws an error.
134
+ * This maintains data integrity in the repository.
135
+ */
136
+ it("should throw an error when parent message is not found", () => {
137
+ const message = createTestMessage();
138
+
139
+ expect(() => {
140
+ repository.addOrUpdateMessage("non-existent-id", message);
141
+ }).toThrow(/Parent message not found/);
142
+ });
143
+
144
+ /**
145
+ * Tests that getMessages() returns all messages in the active branch in the correct order.
146
+ * The order should be from root to head.
147
+ */
148
+ it("should retrieve all messages in the current branch", () => {
149
+ const parent = createTestMessage({ id: "parent-id" });
150
+ const child = createTestMessage({ id: "child-id" });
151
+ const grandchild = createTestMessage({ id: "grandchild-id" });
152
+
153
+ repository.addOrUpdateMessage(null, parent);
154
+ repository.addOrUpdateMessage("parent-id", child);
155
+ repository.addOrUpdateMessage("child-id", grandchild);
156
+
157
+ const messages = repository.getMessages();
158
+
159
+ // Should return messages in order from root to head
160
+ expect(messages.map((m) => m.id)).toEqual([
161
+ "parent-id",
162
+ "child-id",
163
+ "grandchild-id",
164
+ ]);
165
+ });
166
+
167
+ /**
168
+ * Tests that the head message is updated correctly as messages are added.
169
+ * The head should always point to the most recently added message in the active branch.
170
+ */
171
+ it("should track the head message", () => {
172
+ const parent = createTestMessage({ id: "parent-id" });
173
+ const child = createTestMessage({ id: "child-id" });
174
+
175
+ repository.addOrUpdateMessage(null, parent);
176
+ expect(repository.headId).toBe("parent-id");
177
+
178
+ repository.addOrUpdateMessage("parent-id", child);
179
+ expect(repository.headId).toBe("child-id");
180
+ });
181
+
182
+ /**
183
+ * Tests that deleting a message adjusts the head pointer correctly.
184
+ * After deleting the head, the head should point to its parent.
185
+ */
186
+ it("should delete a message and adjust the head", () => {
187
+ const parent = createTestMessage({ id: "parent-id" });
188
+ const child = createTestMessage({ id: "child-id" });
189
+
190
+ repository.addOrUpdateMessage(null, parent);
191
+ repository.addOrUpdateMessage("parent-id", child);
192
+
193
+ // Initial head should be child
194
+ expect(repository.headId).toBe("child-id");
195
+
196
+ // Delete child
197
+ repository.deleteMessage("child-id");
198
+
199
+ // Head should now be parent
200
+ expect(repository.headId).toBe("parent-id");
201
+
202
+ // Child should be gone
203
+ const messages = repository.getMessages();
204
+ expect(messages.map((m) => m.id)).toEqual(["parent-id"]);
205
+ });
206
+
207
+ /**
208
+ * Tests that clearing the repository removes all messages.
209
+ * The repository should be empty and the head should be null after clearing.
210
+ */
211
+ it("should clear all messages", () => {
212
+ const message = createTestMessage();
213
+ repository.addOrUpdateMessage(null, message);
214
+
215
+ repository.clear();
216
+
217
+ expect(repository.getMessages()).toHaveLength(0);
218
+ expect(repository.headId).toBeNull();
219
+ });
220
+ });
221
+
222
+ describe("Branch management", () => {
223
+ /**
224
+ * Tests creating multiple branches from a parent message.
225
+ * Both branches should have the same parent and be separately accessible.
226
+ */
227
+ it("should create multiple branches from a parent message", () => {
228
+ const parent = createTestMessage({ id: "parent-id" });
229
+ const branch1 = createTestMessage({ id: "branch1-id" });
230
+ const branch2 = createTestMessage({ id: "branch2-id" });
231
+
232
+ repository.addOrUpdateMessage(null, parent);
233
+ repository.addOrUpdateMessage("parent-id", branch1);
234
+ repository.addOrUpdateMessage("parent-id", branch2);
235
+
236
+ // Test we can switch between branches
237
+ repository.switchToBranch("branch1-id");
238
+ expect(repository.headId).toBe("branch1-id");
239
+
240
+ repository.switchToBranch("branch2-id");
241
+ expect(repository.headId).toBe("branch2-id");
242
+
243
+ // Get branches from a child to verify siblings
244
+ const branches = repository.getBranches("branch1-id");
245
+ expect(branches).toContain("branch1-id");
246
+ expect(branches).toContain("branch2-id");
247
+ });
248
+
249
+ /**
250
+ * Tests switching between branches and verifying each branch's content.
251
+ * Each branch should maintain its own path of messages.
252
+ */
253
+ it("should switch between branches and maintain branch state", () => {
254
+ const parent = createTestMessage({ id: "parent-id" });
255
+ const branch1 = createTestMessage({ id: "branch1-id" });
256
+ const branch2 = createTestMessage({ id: "branch2-id" });
257
+
258
+ repository.addOrUpdateMessage(null, parent);
259
+ repository.addOrUpdateMessage("parent-id", branch1);
260
+ repository.addOrUpdateMessage("parent-id", branch2);
261
+
262
+ // Switch to first branch
263
+ repository.switchToBranch("branch1-id");
264
+ expect(repository.headId).toBe("branch1-id");
265
+
266
+ // Messages should show parent -> branch1 path
267
+ const messages1 = repository.getMessages();
268
+ expect(messages1.map((m) => m.id)).toEqual(["parent-id", "branch1-id"]);
269
+
270
+ // Switch to second branch
271
+ repository.switchToBranch("branch2-id");
272
+ expect(repository.headId).toBe("branch2-id");
273
+
274
+ // Messages should show parent -> branch2 path
275
+ const messages2 = repository.getMessages();
276
+ expect(messages2.map((m) => m.id)).toEqual(["parent-id", "branch2-id"]);
277
+ });
278
+
279
+ /**
280
+ * Tests that trying to switch to a non-existent branch throws an error.
281
+ * This ensures that the repository maintains valid state.
282
+ */
283
+ it("should throw error when switching to a non-existent branch", () => {
284
+ expect(() => {
285
+ repository.switchToBranch("non-existent-id");
286
+ }).toThrow(/Branch not found/);
287
+ });
288
+
289
+ /**
290
+ * Tests resetting the head to an earlier message in the tree.
291
+ * This should truncate the active branch at the specified message.
292
+ */
293
+ it("should reset head to an earlier message in the tree", () => {
294
+ const parent = createTestMessage({ id: "parent-id" });
295
+ const child = createTestMessage({ id: "child-id" });
296
+ const grandchild = createTestMessage({ id: "grandchild-id" });
297
+
298
+ repository.addOrUpdateMessage(null, parent);
299
+ repository.addOrUpdateMessage("parent-id", child);
300
+ repository.addOrUpdateMessage("child-id", grandchild);
301
+
302
+ // Reset to parent
303
+ repository.resetHead("parent-id");
304
+
305
+ // Head should be parent
306
+ expect(repository.headId).toBe("parent-id");
307
+
308
+ // Messages should only include parent
309
+ const messages = repository.getMessages();
310
+ expect(messages.map((m) => m.id)).toEqual(["parent-id"]);
311
+ });
312
+
313
+ /**
314
+ * Tests resetting the head to null.
315
+ * This should clear the active branch completely.
316
+ */
317
+ it("should reset head to null when null is passed", () => {
318
+ const message = createTestMessage();
319
+ repository.addOrUpdateMessage(null, message);
320
+
321
+ repository.resetHead(null);
322
+
323
+ expect(repository.headId).toBeNull();
324
+ expect(repository.getMessages()).toHaveLength(0);
325
+ });
326
+ });
327
+
328
+ describe("Optimistic messages", () => {
329
+ /**
330
+ * Tests creating an optimistic message with a unique ID.
331
+ * The message should have a running status and the correct ID.
332
+ */
333
+ it("should create an optimistic message with a unique ID", () => {
334
+ mockGenerateOptimisticId.mockReturnValue("__optimistic__generated-id");
335
+
336
+ const coreMessage = createTestCoreMessage();
337
+ const optimisticId = repository.appendOptimisticMessage(
338
+ null,
339
+ coreMessage,
340
+ );
341
+
342
+ expect(optimisticId).toBe("__optimistic__generated-id");
343
+ expect(repository.getMessage(optimisticId).message.status?.type).toBe(
344
+ "running",
345
+ );
346
+ });
347
+
348
+ /**
349
+ * Tests creating an optimistic message as a child of a specified parent.
350
+ * The message should have the correct parent relationship.
351
+ */
352
+ it("should create an optimistic message as a child of a specified parent", () => {
353
+ const parent = createTestMessage({ id: "parent-id" });
354
+ repository.addOrUpdateMessage(null, parent);
355
+
356
+ const coreMessage = createTestCoreMessage();
357
+ const optimisticId = repository.appendOptimisticMessage(
358
+ "parent-id",
359
+ coreMessage,
360
+ );
361
+
362
+ // Verify parent relationship
363
+ const result = repository.getMessage(optimisticId);
364
+ expect(result.parentId).toBe("parent-id");
365
+ });
366
+
367
+ /**
368
+ * Tests that optimistic IDs are unique even if the first generated ID
369
+ * already exists in the repository.
370
+ */
371
+ it("should retry generating unique optimistic IDs if initial one exists", () => {
372
+ // First call returns an ID that already exists
373
+ mockGenerateOptimisticId.mockReturnValueOnce("__optimistic__existing-id");
374
+
375
+ // Create a message with the ID that will conflict
376
+ const existingMessage = createTestMessage({
377
+ id: "__optimistic__existing-id",
378
+ });
379
+ repository.addOrUpdateMessage(null, existingMessage);
380
+
381
+ // Second call returns a unique ID
382
+ mockGenerateOptimisticId.mockReturnValueOnce("__optimistic__unique-id");
383
+
384
+ const coreMessage = createTestCoreMessage();
385
+ const optimisticId = repository.appendOptimisticMessage(
386
+ null,
387
+ coreMessage,
388
+ );
389
+
390
+ // Should have used the second ID
391
+ expect(optimisticId).toBe("__optimistic__unique-id");
392
+ expect(mockGenerateOptimisticId).toHaveBeenCalledTimes(2);
393
+ });
394
+ });
395
+
396
+ describe("Export and import", () => {
397
+ /**
398
+ * Tests exporting the repository state.
399
+ * The exported state should correctly represent all messages and relationships.
400
+ */
401
+ it("should export the repository state", () => {
402
+ const parent = createTestMessage({ id: "parent-id" });
403
+ const child = createTestMessage({ id: "child-id" });
404
+
405
+ repository.addOrUpdateMessage(null, parent);
406
+ repository.addOrUpdateMessage("parent-id", child);
407
+
408
+ const exported = repository.export();
409
+
410
+ expect(exported.headId).toBe("child-id");
411
+ expect(exported.messages).toHaveLength(2);
412
+ expect(
413
+ exported.messages.find((m) => m.message.id === "parent-id")?.parentId,
414
+ ).toBeNull();
415
+ expect(
416
+ exported.messages.find((m) => m.message.id === "child-id")?.parentId,
417
+ ).toBe("parent-id");
418
+ });
419
+
420
+ /**
421
+ * Tests importing repository state.
422
+ * The imported state should correctly restore all messages and relationships.
423
+ */
424
+ it("should import repository state", () => {
425
+ const parent = createTestMessage({ id: "parent-id" });
426
+ const child = createTestMessage({ id: "child-id" });
427
+
428
+ const exported = {
429
+ headId: "child-id",
430
+ messages: [
431
+ { message: parent, parentId: null },
432
+ { message: child, parentId: "parent-id" },
433
+ ],
434
+ };
435
+
436
+ repository.import(exported);
437
+
438
+ expect(repository.headId).toBe("child-id");
439
+ const messages = repository.getMessages();
440
+ expect(messages.map((m) => m.id)).toEqual(["parent-id", "child-id"]);
441
+ });
442
+
443
+ /**
444
+ * Tests importing with a specified head that is not the most recent message.
445
+ * This simulates restoring a specific branch even if it's not the latest one.
446
+ */
447
+ it("should import with a specified head that is not the most recent message", () => {
448
+ const parent = createTestMessage({ id: "parent-id" });
449
+ const child1 = createTestMessage({ id: "child1-id" });
450
+ const child2 = createTestMessage({ id: "child2-id" });
451
+
452
+ const exported = {
453
+ headId: "child1-id", // Specify child1 as head, not the last message
454
+ messages: [
455
+ { message: parent, parentId: null },
456
+ { message: child1, parentId: "parent-id" },
457
+ { message: child2, parentId: "parent-id" }, // Sibling of child1
458
+ ],
459
+ };
460
+
461
+ repository.import(exported);
462
+
463
+ // Head should be as specified
464
+ expect(repository.headId).toBe("child1-id");
465
+
466
+ // Active branch should be parent -> child1
467
+ const messages = repository.getMessages();
468
+ expect(messages.map((m) => m.id)).toEqual(["parent-id", "child1-id"]);
469
+
470
+ // We should be able to switch to child2
471
+ repository.switchToBranch("child2-id");
472
+ expect(repository.headId).toBe("child2-id");
473
+ });
474
+
475
+ /**
476
+ * Tests that importing with invalid parent references throws an error.
477
+ * This ensures data integrity during import.
478
+ */
479
+ it("should throw an error when importing with invalid parent references", () => {
480
+ const child = createTestMessage({ id: "child-id" });
481
+
482
+ const exported = {
483
+ headId: "child-id",
484
+ messages: [{ message: child, parentId: "non-existent-id" }],
485
+ };
486
+
487
+ expect(() => {
488
+ repository.import(exported);
489
+ }).toThrow(/Parent message not found/);
490
+ });
491
+ });
492
+
493
+ describe("ExportedMessageRepository utility", () => {
494
+ /**
495
+ * Tests converting an array of messages to repository format.
496
+ * The converted format should establish proper parent-child relationships.
497
+ */
498
+ it("should convert an array of messages to repository format", () => {
499
+ mockGenerateId.mockReturnValue("generated-id");
500
+
501
+ const messages: CoreMessage[] = [
502
+ {
503
+ role: "user" as const,
504
+ content: [
505
+ { type: "text" as const, text: "Hello" },
506
+ ] as TextContentPart[],
507
+ },
508
+ {
509
+ role: "assistant" as const,
510
+ content: [
511
+ { type: "text" as const, text: "Hi there" },
512
+ ] as TextContentPart[],
513
+ },
514
+ ];
515
+
516
+ const result = ExportedMessageRepository.fromArray(messages);
517
+
518
+ expect(result.messages).toHaveLength(2);
519
+ expect(result.messages[0]!.parentId).toBeNull();
520
+ expect(result.messages[1]!.parentId).toBe("generated-id");
521
+ });
522
+
523
+ /**
524
+ * Tests handling empty message arrays.
525
+ * The repository should handle this gracefully.
526
+ */
527
+ it("should handle empty message arrays", () => {
528
+ const result = ExportedMessageRepository.fromArray([]);
529
+ expect(result.messages).toHaveLength(0);
530
+ });
531
+ });
532
+
533
+ describe("Complex scenarios", () => {
534
+ /**
535
+ * Tests that the tree structure is maintained after deleting nodes.
536
+ * Child nodes should be preserved and accessible after deleting a sibling.
537
+ */
538
+ it("should maintain tree structure after deletions", () => {
539
+ // Create tree:
540
+ // root
541
+ // └── A
542
+ // ├── B
543
+ // └── C
544
+
545
+ const root = createTestMessage({ id: "root-id" });
546
+ const nodeA = createTestMessage({ id: "A-id" });
547
+ const nodeB = createTestMessage({ id: "B-id" });
548
+ const nodeC = createTestMessage({ id: "C-id" });
549
+
550
+ repository.addOrUpdateMessage(null, root);
551
+ repository.addOrUpdateMessage("root-id", nodeA);
552
+ repository.addOrUpdateMessage("A-id", nodeB);
553
+ repository.addOrUpdateMessage("A-id", nodeC);
554
+
555
+ // Delete B
556
+ repository.deleteMessage("B-id");
557
+
558
+ // Verify A still has C as child
559
+ repository.switchToBranch("C-id");
560
+ expect(repository.headId).toBe("C-id");
561
+
562
+ // Check that we still have root -> A -> C path
563
+ const messages = repository.getMessages();
564
+ expect(messages.map((m) => m.id)).toEqual(["root-id", "A-id", "C-id"]);
565
+ });
566
+
567
+ /**
568
+ * Tests relinking children when deleting a middle node.
569
+ * Children of the deleted node should be relinked to the specified replacement.
570
+ */
571
+ it("should relink children when deleting a middle node", () => {
572
+ // Create: root -> A -> B -> C
573
+ const root = createTestMessage({ id: "root-id" });
574
+ const nodeA = createTestMessage({ id: "A-id" });
575
+ const nodeB = createTestMessage({ id: "B-id" });
576
+ const nodeC = createTestMessage({ id: "C-id" });
577
+
578
+ repository.addOrUpdateMessage(null, root);
579
+ repository.addOrUpdateMessage("root-id", nodeA);
580
+ repository.addOrUpdateMessage("A-id", nodeB);
581
+ repository.addOrUpdateMessage("B-id", nodeC);
582
+
583
+ // Delete B, specifying A as the new parent for B's children
584
+ repository.deleteMessage("B-id", "A-id");
585
+
586
+ // Verify C is now a child of A directly
587
+ const c = repository.getMessage("C-id");
588
+ expect(c.parentId).toBe("A-id");
589
+
590
+ // Check that we have a path from root to C
591
+ repository.switchToBranch("C-id");
592
+ const messages = repository.getMessages();
593
+
594
+ // Must contain root, A, and C (B was deleted)
595
+ expect(messages.some((m) => m.id === "root-id")).toBe(true);
596
+ expect(messages.some((m) => m.id === "A-id")).toBe(true);
597
+ expect(messages.some((m) => m.id === "C-id")).toBe(true);
598
+ expect(messages.some((m) => m.id === "B-id")).toBe(false);
599
+ });
600
+
601
+ /**
602
+ * Tests deleting a node with multiple children and ensuring all children
603
+ * are properly relinked to the specified replacement.
604
+ */
605
+ it("should relink multiple children when deleting a parent node", () => {
606
+ // Create: root -> A -> B (and A -> C, A -> D)
607
+ const root = createTestMessage({ id: "root-id" });
608
+ const nodeA = createTestMessage({ id: "A-id" });
609
+ const nodeB = createTestMessage({ id: "B-id" });
610
+ const nodeC = createTestMessage({ id: "C-id" });
611
+ const nodeD = createTestMessage({ id: "D-id" });
612
+
613
+ repository.addOrUpdateMessage(null, root);
614
+ repository.addOrUpdateMessage("root-id", nodeA);
615
+ repository.addOrUpdateMessage("A-id", nodeB);
616
+ repository.addOrUpdateMessage("A-id", nodeC);
617
+ repository.addOrUpdateMessage("A-id", nodeD);
618
+
619
+ // Delete A, specifying root as the new parent for A's children
620
+ repository.deleteMessage("A-id", "root-id");
621
+
622
+ // Verify B, C, D are now children of root
623
+ expect(repository.getMessage("B-id").parentId).toBe("root-id");
624
+ expect(repository.getMessage("C-id").parentId).toBe("root-id");
625
+ expect(repository.getMessage("D-id").parentId).toBe("root-id");
626
+
627
+ // This test is checking specifically that after deletion and relinking,
628
+ // we can still access each branch. The exact message structure may vary depending
629
+ // on implementation details of MessageRepository's internal tree management.
630
+ // Instead of checking array length and order exactly, we'll verify that:
631
+ // 1. We can access each branch
632
+ // 2. Each branch contains both root and the target message
633
+
634
+ // Verify B branch
635
+ repository.switchToBranch("B-id");
636
+ const bMessages = repository.getMessages();
637
+ expect(bMessages.some((m) => m.id === "root-id")).toBe(true);
638
+ expect(bMessages.some((m) => m.id === "B-id")).toBe(true);
639
+ expect(bMessages.some((m) => m.id === "A-id")).toBe(false);
640
+
641
+ // Verify C branch
642
+ repository.switchToBranch("C-id");
643
+ const cMessages = repository.getMessages();
644
+ expect(cMessages.some((m) => m.id === "root-id")).toBe(true);
645
+ expect(cMessages.some((m) => m.id === "C-id")).toBe(true);
646
+ expect(cMessages.some((m) => m.id === "A-id")).toBe(false);
647
+
648
+ // Verify D branch
649
+ repository.switchToBranch("D-id");
650
+ const dMessages = repository.getMessages();
651
+ expect(dMessages.some((m) => m.id === "root-id")).toBe(true);
652
+ expect(dMessages.some((m) => m.id === "D-id")).toBe(true);
653
+ expect(dMessages.some((m) => m.id === "A-id")).toBe(false);
654
+ });
655
+
656
+ /**
657
+ * Tests that updating a message preserves its position in the tree.
658
+ */
659
+ it("should preserve message position when updating content", () => {
660
+ const parent = createTestMessage({ id: "parent-id" });
661
+ const child1 = createTestMessage({ id: "child1-id" });
662
+ const child2 = createTestMessage({ id: "child2-id" });
663
+
664
+ repository.addOrUpdateMessage(null, parent);
665
+ repository.addOrUpdateMessage("parent-id", child1);
666
+ repository.addOrUpdateMessage("child1-id", child2);
667
+
668
+ // Update child1 with new content
669
+ const updatedChild1 = createTestMessage({
670
+ id: "child1-id",
671
+ content: [{ type: "text", text: "Updated content" }],
672
+ });
673
+
674
+ repository.addOrUpdateMessage("parent-id", updatedChild1);
675
+
676
+ // Verify structure is preserved
677
+ const messages = repository.getMessages();
678
+ expect(messages.map((m) => m.id)).toEqual([
679
+ "parent-id",
680
+ "child1-id",
681
+ "child2-id",
682
+ ]);
683
+
684
+ // Verify content was updated
685
+ const contentPart = messages[1]!.content[0];
686
+ expect(contentPart.type).toBe("text");
687
+ expect((contentPart as TextContentPart).text).toBe("Updated content");
688
+ });
689
+ });
690
+ });
@@ -0,0 +1,11 @@
1
+ // This file contains setup code for tests
2
+ import { vi } from "vitest";
3
+
4
+ // Set up global mocks if needed
5
+ // Using a fixed date to avoid recursive calls
6
+ const OriginalDate = global.Date;
7
+ const fixedDate = new OriginalDate("2023-01-01");
8
+ global.Date = vi.fn(() => fixedDate) as any;
9
+ global.Date.now = vi.fn(() => fixedDate.getTime());
10
+
11
+ // Add any other global setup needed for tests