@lobehub/lobehub 2.0.0-next.7 → 2.0.0-next.9

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 (127) hide show
  1. package/.github/workflows/desktop-pr-build.yml +8 -8
  2. package/.github/workflows/docker.yml +17 -16
  3. package/.github/workflows/e2e.yml +3 -3
  4. package/.github/workflows/release-desktop-beta.yml +8 -8
  5. package/.github/workflows/release.yml +1 -1
  6. package/.github/workflows/test.yml +4 -4
  7. package/CHANGELOG.md +50 -0
  8. package/changelog/v1.json +18 -0
  9. package/locales/ar/models.json +6 -6
  10. package/locales/bg-BG/models.json +6 -6
  11. package/locales/de-DE/models.json +6 -6
  12. package/locales/en-US/models.json +6 -6
  13. package/locales/es-ES/models.json +6 -6
  14. package/locales/fa-IR/models.json +6 -6
  15. package/locales/fr-FR/models.json +6 -6
  16. package/locales/it-IT/models.json +6 -6
  17. package/locales/ja-JP/models.json +6 -6
  18. package/locales/ko-KR/models.json +6 -6
  19. package/locales/nl-NL/models.json +6 -6
  20. package/locales/pl-PL/models.json +6 -6
  21. package/locales/pt-BR/models.json +6 -6
  22. package/locales/ru-RU/models.json +6 -6
  23. package/locales/tr-TR/models.json +6 -6
  24. package/locales/vi-VN/models.json +6 -6
  25. package/locales/zh-CN/models.json +6 -6
  26. package/locales/zh-TW/models.json +6 -6
  27. package/package.json +1 -1
  28. package/packages/const/src/index.ts +0 -1
  29. package/packages/const/src/url.ts +1 -4
  30. package/packages/context-engine/src/index.ts +1 -6
  31. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +12 -2
  32. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +73 -9
  33. package/packages/context-engine/src/providers/index.ts +0 -2
  34. package/packages/database/package.json +1 -1
  35. package/packages/database/src/models/__tests__/message.grouping.test.ts +812 -0
  36. package/packages/database/src/models/__tests__/message.test.ts +322 -170
  37. package/packages/database/src/models/message.ts +62 -24
  38. package/packages/database/src/utils/__tests__/groupMessages.test.ts +145 -2
  39. package/packages/database/src/utils/groupMessages.ts +7 -5
  40. package/packages/types/src/message/common/base.ts +13 -0
  41. package/packages/types/src/message/common/image.ts +8 -0
  42. package/packages/types/src/message/common/metadata.ts +39 -0
  43. package/packages/types/src/message/common/tools.ts +10 -0
  44. package/packages/types/src/message/db/params.ts +47 -1
  45. package/packages/types/src/message/ui/chat.ts +4 -1
  46. package/packages/types/src/search.ts +16 -0
  47. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  48. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/useSend.ts +6 -4
  49. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +15 -10
  50. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +4 -2
  51. package/src/components/Thinking/index.tsx +4 -3
  52. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  53. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  54. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +1 -3
  56. package/src/features/Conversation/Error/ErrorJsonViewer.tsx +4 -3
  57. package/src/features/Conversation/Error/OllamaBizError/index.tsx +7 -2
  58. package/src/features/Conversation/Error/index.tsx +15 -5
  59. package/src/features/Conversation/MarkdownElements/LobeArtifact/Render/index.tsx +2 -2
  60. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -2
  61. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +5 -3
  62. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/BuiltinPluginTitle.tsx +2 -2
  63. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +4 -2
  64. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +2 -2
  65. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -2
  66. package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +2 -2
  67. package/src/features/Conversation/Messages/Assistant/index.tsx +4 -4
  68. package/src/features/Conversation/Messages/Default.tsx +2 -2
  69. package/src/features/Conversation/Messages/User/Extra.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/index.tsx +4 -4
  71. package/src/features/Conversation/Messages/index.tsx +3 -3
  72. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  73. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +9 -6
  74. package/src/features/PluginTag/index.tsx +1 -3
  75. package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +37 -28
  76. package/src/features/Portal/Artifacts/Body/index.tsx +2 -2
  77. package/src/server/modules/ModelRuntime/trace.ts +11 -4
  78. package/src/server/routers/lambda/message.ts +14 -3
  79. package/src/services/chat/chat.test.ts +1 -40
  80. package/src/services/chat/contextEngineering.test.ts +0 -30
  81. package/src/services/chat/contextEngineering.ts +1 -12
  82. package/src/services/chat/index.ts +2 -7
  83. package/src/services/chat/types.ts +1 -1
  84. package/src/services/message/_deprecated.ts +1 -1
  85. package/src/services/message/client.ts +8 -2
  86. package/src/services/message/server.ts +7 -2
  87. package/src/services/message/type.ts +6 -1
  88. package/src/store/chat/helpers.test.ts +99 -0
  89. package/src/store/chat/helpers.ts +21 -2
  90. package/src/store/chat/selectors.ts +1 -1
  91. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +3 -3
  92. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -4
  93. package/src/store/chat/slices/message/action.test.ts +5 -1
  94. package/src/store/chat/slices/message/action.ts +102 -14
  95. package/src/store/chat/slices/message/reducer.test.ts +363 -5
  96. package/src/store/chat/slices/message/reducer.ts +87 -3
  97. package/src/store/chat/slices/message/{selectors.test.ts → selectors/chat.test.ts} +266 -30
  98. package/src/store/chat/slices/message/{selectors.ts → selectors/chat.ts} +29 -79
  99. package/src/store/chat/slices/message/selectors/index.ts +2 -0
  100. package/src/store/chat/slices/message/selectors/messageState.test.ts +36 -0
  101. package/src/store/chat/slices/message/selectors/messageState.ts +80 -0
  102. package/src/store/chat/slices/plugin/action.test.ts +34 -132
  103. package/src/store/chat/slices/plugin/action.ts +1 -44
  104. package/src/store/tool/selectors/tool.test.ts +1 -1
  105. package/src/store/tool/selectors/tool.ts +6 -8
  106. package/src/store/tool/slices/builtin/action.test.ts +83 -35
  107. package/src/store/tool/slices/builtin/action.ts +0 -9
  108. package/src/store/tool/slices/builtin/selectors.test.ts +4 -30
  109. package/src/store/tool/slices/builtin/selectors.ts +15 -21
  110. package/src/tools/index.ts +0 -6
  111. package/src/tools/renders.ts +0 -3
  112. package/src/tools/web-browsing/Portal/Search/Footer.tsx +2 -2
  113. package/packages/const/src/guide.ts +0 -89
  114. package/packages/context-engine/src/providers/InboxGuide.ts +0 -102
  115. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +0 -121
  116. package/src/services/chat/__snapshots__/chat.test.ts.snap +0 -110
  117. package/src/store/chat/slices/builtinTool/actions/__tests__/dalle.test.ts +0 -121
  118. package/src/store/chat/slices/builtinTool/actions/dalle.ts +0 -124
  119. package/src/tools/dalle/Render/GalleyGrid.tsx +0 -60
  120. package/src/tools/dalle/Render/Item/EditMode.tsx +0 -66
  121. package/src/tools/dalle/Render/Item/Error.tsx +0 -49
  122. package/src/tools/dalle/Render/Item/Image.tsx +0 -44
  123. package/src/tools/dalle/Render/Item/ImageFileItem.tsx +0 -57
  124. package/src/tools/dalle/Render/Item/index.tsx +0 -88
  125. package/src/tools/dalle/Render/ToolBar.tsx +0 -56
  126. package/src/tools/dalle/Render/index.tsx +0 -52
  127. package/src/tools/dalle/index.ts +0 -92
