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

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 (156) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
  29. package/packages/database/src/models/message.ts +18 -19
  30. package/packages/types/src/aiChat.ts +2 -0
  31. package/packages/types/src/importer.ts +2 -2
  32. package/packages/types/src/message/ui/chat.ts +17 -1
  33. package/packages/types/src/message/ui/extra.ts +2 -2
  34. package/packages/types/src/message/ui/params.ts +2 -2
  35. package/packages/types/src/user/preference.ts +0 -4
  36. package/packages/utils/src/tokenizer/index.ts +3 -11
  37. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  38. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
  40. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
  41. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
  42. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  43. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  44. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  45. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  46. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  47. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  48. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  49. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  50. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  51. package/src/features/Conversation/Error/index.tsx +0 -5
  52. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  53. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  54. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  55. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  56. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  59. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  60. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  61. package/src/features/Conversation/Messages/Default.tsx +1 -0
  62. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  63. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  64. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  65. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  66. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  67. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  68. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  69. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  71. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  72. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  73. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  74. package/src/features/Conversation/Messages/index.tsx +3 -3
  75. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  76. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  77. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  78. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  80. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  81. package/src/hooks/useHotkeys/chatScope.ts +15 -7
  82. package/src/libs/trpc/client/lambda.ts +4 -3
  83. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  84. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  85. package/src/server/routers/lambda/aiChat.ts +3 -2
  86. package/src/server/routers/lambda/message.ts +8 -16
  87. package/src/server/services/message/__tests__/index.test.ts +29 -39
  88. package/src/server/services/message/index.ts +41 -36
  89. package/src/services/electron/desktopNotification.ts +6 -6
  90. package/src/services/electron/file.ts +6 -6
  91. package/src/services/file/ClientS3/index.ts +8 -8
  92. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  93. package/src/services/message/index.ts +21 -15
  94. package/src/services/upload.ts +11 -11
  95. package/src/services/utils/abortableRequest.test.ts +161 -0
  96. package/src/services/utils/abortableRequest.ts +67 -0
  97. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  98. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  99. package/src/store/chat/helpers.test.ts +0 -99
  100. package/src/store/chat/helpers.ts +0 -11
  101. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  102. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  103. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  104. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  105. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  106. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  107. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  108. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  109. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  110. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  111. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  112. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  113. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  114. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  115. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  116. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  117. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  118. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  119. package/src/store/chat/slices/message/action.test.ts +79 -68
  120. package/src/store/chat/slices/message/actions/index.ts +39 -0
  121. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  122. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  123. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  124. package/src/store/chat/slices/message/actions/query.ts +120 -0
  125. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  126. package/src/store/chat/slices/message/initialState.ts +13 -0
  127. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  128. package/src/store/chat/slices/message/reducer.ts +17 -81
  129. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  130. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  131. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  132. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  133. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  134. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  135. package/src/store/chat/slices/plugin/action.ts +34 -28
  136. package/src/store/chat/slices/thread/action.test.ts +28 -31
  137. package/src/store/chat/slices/thread/action.ts +13 -10
  138. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  139. package/src/store/chat/slices/topic/reducer.ts +11 -3
  140. package/src/store/chat/store.ts +1 -1
  141. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  142. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  143. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  144. package/packages/database/src/utils/groupMessages.ts +0 -361
  145. package/packages/utils/src/tokenizer/client.ts +0 -35
  146. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  147. package/packages/utils/src/tokenizer/server.ts +0 -11
  148. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  149. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  150. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  151. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  152. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  153. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  154. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  155. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  156. package/src/store/chat/slices/message/action.ts +0 -629
