@assistant-ui/react 0.8.15 → 0.8.17
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.
- package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.js +2 -2
- package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.js.map +1 -1
- package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.mjs +1 -1
- package/dist/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.mjs.map +1 -1
- package/dist/runtimes/edge/{EdgeChatAdapter.d.ts → EdgeModelAdapter.d.ts} +4 -4
- package/dist/runtimes/edge/EdgeModelAdapter.d.ts.map +1 -0
- package/dist/runtimes/edge/{EdgeChatAdapter.js → EdgeModelAdapter.js} +8 -8
- package/dist/runtimes/edge/EdgeModelAdapter.js.map +1 -0
- package/dist/runtimes/edge/{EdgeChatAdapter.mjs → EdgeModelAdapter.mjs} +4 -4
- package/dist/runtimes/edge/EdgeModelAdapter.mjs.map +1 -0
- package/dist/runtimes/edge/converters/toLanguageModelMessages.d.ts.map +1 -1
- package/dist/runtimes/edge/converters/toLanguageModelMessages.js +1 -0
- package/dist/runtimes/edge/converters/toLanguageModelMessages.js.map +1 -1
- package/dist/runtimes/edge/converters/toLanguageModelMessages.mjs +1 -0
- package/dist/runtimes/edge/converters/toLanguageModelMessages.mjs.map +1 -1
- package/dist/runtimes/edge/index.d.ts +2 -1
- package/dist/runtimes/edge/index.d.ts.map +1 -1
- package/dist/runtimes/edge/index.js +5 -2
- package/dist/runtimes/edge/index.js.map +1 -1
- package/dist/runtimes/edge/index.mjs +4 -2
- package/dist/runtimes/edge/index.mjs.map +1 -1
- package/dist/runtimes/edge/streams/toolResultStream.d.ts +2 -1
- package/dist/runtimes/edge/streams/toolResultStream.d.ts.map +1 -1
- package/dist/runtimes/edge/streams/toolResultStream.js +60 -21
- package/dist/runtimes/edge/streams/toolResultStream.js.map +1 -1
- package/dist/runtimes/edge/streams/toolResultStream.mjs +58 -20
- package/dist/runtimes/edge/streams/toolResultStream.mjs.map +1 -1
- package/dist/runtimes/edge/useEdgeRuntime.d.ts +2 -2
- package/dist/runtimes/edge/useEdgeRuntime.d.ts.map +1 -1
- package/dist/runtimes/edge/useEdgeRuntime.js +2 -2
- package/dist/runtimes/edge/useEdgeRuntime.js.map +1 -1
- package/dist/runtimes/edge/useEdgeRuntime.mjs +2 -2
- package/dist/runtimes/edge/useEdgeRuntime.mjs.map +1 -1
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js +3 -0
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js.map +1 -1
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.mjs +3 -0
- package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.mjs.map +1 -1
- package/dist/runtimes/utils/MessageRepository.d.ts +112 -0
- package/dist/runtimes/utils/MessageRepository.d.ts.map +1 -1
- package/dist/runtimes/utils/MessageRepository.js +103 -1
- package/dist/runtimes/utils/MessageRepository.js.map +1 -1
- package/dist/runtimes/utils/MessageRepository.mjs +103 -1
- package/dist/runtimes/utils/MessageRepository.mjs.map +1 -1
- package/dist/tests/MessageRepository.test.d.ts +2 -0
- package/dist/tests/MessageRepository.test.d.ts.map +1 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +2656 -0
- package/dist/tests/setup.js.map +1 -0
- package/dist/tests/setup.mjs +2632 -0
- package/dist/tests/setup.mjs.map +1 -0
- package/dist/types/AssistantTypes.d.ts +1 -1
- package/dist/types/AssistantTypes.d.ts.map +1 -1
- package/dist/types/AssistantTypes.js.map +1 -1
- package/package.json +12 -6
- package/src/runtimes/dangerous-in-browser/DangerousInBrowserAdapter.ts +1 -1
- package/src/runtimes/edge/{EdgeChatAdapter.ts → EdgeModelAdapter.ts} +3 -3
- package/src/runtimes/edge/converters/toLanguageModelMessages.ts +3 -1
- package/src/runtimes/edge/index.ts +3 -1
- package/src/runtimes/edge/streams/toolResultStream.ts +76 -27
- package/src/runtimes/edge/useEdgeRuntime.ts +3 -3
- package/src/runtimes/remote-thread-list/EMPTY_THREAD_CORE.tsx +4 -0
- package/src/runtimes/utils/MessageRepository.tsx +142 -1
- package/src/tests/MessageRepository.test.ts +690 -0
- package/src/tests/setup.ts +11 -0
- package/src/types/AssistantTypes.ts +1 -1
- package/dist/runtimes/edge/EdgeChatAdapter.d.ts.map +0 -1
- package/dist/runtimes/edge/EdgeChatAdapter.js.map +0 -1
- 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
|