@@ -0,0 +1,812 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { getTestDB } from '../../models/__tests__/_util';
4
+ import { files, messagePlugins, messages, messagesFiles, sessions, users } from '../../schemas';
5
+ import { LobeChatDatabase } from '../../type';
6
+ import { MessageModel } from '../message';
7
+
8
+ const serverDB: LobeChatDatabase = await getTestDB();
9
+
10
+ const userId = 'message-grouping-test';
11
+ const messageModel = new MessageModel(serverDB, userId);
12
+
13
+ beforeEach(async () => {
14
+ // Clear tables before each test
15
+ await serverDB.transaction(async (trx) => {
16
+ await trx.delete(users);
17
+ await trx.insert(users).values([{ id: userId }, { id: '456' }]);
18
+ await trx.insert(sessions).values([{ id: '1', userId }]);
19
+ await trx.insert(files).values({
20
+ id: 'f1',
21
+ userId: userId,
22
+ name: 'test.png',
23
+ fileType: 'image/png',
24
+ size: 100,
25
+ url: 'url1',
26
+ });
27
+ });
28
+ });
29
+
30
+ afterEach(async () => {
31
+ // Clean up after each test
32
+ await serverDB.delete(messages);
33
+ await serverDB.delete(messagePlugins);
34
+ await serverDB.delete(messagesFiles);
35
+ });
36
+
37
+ describe('MessageModel - Message Grouping', () => {
38
+ describe('Basic Grouping Scenarios', () => {
39
+ it('should group assistant message with single tool result', async () => {
40
+ // Create assistant message with tool
41
+ await serverDB.insert(messages).values({
42
+ id: 'msg-1',
43
+ userId,
44
+ role: 'assistant',
45
+ content: 'Checking weather',
46
+ tools: [
47
+ {
48
+ id: 'tool-1',
49
+ identifier: 'weather',
50
+ apiName: 'getWeather',
51
+ arguments: '{"city":"Beijing"}',
52
+ type: 'default',
53
+ },
54
+ ],
55
+ });
56
+
57
+ // Create tool message
58
+ await serverDB.insert(messages).values({
59
+ id: 'msg-2',
60
+ userId,
61
+ role: 'tool',
62
+ content: 'Beijing: Sunny, 25°C',
63
+ });
64
+
65
+ await serverDB.insert(messagePlugins).values({
66
+ id: 'msg-2',
67
+ userId,
68
+ toolCallId: 'tool-1',
69
+ identifier: 'weather',
70
+ state: { cached: true },
71
+ });
72
+
73
+ // Query messages
74
+ const result = await messageModel.query(
75
+ { sessionId: null },
76
+ { groupAssistantMessages: true },
77
+ );
78
+
79
+ // Verify grouping
80
+ expect(result).toHaveLength(1);
81
+ expect(result[0].role).toBe('group');
82
+ expect(result[0].content).toBe('');
83
+ expect(result[0].children).toHaveLength(1);
84
+
85
+ const block = result[0].children![0];
86
+ expect(block.content).toBe('Checking weather');
87
+ expect(block.tools).toHaveLength(1);
88
+ expect(block.tools![0]).toMatchObject({
89
+ id: 'tool-1',
90
+ identifier: 'weather',
91
+ apiName: 'getWeather',
92
+ });
93
+ expect(block.tools![0].result).toMatchObject({
94
+ content: 'Beijing: Sunny, 25°C',
95
+ state: { cached: true },
96
+ });
97
+ });
98
+
99
+ it('should group assistant message with multiple tool results', async () => {
100
+ // Create assistant message with multiple tools
101
+ await serverDB.insert(messages).values({
102
+ id: 'msg-1',
103
+ userId,
104
+ role: 'assistant',
105
+ content: 'Checking weather and news',
106
+ tools: [
107
+ {
108
+ id: 'tool-1',
109
+ identifier: 'weather',
110
+ apiName: 'getWeather',
111
+ arguments: '{}',
112
+ type: 'default',
113
+ },
114
+ {
115
+ id: 'tool-2',
116
+ identifier: 'news',
117
+ apiName: 'getNews',
118
+ arguments: '{}',
119
+ type: 'default',
120
+ },
121
+ ],
122
+ });
123
+
124
+ // Create tool messages
125
+ await serverDB.insert(messages).values([
126
+ {
127
+ id: 'msg-2',
128
+ userId,
129
+ role: 'tool',
130
+ content: 'Beijing: Sunny, 25°C',
131
+ },
132
+ {
133
+ id: 'msg-3',
134
+ userId,
135
+ role: 'tool',
136
+ content: 'Latest tech news: AI breakthrough',
137
+ },
138
+ ]);
139
+
140
+ await serverDB.insert(messagePlugins).values([
141
+ {
142
+ id: 'msg-2',
143
+ userId,
144
+ toolCallId: 'tool-1',
145
+ identifier: 'weather',
146
+ },
147
+ {
148
+ id: 'msg-3',
149
+ userId,
150
+ toolCallId: 'tool-2',
151
+ identifier: 'news',
152
+ },
153
+ ]);
154
+
155
+ // Query messages
156
+ const result = await messageModel.query(
157
+ { sessionId: null },
158
+ { groupAssistantMessages: true },
159
+ );
160
+
161
+ // Verify grouping
162
+ expect(result).toHaveLength(1);
163
+ expect(result[0].role).toBe('group');
164
+ expect(result[0].children).toHaveLength(1);
165
+
166
+ const block = result[0].children![0];
167
+ expect(block.tools).toHaveLength(2);
168
+ expect(block.tools![0].result?.content).toBe('Beijing: Sunny, 25°C');
169
+ expect(block.tools![1].result?.content).toBe('Latest tech news: AI breakthrough');
170
+ });
171
+
172
+ it('should not group assistant message without tools', async () => {
173
+ // Create assistant message without tools
174
+ await serverDB.insert(messages).values({
175
+ id: 'msg-1',
176
+ userId,
177
+ role: 'assistant',
178
+ content: 'Hello!',
179
+ });
180
+
181
+ // Query messages
182
+ const result = await messageModel.query(
183
+ { sessionId: null },
184
+ { groupAssistantMessages: true },
185
+ );
186
+
187
+ // Verify no grouping
188
+ expect(result).toHaveLength(1);
189
+ expect(result[0].role).toBe('assistant');
190
+ expect(result[0].content).toBe('Hello!');
191
+ expect(result[0].children).toBeUndefined();
192
+ });
193
+
194
+ it('should handle assistant message with tool but no result yet', async () => {
195
+ // Create assistant message with tool
196
+ await serverDB.insert(messages).values({
197
+ id: 'msg-1',
198
+ userId,
199
+ role: 'assistant',
200
+ content: 'Checking weather',
201
+ tools: [
202
+ {
203
+ id: 'tool-1',
204
+ identifier: 'weather',
205
+ apiName: 'getWeather',
206
+ arguments: '{}',
207
+ type: 'default',
208
+ },
209
+ ],
210
+ });
211
+
212
+ // No tool message created yet
213
+
214
+ // Query messages
215
+ const result = await messageModel.query(
216
+ { sessionId: null },
217
+ { groupAssistantMessages: true },
218
+ );
219
+
220
+ // Verify grouping without result
221
+ expect(result).toHaveLength(1);
222
+ expect(result[0].role).toBe('group');
223
+ expect(result[0].children).toHaveLength(1);
224
+
225
+ const block = result[0].children![0];
226
+ expect(block.tools).toHaveLength(1);
227
+ expect(block.tools![0].result).toBeUndefined();
228
+ });
229
+ });
230
+
231
+ describe('Multi-turn Conversation Grouping', () => {
232
+ it('should group assistant with follow-up assistant (parentId→tool)', async () => {
233
+ // Scenario: assistant → tool → assistant (parentId → tool)
234
+ await serverDB.insert(messages).values([
235
+ {
236
+ id: 'msg-1',
237
+ userId,
238
+ role: 'assistant',
239
+ content: 'Let me check the weather',
240
+ createdAt: new Date('2023-01-01T10:00:00Z'),
241
+ tools: [
242
+ {
243
+ id: 'tool-1',
244
+ identifier: 'weather',
245
+ apiName: 'getWeather',
246
+ arguments: '{}',
247
+ type: 'default',
248
+ },
249
+ ],
250
+ },
251
+ {
252
+ id: 'msg-2',
253
+ userId,
254
+ role: 'tool',
255
+ content: 'Sunny, 25°C',
256
+ createdAt: new Date('2023-01-01T10:00:01Z'),
257
+ },
258
+ {
259
+ id: 'msg-3',
260
+ userId,
261
+ role: 'assistant',
262
+ content: 'Based on the weather, let me check the news',
263
+ parentId: 'msg-2',
264
+ createdAt: new Date('2023-01-01T10:00:02Z'),
265
+ tools: [
266
+ {
267
+ id: 'tool-2',
268
+ identifier: 'news',
269
+ apiName: 'getNews',
270
+ arguments: '{}',
271
+ type: 'default',
272
+ },
273
+ ],
274
+ },
275
+ {
276
+ id: 'msg-4',
277
+ userId,
278
+ role: 'tool',
279
+ content: 'Breaking: AI news',
280
+ createdAt: new Date('2023-01-01T10:00:03Z'),
281
+ },
282
+ ]);
283
+
284
+ await serverDB.insert(messagePlugins).values([
285
+ {
286
+ id: 'msg-2',
287
+ userId,
288
+ toolCallId: 'tool-1',
289
+ identifier: 'weather',
290
+ },
291
+ {
292
+ id: 'msg-4',
293
+ userId,
294
+ toolCallId: 'tool-2',
295
+ identifier: 'news',
296
+ },
297
+ ]);
298
+
299
+ // Query messages
300
+ const result = await messageModel.query(
301
+ { sessionId: null },
302
+ { groupAssistantMessages: true },
303
+ );
304
+
305
+ // Should have 1 group with 2 children
306
+ expect(result).toHaveLength(1);
307
+ expect(result[0].role).toBe('group');
308
+ expect(result[0].children).toHaveLength(2);
309
+
310
+ // First child: original assistant with tool result
311
+ expect(result[0].children![0].id).toBe('msg-1');
312
+ expect(result[0].children![0].content).toBe('Let me check the weather');
313
+ expect(result[0].children![0].tools).toHaveLength(1);
314
+ expect(result[0].children![0].tools![0].result?.content).toBe('Sunny, 25°C');
315
+
316
+ // Second child: follow-up assistant with its own tool result
317
+ expect(result[0].children![1].id).toBe('msg-3');
318
+ expect(result[0].children![1].content).toBe('Based on the weather, let me check the news');
319
+ expect(result[0].children![1].tools).toHaveLength(1);
320
+ expect(result[0].children![1].tools![0].result?.content).toBe('Breaking: AI news');
321
+ });
322
+
323
+ it('should group multiple follow-up assistants in chain (3+ assistants)', async () => {
324
+ // Scenario: assistant → tool → assistant → tool → assistant (chain of parentId→tool)
325
+ await serverDB.insert(messages).values([
326
+ {
327
+ id: 'msg-1',
328
+ userId,
329
+ role: 'assistant',
330
+ content: 'Step 1: Check weather',
331
+ createdAt: new Date('2023-01-01T10:00:00Z'),
332
+ tools: [
333
+ {
334
+ id: 'tool-1',
335
+ identifier: 'weather',
336
+ apiName: 'getWeather',
337
+ arguments: '{}',
338
+ type: 'default',
339
+ },
340
+ ],
341
+ },
342
+ {
343
+ id: 'msg-2',
344
+ userId,
345
+ role: 'tool',
346
+ content: 'Sunny, 25°C',
347
+ createdAt: new Date('2023-01-01T10:00:01Z'),
348
+ },
349
+ {
350
+ id: 'msg-3',
351
+ userId,
352
+ role: 'assistant',
353
+ content: 'Step 2: Based on weather, check news',
354
+ parentId: 'msg-2',
355
+ createdAt: new Date('2023-01-01T10:00:02Z'),
356
+ tools: [
357
+ {
358
+ id: 'tool-2',
359
+ identifier: 'news',
360
+ apiName: 'getNews',
361
+ arguments: '{}',
362
+ type: 'default',
363
+ },
364
+ ],
365
+ },
366
+ {
367
+ id: 'msg-4',
368
+ userId,
369
+ role: 'tool',
370
+ content: 'Breaking: AI news',
371
+ createdAt: new Date('2023-01-01T10:00:03Z'),
372
+ },
373
+ {
374
+ id: 'msg-5',
375
+ userId,
376
+ role: 'assistant',
377
+ content: 'Step 3: Final summary based on weather and news',
378
+ parentId: 'msg-4',
379
+ createdAt: new Date('2023-01-01T10:00:04Z'),
380
+ },
381
+ ]);
382
+
383
+ await serverDB.insert(messagePlugins).values([
384
+ {
385
+ id: 'msg-2',
386
+ userId,
387
+ toolCallId: 'tool-1',
388
+ identifier: 'weather',
389
+ },
390
+ {
391
+ id: 'msg-4',
392
+ userId,
393
+ toolCallId: 'tool-2',
394
+ identifier: 'news',
395
+ },
396
+ ]);
397
+
398
+ // Query messages
399
+ const result = await messageModel.query(
400
+ { sessionId: null },
401
+ { groupAssistantMessages: true },
402
+ );
403
+
404
+ // Should have 1 group with 3 children
405
+ expect(result).toHaveLength(1);
406
+ expect(result[0].role).toBe('group');
407
+ expect(result[0].children).toHaveLength(3);
408
+
409
+ // First child: original assistant with tool result
410
+ expect(result[0].children![0].id).toBe('msg-1');
411
+ expect(result[0].children![0].content).toBe('Step 1: Check weather');
412
+ expect(result[0].children![0].tools![0].result?.content).toBe('Sunny, 25°C');
413
+
414
+ // Second child: follow-up assistant with its own tool result
415
+ expect(result[0].children![1].id).toBe('msg-3');
416
+ expect(result[0].children![1].content).toBe('Step 2: Based on weather, check news');
417
+ expect(result[0].children![1].tools![0].result?.content).toBe('Breaking: AI news');
418
+
419
+ // Third child: final assistant (parentId pointed to second tool)
420
+ expect(result[0].children![2].id).toBe('msg-5');
421
+ expect(result[0].children![2].content).toBe(
422
+ 'Step 3: Final summary based on weather and news',
423
+ );
424
+ });
425
+
426
+ it('should group messages in multi-turn conversation', async () => {
427
+ // Create multi-turn conversation
428
+ await serverDB.insert(messages).values([
429
+ {
430
+ id: 'msg-1',
431
+ userId,
432
+ role: 'user',
433
+ content: 'What is the weather?',
434
+ createdAt: new Date('2023-01-01T10:00:00Z'),
435
+ },
436
+ {
437
+ id: 'msg-2',
438
+ userId,
439
+ role: 'assistant',
440
+ content: 'Checking weather',
441
+ createdAt: new Date('2023-01-01T10:00:01Z'),
442
+ tools: [
443
+ {
444
+ id: 'tool-1',
445
+ identifier: 'weather',
446
+ apiName: 'getWeather',
447
+ arguments: '{}',
448
+ type: 'default',
449
+ },
450
+ ],
451
+ },
452
+ {
453
+ id: 'msg-3',
454
+ userId,
455
+ role: 'tool',
456
+ content: 'Sunny, 25°C',
457
+ createdAt: new Date('2023-01-01T10:00:02Z'),
458
+ },
459
+ {
460
+ id: 'msg-4',
461
+ userId,
462
+ role: 'user',
463
+ content: 'What about news?',
464
+ createdAt: new Date('2023-01-01T10:00:03Z'),
465
+ },
466
+ {
467
+ id: 'msg-5',
468
+ userId,
469
+ role: 'assistant',
470
+ content: 'Checking news',
471
+ createdAt: new Date('2023-01-01T10:00:04Z'),
472
+ tools: [
473
+ {
474
+ id: 'tool-2',
475
+ identifier: 'news',
476
+ apiName: 'getNews',
477
+ arguments: '{}',
478
+ type: 'default',
479
+ },
480
+ ],
481
+ },
482
+ {
483
+ id: 'msg-6',
484
+ userId,
485
+ role: 'tool',
486
+ content: 'AI breakthrough',
487
+ createdAt: new Date('2023-01-01T10:00:05Z'),
488
+ },
489
+ ]);
490
+
491
+ await serverDB.insert(messagePlugins).values([
492
+ {
493
+ id: 'msg-3',
494
+ userId,
495
+ toolCallId: 'tool-1',
496
+ identifier: 'weather',
497
+ },
498
+ {
499
+ id: 'msg-6',
500
+ userId,
501
+ toolCallId: 'tool-2',
502
+ identifier: 'news',
503
+ },
504
+ ]);
505
+
506
+ // Query messages
507
+ const result = await messageModel.query(
508
+ { sessionId: null },
509
+ { groupAssistantMessages: true },
510
+ );
511
+
512
+ // Verify grouping
513
+ expect(result).toHaveLength(4); // 2 users + 2 grouped assistants
514
+ expect(result[0].role).toBe('user');
515
+ expect(result[1].role).toBe('group');
516
+ expect(result[2].role).toBe('user');
517
+ expect(result[3].role).toBe('group');
518
+ });
519
+
520
+ it('should handle mixed grouped and non-grouped messages', async () => {
521
+ // Create mixed messages
522
+ await serverDB.insert(messages).values([
523
+ {
524
+ id: 'msg-1',
525
+ userId,
526
+ role: 'assistant',
527
+ content: 'Hello!',
528
+ createdAt: new Date('2023-01-01T10:00:00Z'),
529
+ },
530
+ {
531
+ id: 'msg-2',
532
+ userId,
533
+ role: 'assistant',
534
+ content: 'Using tools',
535
+ createdAt: new Date('2023-01-01T10:00:01Z'),
536
+ tools: [
537
+ {
538
+ id: 'tool-1',
539
+ identifier: 'test',
540
+ apiName: 'test',
541
+ arguments: '{}',
542
+ type: 'default',
543
+ },
544
+ ],
545
+ },
546
+ {
547
+ id: 'msg-3',
548
+ userId,
549
+ role: 'tool',
550
+ content: 'Result',
551
+ createdAt: new Date('2023-01-01T10:00:02Z'),
552
+ },
553
+ ]);
554
+
555
+ await serverDB.insert(messagePlugins).values({
556
+ id: 'msg-3',
557
+ userId,
558
+ toolCallId: 'tool-1',
559
+ identifier: 'test',
560
+ });
561
+
562
+ // Query messages
563
+ const result = await messageModel.query(
564
+ { sessionId: null },
565
+ { groupAssistantMessages: true },
566
+ );
567
+
568
+ // Verify grouping
569
+ expect(result).toHaveLength(2);
570
+ expect(result[0].role).toBe('assistant');
571
+ expect(result[0].children).toBeUndefined();
572
+ expect(result[1].role).toBe('group');
573
+ expect(result[1].children).toHaveLength(1);
574
+ });
575
+ });
576
+
577
+ describe('Edge Cases', () => {
578
+ it('should handle tool messages with errors', async () => {
579
+ // Create assistant with tool
580
+ await serverDB.insert(messages).values({
581
+ id: 'msg-1',
582
+ userId,
583
+ role: 'assistant',
584
+ content: 'Checking',
585
+ tools: [
586
+ {
587
+ id: 'tool-1',
588
+ identifier: 'test',
589
+ apiName: 'test',
590
+ arguments: '{}',
591
+ type: 'default',
592
+ },
593
+ ],
594
+ });
595
+
596
+ // Create tool message with error
597
+ await serverDB.insert(messages).values({
598
+ id: 'msg-2',
599
+ userId,
600
+ role: 'tool',
601
+ content: '',
602
+ });
603
+
604
+ await serverDB.insert(messagePlugins).values({
605
+ id: 'msg-2',
606
+ userId,
607
+ toolCallId: 'tool-1',
608
+ identifier: 'test',
609
+ error: { message: 'Failed to execute' },
610
+ });
611
+
612
+ // Query messages
613
+ const result = await messageModel.query(
614
+ { sessionId: null },
615
+ { groupAssistantMessages: true },
616
+ );
617
+
618
+ // Verify error is preserved
619
+ expect(result).toHaveLength(1);
620
+ expect(result[0].role).toBe('group');
621
+ expect(result[0].children![0].tools![0].result?.error).toEqual({
622
+ message: 'Failed to execute',
623
+ });
624
+ });
625
+
626
+ it('should preserve message order', async () => {
627
+ // Create messages in specific order
628
+ await serverDB.insert(messages).values([
629
+ {
630
+ id: 'msg-1',
631
+ userId,
632
+ role: 'user',
633
+ content: 'First',
634
+ createdAt: new Date('2023-01-01T10:00:00Z'),
635
+ },
636
+ {
637
+ id: 'msg-2',
638
+ userId,
639
+ role: 'assistant',
640
+ content: 'Second',
641
+ createdAt: new Date('2023-01-01T10:00:01Z'),
642
+ },
643
+ {
644
+ id: 'msg-3',
645
+ userId,
646
+ role: 'user',
647
+ content: 'Third',
648
+ createdAt: new Date('2023-01-01T10:00:02Z'),
649
+ },
650
+ ]);
651
+
652
+ // Query messages
653
+ const result = await messageModel.query(
654
+ { sessionId: null },
655
+ { groupAssistantMessages: true },
656
+ );
657
+
658
+ // Verify order
659
+ expect(result).toHaveLength(3);
660
+ expect(result[0].content).toBe('First');
661
+ expect(result[1].content).toBe('Second');
662
+ expect(result[2].content).toBe('Third');
663
+ });
664
+
665
+ it('should handle orphaned tool messages', async () => {
666
+ // Create orphaned tool message
667
+ await serverDB.insert(messages).values({
668
+ id: 'msg-1',
669
+ userId,
670
+ role: 'tool',
671
+ content: 'Orphaned result',
672
+ });
673
+
674
+ await serverDB.insert(messagePlugins).values({
675
+ id: 'msg-1',
676
+ userId,
677
+ toolCallId: 'unknown-tool',
678
+ identifier: 'test',
679
+ });
680
+
681
+ // Query messages
682
+ const result = await messageModel.query(
683
+ { sessionId: null },
684
+ { groupAssistantMessages: true },
685
+ );
686
+
687
+ // Verify orphaned tool is not filtered
688
+ expect(result).toHaveLength(1);
689
+ expect(result[0].role).toBe('tool');
690
+ });
691
+ });
692
+
693
+ describe('Children Structure Validation', () => {
694
+ it('should use message ID as block ID', async () => {
695
+ // Create assistant with tool
696
+ await serverDB.insert(messages).values({
697
+ id: 'msg-1',
698
+ userId,
699
+ role: 'assistant',
700
+ content: 'Test',
701
+ tools: [
702
+ {
703
+ id: 'tool-1',
704
+ identifier: 'test',
705
+ apiName: 'test',
706
+ arguments: '{}',
707
+ type: 'default',
708
+ },
709
+ ],
710
+ });
711
+
712
+ // Query messages
713
+ const result = await messageModel.query(
714
+ { sessionId: null },
715
+ { groupAssistantMessages: true },
716
+ );
717
+
718
+ // Verify block ID uses message ID
719
+ expect(result[0].children![0].id).toBe('msg-1');
720
+ });
721
+
722
+ it('should convert empty imageList/fileList to undefined in children', async () => {
723
+ // Create assistant with tools but empty imageList/fileList
724
+ await serverDB.insert(messages).values({
725
+ id: 'msg-1',
726
+ userId,
727
+ role: 'assistant',
728
+ content: 'Test',
729
+ tools: [
730
+ {
731
+ id: 'tool-1',
732
+ identifier: 'test',
733
+ apiName: 'test',
734
+ arguments: '{}',
735
+ type: 'default',
736
+ },
737
+ ],
738
+ });
739
+
740
+ // Query messages (no files attached, so imageList/fileList will be empty)
741
+ const result = await messageModel.query(
742
+ { sessionId: null },
743
+ { groupAssistantMessages: true },
744
+ );
745
+
746
+ // Verify empty arrays become undefined
747
+ expect(result[0].children![0].imageList).toBeUndefined();
748
+ });
749
+
750
+ it('should move tools/imageList/fileList to children', async () => {
751
+ // Create files
752
+ await serverDB.insert(files).values([
753
+ {
754
+ id: 'img-1',
755
+ userId,
756
+ name: 'test.png',
757
+ fileType: 'image/png',
758
+ size: 1024,
759
+ url: 'http://example.com/img.png',
760
+ },
761
+ {
762
+ id: 'file-1',
763
+ userId,
764
+ name: 'test.pdf',
765
+ fileType: 'application/pdf',
766
+ size: 2048,
767
+ url: 'http://example.com/file.pdf',
768
+ },
769
+ ]);
770
+
771
+ // Create assistant with tools and files
772
+ await serverDB.insert(messages).values({
773
+ id: 'msg-1',
774
+ userId,
775
+ role: 'assistant',
776
+ content: 'Test',
777
+ tools: [
778
+ {
779
+ id: 'tool-1',
780
+ identifier: 'test',
781
+ apiName: 'test',
782
+ arguments: '{}',
783
+ type: 'default',
784
+ },
785
+ ],
786
+ });
787
+
788
+ await serverDB.insert(messagesFiles).values([
789
+ { messageId: 'msg-1', fileId: 'img-1', userId },
790
+ { messageId: 'msg-1', fileId: 'file-1', userId },
791
+ ]);
792
+
793
+ // Query messages
794
+ const result = await messageModel.query(
795
+ { sessionId: null },
796
+ { groupAssistantMessages: true },
797
+ );
798
+
799
+ // Verify parent fields are cleared
800
+ expect(result[0].tools).toBeUndefined();
801
+ expect(result[0].imageList).toBeUndefined();
802
+ expect(result[0].fileList).toBeUndefined();
803
+ expect(result[0].content).toBe('');
804
+
805
+ // Verify children have the data
806
+ const block = result[0].children![0];
807
+ expect(block.content).toBe('Test');
808
+ expect(block.tools).toHaveLength(1);
809
+ expect(block.imageList).toHaveLength(1);
810
+ });
811
+ });
812
+ });