@lobehub/lobehub 2.0.0-next.37 → 2.0.0-next.39

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/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
  3. package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
  4. package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
  5. package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
  6. package/changelog/v1.json +14 -0
  7. package/locales/ar/auth.json +45 -1
  8. package/locales/ar/modelProvider.json +13 -1
  9. package/locales/bg-BG/auth.json +45 -1
  10. package/locales/bg-BG/modelProvider.json +13 -1
  11. package/locales/de-DE/auth.json +45 -1
  12. package/locales/de-DE/modelProvider.json +13 -1
  13. package/locales/en-US/auth.json +45 -1
  14. package/locales/en-US/modelProvider.json +13 -1
  15. package/locales/es-ES/auth.json +45 -1
  16. package/locales/es-ES/modelProvider.json +13 -1
  17. package/locales/fa-IR/auth.json +45 -1
  18. package/locales/fa-IR/modelProvider.json +13 -1
  19. package/locales/fr-FR/auth.json +45 -1
  20. package/locales/fr-FR/modelProvider.json +13 -1
  21. package/locales/it-IT/auth.json +45 -1
  22. package/locales/it-IT/modelProvider.json +13 -1
  23. package/locales/ja-JP/auth.json +45 -1
  24. package/locales/ja-JP/modelProvider.json +13 -1
  25. package/locales/ko-KR/auth.json +45 -1
  26. package/locales/ko-KR/modelProvider.json +13 -1
  27. package/locales/nl-NL/auth.json +45 -1
  28. package/locales/nl-NL/modelProvider.json +13 -1
  29. package/locales/pl-PL/auth.json +45 -1
  30. package/locales/pl-PL/modelProvider.json +13 -1
  31. package/locales/pt-BR/auth.json +45 -1
  32. package/locales/pt-BR/modelProvider.json +13 -1
  33. package/locales/ru-RU/auth.json +45 -1
  34. package/locales/ru-RU/modelProvider.json +13 -1
  35. package/locales/tr-TR/auth.json +45 -1
  36. package/locales/tr-TR/modelProvider.json +13 -1
  37. package/locales/vi-VN/auth.json +45 -1
  38. package/locales/vi-VN/modelProvider.json +13 -1
  39. package/locales/zh-CN/auth.json +45 -1
  40. package/locales/zh-CN/modelProvider.json +13 -1
  41. package/locales/zh-TW/auth.json +45 -1
  42. package/locales/zh-TW/modelProvider.json +13 -1
  43. package/package.json +1 -1
  44. package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
  45. package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
  46. package/packages/obervability-otel/package.json +3 -1
  47. package/packages/obervability-otel/src/api.ts +2 -0
  48. package/packages/obervability-otel/src/trpc/convention.ts +16 -0
  49. package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
  50. package/packages/obervability-otel/src/trpc/index.ts +62 -0
  51. package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
  52. package/packages/types/src/usage/usageRecord.ts +54 -0
  53. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  54. package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
  55. package/packages/web-crawler/src/crawler.ts +5 -5
  56. package/packages/web-crawler/src/urlRules.ts +13 -13
  57. package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
  58. package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
  59. package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
  60. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
  61. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
  62. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
  63. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
  64. package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
  65. package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
  66. package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
  67. package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
  68. package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
  69. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
  70. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
  71. package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
  72. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
  73. package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
  74. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
  75. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
  76. package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
  77. package/src/features/Conversation/Messages/Group/index.tsx +7 -2
  78. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +3 -0
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
  80. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  81. package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
  82. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
  83. package/src/features/PluginsUI/Render/index.tsx +17 -0
  84. package/src/libs/mcp/client.ts +3 -2
  85. package/src/libs/mcp/types.ts +71 -0
  86. package/src/libs/trpc/lambda/index.ts +5 -2
  87. package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
  88. package/src/locales/default/auth.ts +44 -0
  89. package/src/locales/default/chat.ts +1 -0
  90. package/src/server/routers/desktop/mcp.ts +1 -3
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/usage.ts +36 -0
  93. package/src/server/routers/tools/mcp.ts +1 -3
  94. package/src/server/services/mcp/index.test.ts +28 -15
  95. package/src/server/services/mcp/index.ts +29 -18
  96. package/src/server/services/usage/index.test.ts +310 -0
  97. package/src/server/services/usage/index.ts +164 -0
  98. package/src/services/chat/contextEngineering.test.ts +4 -0
  99. package/src/services/mcp.test.ts +7 -1
  100. package/src/services/mcp.ts +13 -12
  101. package/src/services/usage.ts +13 -0
  102. package/src/store/chat/agents/createAgentExecutors.ts +2 -3
  103. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
  104. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
  105. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
  106. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
  107. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
  108. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  109. package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
  110. package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
  111. package/src/store/chat/slices/message/initialState.ts +5 -0
  112. package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
  113. package/src/store/chat/slices/message/selectors/chat.ts +0 -2
  114. package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
  115. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
  116. package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
  117. package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
  118. package/src/store/chat/slices/plugin/action.test.ts +4 -4
  119. package/src/store/chat/slices/plugin/actions/index.ts +39 -0
  120. package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
  121. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
  122. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
  123. package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
  124. package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
  125. package/src/store/chat/store.ts +1 -1
  126. package/src/store/global/initialState.ts +1 -0
  127. package/src/store/chat/slices/plugin/action.ts +0 -539
