@lobehub/chat 1.22.3 → 1.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/features/ChatInput/useSend.ts +1 -1
- package/src/store/chat/initialState.ts +3 -0
- package/src/store/chat/slices/aiChat/action.test.ts +946 -0
- package/src/store/chat/slices/aiChat/action.ts +536 -0
- package/src/store/chat/slices/aiChat/initialState.ts +27 -0
- package/src/store/chat/slices/enchance/action.test.ts +1 -1
- package/src/store/chat/slices/message/action.test.ts +49 -900
- package/src/store/chat/slices/message/action.ts +8 -506
- package/src/store/chat/slices/message/initialState.ts +1 -22
- package/src/store/chat/slices/message/selectors.test.ts +1 -1
- package/src/store/chat/slices/message/selectors.ts +1 -1
- package/src/store/chat/slices/plugin/action.test.ts +1 -1
- package/src/store/chat/slices/share/action.test.ts +1 -1
- package/src/store/chat/slices/topic/action.test.ts +1 -1
- package/src/store/chat/store.ts +3 -0
- /package/src/store/chat/slices/{message → aiChat}/actions/rag.ts +0 -0
- /package/src/store/chat/{slices/message/utils.ts → utils/messageMapKey.ts} +0 -0
@@ -1,20 +1,12 @@
|
|
1
1
|
import * as lobeUIModules from '@lobehub/ui';
|
2
2
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
3
|
-
import
|
3
|
+
import { mutate } from 'swr';
|
4
4
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
5
5
|
|
6
|
-
import { LOADING_FLAT } from '@/const/message';
|
7
|
-
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
8
6
|
import { TraceEventType } from '@/const/trace';
|
9
|
-
import { chatService } from '@/services/chat';
|
10
7
|
import { messageService } from '@/services/message';
|
11
8
|
import { topicService } from '@/services/topic';
|
12
|
-
import {
|
13
|
-
import { agentSelectors } from '@/store/agent/selectors';
|
14
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
15
|
-
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
16
|
-
import { sessionMetaSelectors } from '@/store/session/selectors';
|
17
|
-
import { UploadFileItem } from '@/types/files/upload';
|
9
|
+
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
18
10
|
import { ChatMessage } from '@/types/message';
|
19
11
|
|
20
12
|
import { useChatStore } from '../../store';
|
@@ -44,18 +36,7 @@ vi.mock('@/services/topic', () => ({
|
|
44
36
|
removeTopic: vi.fn(() => Promise.resolve()),
|
45
37
|
},
|
46
38
|
}));
|
47
|
-
vi.mock('@/services/chat', async (importOriginal) => {
|
48
|
-
const module = await importOriginal();
|
49
|
-
|
50
|
-
return {
|
51
|
-
chatService: {
|
52
|
-
createAssistantMessage: vi.fn(() => Promise.resolve('assistant-message')),
|
53
|
-
createAssistantMessageStream: (module as any).chatService.createAssistantMessageStream,
|
54
|
-
},
|
55
|
-
};
|
56
|
-
});
|
57
39
|
|
58
|
-
const realCoreProcessMessage = useChatStore.getState().internal_coreProcessMessage;
|
59
40
|
const realRefreshMessages = useChatStore.getState().refreshMessages;
|
60
41
|
// Mock state
|
61
42
|
const mockState = {
|
@@ -71,11 +52,6 @@ const mockState = {
|
|
71
52
|
beforeEach(() => {
|
72
53
|
vi.clearAllMocks();
|
73
54
|
useChatStore.setState(mockState, false);
|
74
|
-
vi.spyOn(agentSelectors, 'currentAgentConfig').mockImplementation(() => DEFAULT_AGENT_CONFIG);
|
75
|
-
vi.spyOn(agentSelectors, 'currentAgentChatConfig').mockImplementation(
|
76
|
-
() => DEFAULT_AGENT_CHAT_CONFIG,
|
77
|
-
);
|
78
|
-
vi.spyOn(sessionMetaSelectors, 'currentAgentMeta').mockImplementation(() => ({ tags: [] }));
|
79
55
|
});
|
80
56
|
|
81
57
|
afterEach(() => {
|
@@ -177,74 +153,42 @@ describe('chatMessage actions', () => {
|
|
177
153
|
});
|
178
154
|
});
|
179
155
|
|
180
|
-
describe('
|
181
|
-
it('
|
182
|
-
const { result } = renderHook(() => useChatStore());
|
156
|
+
describe('copyMessage', () => {
|
157
|
+
it('should call copyToClipboard with correct content', async () => {
|
183
158
|
const messageId = 'message-id';
|
184
|
-
const
|
185
|
-
const
|
159
|
+
const content = 'Test content';
|
160
|
+
const { result } = renderHook(() => useChatStore());
|
161
|
+
const copyToClipboardSpy = vi.spyOn(lobeUIModules, 'copyToClipboard');
|
186
162
|
|
187
|
-
act(() => {
|
188
|
-
useChatStore.setState({
|
189
|
-
activeId: 'session-id',
|
190
|
-
activeTopicId: undefined,
|
191
|
-
messagesMap: {
|
192
|
-
[messageMapKey('session-id')]: [
|
193
|
-
{
|
194
|
-
id: messageId,
|
195
|
-
role: 'assistant',
|
196
|
-
tools: [{ id: 'tool1' }, { id: 'tool2' }],
|
197
|
-
} as ChatMessage,
|
198
|
-
{ id: '2', parentId: messageId, tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
|
199
|
-
{ id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
|
200
|
-
],
|
201
|
-
},
|
202
|
-
});
|
203
|
-
});
|
204
163
|
await act(async () => {
|
205
|
-
await result.current.
|
164
|
+
await result.current.copyMessage(messageId, content);
|
206
165
|
});
|
207
166
|
|
208
|
-
expect(
|
209
|
-
expect(updateMessageSpy).toHaveBeenCalledWith('message-id', {
|
210
|
-
tools: [{ id: 'tool2' }],
|
211
|
-
});
|
212
|
-
expect(result.current.refreshMessages).toHaveBeenCalled();
|
167
|
+
expect(copyToClipboardSpy).toHaveBeenCalledWith(content);
|
213
168
|
});
|
214
|
-
});
|
215
169
|
|
216
|
-
|
217
|
-
it('should remove a message and create a new message', async () => {
|
218
|
-
const { result } = renderHook(() => useChatStore());
|
170
|
+
it('should call internal_traceMessage with correct parameters', async () => {
|
219
171
|
const messageId = 'message-id';
|
220
|
-
const
|
221
|
-
const
|
172
|
+
const content = 'Test content';
|
173
|
+
const { result } = renderHook(() => useChatStore());
|
174
|
+
const internal_traceMessageSpy = vi.spyOn(result.current, 'internal_traceMessage');
|
222
175
|
|
223
|
-
act(() => {
|
224
|
-
useChatStore.setState({
|
225
|
-
activeId: 'session-id',
|
226
|
-
activeTopicId: undefined,
|
227
|
-
messagesMap: {
|
228
|
-
[messageMapKey('session-id')]: [
|
229
|
-
{ id: messageId, tools: [{ id: 'tool1' }, { id: 'tool2' }] } as ChatMessage,
|
230
|
-
],
|
231
|
-
},
|
232
|
-
});
|
233
|
-
});
|
234
176
|
await act(async () => {
|
235
|
-
await result.current.
|
177
|
+
await result.current.copyMessage(messageId, content);
|
236
178
|
});
|
237
179
|
|
238
|
-
expect(
|
239
|
-
|
240
|
-
|
180
|
+
expect(internal_traceMessageSpy).toHaveBeenCalledWith(messageId, {
|
181
|
+
eventType: TraceEventType.CopyMessage,
|
182
|
+
});
|
241
183
|
});
|
242
184
|
});
|
243
|
-
|
244
|
-
|
185
|
+
|
186
|
+
describe('deleteToolMessage', () => {
|
187
|
+
it('deleteMessage should remove a message by id', async () => {
|
245
188
|
const { result } = renderHook(() => useChatStore());
|
246
189
|
const messageId = 'message-id';
|
247
|
-
const
|
190
|
+
const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
|
191
|
+
const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
|
248
192
|
|
249
193
|
act(() => {
|
250
194
|
useChatStore.setState({
|
@@ -254,18 +198,24 @@ describe('chatMessage actions', () => {
|
|
254
198
|
[messageMapKey('session-id')]: [
|
255
199
|
{
|
256
200
|
id: messageId,
|
201
|
+
role: 'assistant',
|
257
202
|
tools: [{ id: 'tool1' }, { id: 'tool2' }],
|
258
|
-
traceId: 'abc',
|
259
203
|
} as ChatMessage,
|
204
|
+
{ id: '2', parentId: messageId, tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
|
205
|
+
{ id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
|
260
206
|
],
|
261
207
|
},
|
262
208
|
});
|
263
209
|
});
|
264
210
|
await act(async () => {
|
265
|
-
await result.current.
|
211
|
+
await result.current.deleteToolMessage('2');
|
266
212
|
});
|
267
213
|
|
268
|
-
expect(
|
214
|
+
expect(removeMessageSpy).toHaveBeenCalled();
|
215
|
+
expect(updateMessageSpy).toHaveBeenCalledWith('message-id', {
|
216
|
+
tools: [{ id: 'tool2' }],
|
217
|
+
});
|
218
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
269
219
|
});
|
270
220
|
});
|
271
221
|
|
@@ -293,35 +243,17 @@ describe('chatMessage actions', () => {
|
|
293
243
|
|
294
244
|
expect(result.current.inputMessage).toEqual(newInputMessage);
|
295
245
|
});
|
296
|
-
});
|
297
|
-
|
298
|
-
describe('copyMessage', () => {
|
299
|
-
it('should call copyToClipboard with correct content', async () => {
|
300
|
-
const messageId = 'message-id';
|
301
|
-
const content = 'Test content';
|
302
|
-
const { result } = renderHook(() => useChatStore());
|
303
|
-
const copyToClipboardSpy = vi.spyOn(lobeUIModules, 'copyToClipboard');
|
304
|
-
|
305
|
-
await act(async () => {
|
306
|
-
await result.current.copyMessage(messageId, content);
|
307
|
-
});
|
308
246
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
it('should call internal_traceMessage with correct parameters', async () => {
|
313
|
-
const messageId = 'message-id';
|
314
|
-
const content = 'Test content';
|
247
|
+
it('should not update state if message is the same as current inputMessage', () => {
|
248
|
+
const inputMessage = 'Test input message';
|
249
|
+
useChatStore.setState({ inputMessage });
|
315
250
|
const { result } = renderHook(() => useChatStore());
|
316
|
-
const internal_traceMessageSpy = vi.spyOn(result.current, 'internal_traceMessage');
|
317
251
|
|
318
|
-
|
319
|
-
|
252
|
+
act(() => {
|
253
|
+
result.current.updateInputMessage(inputMessage);
|
320
254
|
});
|
321
255
|
|
322
|
-
expect(
|
323
|
-
eventType: TraceEventType.CopyMessage,
|
324
|
-
});
|
256
|
+
expect(result.current.inputMessage).toBe(inputMessage);
|
325
257
|
});
|
326
258
|
});
|
327
259
|
|
@@ -383,387 +315,7 @@ describe('chatMessage actions', () => {
|
|
383
315
|
});
|
384
316
|
});
|
385
317
|
|
386
|
-
describe('
|
387
|
-
it('should not send message if there is no active session', async () => {
|
388
|
-
useChatStore.setState({ activeId: undefined });
|
389
|
-
const { result } = renderHook(() => useChatStore());
|
390
|
-
const message = 'Test message';
|
391
|
-
|
392
|
-
await act(async () => {
|
393
|
-
await result.current.sendMessage({ message });
|
394
|
-
});
|
395
|
-
|
396
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
397
|
-
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
398
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
399
|
-
});
|
400
|
-
|
401
|
-
it('should not send message if message is empty and there are no files', async () => {
|
402
|
-
const { result } = renderHook(() => useChatStore());
|
403
|
-
const message = '';
|
404
|
-
|
405
|
-
await act(async () => {
|
406
|
-
await result.current.sendMessage({ message });
|
407
|
-
});
|
408
|
-
|
409
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
410
|
-
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
411
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
412
|
-
});
|
413
|
-
|
414
|
-
it('should not send message if message is empty and there are empty files', async () => {
|
415
|
-
const { result } = renderHook(() => useChatStore());
|
416
|
-
const message = '';
|
417
|
-
|
418
|
-
await act(async () => {
|
419
|
-
await result.current.sendMessage({ message, files: [] });
|
420
|
-
});
|
421
|
-
|
422
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
423
|
-
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
424
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
425
|
-
});
|
426
|
-
|
427
|
-
it('should create message and call internal_coreProcessMessage if message or files are provided', async () => {
|
428
|
-
const { result } = renderHook(() => useChatStore());
|
429
|
-
const message = 'Test message';
|
430
|
-
const files = [{ id: 'file-id' } as UploadFileItem];
|
431
|
-
|
432
|
-
// Mock messageService.create to resolve with a message id
|
433
|
-
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
434
|
-
|
435
|
-
await act(async () => {
|
436
|
-
await result.current.sendMessage({ message, files });
|
437
|
-
});
|
438
|
-
|
439
|
-
expect(messageService.createMessage).toHaveBeenCalledWith({
|
440
|
-
content: message,
|
441
|
-
files: files.map((f) => f.id),
|
442
|
-
role: 'user',
|
443
|
-
sessionId: mockState.activeId,
|
444
|
-
topicId: mockState.activeTopicId,
|
445
|
-
});
|
446
|
-
expect(result.current.internal_coreProcessMessage).toHaveBeenCalled();
|
447
|
-
});
|
448
|
-
|
449
|
-
describe('auto-create topic', () => {
|
450
|
-
it('should not auto-create topic if enableAutoCreateTopic is false', async () => {
|
451
|
-
const { result } = renderHook(() => useChatStore());
|
452
|
-
const message = 'Test message';
|
453
|
-
const autoCreateTopicThreshold = 5;
|
454
|
-
const enableAutoCreateTopic = false;
|
455
|
-
|
456
|
-
// Mock messageService.create to resolve with a message id
|
457
|
-
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
458
|
-
|
459
|
-
// Mock agent config to simulate auto-create topic behavior
|
460
|
-
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => ({
|
461
|
-
autoCreateTopicThreshold,
|
462
|
-
enableAutoCreateTopic,
|
463
|
-
}));
|
464
|
-
|
465
|
-
// Mock saveToTopic and switchTopic to simulate not being called
|
466
|
-
const saveToTopicMock = vi.fn();
|
467
|
-
const switchTopicMock = vi.fn();
|
468
|
-
|
469
|
-
await act(async () => {
|
470
|
-
useChatStore.setState({
|
471
|
-
...mockState,
|
472
|
-
// Mock the currentChats selector to return a list that does not reach the threshold
|
473
|
-
messagesMap: {
|
474
|
-
[messageMapKey('session-id')]: Array.from(
|
475
|
-
{ length: autoCreateTopicThreshold + 1 },
|
476
|
-
(_, i) => ({
|
477
|
-
id: `msg-${i}`,
|
478
|
-
}),
|
479
|
-
) as any,
|
480
|
-
},
|
481
|
-
activeTopicId: undefined,
|
482
|
-
saveToTopic: saveToTopicMock,
|
483
|
-
switchTopic: switchTopicMock,
|
484
|
-
});
|
485
|
-
|
486
|
-
await result.current.sendMessage({ message });
|
487
|
-
});
|
488
|
-
|
489
|
-
expect(saveToTopicMock).not.toHaveBeenCalled();
|
490
|
-
expect(switchTopicMock).not.toHaveBeenCalled();
|
491
|
-
});
|
492
|
-
|
493
|
-
it('should auto-create topic and switch to it if enabled and threshold is reached', async () => {
|
494
|
-
const { result } = renderHook(() => useChatStore());
|
495
|
-
const message = 'Test message';
|
496
|
-
const autoCreateTopicThreshold = 5;
|
497
|
-
const enableAutoCreateTopic = true;
|
498
|
-
|
499
|
-
// Mock agent config to simulate auto-create topic behavior
|
500
|
-
(agentSelectors.currentAgentConfig as Mock).mockImplementation(() => ({
|
501
|
-
autoCreateTopicThreshold,
|
502
|
-
enableAutoCreateTopic,
|
503
|
-
}));
|
504
|
-
|
505
|
-
// Mock messageService.create to resolve with a message id
|
506
|
-
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
507
|
-
|
508
|
-
// Mock saveToTopic to resolve with a topic id and switchTopic to switch to the new topic
|
509
|
-
const createTopicMock = vi.fn(() => Promise.resolve('new-topic-id'));
|
510
|
-
const switchTopicMock = vi.fn();
|
511
|
-
|
512
|
-
act(() => {
|
513
|
-
useChatStore.setState({
|
514
|
-
...mockState,
|
515
|
-
activeId: 'session_id',
|
516
|
-
messagesMap: {
|
517
|
-
[messageMapKey('session_id')]: Array.from(
|
518
|
-
{ length: autoCreateTopicThreshold },
|
519
|
-
(_, i) => ({
|
520
|
-
id: `msg-${i}`,
|
521
|
-
}),
|
522
|
-
) as any,
|
523
|
-
},
|
524
|
-
activeTopicId: undefined,
|
525
|
-
createTopic: createTopicMock,
|
526
|
-
switchTopic: switchTopicMock,
|
527
|
-
});
|
528
|
-
});
|
529
|
-
|
530
|
-
await act(async () => {
|
531
|
-
await result.current.sendMessage({ message });
|
532
|
-
});
|
533
|
-
|
534
|
-
expect(createTopicMock).toHaveBeenCalled();
|
535
|
-
expect(switchTopicMock).toHaveBeenCalledWith('new-topic-id', true);
|
536
|
-
});
|
537
|
-
|
538
|
-
it('should not auto-create topic, if autoCreateTopic = false and reached topic threshold', async () => {
|
539
|
-
const { result } = renderHook(() => useChatStore());
|
540
|
-
act(() => {
|
541
|
-
useAgentStore.setState({
|
542
|
-
activeId: 'abc',
|
543
|
-
agentMap: {
|
544
|
-
abc: {
|
545
|
-
chatConfig: {
|
546
|
-
enableAutoCreateTopic: false,
|
547
|
-
autoCreateTopicThreshold: 1,
|
548
|
-
},
|
549
|
-
},
|
550
|
-
},
|
551
|
-
});
|
552
|
-
|
553
|
-
useChatStore.setState({
|
554
|
-
// Mock the currentChats selector to return a list that does not reach the threshold
|
555
|
-
messagesMap: {
|
556
|
-
[messageMapKey('inbox')]: [{ id: '1' }, { id: '2' }] as ChatMessage[],
|
557
|
-
},
|
558
|
-
activeTopicId: 'inbox',
|
559
|
-
});
|
560
|
-
});
|
561
|
-
|
562
|
-
await act(async () => {
|
563
|
-
await result.current.sendMessage({ message: 'test' });
|
564
|
-
});
|
565
|
-
|
566
|
-
expect(topicService.createTopic).not.toHaveBeenCalled();
|
567
|
-
});
|
568
|
-
|
569
|
-
it('should not auto-create topic if autoCreateTopicThreshold is not reached', async () => {
|
570
|
-
const { result } = renderHook(() => useChatStore());
|
571
|
-
const message = 'Test message';
|
572
|
-
const autoCreateTopicThreshold = 5;
|
573
|
-
const enableAutoCreateTopic = true;
|
574
|
-
|
575
|
-
// Mock messageService.create to resolve with a message id
|
576
|
-
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
577
|
-
|
578
|
-
// Mock agent config to simulate auto-create topic behavior
|
579
|
-
(agentSelectors.currentAgentChatConfig as Mock).mockImplementation(() => ({
|
580
|
-
autoCreateTopicThreshold,
|
581
|
-
enableAutoCreateTopic,
|
582
|
-
}));
|
583
|
-
|
584
|
-
// Mock saveToTopic and switchTopic to simulate not being called
|
585
|
-
const createTopicMock = vi.fn();
|
586
|
-
const switchTopicMock = vi.fn();
|
587
|
-
|
588
|
-
await act(async () => {
|
589
|
-
useChatStore.setState({
|
590
|
-
...mockState,
|
591
|
-
activeId: 'session_id',
|
592
|
-
messagesMap: {
|
593
|
-
// Mock the currentChats selector to return a list that does not reach the threshold
|
594
|
-
[messageMapKey('session_id')]: Array.from(
|
595
|
-
{ length: autoCreateTopicThreshold - 3 },
|
596
|
-
(_, i) => ({
|
597
|
-
id: `msg-${i}`,
|
598
|
-
}),
|
599
|
-
) as any,
|
600
|
-
},
|
601
|
-
activeTopicId: undefined,
|
602
|
-
createTopic: createTopicMock,
|
603
|
-
switchTopic: switchTopicMock,
|
604
|
-
});
|
605
|
-
|
606
|
-
await result.current.sendMessage({ message });
|
607
|
-
});
|
608
|
-
|
609
|
-
expect(createTopicMock).not.toHaveBeenCalled();
|
610
|
-
expect(switchTopicMock).not.toHaveBeenCalled();
|
611
|
-
});
|
612
|
-
});
|
613
|
-
|
614
|
-
it('should add user message and not call internal_coreProcessMessage if onlyAddUserMessage = true', async () => {
|
615
|
-
const { result } = renderHook(() => useChatStore());
|
616
|
-
|
617
|
-
await act(async () => {
|
618
|
-
await result.current.sendMessage({ message: 'test', onlyAddUserMessage: true });
|
619
|
-
});
|
620
|
-
|
621
|
-
expect(messageService.createMessage).toHaveBeenCalled();
|
622
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
623
|
-
});
|
624
|
-
|
625
|
-
it('当 isWelcomeQuestion 为 true 时,正确地传递给 internal_coreProcessMessage', async () => {
|
626
|
-
const { result } = renderHook(() => useChatStore());
|
627
|
-
|
628
|
-
await act(async () => {
|
629
|
-
await result.current.sendMessage({ message: 'test', isWelcomeQuestion: true });
|
630
|
-
});
|
631
|
-
|
632
|
-
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
633
|
-
expect.anything(),
|
634
|
-
expect.anything(),
|
635
|
-
{ isWelcomeQuestion: true },
|
636
|
-
);
|
637
|
-
});
|
638
|
-
|
639
|
-
it('当只有文件而没有消息内容时,正确发送消息', async () => {
|
640
|
-
const { result } = renderHook(() => useChatStore());
|
641
|
-
|
642
|
-
await act(async () => {
|
643
|
-
await result.current.sendMessage({ message: '', files: [{ id: 'file-1' }] as any });
|
644
|
-
});
|
645
|
-
|
646
|
-
expect(messageService.createMessage).toHaveBeenCalledWith({
|
647
|
-
content: '',
|
648
|
-
files: ['file-1'],
|
649
|
-
role: 'user',
|
650
|
-
sessionId: 'session-id',
|
651
|
-
topicId: 'topic-id',
|
652
|
-
});
|
653
|
-
});
|
654
|
-
|
655
|
-
it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
|
656
|
-
const { result } = renderHook(() => useChatStore());
|
657
|
-
|
658
|
-
await act(async () => {
|
659
|
-
await result.current.sendMessage({ message: 'test', files: [{ id: 'file-1' }] as any });
|
660
|
-
});
|
661
|
-
|
662
|
-
expect(messageService.createMessage).toHaveBeenCalledWith({
|
663
|
-
content: 'test',
|
664
|
-
files: ['file-1'],
|
665
|
-
role: 'user',
|
666
|
-
sessionId: 'session-id',
|
667
|
-
topicId: 'topic-id',
|
668
|
-
});
|
669
|
-
});
|
670
|
-
|
671
|
-
it('当 createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
|
672
|
-
const { result } = renderHook(() => useChatStore());
|
673
|
-
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
|
674
|
-
new Error('create message error'),
|
675
|
-
);
|
676
|
-
|
677
|
-
await expect(result.current.sendMessage({ message: 'test' })).rejects.toThrow(
|
678
|
-
'create message error',
|
679
|
-
);
|
680
|
-
|
681
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
682
|
-
});
|
683
|
-
|
684
|
-
// it('自动创建主题成功后,正确地将消息复制到新主题,并删除之前的临时消息', async () => {
|
685
|
-
// const { result } = renderHook(() => useChatStore());
|
686
|
-
// act(() => {
|
687
|
-
// useAgentStore.setState({
|
688
|
-
// agentConfig: { enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 },
|
689
|
-
// });
|
690
|
-
//
|
691
|
-
// useChatStore.setState({
|
692
|
-
// // Mock the currentChats selector to return a list that does not reach the threshold
|
693
|
-
// messagesMap: {
|
694
|
-
// [messageMapKey('inbox')]: [{ id: '1' }, { id: '2' }] as ChatMessage[],
|
695
|
-
// },
|
696
|
-
// activeId: 'inbox',
|
697
|
-
// });
|
698
|
-
// });
|
699
|
-
// vi.spyOn(topicService, 'createTopic').mockResolvedValue('new-topic');
|
700
|
-
//
|
701
|
-
// await act(async () => {
|
702
|
-
// await result.current.sendMessage({ message: 'test' });
|
703
|
-
// });
|
704
|
-
//
|
705
|
-
// expect(result.current.messagesMap[messageMapKey('inbox')]).toEqual([
|
706
|
-
// // { id: '1' },
|
707
|
-
// // { id: '2' },
|
708
|
-
// // { id: 'temp-id', content: 'test', role: 'user' },
|
709
|
-
// ]);
|
710
|
-
// // expect(result.current.getMessages('session-id')).toEqual([]);
|
711
|
-
// });
|
712
|
-
|
713
|
-
// it('自动创建主题失败时,正确地处理错误,不会影响后续的消息发送', async () => {
|
714
|
-
// const { result } = renderHook(() => useChatStore());
|
715
|
-
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 });
|
716
|
-
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
717
|
-
// vi.spyOn(topicService, 'createTopic').mockRejectedValue(new Error('create topic error'));
|
718
|
-
//
|
719
|
-
// await act(async () => {
|
720
|
-
// await result.current.sendMessage({ message: 'test' });
|
721
|
-
// });
|
722
|
-
//
|
723
|
-
// expect(result.current.getMessages('session-id')).toEqual([
|
724
|
-
// { id: '1' },
|
725
|
-
// { id: '2' },
|
726
|
-
// { id: 'new-message-id', content: 'test', role: 'user' },
|
727
|
-
// ]);
|
728
|
-
// });
|
729
|
-
|
730
|
-
// it('当 activeTopicId 不存在且 autoCreateTopic 为 true,但消息数量未达到阈值时,正确地总结主题标题', async () => {
|
731
|
-
// const { result } = renderHook(() => useChatStore());
|
732
|
-
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 10 });
|
733
|
-
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
734
|
-
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
735
|
-
//
|
736
|
-
// await act(async () => {
|
737
|
-
// await result.current.sendMessage({ message: 'test' });
|
738
|
-
// });
|
739
|
-
//
|
740
|
-
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
741
|
-
// { id: '1' },
|
742
|
-
// { id: '2' },
|
743
|
-
// { id: 'new-message-id', content: 'test', role: 'user' },
|
744
|
-
// { id: 'assistant-message', role: 'assistant' },
|
745
|
-
// ]);
|
746
|
-
// });
|
747
|
-
//
|
748
|
-
// it('当 activeTopicId 存在且主题标题为空时,正确地总结主题标题', async () => {
|
749
|
-
// const { result } = renderHook(() => useChatStore());
|
750
|
-
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
751
|
-
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any, 'session-id', 'topic-1');
|
752
|
-
//
|
753
|
-
// await act(async () => {
|
754
|
-
// await result.current.sendMessage({ message: 'test' });
|
755
|
-
// });
|
756
|
-
//
|
757
|
-
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
758
|
-
// { id: '1' },
|
759
|
-
// { id: '2' },
|
760
|
-
// { id: 'new-message-id', content: 'test', role: 'user' },
|
761
|
-
// { id: 'assistant-message', role: 'assistant' },
|
762
|
-
// ]);
|
763
|
-
// });
|
764
|
-
});
|
765
|
-
|
766
|
-
describe('toggleMessageEditing action', () => {
|
318
|
+
describe('toggleMessageEditing ', () => {
|
767
319
|
it('should add message id to messageEditingIds when editing is true', () => {
|
768
320
|
const { result } = renderHook(() => useChatStore());
|
769
321
|
const messageId = 'message-id';
|
@@ -786,67 +338,32 @@ describe('chatMessage actions', () => {
|
|
786
338
|
|
787
339
|
expect(result.current.messageEditingIds).not.toContain(messageId);
|
788
340
|
});
|
789
|
-
});
|
790
341
|
|
791
|
-
|
792
|
-
it('should resend a message by id and refresh messages', async () => {
|
793
|
-
const { result } = renderHook(() => useChatStore());
|
342
|
+
it('should update messageEditingIds correctly when enabling editing', () => {
|
794
343
|
const messageId = 'message-id';
|
344
|
+
const { result } = renderHook(() => useChatStore());
|
795
345
|
|
796
346
|
act(() => {
|
797
|
-
|
798
|
-
activeId: 'session-id',
|
799
|
-
activeTopicId: undefined,
|
800
|
-
// Mock the currentChats selector to return a list that includes the message to be resent
|
801
|
-
messagesMap: {
|
802
|
-
[messageMapKey('session-id')]: [
|
803
|
-
{ id: messageId, role: 'user', content: 'Resend this message' } as ChatMessage,
|
804
|
-
],
|
805
|
-
},
|
806
|
-
});
|
807
|
-
});
|
808
|
-
|
809
|
-
// Mock the internal_coreProcessMessage function to resolve immediately
|
810
|
-
mockState.internal_coreProcessMessage.mockResolvedValue(undefined);
|
811
|
-
|
812
|
-
await act(async () => {
|
813
|
-
await result.current.internal_resendMessage(messageId);
|
347
|
+
result.current.toggleMessageEditing(messageId, true);
|
814
348
|
});
|
815
349
|
|
816
|
-
expect(
|
817
|
-
expect(mockState.internal_coreProcessMessage).toHaveBeenCalledWith(
|
818
|
-
expect.any(Array),
|
819
|
-
messageId,
|
820
|
-
{},
|
821
|
-
);
|
350
|
+
expect(result.current.messageEditingIds).toContain(messageId);
|
822
351
|
});
|
823
352
|
|
824
|
-
it('should
|
353
|
+
it('should update messageEditingIds correctly when disabling editing', () => {
|
354
|
+
const messageId = 'message-id';
|
355
|
+
useChatStore.setState({ messageEditingIds: [messageId] });
|
825
356
|
const { result } = renderHook(() => useChatStore());
|
826
|
-
const messageId = 'non-existing-message-id';
|
827
357
|
|
828
358
|
act(() => {
|
829
|
-
|
830
|
-
activeId: 'session-id',
|
831
|
-
activeTopicId: undefined,
|
832
|
-
// Mock the currentChats selector to return a list that does not include the message to be resent
|
833
|
-
messagesMap: {
|
834
|
-
[messageMapKey('session-id')]: [],
|
835
|
-
},
|
836
|
-
});
|
837
|
-
});
|
838
|
-
|
839
|
-
await act(async () => {
|
840
|
-
await result.current.internal_resendMessage(messageId);
|
359
|
+
result.current.toggleMessageEditing(messageId, false);
|
841
360
|
});
|
842
361
|
|
843
|
-
expect(
|
844
|
-
expect(mockState.internal_coreProcessMessage).not.toHaveBeenCalled();
|
845
|
-
expect(mockState.refreshMessages).not.toHaveBeenCalled();
|
362
|
+
expect(result.current.messageEditingIds).not.toContain(messageId);
|
846
363
|
});
|
847
364
|
});
|
848
365
|
|
849
|
-
describe('internal_updateMessageContent
|
366
|
+
describe('internal_updateMessageContent', () => {
|
850
367
|
it('should call messageService.internal_updateMessageContent with correct parameters', async () => {
|
851
368
|
const { result } = renderHook(() => useChatStore());
|
852
369
|
const messageId = 'message-id';
|
@@ -889,113 +406,6 @@ describe('chatMessage actions', () => {
|
|
889
406
|
});
|
890
407
|
});
|
891
408
|
|
892
|
-
describe('internal_coreProcessMessage action', () => {
|
893
|
-
it('should handle the core AI message processing', async () => {
|
894
|
-
useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
|
895
|
-
|
896
|
-
const { result } = renderHook(() => useChatStore());
|
897
|
-
const userMessage = {
|
898
|
-
id: 'user-message-id',
|
899
|
-
role: 'user',
|
900
|
-
content: 'Hello, world!',
|
901
|
-
sessionId: mockState.activeId,
|
902
|
-
topicId: mockState.activeTopicId,
|
903
|
-
} as ChatMessage;
|
904
|
-
const messages = [userMessage];
|
905
|
-
|
906
|
-
// 模拟 AI 响应
|
907
|
-
const aiResponse = 'Hello, human!';
|
908
|
-
(chatService.createAssistantMessage as Mock).mockResolvedValue(aiResponse);
|
909
|
-
const spy = vi.spyOn(chatService, 'createAssistantMessageStream');
|
910
|
-
// 模拟消息创建
|
911
|
-
(messageService.createMessage as Mock).mockResolvedValue('assistant-message-id');
|
912
|
-
|
913
|
-
await act(async () => {
|
914
|
-
await result.current.internal_coreProcessMessage(messages, userMessage.id);
|
915
|
-
});
|
916
|
-
|
917
|
-
// 验证是否创建了代表 AI 响应的消息
|
918
|
-
expect(messageService.createMessage).toHaveBeenCalledWith(
|
919
|
-
expect.objectContaining({
|
920
|
-
role: 'assistant',
|
921
|
-
content: LOADING_FLAT,
|
922
|
-
fromModel: expect.anything(),
|
923
|
-
parentId: userMessage.id,
|
924
|
-
sessionId: mockState.activeId,
|
925
|
-
topicId: mockState.activeTopicId,
|
926
|
-
}),
|
927
|
-
);
|
928
|
-
|
929
|
-
// 验证 AI 服务是否被调用
|
930
|
-
expect(spy).toHaveBeenCalled();
|
931
|
-
|
932
|
-
// 验证消息列表是否刷新
|
933
|
-
expect(mockState.refreshMessages).toHaveBeenCalled();
|
934
|
-
});
|
935
|
-
});
|
936
|
-
|
937
|
-
describe('stopGenerateMessage action', () => {
|
938
|
-
it('should stop generating message and set loading states correctly', async () => {
|
939
|
-
const { result } = renderHook(() => useChatStore());
|
940
|
-
const internal_toggleChatLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
941
|
-
const abortController = new AbortController();
|
942
|
-
|
943
|
-
act(() => {
|
944
|
-
useChatStore.setState({ abortController });
|
945
|
-
});
|
946
|
-
|
947
|
-
await act(async () => {
|
948
|
-
result.current.stopGenerateMessage();
|
949
|
-
});
|
950
|
-
|
951
|
-
expect(abortController.signal.aborted).toBe(true);
|
952
|
-
expect(internal_toggleChatLoadingSpy).toHaveBeenCalledWith(
|
953
|
-
false,
|
954
|
-
undefined,
|
955
|
-
expect.any(String),
|
956
|
-
);
|
957
|
-
});
|
958
|
-
|
959
|
-
it('should not do anything if there is no abortController', async () => {
|
960
|
-
const { result } = renderHook(() => useChatStore());
|
961
|
-
|
962
|
-
await act(async () => {
|
963
|
-
// 确保没有设置 abortController
|
964
|
-
useChatStore.setState({ abortController: undefined });
|
965
|
-
|
966
|
-
result.current.stopGenerateMessage();
|
967
|
-
});
|
968
|
-
|
969
|
-
// 由于没有 abortController,不应调用任何方法
|
970
|
-
expect(result.current.abortController).toBeUndefined();
|
971
|
-
});
|
972
|
-
});
|
973
|
-
|
974
|
-
describe('toggleMessageEditing', () => {
|
975
|
-
it('should update messageEditingIds correctly when enabling editing', () => {
|
976
|
-
const messageId = 'message-id';
|
977
|
-
const { result } = renderHook(() => useChatStore());
|
978
|
-
|
979
|
-
act(() => {
|
980
|
-
result.current.toggleMessageEditing(messageId, true);
|
981
|
-
});
|
982
|
-
|
983
|
-
expect(result.current.messageEditingIds).toContain(messageId);
|
984
|
-
});
|
985
|
-
|
986
|
-
it('should update messageEditingIds correctly when disabling editing', () => {
|
987
|
-
const messageId = 'message-id';
|
988
|
-
useChatStore.setState({ messageEditingIds: [messageId] });
|
989
|
-
const { result } = renderHook(() => useChatStore());
|
990
|
-
|
991
|
-
act(() => {
|
992
|
-
result.current.toggleMessageEditing(messageId, false);
|
993
|
-
});
|
994
|
-
|
995
|
-
expect(result.current.messageEditingIds).not.toContain(messageId);
|
996
|
-
});
|
997
|
-
});
|
998
|
-
|
999
409
|
describe('refreshMessages action', () => {
|
1000
410
|
beforeEach(() => {
|
1001
411
|
vi.mock('swr', async () => {
|
@@ -1065,181 +475,7 @@ describe('chatMessage actions', () => {
|
|
1065
475
|
});
|
1066
476
|
});
|
1067
477
|
|
1068
|
-
describe('
|
1069
|
-
it('should fetch AI chat message and return content', async () => {
|
1070
|
-
const { result } = renderHook(() => useChatStore());
|
1071
|
-
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
1072
|
-
const assistantMessageId = 'assistant-message-id';
|
1073
|
-
const aiResponse = 'Hello, human!';
|
1074
|
-
|
1075
|
-
(fetch as Mock).mockResolvedValueOnce(new Response(aiResponse));
|
1076
|
-
|
1077
|
-
await act(async () => {
|
1078
|
-
const response = await result.current.internal_fetchAIChatMessage(
|
1079
|
-
messages,
|
1080
|
-
assistantMessageId,
|
1081
|
-
);
|
1082
|
-
expect(response.isFunctionCall).toEqual(false);
|
1083
|
-
});
|
1084
|
-
});
|
1085
|
-
|
1086
|
-
it('should handle errors during AI response fetching', async () => {
|
1087
|
-
const { result } = renderHook(() => useChatStore());
|
1088
|
-
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
1089
|
-
const assistantMessageId = 'assistant-message-id';
|
1090
|
-
|
1091
|
-
// Mock fetch to reject with an error
|
1092
|
-
const errorMessage = 'Error fetching AI response';
|
1093
|
-
vi.mocked(fetch).mockRejectedValueOnce(new Error(errorMessage));
|
1094
|
-
|
1095
|
-
await act(async () => {
|
1096
|
-
expect(
|
1097
|
-
await result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
|
1098
|
-
).toEqual({
|
1099
|
-
isFunctionCall: false,
|
1100
|
-
});
|
1101
|
-
});
|
1102
|
-
});
|
1103
|
-
|
1104
|
-
it('should generate correct contextMessages for "user" role', async () => {
|
1105
|
-
const messageId = 'message-id';
|
1106
|
-
const messages = [
|
1107
|
-
{ id: 'msg-1', role: 'system' },
|
1108
|
-
{ id: messageId, role: 'user', meta: { avatar: '😀' } },
|
1109
|
-
{ id: 'msg-3', role: 'assistant' },
|
1110
|
-
];
|
1111
|
-
act(() => {
|
1112
|
-
useChatStore.setState({
|
1113
|
-
messagesMap: {
|
1114
|
-
[chatSelectors.currentChatKey(mockState as any)]: messages as ChatMessage[],
|
1115
|
-
},
|
1116
|
-
});
|
1117
|
-
});
|
1118
|
-
const { result } = renderHook(() => useChatStore());
|
1119
|
-
|
1120
|
-
await act(async () => {
|
1121
|
-
await result.current.internal_resendMessage(messageId);
|
1122
|
-
});
|
1123
|
-
|
1124
|
-
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
1125
|
-
messages.slice(0, 2),
|
1126
|
-
messageId,
|
1127
|
-
{ traceId: undefined },
|
1128
|
-
);
|
1129
|
-
});
|
1130
|
-
|
1131
|
-
it('should generate correct contextMessages for "assistant" role', async () => {
|
1132
|
-
const messageId = 'message-id';
|
1133
|
-
const messages = [
|
1134
|
-
{ id: 'msg-1', role: 'system' },
|
1135
|
-
{ id: 'msg-2', role: 'user', meta: { avatar: '😀' } },
|
1136
|
-
{ id: messageId, role: 'assistant', parentId: 'msg-2' },
|
1137
|
-
];
|
1138
|
-
useChatStore.setState({
|
1139
|
-
messagesMap: {
|
1140
|
-
[chatSelectors.currentChatKey(mockState as any)]: messages as ChatMessage[],
|
1141
|
-
},
|
1142
|
-
});
|
1143
|
-
const { result } = renderHook(() => useChatStore());
|
1144
|
-
|
1145
|
-
await act(async () => {
|
1146
|
-
await result.current.internal_resendMessage(messageId);
|
1147
|
-
});
|
1148
|
-
|
1149
|
-
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
1150
|
-
messages.slice(0, 2),
|
1151
|
-
'msg-2',
|
1152
|
-
{ traceId: undefined },
|
1153
|
-
);
|
1154
|
-
});
|
1155
|
-
|
1156
|
-
it('should return early if contextMessages is empty', async () => {
|
1157
|
-
const messageId = 'message-id';
|
1158
|
-
useChatStore.setState({
|
1159
|
-
messagesMap: { [chatSelectors.currentChatKey(mockState as any)]: [] },
|
1160
|
-
});
|
1161
|
-
const { result } = renderHook(() => useChatStore());
|
1162
|
-
|
1163
|
-
await act(async () => {
|
1164
|
-
await result.current.internal_resendMessage(messageId);
|
1165
|
-
});
|
1166
|
-
|
1167
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
1168
|
-
});
|
1169
|
-
});
|
1170
|
-
|
1171
|
-
describe('internal_toggleChatLoading', () => {
|
1172
|
-
it('should set loading state and create an AbortController when loading is true', () => {
|
1173
|
-
const { result } = renderHook(() => useChatStore());
|
1174
|
-
const action = 'loading-action';
|
1175
|
-
|
1176
|
-
act(() => {
|
1177
|
-
result.current.internal_toggleChatLoading(true, 'message-id', action);
|
1178
|
-
});
|
1179
|
-
|
1180
|
-
const state = useChatStore.getState();
|
1181
|
-
expect(state.abortController).toBeInstanceOf(AbortController);
|
1182
|
-
expect(state.chatLoadingIds).toEqual(['message-id']);
|
1183
|
-
});
|
1184
|
-
|
1185
|
-
it('should clear loading state and abort controller when loading is false', () => {
|
1186
|
-
const { result } = renderHook(() => useChatStore());
|
1187
|
-
const action = 'stop-loading-action';
|
1188
|
-
|
1189
|
-
// Set initial loading state
|
1190
|
-
act(() => {
|
1191
|
-
result.current.internal_toggleChatLoading(true, 'message-id', 'start-loading-action');
|
1192
|
-
});
|
1193
|
-
|
1194
|
-
// Stop loading
|
1195
|
-
act(() => {
|
1196
|
-
result.current.internal_toggleChatLoading(false, undefined, action);
|
1197
|
-
});
|
1198
|
-
|
1199
|
-
const state = useChatStore.getState();
|
1200
|
-
expect(state.abortController).toBeUndefined();
|
1201
|
-
expect(state.chatLoadingIds).toEqual([]);
|
1202
|
-
});
|
1203
|
-
|
1204
|
-
it('should attach beforeunload event listener when loading starts', () => {
|
1205
|
-
const { result } = renderHook(() => useChatStore());
|
1206
|
-
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
1207
|
-
|
1208
|
-
act(() => {
|
1209
|
-
result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
|
1210
|
-
});
|
1211
|
-
|
1212
|
-
expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
|
1213
|
-
});
|
1214
|
-
|
1215
|
-
it('should remove beforeunload event listener when loading stops', () => {
|
1216
|
-
const { result } = renderHook(() => useChatStore());
|
1217
|
-
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
1218
|
-
|
1219
|
-
// Start and then stop loading to trigger the removal of the event listener
|
1220
|
-
act(() => {
|
1221
|
-
result.current.internal_toggleChatLoading(true, 'message-id', 'start-loading-action');
|
1222
|
-
result.current.internal_toggleChatLoading(false, undefined, 'stop-loading-action');
|
1223
|
-
});
|
1224
|
-
|
1225
|
-
expect(removeEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
|
1226
|
-
});
|
1227
|
-
|
1228
|
-
it('should not create a new AbortController if one already exists', () => {
|
1229
|
-
const { result } = renderHook(() => useChatStore());
|
1230
|
-
const abortController = new AbortController();
|
1231
|
-
|
1232
|
-
act(() => {
|
1233
|
-
useChatStore.setState({ abortController });
|
1234
|
-
result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
|
1235
|
-
});
|
1236
|
-
|
1237
|
-
const state = useChatStore.getState();
|
1238
|
-
expect(state.abortController).toEqual(abortController);
|
1239
|
-
});
|
1240
|
-
});
|
1241
|
-
|
1242
|
-
describe('internal_toggleMessageLoading action', () => {
|
478
|
+
describe('internal_toggleMessageLoading', () => {
|
1243
479
|
it('should add message id to messageLoadingIds when loading is true', () => {
|
1244
480
|
const { result } = renderHook(() => useChatStore());
|
1245
481
|
const messageId = 'message-id';
|
@@ -1264,93 +500,6 @@ describe('chatMessage actions', () => {
|
|
1264
500
|
});
|
1265
501
|
});
|
1266
502
|
|
1267
|
-
describe('internal_toggleToolCallingStreaming action', () => {
|
1268
|
-
it('should add message id to messageLoadingIds when loading is true', () => {
|
1269
|
-
const { result } = renderHook(() => useChatStore());
|
1270
|
-
const messageId = 'message-id';
|
1271
|
-
|
1272
|
-
act(() => {
|
1273
|
-
result.current.internal_toggleToolCallingStreaming(messageId, [true]);
|
1274
|
-
});
|
1275
|
-
|
1276
|
-
expect(result.current.toolCallingStreamIds[messageId]).toEqual([true]);
|
1277
|
-
});
|
1278
|
-
|
1279
|
-
it('should remove message id from messageLoadingIds when loading is false', () => {
|
1280
|
-
const { result } = renderHook(() => useChatStore());
|
1281
|
-
const messageId = 'ddd-id';
|
1282
|
-
|
1283
|
-
act(() => {
|
1284
|
-
result.current.internal_toggleToolCallingStreaming(messageId, [true]);
|
1285
|
-
result.current.internal_toggleToolCallingStreaming(messageId, undefined);
|
1286
|
-
});
|
1287
|
-
|
1288
|
-
expect(result.current.toolCallingStreamIds[messageId]).toBeUndefined();
|
1289
|
-
});
|
1290
|
-
});
|
1291
|
-
|
1292
|
-
describe('stopGenerateMessage', () => {
|
1293
|
-
it('should return early if abortController is undefined', () => {
|
1294
|
-
act(() => {
|
1295
|
-
useChatStore.setState({ abortController: undefined });
|
1296
|
-
});
|
1297
|
-
|
1298
|
-
const { result } = renderHook(() => useChatStore());
|
1299
|
-
|
1300
|
-
const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
1301
|
-
|
1302
|
-
act(() => {
|
1303
|
-
result.current.stopGenerateMessage();
|
1304
|
-
});
|
1305
|
-
|
1306
|
-
expect(spy).not.toHaveBeenCalled();
|
1307
|
-
});
|
1308
|
-
|
1309
|
-
it('should call abortController.abort()', () => {
|
1310
|
-
const abortMock = vi.fn();
|
1311
|
-
const abortController = { abort: abortMock } as unknown as AbortController;
|
1312
|
-
act(() => {
|
1313
|
-
useChatStore.setState({ abortController });
|
1314
|
-
});
|
1315
|
-
const { result } = renderHook(() => useChatStore());
|
1316
|
-
|
1317
|
-
act(() => {
|
1318
|
-
result.current.stopGenerateMessage();
|
1319
|
-
});
|
1320
|
-
|
1321
|
-
expect(abortMock).toHaveBeenCalled();
|
1322
|
-
});
|
1323
|
-
|
1324
|
-
it('should call internal_toggleChatLoading with correct parameters', () => {
|
1325
|
-
const abortController = new AbortController();
|
1326
|
-
act(() => {
|
1327
|
-
useChatStore.setState({ abortController });
|
1328
|
-
});
|
1329
|
-
const { result } = renderHook(() => useChatStore());
|
1330
|
-
const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
1331
|
-
|
1332
|
-
act(() => {
|
1333
|
-
result.current.stopGenerateMessage();
|
1334
|
-
});
|
1335
|
-
|
1336
|
-
expect(spy).toHaveBeenCalledWith(false, undefined, expect.any(String));
|
1337
|
-
});
|
1338
|
-
});
|
1339
|
-
|
1340
|
-
describe('updateInputMessage', () => {
|
1341
|
-
it('should not update state if message is the same as current inputMessage', () => {
|
1342
|
-
const inputMessage = 'Test input message';
|
1343
|
-
useChatStore.setState({ inputMessage });
|
1344
|
-
const { result } = renderHook(() => useChatStore());
|
1345
|
-
|
1346
|
-
act(() => {
|
1347
|
-
result.current.updateInputMessage(inputMessage);
|
1348
|
-
});
|
1349
|
-
|
1350
|
-
expect(result.current.inputMessage).toBe(inputMessage);
|
1351
|
-
});
|
1352
|
-
});
|
1353
|
-
|
1354
503
|
describe('modifyMessageContent', () => {
|
1355
504
|
it('should call internal_traceMessage with correct parameters before updating', async () => {
|
1356
505
|
const messageId = 'message-id';
|