@@ -1,975 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { LOADING_FLAT } from '@/const/message';
5
- import { chatService } from '@/services/chat';
6
- import { messageService } from '@/services/message';
7
- import { chatSelectors } from '@/store/chat/selectors';
8
- import { messageMapKey } from '@/store/chat/utils/messageMapKey';
9
- import { UploadFileItem } from '@/types/files/upload';
10
-
11
- import { useChatStore } from '../../../../store';
12
- import { TEST_CONTENT, TEST_IDS, createMockMessage, createMockMessages } from './fixtures';
13
- import {
14
- resetTestEnvironment,
15
- setupMockSelectors,
16
- setupStoreWithMessages,
17
- spyOnChatService,
18
- spyOnMessageService,
19
- } from './helpers';
20
-
21
- // Keep zustand mock as it's needed globally
22
- vi.mock('zustand/traditional');
23
-
24
- const realCoreProcessMessage = useChatStore.getState().internal_coreProcessMessage;
25
-
26
- beforeEach(() => {
27
- resetTestEnvironment();
28
- setupMockSelectors();
29
-
30
- // Setup default spies that most tests need
31
- spyOnMessageService();
32
- // ✅ Removed spyOnChatService() - tests should spy chatService only when needed
33
-
34
- // Setup common mock methods that most tests need
35
- act(() => {
36
- useChatStore.setState({
37
- refreshMessages: vi.fn(),
38
- refreshTopic: vi.fn(),
39
- internal_coreProcessMessage: vi.fn(),
40
- });
41
- });
42
- });
43
-
44
- afterEach(() => {
45
- process.env.NEXT_PUBLIC_BASE_PATH = undefined;
46
-
47
- vi.restoreAllMocks();
48
- });
49
-
50
- describe('chatMessage actions', () => {
51
- describe('sendMessage', () => {
52
- describe('validation', () => {
53
- it('should not send when there is no active session', async () => {
54
- act(() => {
55
- useChatStore.setState({ activeId: undefined });
56
- });
57
-
58
- const { result } = renderHook(() => useChatStore());
59
-
60
- await act(async () => {
61
- await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
62
- });
63
-
64
- expect(messageService.createMessage).not.toHaveBeenCalled();
65
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
66
- });
67
-
68
- it('should not send when message is empty and no files are provided', async () => {
69
- const { result } = renderHook(() => useChatStore());
70
-
71
- await act(async () => {
72
- await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
73
- });
74
-
75
- expect(messageService.createMessage).not.toHaveBeenCalled();
76
- });
77
-
78
- it('should not send when message is empty with empty files array', async () => {
79
- const { result } = renderHook(() => useChatStore());
80
-
81
- await act(async () => {
82
- await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
83
- });
84
-
85
- expect(messageService.createMessage).not.toHaveBeenCalled();
86
- });
87
- });
88
-
89
- describe('message creation', () => {
90
- it('should not process AI when onlyAddUserMessage is true', async () => {
91
- const { result } = renderHook(() => useChatStore());
92
-
93
- await act(async () => {
94
- await result.current.sendMessage({
95
- message: TEST_CONTENT.USER_MESSAGE,
96
- onlyAddUserMessage: true,
97
- });
98
- });
99
-
100
- expect(messageService.createMessage).toHaveBeenCalled();
101
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
102
- });
103
-
104
- it('should handle message creation errors gracefully', async () => {
105
- const { result } = renderHook(() => useChatStore());
106
- vi.spyOn(messageService, 'createMessage').mockRejectedValue(
107
- new Error('create message error'),
108
- );
109
-
110
- await act(async () => {
111
- try {
112
- await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
113
- } catch {
114
- // Expected to throw
115
- }
116
- });
117
-
118
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
119
- });
120
- });
121
- });
122
-
123
- describe('regenerateMessage', () => {
124
- it('should trigger message regeneration', async () => {
125
- const { result } = renderHook(() => useChatStore());
126
- const traceId = 'test-trace-id';
127
-
128
- act(() => {
129
- setupStoreWithMessages([
130
- createMockMessage({
131
- id: TEST_IDS.MESSAGE_ID,
132
- tools: [{ id: 'tool1' }, { id: 'tool2' }] as any,
133
- traceId,
134
- }),
135
- ]);
136
- });
137
-
138
- const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
139
-
140
- await act(async () => {
141
- await result.current.regenerateMessage(TEST_IDS.MESSAGE_ID);
142
- });
143
-
144
- expect(resendMessageSpy).toHaveBeenCalledWith(
145
- TEST_IDS.MESSAGE_ID,
146
- expect.objectContaining({}),
147
- );
148
- });
149
- });
150
-
151
- describe('delAndRegenerateMessage', () => {
152
- it('should delete message then regenerate', async () => {
153
- const { result } = renderHook(() => useChatStore());
154
-
155
- act(() => {
156
- setupStoreWithMessages([
157
- createMockMessage({
158
- id: TEST_IDS.MESSAGE_ID,
159
- tools: [{ id: 'tool1' }] as any,
160
- }),
161
- ]);
162
- });
163
-
164
- const deleteMessageSpy = vi.spyOn(result.current, 'deleteMessage');
165
- const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
166
-
167
- await act(async () => {
168
- await result.current.delAndRegenerateMessage(TEST_IDS.MESSAGE_ID);
169
- });
170
-
171
- expect(deleteMessageSpy).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID);
172
- expect(resendMessageSpy).toHaveBeenCalled();
173
- });
174
- });
175
-
176
- describe('stopGenerateMessage', () => {
177
- it('should abort generation and clear loading state when controller exists', () => {
178
- const abortController = new AbortController();
179
-
180
- act(() => {
181
- useChatStore.setState({ chatLoadingIdsAbortController: abortController });
182
- });
183
-
184
- const { result } = renderHook(() => useChatStore());
185
- const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
186
-
187
- act(() => {
188
- result.current.stopGenerateMessage();
189
- });
190
-
191
- expect(abortController.signal.aborted).toBe(true);
192
- expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
193
- });
194
-
195
- it('should do nothing when abort controller is not set', () => {
196
- act(() => {
197
- useChatStore.setState({ chatLoadingIdsAbortController: undefined });
198
- });
199
-
200
- const { result } = renderHook(() => useChatStore());
201
- const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
202
-
203
- act(() => {
204
- result.current.stopGenerateMessage();
205
- });
206
-
207
- expect(toggleLoadingSpy).not.toHaveBeenCalled();
208
- });
209
- });
210
-
211
- describe('internal_coreProcessMessage', () => {
212
- it('should process user message and generate AI response', async () => {
213
- const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
214
-
215
- act(() => {
216
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
217
- });
218
-
219
- const { result } = renderHook(() => useChatStore());
220
- const userMessage = createMockMessage({
221
- id: TEST_IDS.USER_MESSAGE_ID,
222
- role: 'user',
223
- content: TEST_CONTENT.USER_MESSAGE,
224
- });
225
-
226
- // ✅ Spy the direct dependency instead of chatService
227
- const fetchAIChatSpy = vi
228
- .spyOn(result.current, 'internal_fetchAIChatMessage')
229
- .mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
230
-
231
- const createMessageSpy = vi
232
- .spyOn(messageService, 'createMessage')
233
- .mockResolvedValue({ id: TEST_IDS.ASSISTANT_MESSAGE_ID, messages: mockMessages });
234
-
235
- const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
236
-
237
- await act(async () => {
238
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
239
- });
240
-
241
- expect(createMessageSpy).toHaveBeenCalledWith(
242
- expect.objectContaining({
243
- role: 'assistant',
244
- content: LOADING_FLAT,
245
- parentId: TEST_IDS.USER_MESSAGE_ID,
246
- sessionId: TEST_IDS.SESSION_ID,
247
- topicId: TEST_IDS.TOPIC_ID,
248
- }),
249
- );
250
-
251
- expect(fetchAIChatSpy).toHaveBeenCalled();
252
- expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
253
- });
254
-
255
- it('should handle RAG flow when ragQuery is provided', async () => {
256
- act(() => {
257
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
258
- });
259
-
260
- const { result } = renderHook(() => useChatStore());
261
- const userMessage = createMockMessage({
262
- id: TEST_IDS.USER_MESSAGE_ID,
263
- role: 'user',
264
- content: TEST_CONTENT.RAG_QUERY,
265
- });
266
-
267
- const retrieveChunksSpy = vi
268
- .spyOn(result.current, 'internal_retrieveChunks')
269
- .mockResolvedValue({
270
- chunks: [{ id: 'chunk-1', similarity: 0.9, text: 'chunk text' }] as any,
271
- queryId: 'query-1',
272
- rewriteQuery: 'rewritten query',
273
- });
274
-
275
- vi.spyOn(messageService, 'createMessage').mockResolvedValue({
276
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
277
- messages: [],
278
- });
279
-
280
- await act(async () => {
281
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id, {
282
- ragQuery: TEST_CONTENT.RAG_QUERY,
283
- });
284
- });
285
-
286
- expect(retrieveChunksSpy).toHaveBeenCalledWith(
287
- TEST_IDS.USER_MESSAGE_ID,
288
- TEST_CONTENT.RAG_QUERY,
289
- [],
290
- );
291
- });
292
-
293
- it('should not process when createMessage fails', async () => {
294
- act(() => {
295
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
296
- });
297
-
298
- const { result } = renderHook(() => useChatStore());
299
- const userMessage = createMockMessage({
300
- id: TEST_IDS.USER_MESSAGE_ID,
301
- role: 'user',
302
- });
303
-
304
- // ✅ Spy the direct dependency instead of chatService
305
- const fetchAIChatSpy = vi
306
- .spyOn(result.current, 'internal_fetchAIChatMessage')
307
- .mockResolvedValue({ isFunctionCall: false, content: '' });
308
-
309
- vi.spyOn(messageService, 'createMessage').mockResolvedValue(undefined as any);
310
-
311
- await act(async () => {
312
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
313
- });
314
-
315
- expect(fetchAIChatSpy).not.toHaveBeenCalled();
316
- });
317
- });
318
-
319
- describe('internal_fetchAIChatMessage', () => {
320
- it('should fetch and return AI chat response', async () => {
321
- const { result } = renderHook(() => useChatStore());
322
- const messages = [createMockMessage({ role: 'user' })];
323
-
324
- // ✅ Mock chatService instead of global fetch
325
- const streamSpy = vi
326
- .spyOn(chatService, 'createAssistantMessageStream')
327
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
328
- // Simulate text chunks streaming
329
- await onMessageHandle?.({ type: 'text', text: TEST_CONTENT.AI_RESPONSE } as any);
330
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {});
331
- });
332
-
333
- await act(async () => {
334
- const response = await result.current.internal_fetchAIChatMessage({
335
- messages,
336
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
337
- model: 'gpt-4o-mini',
338
- provider: 'openai',
339
- });
340
- expect(response.isFunctionCall).toEqual(false);
341
- expect(response.content).toEqual(TEST_CONTENT.AI_RESPONSE);
342
- });
343
-
344
- streamSpy.mockRestore();
345
- });
346
-
347
- it('should handle streaming errors gracefully', async () => {
348
- const { result } = renderHook(() => useChatStore());
349
- const messages = [createMockMessage({ role: 'user' })];
350
-
351
- // ✅ Mock chatService to simulate error
352
- const streamSpy = vi
353
- .spyOn(chatService, 'createAssistantMessageStream')
354
- .mockImplementation(async ({ onErrorHandle }) => {
355
- await onErrorHandle?.({ type: 'InvalidProviderAPIKey', message: 'Network error' } as any);
356
- });
357
-
358
- const updateMessageErrorSpy = vi.spyOn(messageService, 'updateMessageError');
359
-
360
- await act(async () => {
361
- await result.current.internal_fetchAIChatMessage({
362
- model: 'gpt-4o-mini',
363
- provider: 'openai',
364
- messages,
365
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
366
- });
367
- });
368
-
369
- expect(updateMessageErrorSpy).toHaveBeenCalledWith(
370
- TEST_IDS.ASSISTANT_MESSAGE_ID,
371
- expect.objectContaining({ type: 'InvalidProviderAPIKey' }),
372
- );
373
-
374
- streamSpy.mockRestore();
375
- });
376
-
377
- it('should handle tool call chunks during streaming', async () => {
378
- const { result } = renderHook(() => useChatStore());
379
- const messages = [createMockMessage({ role: 'user' })];
380
-
381
- const streamSpy = vi
382
- .spyOn(chatService, 'createAssistantMessageStream')
383
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
384
- await onMessageHandle?.({
385
- type: 'tool_calls',
386
- isAnimationActives: [true],
387
- tool_calls: [
388
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '{}' } },
389
- ],
390
- } as any);
391
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {
392
- toolCalls: [
393
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '{}' } },
394
- ],
395
- } as any);
396
- });
397
-
398
- await act(async () => {
399
- const response = await result.current.internal_fetchAIChatMessage({
400
- messages,
401
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
402
- model: 'gpt-4o-mini',
403
- provider: 'openai',
404
- });
405
- expect(response.isFunctionCall).toEqual(true);
406
- });
407
-
408
- streamSpy.mockRestore();
409
- });
410
-
411
- it('should handle text chunks during streaming', async () => {
412
- const { result } = renderHook(() => useChatStore());
413
- const messages = [createMockMessage({ role: 'user' })];
414
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
415
-
416
- const streamSpy = vi
417
- .spyOn(chatService, 'createAssistantMessageStream')
418
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
419
- await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
420
- await onMessageHandle?.({ type: 'text', text: ' World' } as any);
421
- await onFinish?.('Hello World', {} as any);
422
- });
423
-
424
- await act(async () => {
425
- await result.current.internal_fetchAIChatMessage({
426
- messages,
427
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
428
- model: 'gpt-4o-mini',
429
- provider: 'openai',
430
- });
431
- });
432
-
433
- expect(dispatchSpy).toHaveBeenCalledWith(
434
- expect.objectContaining({
435
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
436
- type: 'updateMessage',
437
- value: expect.objectContaining({ content: 'Hello' }),
438
- }),
439
- );
440
-
441
- streamSpy.mockRestore();
442
- });
443
-
444
- it('should handle reasoning chunks during streaming', async () => {
445
- const { result } = renderHook(() => useChatStore());
446
- const messages = [createMockMessage({ role: 'user' })];
447
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
448
-
449
- const streamSpy = vi
450
- .spyOn(chatService, 'createAssistantMessageStream')
451
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
452
- await onMessageHandle?.({ type: 'reasoning', text: 'Thinking...' } as any);
453
- await onMessageHandle?.({ type: 'text', text: 'Answer' } as any);
454
- await onFinish?.('Answer', {} as any);
455
- });
456
-
457
- await act(async () => {
458
- await result.current.internal_fetchAIChatMessage({
459
- messages,
460
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
461
- model: 'gpt-4o-mini',
462
- provider: 'openai',
463
- });
464
- });
465
-
466
- expect(dispatchSpy).toHaveBeenCalledWith(
467
- expect.objectContaining({
468
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
469
- type: 'updateMessage',
470
- value: expect.objectContaining({ reasoning: { content: 'Thinking...' } }),
471
- }),
472
- );
473
-
474
- streamSpy.mockRestore();
475
- });
476
-
477
- it('should skip grounding when citations are empty', async () => {
478
- const { result } = renderHook(() => useChatStore());
479
- const messages = [createMockMessage({ role: 'user' })];
480
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
481
-
482
- const streamSpy = vi
483
- .spyOn(chatService, 'createAssistantMessageStream')
484
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
485
- await onMessageHandle?.({
486
- type: 'grounding',
487
- grounding: { citations: [], searchQueries: [] },
488
- } as any);
489
- await onFinish?.('Answer', {} as any);
490
- });
491
-
492
- await act(async () => {
493
- await result.current.internal_fetchAIChatMessage({
494
- messages,
495
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
496
- model: 'gpt-4o-mini',
497
- provider: 'openai',
498
- });
499
- });
500
-
501
- // Should not dispatch when citations are empty
502
- const groundingCalls = dispatchSpy.mock.calls.filter((call) => {
503
- const dispatch = call[0];
504
- return dispatch?.type === 'updateMessage' && 'value' in dispatch && dispatch.value?.search;
505
- });
506
- expect(groundingCalls).toHaveLength(0);
507
-
508
- streamSpy.mockRestore();
509
- });
510
-
511
- it('should handle grounding chunks during streaming', async () => {
512
- const { result } = renderHook(() => useChatStore());
513
- const messages = [createMockMessage({ role: 'user' })];
514
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
515
-
516
- const streamSpy = vi
517
- .spyOn(chatService, 'createAssistantMessageStream')
518
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
519
- await onMessageHandle?.({
520
- type: 'grounding',
521
- grounding: {
522
- citations: [{ url: 'https://example.com', title: 'Example' }],
523
- searchQueries: ['test query'],
524
- },
525
- } as any);
526
- await onFinish?.('Answer', {} as any);
527
- });
528
-
529
- await act(async () => {
530
- await result.current.internal_fetchAIChatMessage({
531
- messages,
532
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
533
- model: 'gpt-4o-mini',
534
- provider: 'openai',
535
- });
536
- });
537
-
538
- expect(dispatchSpy).toHaveBeenCalledWith(
539
- expect.objectContaining({
540
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
541
- type: 'updateMessage',
542
- value: expect.objectContaining({
543
- search: expect.objectContaining({
544
- citations: expect.any(Array),
545
- }),
546
- }),
547
- }),
548
- );
549
-
550
- streamSpy.mockRestore();
551
- });
552
-
553
- it('should handle base64 image chunks during streaming', async () => {
554
- const { result } = renderHook(() => useChatStore());
555
- const messages = [createMockMessage({ role: 'user' })];
556
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
557
-
558
- const streamSpy = vi
559
- .spyOn(chatService, 'createAssistantMessageStream')
560
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
561
- await onMessageHandle?.({
562
- type: 'base64_image',
563
- image: { id: 'img-1', data: 'base64data' },
564
- images: [{ id: 'img-1', data: 'base64data' }],
565
- } as any);
566
- await onFinish?.('Answer', {} as any);
567
- });
568
-
569
- await act(async () => {
570
- await result.current.internal_fetchAIChatMessage({
571
- messages,
572
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
573
- model: 'gpt-4o-mini',
574
- provider: 'openai',
575
- });
576
- });
577
-
578
- expect(dispatchSpy).toHaveBeenCalledWith(
579
- expect.objectContaining({
580
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
581
- type: 'updateMessage',
582
- value: expect.objectContaining({
583
- imageList: expect.any(Array),
584
- }),
585
- }),
586
- );
587
-
588
- streamSpy.mockRestore();
589
- });
590
-
591
- it('should handle empty tool call arguments', async () => {
592
- const { result } = renderHook(() => useChatStore());
593
- const messages = [createMockMessage({ role: 'user' })];
594
-
595
- const streamSpy = vi
596
- .spyOn(chatService, 'createAssistantMessageStream')
597
- .mockImplementation(async ({ onFinish }) => {
598
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {
599
- toolCalls: [
600
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '' } },
601
- ],
602
- } as any);
603
- });
604
-
605
- await act(async () => {
606
- const response = await result.current.internal_fetchAIChatMessage({
607
- messages,
608
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
609
- model: 'gpt-4o-mini',
610
- provider: 'openai',
611
- });
612
- expect(response.isFunctionCall).toEqual(true);
613
- });
614
-
615
- streamSpy.mockRestore();
616
- });
617
-
618
- it('should update message with traceId when provided in onFinish', async () => {
619
- const { result } = renderHook(() => useChatStore());
620
- const messages = [createMockMessage({ role: 'user' })];
621
- const traceId = 'test-trace-123';
622
-
623
- const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
624
- const streamSpy = vi
625
- .spyOn(chatService, 'createAssistantMessageStream')
626
- .mockImplementation(async ({ onFinish }) => {
627
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, { traceId } as any);
628
- });
629
-
630
- await act(async () => {
631
- await result.current.internal_fetchAIChatMessage({
632
- messages,
633
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
634
- model: 'gpt-4o-mini',
635
- provider: 'openai',
636
- });
637
- });
638
-
639
- expect(updateMessageSpy).toHaveBeenCalledWith(
640
- TEST_IDS.ASSISTANT_MESSAGE_ID,
641
- expect.objectContaining({ traceId }),
642
- );
643
-
644
- streamSpy.mockRestore();
645
- });
646
- });
647
-
648
- describe('internal_resendMessage', () => {
649
- it('should not resend when message does not exist', async () => {
650
- const { result } = renderHook(() => useChatStore());
651
- const coreProcessSpy = vi.fn();
652
-
653
- act(() => {
654
- setupStoreWithMessages([]);
655
- useChatStore.setState({ internal_coreProcessMessage: coreProcessSpy });
656
- });
657
-
658
- await act(async () => {
659
- await result.current.internal_resendMessage('non-existent-id');
660
- });
661
-
662
- expect(coreProcessSpy).not.toHaveBeenCalled();
663
- expect(result.current.refreshMessages).not.toHaveBeenCalled();
664
- });
665
-
666
- describe('context generation', () => {
667
- it('should generate correct context for user role message', async () => {
668
- const { result } = renderHook(() => useChatStore());
669
- const messages = [
670
- createMockMessage({ id: 'msg-1', role: 'system' }),
671
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'user', meta: { avatar: '😀' } }),
672
- createMockMessage({ id: 'msg-3', role: 'assistant' }),
673
- ];
674
-
675
- act(() => {
676
- useChatStore.setState({
677
- messagesMap: {
678
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
679
- },
680
- });
681
- });
682
-
683
- await act(async () => {
684
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
685
- });
686
-
687
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
688
- expect.arrayContaining([
689
- expect.objectContaining({ id: 'msg-1' }),
690
- expect.objectContaining({ id: TEST_IDS.MESSAGE_ID }),
691
- ]),
692
- TEST_IDS.MESSAGE_ID,
693
- expect.objectContaining({ traceId: undefined }),
694
- );
695
- });
696
-
697
- it('should generate correct context for assistant role message', async () => {
698
- const { result } = renderHook(() => useChatStore());
699
- const parentId = 'msg-2';
700
- const messages = [
701
- createMockMessage({ id: 'msg-1', role: 'system' }),
702
- createMockMessage({ id: parentId, role: 'user', meta: { avatar: '😀' } }),
703
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'assistant', parentId }),
704
- ];
705
-
706
- act(() => {
707
- useChatStore.setState({
708
- messagesMap: {
709
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
710
- },
711
- });
712
- });
713
-
714
- await act(async () => {
715
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
716
- });
717
-
718
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
719
- expect.arrayContaining([
720
- expect.objectContaining({ id: 'msg-1' }),
721
- expect.objectContaining({ id: parentId }),
722
- ]),
723
- parentId,
724
- expect.objectContaining({ traceId: undefined }),
725
- );
726
- });
727
-
728
- it('should not process when context is empty', async () => {
729
- const { result } = renderHook(() => useChatStore());
730
-
731
- act(() => {
732
- useChatStore.setState({
733
- messagesMap: {
734
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: [],
735
- },
736
- });
737
- });
738
-
739
- await act(async () => {
740
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
741
- });
742
-
743
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
744
- });
745
-
746
- it('should generate correct context for tool role message', async () => {
747
- const { result } = renderHook(() => useChatStore());
748
- const messages = [
749
- createMockMessage({ id: 'msg-1', role: 'user' }),
750
- createMockMessage({ id: 'msg-2', role: 'assistant' }),
751
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'tool' }),
752
- ];
753
-
754
- act(() => {
755
- useChatStore.setState({
756
- messagesMap: {
757
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
758
- },
759
- });
760
- });
761
-
762
- await act(async () => {
763
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
764
- });
765
-
766
- // For tool role, it processes all messages up to tool but uses last user message as parentId
767
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
768
- expect.any(Array),
769
- 'msg-1', // parentId is the last user message
770
- expect.objectContaining({ traceId: undefined }),
771
- );
772
- });
773
- });
774
- });
775
-
776
- describe('internal_toggleChatLoading', () => {
777
- it('should enable loading state with new abort controller', () => {
778
- const { result } = renderHook(() => useChatStore());
779
-
780
- act(() => {
781
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'test-action');
782
- });
783
-
784
- const state = useChatStore.getState();
785
- expect(state.chatLoadingIdsAbortController).toBeInstanceOf(AbortController);
786
- expect(state.chatLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
787
- });
788
-
789
- it('should disable loading state and clear abort controller', () => {
790
- const { result } = renderHook(() => useChatStore());
791
-
792
- act(() => {
793
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'start');
794
- result.current.internal_toggleChatLoading(false, undefined, 'stop');
795
- });
796
-
797
- const state = useChatStore.getState();
798
- expect(state.chatLoadingIdsAbortController).toBeUndefined();
799
- expect(state.chatLoadingIds).toEqual([]);
800
- });
801
-
802
- it('should manage beforeunload event listener', () => {
803
- const { result } = renderHook(() => useChatStore());
804
- const addListenerSpy = vi.spyOn(window, 'addEventListener');
805
- const removeListenerSpy = vi.spyOn(window, 'removeEventListener');
806
-
807
- act(() => {
808
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'start');
809
- });
810
-
811
- expect(addListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
812
-
813
- act(() => {
814
- result.current.internal_toggleChatLoading(false, undefined, 'stop');
815
- });
816
-
817
- expect(removeListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
818
- });
819
-
820
- it('should reuse existing abort controller', () => {
821
- const existingController = new AbortController();
822
-
823
- act(() => {
824
- useChatStore.setState({ chatLoadingIdsAbortController: existingController });
825
- });
826
-
827
- const { result } = renderHook(() => useChatStore());
828
-
829
- act(() => {
830
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'test');
831
- });
832
-
833
- const state = useChatStore.getState();
834
- expect(state.chatLoadingIdsAbortController).toStrictEqual(existingController);
835
- });
836
- });
837
-
838
- describe('internal_toggleToolCallingStreaming', () => {
839
- it('should track tool calling stream status', () => {
840
- const { result } = renderHook(() => useChatStore());
841
-
842
- act(() => {
843
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, [true]);
844
- });
845
-
846
- expect(result.current.toolCallingStreamIds[TEST_IDS.MESSAGE_ID]).toEqual([true]);
847
- });
848
-
849
- it('should clear tool calling stream status', () => {
850
- const { result } = renderHook(() => useChatStore());
851
-
852
- act(() => {
853
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, [true]);
854
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, undefined);
855
- });
856
-
857
- expect(result.current.toolCallingStreamIds[TEST_IDS.MESSAGE_ID]).toBeUndefined();
858
- });
859
- });
860
-
861
- describe('internal_toggleSearchWorkflow', () => {
862
- it('should enable search workflow loading state', () => {
863
- const { result } = renderHook(() => useChatStore());
864
-
865
- act(() => {
866
- result.current.internal_toggleSearchWorkflow(true, TEST_IDS.MESSAGE_ID);
867
- });
868
-
869
- const state = useChatStore.getState();
870
- expect(state.searchWorkflowLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
871
- });
872
-
873
- it('should disable search workflow loading state', () => {
874
- const { result } = renderHook(() => useChatStore());
875
-
876
- act(() => {
877
- result.current.internal_toggleSearchWorkflow(true, TEST_IDS.MESSAGE_ID);
878
- result.current.internal_toggleSearchWorkflow(false, TEST_IDS.MESSAGE_ID);
879
- });
880
-
881
- const state = useChatStore.getState();
882
- expect(state.searchWorkflowLoadingIds).toEqual([]);
883
- });
884
- });
885
-
886
- describe('internal_toggleChatReasoning', () => {
887
- it('should enable reasoning loading state', () => {
888
- const { result } = renderHook(() => useChatStore());
889
-
890
- act(() => {
891
- result.current.internal_toggleChatReasoning(true, TEST_IDS.MESSAGE_ID, 'test-action');
892
- });
893
-
894
- const state = useChatStore.getState();
895
- expect(state.reasoningLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
896
- });
897
-
898
- it('should disable reasoning loading state', () => {
899
- const { result } = renderHook(() => useChatStore());
900
-
901
- act(() => {
902
- result.current.internal_toggleChatReasoning(true, TEST_IDS.MESSAGE_ID, 'start');
903
- result.current.internal_toggleChatReasoning(false, TEST_IDS.MESSAGE_ID, 'stop');
904
- });
905
-
906
- const state = useChatStore.getState();
907
- expect(state.reasoningLoadingIds).toEqual([]);
908
- });
909
- });
910
-
911
- describe('internal_toggleMessageInToolsCalling', () => {
912
- it('should enable tools calling state', () => {
913
- const { result } = renderHook(() => useChatStore());
914
-
915
- act(() => {
916
- result.current.internal_toggleMessageInToolsCalling(true, TEST_IDS.MESSAGE_ID);
917
- });
918
-
919
- const state = useChatStore.getState();
920
- expect(state.messageInToolsCallingIds).toEqual([TEST_IDS.MESSAGE_ID]);
921
- });
922
-
923
- it('should disable tools calling state', () => {
924
- const { result } = renderHook(() => useChatStore());
925
-
926
- act(() => {
927
- result.current.internal_toggleMessageInToolsCalling(true, TEST_IDS.MESSAGE_ID);
928
- result.current.internal_toggleMessageInToolsCalling(false, TEST_IDS.MESSAGE_ID);
929
- });
930
-
931
- const state = useChatStore.getState();
932
- expect(state.messageInToolsCallingIds).toEqual([]);
933
- });
934
- });
935
-
936
- describe('internal_resendMessage with custom params', () => {
937
- it('should use provided messages instead of store messages', async () => {
938
- const { result } = renderHook(() => useChatStore());
939
- const customMessages = [createMockMessage({ id: 'custom-msg', role: 'user' })];
940
-
941
- await act(async () => {
942
- await result.current.internal_resendMessage('custom-msg', { messages: customMessages });
943
- });
944
-
945
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
946
- expect.arrayContaining([expect.objectContaining({ id: 'custom-msg' })]),
947
- 'custom-msg',
948
- expect.anything(),
949
- );
950
- });
951
-
952
- it('should handle assistant message without parentId', async () => {
953
- const { result } = renderHook(() => useChatStore());
954
- const messages = [
955
- createMockMessage({ id: 'msg-1', role: 'user' }),
956
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'assistant', parentId: undefined }),
957
- ];
958
-
959
- act(() => {
960
- useChatStore.setState({
961
- messagesMap: {
962
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
963
- },
964
- });
965
- });
966
-
967
- await act(async () => {
968
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
969
- });
970
-
971
- // Should handle the case where parentId is not found
972
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalled();
973
- });
974
- });
975
- });