@@ -0,0 +1,818 @@
1
+ import { UIChatMessage } from '@lobechat/types';
2
+ import { LobeAgentConfig } from '@lobechat/types';
3
+ import { act } from '@testing-library/react';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
7
+ import { INBOX_SESSION_ID } from '@/const/session';
8
+ import { useAgentStore } from '@/store/agent';
9
+ import { ChatStore } from '@/store/chat';
10
+ import { initialState } from '@/store/chat/initialState';
11
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
12
+ import { createServerConfigStore } from '@/store/serverConfig/store';
13
+ import { merge } from '@/utils/merge';
14
+
15
+ import { displayMessageSelectors } from './displayMessage';
16
+
17
+ vi.mock('i18next', () => ({
18
+ t: vi.fn((key) => key), // Simplified mock return value
19
+ }));
20
+
21
+ const initialStore = initialState as ChatStore;
22
+
23
+ const mockMessages = [
24
+ {
25
+ id: 'msg1',
26
+ content: 'Hello World',
27
+ role: 'user',
28
+ },
29
+ {
30
+ id: 'msg2',
31
+ content: 'Goodbye World',
32
+ role: 'user',
33
+ },
34
+ {
35
+ id: 'msg3',
36
+ content: 'Function Message',
37
+ role: 'tool',
38
+ tools: [
39
+ {
40
+ arguments: ['arg1', 'arg2'],
41
+ identifier: 'func1',
42
+ apiName: 'ttt',
43
+ type: 'pluginType',
44
+ id: 'abc',
45
+ },
46
+ ],
47
+ },
48
+ ] as UIChatMessage[];
49
+
50
+ const mockReasoningMessages = [
51
+ {
52
+ id: 'msg1',
53
+ content: 'Hello World',
54
+ role: 'user',
55
+ },
56
+ {
57
+ id: 'msg2',
58
+ content: 'Goodbye World',
59
+ role: 'user',
60
+ },
61
+ {
62
+ id: 'msg3',
63
+ content: 'Content Message',
64
+ role: 'assistant',
65
+ reasoning: {
66
+ content: 'Reasoning Content',
67
+ },
68
+ },
69
+ ] as UIChatMessage[];
70
+
71
+ const mockedChats = [
72
+ {
73
+ id: 'msg1',
74
+ content: 'Hello World',
75
+ role: 'user',
76
+ meta: {
77
+ avatar: '😀',
78
+ },
79
+ },
80
+ {
81
+ id: 'msg2',
82
+ content: 'Goodbye World',
83
+ role: 'user',
84
+ meta: {
85
+ avatar: '😀',
86
+ },
87
+ },
88
+ {
89
+ id: 'msg3',
90
+ content: 'Function Message',
91
+ role: 'tool',
92
+ meta: {
93
+ avatar: DEFAULT_INBOX_AVATAR,
94
+ backgroundColor: 'rgba(0,0,0,0)',
95
+ description: 'inbox.desc',
96
+ title: 'inbox.title',
97
+ },
98
+ tools: [
99
+ {
100
+ arguments: ['arg1', 'arg2'],
101
+ identifier: 'func1',
102
+ apiName: 'ttt',
103
+ type: 'pluginType',
104
+ id: 'abc',
105
+ },
106
+ ],
107
+ },
108
+ ] as UIChatMessage[];
109
+
110
+ const mockChatStore = {
111
+ messagesMap: {
112
+ [messageMapKey('abc')]: mockMessages,
113
+ },
114
+ activeId: 'abc',
115
+ } as ChatStore;
116
+
117
+ beforeAll(() => {
118
+ createServerConfigStore();
119
+ });
120
+
121
+ afterEach(() => {
122
+ const store = createServerConfigStore();
123
+ store.setState((state) => ({
124
+ featureFlags: { ...state.featureFlags, isAgentEditable: true },
125
+ }));
126
+ });
127
+
128
+ describe('displayMessageSelectors', () => {
129
+ describe('getDisplayMessageById', () => {
130
+ it('should return undefined if the message with the given id does not exist', () => {
131
+ const message =
132
+ displayMessageSelectors.getDisplayMessageById('non-existent-id')(initialStore);
133
+ expect(message).toBeUndefined();
134
+ });
135
+
136
+ it('should return the message object with the matching id', () => {
137
+ const state = merge(initialStore, {
138
+ messagesMap: {
139
+ [messageMapKey('abc')]: mockMessages,
140
+ },
141
+ activeId: 'abc',
142
+ });
143
+ const message = displayMessageSelectors.getDisplayMessageById('msg1')(state);
144
+ expect(message).toEqual(mockedChats[0]);
145
+ });
146
+
147
+ it('should return the message with the matching id', () => {
148
+ const message = displayMessageSelectors.getDisplayMessageById('msg1')(mockChatStore);
149
+ expect(message).toEqual(mockedChats[0]);
150
+ });
151
+
152
+ it('should return undefined if no message matches the id', () => {
153
+ const message = displayMessageSelectors.getDisplayMessageById('nonexistent')(mockChatStore);
154
+ expect(message).toBeUndefined();
155
+ });
156
+ });
157
+
158
+ describe('mainAIChatsWithHistoryConfig', () => {
159
+ it('should slice the messages according to the current agent config', () => {
160
+ const state = merge(initialStore, {
161
+ messagesMap: {
162
+ [messageMapKey('abc')]: mockMessages,
163
+ },
164
+ activeId: 'abc',
165
+ });
166
+
167
+ const chats = displayMessageSelectors.mainAIChatsWithHistoryConfig(state);
168
+ expect(chats).toHaveLength(3);
169
+ expect(chats).toEqual(mockedChats);
170
+ });
171
+
172
+ it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
173
+ const state = merge(initialStore, {
174
+ messagesMap: {
175
+ [messageMapKey('abc')]: mockMessages,
176
+ },
177
+ activeId: 'abc',
178
+ });
179
+ act(() => {
180
+ useAgentStore.setState({
181
+ activeId: 'inbox',
182
+ agentMap: {
183
+ inbox: {
184
+ chatConfig: {
185
+ historyCount: 2,
186
+ enableHistoryCount: true,
187
+ },
188
+ model: 'abc',
189
+ } as LobeAgentConfig,
190
+ },
191
+ });
192
+ });
193
+
194
+ const chats = displayMessageSelectors.mainAIChatsWithHistoryConfig(state);
195
+
196
+ expect(chats).toHaveLength(2);
197
+ expect(chats).toEqual([
198
+ {
199
+ id: 'msg2',
200
+ content: 'Goodbye World',
201
+ role: 'user',
202
+ meta: {
203
+ avatar: '😀',
204
+ },
205
+ },
206
+ {
207
+ id: 'msg3',
208
+ content: 'Function Message',
209
+ role: 'tool',
210
+ meta: {
211
+ avatar: DEFAULT_INBOX_AVATAR,
212
+ backgroundColor: 'rgba(0,0,0,0)',
213
+ description: 'inbox.desc',
214
+ title: 'inbox.title',
215
+ },
216
+ tools: [
217
+ {
218
+ apiName: 'ttt',
219
+ arguments: ['arg1', 'arg2'],
220
+ identifier: 'func1',
221
+ id: 'abc',
222
+ type: 'pluginType',
223
+ },
224
+ ],
225
+ },
226
+ ]);
227
+ });
228
+ });
229
+
230
+ describe('mainAIChatsMessageString', () => {
231
+ it('should concatenate the contents of all messages returned by mainAIChatsWithHistoryConfig', () => {
232
+ // Prepare a state with a few messages
233
+ const state = merge(initialStore, {
234
+ messagesMap: {
235
+ [messageMapKey('active-session')]: mockMessages,
236
+ },
237
+ activeId: 'active-session',
238
+ });
239
+
240
+ // Assume that the mainAIChatsWithHistoryConfig will return the last two messages
241
+ const expectedString = mockMessages
242
+ .slice(-2)
243
+ .map((m) => m.content)
244
+ .join('');
245
+
246
+ // Call the selector and verify the result
247
+ const concatenatedString = displayMessageSelectors.mainAIChatsMessageString(state);
248
+ expect(concatenatedString).toBe(expectedString);
249
+
250
+ // Restore the mocks after the test
251
+ vi.restoreAllMocks();
252
+ });
253
+ });
254
+
255
+ describe('mainAILatestMessageReasoningContent', () => {
256
+ it('should return the reasoning content of the latest message', () => {
257
+ // Prepare a state with a few messages
258
+ const state = merge(initialStore, {
259
+ messagesMap: {
260
+ [messageMapKey('active-session')]: mockReasoningMessages,
261
+ },
262
+ activeId: 'active-session',
263
+ });
264
+
265
+ const expectedString = mockReasoningMessages.at(-1)?.reasoning?.content;
266
+
267
+ // Call the selector and verify the result
268
+ const reasoningContent = displayMessageSelectors.mainAILatestMessageReasoningContent(state);
269
+ expect(reasoningContent).toBe(expectedString);
270
+
271
+ // Restore the mocks after the test
272
+ vi.restoreAllMocks();
273
+ });
274
+ });
275
+
276
+ describe('showInboxWelcome', () => {
277
+ it('should return false if the active session is not the inbox session', () => {
278
+ const state = merge(initialStore, { activeId: 'someActiveId' });
279
+ const result = displayMessageSelectors.showInboxWelcome(state);
280
+ expect(result).toBe(false);
281
+ });
282
+
283
+ it('should return false if there are existing messages in the inbox session', () => {
284
+ const state = merge(initialStore, {
285
+ activeId: INBOX_SESSION_ID,
286
+ messagesMap: {
287
+ [messageMapKey('inbox')]: mockMessages,
288
+ },
289
+ });
290
+ const result = displayMessageSelectors.showInboxWelcome(state);
291
+ expect(result).toBe(false);
292
+ });
293
+
294
+ it('should return true if the active session is the inbox session and there are no existing messages', () => {
295
+ const state = merge(initialStore, {
296
+ activeId: INBOX_SESSION_ID,
297
+ messages: [],
298
+ });
299
+ const result = displayMessageSelectors.showInboxWelcome(state);
300
+ expect(result).toBe(true);
301
+ });
302
+ });
303
+
304
+ describe('currentDisplayChatKey', () => {
305
+ it('should generate correct key with activeId only', () => {
306
+ const state: Partial<ChatStore> = {
307
+ activeId: 'testId',
308
+ activeTopicId: undefined,
309
+ };
310
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
311
+ expect(result).toBe(messageMapKey('testId', undefined));
312
+ });
313
+
314
+ it('should generate correct key with both activeId and activeTopicId', () => {
315
+ const state: Partial<ChatStore> = {
316
+ activeId: 'testId',
317
+ activeTopicId: 'topicId',
318
+ };
319
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
320
+ expect(result).toBe(messageMapKey('testId', 'topicId'));
321
+ });
322
+
323
+ it('should generate key with undefined activeId', () => {
324
+ const state: Partial<ChatStore> = {
325
+ activeId: undefined,
326
+ activeTopicId: 'topicId',
327
+ };
328
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
329
+ expect(result).toBe(messageMapKey(undefined as any, 'topicId'));
330
+ });
331
+
332
+ it('should generate key with empty string activeId', () => {
333
+ const state: Partial<ChatStore> = {
334
+ activeId: '',
335
+ activeTopicId: undefined,
336
+ };
337
+ const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
338
+ expect(result).toBe(messageMapKey('', undefined));
339
+ });
340
+ });
341
+
342
+ describe('activeDisplayMessages with group chat messages', () => {
343
+ it('should retrieve agent meta for group chat messages with groupId and agentId', () => {
344
+ const groupChatMessages = [
345
+ {
346
+ id: 'msg1',
347
+ content: 'Hello from agent',
348
+ role: 'assistant',
349
+ groupId: 'group-123',
350
+ agentId: 'agent-456',
351
+ },
352
+ ] as UIChatMessage[];
353
+
354
+ const state = merge(initialStore, {
355
+ messagesMap: {
356
+ [messageMapKey('group-123')]: groupChatMessages,
357
+ },
358
+ activeId: 'group-123',
359
+ });
360
+
361
+ const chats = displayMessageSelectors.activeDisplayMessages(state);
362
+ expect(chats).toHaveLength(1);
363
+ expect(chats[0].id).toBe('msg1');
364
+ expect(chats[0].meta).toBeDefined();
365
+ });
366
+ });
367
+
368
+ describe('getGroupLatestMessageWithoutTools', () => {
369
+ it('should return the last child without tools', () => {
370
+ const groupMessage = {
371
+ id: 'group-1',
372
+ role: 'assistantGroup',
373
+ content: '',
374
+ children: [
375
+ {
376
+ id: 'child-1',
377
+ content: 'First response',
378
+ tools: [
379
+ {
380
+ id: 'tool-1',
381
+ identifier: 'test',
382
+ apiName: 'test',
383
+ arguments: '{}',
384
+ type: 'default',
385
+ },
386
+ ],
387
+ },
388
+ {
389
+ id: 'child-2',
390
+ content: 'Second response',
391
+ tools: [],
392
+ },
393
+ {
394
+ id: 'child-3',
395
+ content: 'Final response',
396
+ },
397
+ ],
398
+ } as UIChatMessage;
399
+
400
+ const state: Partial<ChatStore> = {
401
+ activeId: 'test-id',
402
+ messagesMap: {
403
+ [messageMapKey('test-id')]: [groupMessage],
404
+ },
405
+ };
406
+
407
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-1')(
408
+ state as ChatStore,
409
+ );
410
+ expect(result).toBeDefined();
411
+ expect(result?.id).toBe('child-3');
412
+ expect(result?.content).toBe('Final response');
413
+ });
414
+
415
+ it('should return undefined if the last child has tools', () => {
416
+ const groupMessage = {
417
+ id: 'group-2',
418
+ role: 'assistantGroup',
419
+ content: '',
420
+ children: [
421
+ {
422
+ id: 'child-1',
423
+ content: 'First response',
424
+ },
425
+ {
426
+ id: 'child-2',
427
+ content: 'Second response with tools',
428
+ tools: [
429
+ {
430
+ id: 'tool-1',
431
+ identifier: 'test',
432
+ apiName: 'test',
433
+ arguments: '{}',
434
+ type: 'default',
435
+ },
436
+ ],
437
+ },
438
+ ],
439
+ } as UIChatMessage;
440
+
441
+ const state: Partial<ChatStore> = {
442
+ activeId: 'test-id',
443
+ messagesMap: {
444
+ [messageMapKey('test-id')]: [groupMessage],
445
+ },
446
+ };
447
+
448
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-2')(
449
+ state as ChatStore,
450
+ );
451
+ expect(result).toBeUndefined();
452
+ });
453
+
454
+ it('should return the last child when it has empty tools array', () => {
455
+ const groupMessage = {
456
+ id: 'group-3',
457
+ role: 'assistantGroup',
458
+ content: '',
459
+ children: [
460
+ {
461
+ id: 'child-1',
462
+ content: 'First response with tools',
463
+ tools: [
464
+ {
465
+ id: 'tool-1',
466
+ identifier: 'test',
467
+ apiName: 'test',
468
+ arguments: '{}',
469
+ type: 'default',
470
+ },
471
+ ],
472
+ },
473
+ {
474
+ id: 'child-2',
475
+ content: 'Final response',
476
+ tools: [],
477
+ },
478
+ ],
479
+ } as UIChatMessage;
480
+
481
+ const state: Partial<ChatStore> = {
482
+ activeId: 'test-id',
483
+ messagesMap: {
484
+ [messageMapKey('test-id')]: [groupMessage],
485
+ },
486
+ };
487
+
488
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-3')(
489
+ state as ChatStore,
490
+ );
491
+ expect(result).toBeDefined();
492
+ expect(result?.id).toBe('child-2');
493
+ expect(result?.content).toBe('Final response');
494
+ });
495
+
496
+ it('should return undefined for non-group messages', () => {
497
+ const assistantMessage = {
498
+ id: 'msg-1',
499
+ role: 'assistant',
500
+ content: 'Regular message',
501
+ } as UIChatMessage;
502
+
503
+ const state: Partial<ChatStore> = {
504
+ activeId: 'test-id',
505
+ messagesMap: {
506
+ [messageMapKey('test-id')]: [assistantMessage],
507
+ },
508
+ };
509
+
510
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('msg-1')(
511
+ state as ChatStore,
512
+ );
513
+ expect(result).toBeUndefined();
514
+ });
515
+
516
+ it('should return undefined for group messages without children', () => {
517
+ const groupMessage = {
518
+ id: 'group-4',
519
+ role: 'assistantGroup',
520
+ content: '',
521
+ children: undefined,
522
+ } as UIChatMessage;
523
+
524
+ const state: Partial<ChatStore> = {
525
+ activeId: 'test-id',
526
+ messagesMap: {
527
+ [messageMapKey('test-id')]: [groupMessage],
528
+ },
529
+ };
530
+
531
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-4')(
532
+ state as ChatStore,
533
+ );
534
+ expect(result).toBeUndefined();
535
+ });
536
+
537
+ it('should return undefined for group messages with empty children array', () => {
538
+ const groupMessage = {
539
+ id: 'group-5',
540
+ role: 'assistantGroup',
541
+ content: '',
542
+ children: [],
543
+ } as unknown as UIChatMessage;
544
+
545
+ const state: Partial<ChatStore> = {
546
+ activeId: 'test-id',
547
+ messagesMap: {
548
+ [messageMapKey('test-id')]: [groupMessage],
549
+ },
550
+ };
551
+
552
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-5')(
553
+ state as ChatStore,
554
+ );
555
+ expect(result).toBeUndefined();
556
+ });
557
+
558
+ it('should return undefined if all children have tools', () => {
559
+ const groupMessage = {
560
+ id: 'group-6',
561
+ role: 'assistantGroup',
562
+ content: '',
563
+ children: [
564
+ {
565
+ id: 'child-1',
566
+ content: 'First response',
567
+ tools: [
568
+ {
569
+ id: 'tool-1',
570
+ identifier: 'test',
571
+ apiName: 'test',
572
+ arguments: '{}',
573
+ type: 'default',
574
+ },
575
+ ],
576
+ },
577
+ {
578
+ id: 'child-2',
579
+ content: 'Second response',
580
+ tools: [
581
+ {
582
+ id: 'tool-2',
583
+ identifier: 'test2',
584
+ apiName: 'test2',
585
+ arguments: '{}',
586
+ type: 'default',
587
+ },
588
+ ],
589
+ },
590
+ ],
591
+ } as unknown as UIChatMessage;
592
+
593
+ const state: Partial<ChatStore> = {
594
+ activeId: 'test-id',
595
+ messagesMap: {
596
+ [messageMapKey('test-id')]: [groupMessage],
597
+ },
598
+ };
599
+
600
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-6')(
601
+ state as ChatStore,
602
+ );
603
+ expect(result).toBeUndefined();
604
+ });
605
+
606
+ it('should handle empty tools array as no tools', () => {
607
+ const groupMessage = {
608
+ id: 'group-7',
609
+ role: 'assistantGroup',
610
+ content: '',
611
+ children: [
612
+ {
613
+ id: 'child-1',
614
+ content: 'Response with empty tools',
615
+ tools: [],
616
+ },
617
+ ],
618
+ } as unknown as UIChatMessage;
619
+
620
+ const state: Partial<ChatStore> = {
621
+ activeId: 'test-id',
622
+ messagesMap: {
623
+ [messageMapKey('test-id')]: [groupMessage],
624
+ },
625
+ };
626
+
627
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('group-7')(
628
+ state as ChatStore,
629
+ );
630
+ expect(result).toBeDefined();
631
+ expect(result?.id).toBe('child-1');
632
+ });
633
+
634
+ it('should return undefined when message is not found', () => {
635
+ const state: Partial<ChatStore> = {
636
+ activeId: 'test-id',
637
+ messagesMap: {
638
+ [messageMapKey('test-id')]: [],
639
+ },
640
+ };
641
+
642
+ const result = displayMessageSelectors.getGroupLatestMessageWithoutTools('non-existent')(
643
+ state as ChatStore,
644
+ );
645
+ expect(result).toBeUndefined();
646
+ });
647
+ });
648
+
649
+ describe('findLastMessageId', () => {
650
+ it('should return message id when no children or tools', () => {
651
+ const message = {
652
+ id: 'msg-1',
653
+ role: 'assistant',
654
+ content: 'Simple message',
655
+ } as UIChatMessage;
656
+
657
+ const state: Partial<ChatStore> = {
658
+ activeId: 'test-id',
659
+ messagesMap: {
660
+ [messageMapKey('test-id')]: [message],
661
+ },
662
+ };
663
+
664
+ const result = displayMessageSelectors.findLastMessageId('msg-1')(state as ChatStore);
665
+ expect(result).toBe('msg-1');
666
+ });
667
+
668
+ it('should find the last child id', () => {
669
+ const groupMessage = {
670
+ id: 'group-1',
671
+ role: 'assistantGroup',
672
+ content: '',
673
+ children: [
674
+ {
675
+ id: 'child-1',
676
+ content: 'First response',
677
+ },
678
+ {
679
+ id: 'child-2',
680
+ content: 'Second response',
681
+ },
682
+ ],
683
+ } as UIChatMessage;
684
+
685
+ const state: Partial<ChatStore> = {
686
+ activeId: 'test-id',
687
+ messagesMap: {
688
+ [messageMapKey('test-id')]: [groupMessage],
689
+ },
690
+ };
691
+
692
+ const result = displayMessageSelectors.findLastMessageId('group-1')(state as ChatStore);
693
+ expect(result).toBe('child-2');
694
+ });
695
+
696
+ it('should return tool result_msg_id when no children', () => {
697
+ const messageWithTools = {
698
+ id: 'msg-with-tools',
699
+ role: 'assistant',
700
+ content: 'Message with tools',
701
+ tools: [
702
+ {
703
+ id: 'tool-1',
704
+ identifier: 'test',
705
+ apiName: 'test',
706
+ arguments: '{}',
707
+ type: 'default',
708
+ result_msg_id: 'tool-result-1',
709
+ },
710
+ {
711
+ id: 'tool-2',
712
+ identifier: 'test2',
713
+ apiName: 'test2',
714
+ arguments: '{}',
715
+ type: 'default',
716
+ result_msg_id: 'tool-result-2',
717
+ },
718
+ ],
719
+ } as unknown as UIChatMessage;
720
+
721
+ const state: Partial<ChatStore> = {
722
+ activeId: 'test-id',
723
+ messagesMap: {
724
+ [messageMapKey('test-id')]: [messageWithTools],
725
+ },
726
+ };
727
+
728
+ const result = displayMessageSelectors.findLastMessageId('msg-with-tools')(
729
+ state as ChatStore,
730
+ );
731
+ expect(result).toBe('tool-result-2');
732
+ });
733
+
734
+ it('should prioritize children over tools', () => {
735
+ const message = {
736
+ id: 'msg-1',
737
+ role: 'assistantGroup',
738
+ content: '',
739
+ children: [
740
+ {
741
+ id: 'child-1',
742
+ content: 'Child message',
743
+ },
744
+ ],
745
+ tools: [
746
+ {
747
+ id: 'tool-1',
748
+ identifier: 'test',
749
+ apiName: 'test',
750
+ arguments: '{}',
751
+ type: 'default',
752
+ result_msg_id: 'tool-result-1',
753
+ },
754
+ ],
755
+ } as unknown as UIChatMessage;
756
+
757
+ const state: Partial<ChatStore> = {
758
+ activeId: 'test-id',
759
+ messagesMap: {
760
+ [messageMapKey('test-id')]: [message],
761
+ },
762
+ };
763
+
764
+ const result = displayMessageSelectors.findLastMessageId('msg-1')(state as ChatStore);
765
+ expect(result).toBe('child-1');
766
+ });
767
+
768
+ it('should return undefined for non-existent message', () => {
769
+ const state: Partial<ChatStore> = {
770
+ activeId: 'test-id',
771
+ messagesMap: {
772
+ [messageMapKey('test-id')]: [],
773
+ },
774
+ };
775
+
776
+ const result = displayMessageSelectors.findLastMessageId('non-existent')(state as ChatStore);
777
+ expect(result).toBeUndefined();
778
+ });
779
+
780
+ it('should return last child with tools result_msg_id', () => {
781
+ const messageWithChildrenAndTools = {
782
+ id: 'msg-1',
783
+ role: 'assistantGroup',
784
+ content: '',
785
+ children: [
786
+ {
787
+ id: 'child-1',
788
+ content: 'First child',
789
+ },
790
+ {
791
+ id: 'child-2',
792
+ content: 'Second child with tools',
793
+ tools: [
794
+ {
795
+ id: 'tool-1',
796
+ identifier: 'test',
797
+ apiName: 'test',
798
+ arguments: '{}',
799
+ type: 'default',
800
+ result_msg_id: 'tool-result-id',
801
+ },
802
+ ],
803
+ },
804
+ ],
805
+ } as unknown as UIChatMessage;
806
+
807
+ const state: Partial<ChatStore> = {
808
+ activeId: 'test-id',
809
+ messagesMap: {
810
+ [messageMapKey('test-id')]: [messageWithChildrenAndTools],
811
+ },
812
+ };
813
+
814
+ const result = displayMessageSelectors.findLastMessageId('msg-1')(state as ChatStore);
815
+ expect(result).toBe('tool-result-id');
816
+ });
817
+ });
818
+ });