@lobehub/chat 0.159.10 → 0.159.12
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 +50 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/docs/usage/start.mdx +1 -1
- package/docs/usage/start.zh-CN.mdx +1 -1
- package/package.json +1 -1
- package/src/app/(main)/chat/@session/features/SessionHydration.tsx +2 -0
- package/src/config/modelProviders/google.ts +12 -2
- package/src/features/Conversation/Error/index.tsx +13 -4
- package/src/features/Conversation/components/VirtualizedList/index.tsx +27 -21
- package/src/services/message/type.ts +6 -3
- package/src/store/chat/slices/enchance/action.test.ts +16 -12
- package/src/store/chat/slices/message/action.test.ts +495 -24
- package/src/store/chat/slices/message/action.ts +143 -32
- package/src/store/chat/slices/message/initialState.ts +2 -2
- package/src/store/chat/slices/message/selectors.test.ts +39 -9
- package/src/store/chat/slices/message/selectors.ts +13 -3
- package/src/store/chat/slices/message/utils.ts +7 -0
- package/src/store/chat/slices/plugin/action.test.ts +7 -2
- package/src/store/chat/slices/share/action.test.ts +19 -3
- package/src/store/chat/slices/topic/action.test.ts +13 -2
- package/src/store/chat/slices/topic/action.ts +27 -6
- package/src/store/chat/slices/topic/initialState.ts +2 -0
- package/src/utils/fetch.test.ts +17 -11
- package/src/utils/fetch.ts +20 -22
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import * as lobeUIModules from '@lobehub/ui';
|
|
1
2
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
2
3
|
import useSWR, { mutate } from 'swr';
|
|
3
4
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
|
|
5
6
|
import { LOADING_FLAT } from '@/const/message';
|
|
6
7
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
8
|
+
import { TraceEventType } from '@/const/trace';
|
|
7
9
|
import { chatService } from '@/services/chat';
|
|
8
10
|
import { messageService } from '@/services/message';
|
|
9
11
|
import { topicService } from '@/services/topic';
|
|
12
|
+
import { useAgentStore } from '@/store/agent';
|
|
10
13
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
11
14
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
15
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
12
16
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
|
13
17
|
import { ChatMessage } from '@/types/message';
|
|
14
18
|
|
|
@@ -34,6 +38,7 @@ vi.mock('@/services/message', () => ({
|
|
|
34
38
|
}));
|
|
35
39
|
vi.mock('@/services/topic', () => ({
|
|
36
40
|
topicService: {
|
|
41
|
+
createTopic: vi.fn(() => Promise.resolve()),
|
|
37
42
|
removeTopic: vi.fn(() => Promise.resolve()),
|
|
38
43
|
},
|
|
39
44
|
}));
|
|
@@ -75,6 +80,48 @@ afterEach(() => {
|
|
|
75
80
|
});
|
|
76
81
|
|
|
77
82
|
describe('chatMessage actions', () => {
|
|
83
|
+
describe('addAIMessage', () => {
|
|
84
|
+
it('should return early if activeId is undefined', async () => {
|
|
85
|
+
useChatStore.setState({ activeId: undefined });
|
|
86
|
+
const { result } = renderHook(() => useChatStore());
|
|
87
|
+
const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
|
|
88
|
+
|
|
89
|
+
await act(async () => {
|
|
90
|
+
await result.current.addAIMessage();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
94
|
+
expect(updateInputMessageSpy).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should call internal_createMessage with correct parameters', async () => {
|
|
98
|
+
const inputMessage = 'Test input message';
|
|
99
|
+
useChatStore.setState({ inputMessage });
|
|
100
|
+
const { result } = renderHook(() => useChatStore());
|
|
101
|
+
|
|
102
|
+
await act(async () => {
|
|
103
|
+
await result.current.addAIMessage();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
107
|
+
content: inputMessage,
|
|
108
|
+
role: 'assistant',
|
|
109
|
+
sessionId: mockState.activeId,
|
|
110
|
+
topicId: mockState.activeTopicId,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should call updateInputMessage with empty string', async () => {
|
|
115
|
+
const { result } = renderHook(() => useChatStore());
|
|
116
|
+
const updateInputMessageSpy = vi.spyOn(result.current, 'updateInputMessage');
|
|
117
|
+
await act(async () => {
|
|
118
|
+
await result.current.addAIMessage();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(updateInputMessageSpy).toHaveBeenCalledWith('');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
78
125
|
describe('deleteMessage', () => {
|
|
79
126
|
it('deleteMessage should remove a message by id', async () => {
|
|
80
127
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -82,7 +129,13 @@ describe('chatMessage actions', () => {
|
|
|
82
129
|
const deleteSpy = vi.spyOn(result.current, 'deleteMessage');
|
|
83
130
|
|
|
84
131
|
act(() => {
|
|
85
|
-
useChatStore.setState({
|
|
132
|
+
useChatStore.setState({
|
|
133
|
+
activeId: 'session-id',
|
|
134
|
+
activeTopicId: undefined,
|
|
135
|
+
messagesMap: {
|
|
136
|
+
[messageMapKey('session-id')]: [{ id: messageId } as ChatMessage],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
86
139
|
});
|
|
87
140
|
await act(async () => {
|
|
88
141
|
await result.current.deleteMessage(messageId);
|
|
@@ -119,6 +172,36 @@ describe('chatMessage actions', () => {
|
|
|
119
172
|
});
|
|
120
173
|
});
|
|
121
174
|
|
|
175
|
+
describe('copyMessage', () => {
|
|
176
|
+
it('should call copyToClipboard with correct content', async () => {
|
|
177
|
+
const messageId = 'message-id';
|
|
178
|
+
const content = 'Test content';
|
|
179
|
+
const { result } = renderHook(() => useChatStore());
|
|
180
|
+
const copyToClipboardSpy = vi.spyOn(lobeUIModules, 'copyToClipboard');
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
await result.current.copyMessage(messageId, content);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(copyToClipboardSpy).toHaveBeenCalledWith(content);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should call internal_traceMessage with correct parameters', async () => {
|
|
190
|
+
const messageId = 'message-id';
|
|
191
|
+
const content = 'Test content';
|
|
192
|
+
const { result } = renderHook(() => useChatStore());
|
|
193
|
+
const internal_traceMessageSpy = vi.spyOn(result.current, 'internal_traceMessage');
|
|
194
|
+
|
|
195
|
+
await act(async () => {
|
|
196
|
+
await result.current.copyMessage(messageId, content);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(internal_traceMessageSpy).toHaveBeenCalledWith(messageId, {
|
|
200
|
+
eventType: TraceEventType.CopyMessage,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
122
205
|
describe('clearMessage', () => {
|
|
123
206
|
beforeEach(() => {
|
|
124
207
|
vi.clearAllMocks(); // 清除 mocks
|
|
@@ -157,7 +240,7 @@ describe('chatMessage actions', () => {
|
|
|
157
240
|
expect(switchTopicSpy).toHaveBeenCalled();
|
|
158
241
|
|
|
159
242
|
// 检查 activeTopicId 是否被清除,需要在状态更新后进行检查
|
|
160
|
-
expect(useChatStore.getState().activeTopicId).
|
|
243
|
+
expect(useChatStore.getState().activeTopicId).toBeNull();
|
|
161
244
|
});
|
|
162
245
|
|
|
163
246
|
it('should call removeTopic if there is an activeTopicId', async () => {
|
|
@@ -237,7 +320,6 @@ describe('chatMessage actions', () => {
|
|
|
237
320
|
sessionId: mockState.activeId,
|
|
238
321
|
topicId: mockState.activeTopicId,
|
|
239
322
|
});
|
|
240
|
-
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
241
323
|
expect(result.current.internal_coreProcessMessage).toHaveBeenCalled();
|
|
242
324
|
});
|
|
243
325
|
|
|
@@ -265,9 +347,14 @@ describe('chatMessage actions', () => {
|
|
|
265
347
|
useChatStore.setState({
|
|
266
348
|
...mockState,
|
|
267
349
|
// Mock the currentChats selector to return a list that does not reach the threshold
|
|
268
|
-
|
|
269
|
-
id:
|
|
270
|
-
|
|
350
|
+
messagesMap: {
|
|
351
|
+
[messageMapKey('session-id')]: Array.from(
|
|
352
|
+
{ length: autoCreateTopicThreshold + 1 },
|
|
353
|
+
(_, i) => ({
|
|
354
|
+
id: `msg-${i}`,
|
|
355
|
+
}),
|
|
356
|
+
) as any,
|
|
357
|
+
},
|
|
271
358
|
activeTopicId: undefined,
|
|
272
359
|
saveToTopic: saveToTopicMock,
|
|
273
360
|
switchTopic: switchTopicMock,
|
|
@@ -296,17 +383,23 @@ describe('chatMessage actions', () => {
|
|
|
296
383
|
(messageService.createMessage as Mock).mockResolvedValue('new-message-id');
|
|
297
384
|
|
|
298
385
|
// Mock saveToTopic to resolve with a topic id and switchTopic to switch to the new topic
|
|
299
|
-
const
|
|
386
|
+
const createTopicMock = vi.fn(() => Promise.resolve('new-topic-id'));
|
|
300
387
|
const switchTopicMock = vi.fn();
|
|
301
388
|
|
|
302
389
|
act(() => {
|
|
303
390
|
useChatStore.setState({
|
|
304
391
|
...mockState,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
392
|
+
activeId: 'session_id',
|
|
393
|
+
messagesMap: {
|
|
394
|
+
[messageMapKey('session_id')]: Array.from(
|
|
395
|
+
{ length: autoCreateTopicThreshold },
|
|
396
|
+
(_, i) => ({
|
|
397
|
+
id: `msg-${i}`,
|
|
398
|
+
}),
|
|
399
|
+
) as any,
|
|
400
|
+
},
|
|
308
401
|
activeTopicId: undefined,
|
|
309
|
-
|
|
402
|
+
createTopic: createTopicMock,
|
|
310
403
|
switchTopic: switchTopicMock,
|
|
311
404
|
});
|
|
312
405
|
});
|
|
@@ -315,8 +408,33 @@ describe('chatMessage actions', () => {
|
|
|
315
408
|
await result.current.sendMessage({ message });
|
|
316
409
|
});
|
|
317
410
|
|
|
318
|
-
expect(
|
|
319
|
-
expect(switchTopicMock).toHaveBeenCalledWith('new-topic-id');
|
|
411
|
+
expect(createTopicMock).toHaveBeenCalled();
|
|
412
|
+
expect(switchTopicMock).toHaveBeenCalledWith('new-topic-id', true);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should not auto-create topic, if autoCreateTopic = false and reached topic threshold', async () => {
|
|
416
|
+
const { result } = renderHook(() => useChatStore());
|
|
417
|
+
act(() => {
|
|
418
|
+
useAgentStore.setState({
|
|
419
|
+
agentConfig: {
|
|
420
|
+
enableAutoCreateTopic: false,
|
|
421
|
+
autoCreateTopicThreshold: 1,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
useChatStore.setState({
|
|
425
|
+
// Mock the currentChats selector to return a list that does not reach the threshold
|
|
426
|
+
messagesMap: {
|
|
427
|
+
[messageMapKey('inbox')]: [{ id: '1' }, { id: '2' }] as ChatMessage[],
|
|
428
|
+
},
|
|
429
|
+
activeTopicId: 'inbox',
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await act(async () => {
|
|
434
|
+
await result.current.sendMessage({ message: 'test' });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(topicService.createTopic).not.toHaveBeenCalled();
|
|
320
438
|
});
|
|
321
439
|
|
|
322
440
|
it('should not auto-create topic if autoCreateTopicThreshold is not reached', async () => {
|
|
@@ -335,28 +453,185 @@ describe('chatMessage actions', () => {
|
|
|
335
453
|
}));
|
|
336
454
|
|
|
337
455
|
// Mock saveToTopic and switchTopic to simulate not being called
|
|
338
|
-
const
|
|
456
|
+
const createTopicMock = vi.fn();
|
|
339
457
|
const switchTopicMock = vi.fn();
|
|
340
458
|
|
|
341
459
|
await act(async () => {
|
|
342
460
|
useChatStore.setState({
|
|
343
461
|
...mockState,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
462
|
+
activeId: 'session_id',
|
|
463
|
+
messagesMap: {
|
|
464
|
+
// Mock the currentChats selector to return a list that does not reach the threshold
|
|
465
|
+
[messageMapKey('session_id')]: Array.from(
|
|
466
|
+
{ length: autoCreateTopicThreshold - 3 },
|
|
467
|
+
(_, i) => ({
|
|
468
|
+
id: `msg-${i}`,
|
|
469
|
+
}),
|
|
470
|
+
) as any,
|
|
471
|
+
},
|
|
348
472
|
activeTopicId: undefined,
|
|
349
|
-
|
|
473
|
+
createTopic: createTopicMock,
|
|
350
474
|
switchTopic: switchTopicMock,
|
|
351
475
|
});
|
|
352
476
|
|
|
353
477
|
await result.current.sendMessage({ message });
|
|
354
478
|
});
|
|
355
479
|
|
|
356
|
-
expect(
|
|
480
|
+
expect(createTopicMock).not.toHaveBeenCalled();
|
|
357
481
|
expect(switchTopicMock).not.toHaveBeenCalled();
|
|
358
482
|
});
|
|
359
483
|
});
|
|
484
|
+
|
|
485
|
+
it('should add user message and not call internal_coreProcessMessage if onlyAddUserMessage = true', async () => {
|
|
486
|
+
const { result } = renderHook(() => useChatStore());
|
|
487
|
+
|
|
488
|
+
await act(async () => {
|
|
489
|
+
await result.current.sendMessage({ message: 'test', onlyAddUserMessage: true });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(messageService.createMessage).toHaveBeenCalled();
|
|
493
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('当 isWelcomeQuestion 为 true 时,正确地传递给 internal_coreProcessMessage', async () => {
|
|
497
|
+
const { result } = renderHook(() => useChatStore());
|
|
498
|
+
|
|
499
|
+
await act(async () => {
|
|
500
|
+
await result.current.sendMessage({ message: 'test', isWelcomeQuestion: true });
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
|
504
|
+
expect.anything(),
|
|
505
|
+
expect.anything(),
|
|
506
|
+
{ isWelcomeQuestion: true },
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('当只有文件而没有消息内容时,正确发送消息', async () => {
|
|
511
|
+
const { result } = renderHook(() => useChatStore());
|
|
512
|
+
|
|
513
|
+
await act(async () => {
|
|
514
|
+
await result.current.sendMessage({ message: '', files: [{ id: 'file-1' }] as any });
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
518
|
+
content: '',
|
|
519
|
+
files: ['file-1'],
|
|
520
|
+
role: 'user',
|
|
521
|
+
sessionId: 'session-id',
|
|
522
|
+
topicId: 'topic-id',
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
|
|
527
|
+
const { result } = renderHook(() => useChatStore());
|
|
528
|
+
|
|
529
|
+
await act(async () => {
|
|
530
|
+
await result.current.sendMessage({ message: 'test', files: [{ id: 'file-1' }] as any });
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(messageService.createMessage).toHaveBeenCalledWith({
|
|
534
|
+
content: 'test',
|
|
535
|
+
files: ['file-1'],
|
|
536
|
+
role: 'user',
|
|
537
|
+
sessionId: 'session-id',
|
|
538
|
+
topicId: 'topic-id',
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('当 createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
|
|
543
|
+
const { result } = renderHook(() => useChatStore());
|
|
544
|
+
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
|
|
545
|
+
new Error('create message error'),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
await expect(result.current.sendMessage({ message: 'test' })).rejects.toThrow(
|
|
549
|
+
'create message error',
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// it('自动创建主题成功后,正确地将消息复制到新主题,并删除之前的临时消息', async () => {
|
|
556
|
+
// const { result } = renderHook(() => useChatStore());
|
|
557
|
+
// act(() => {
|
|
558
|
+
// useAgentStore.setState({
|
|
559
|
+
// agentConfig: { enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 },
|
|
560
|
+
// });
|
|
561
|
+
//
|
|
562
|
+
// useChatStore.setState({
|
|
563
|
+
// // Mock the currentChats selector to return a list that does not reach the threshold
|
|
564
|
+
// messagesMap: {
|
|
565
|
+
// [messageMapKey('inbox')]: [{ id: '1' }, { id: '2' }] as ChatMessage[],
|
|
566
|
+
// },
|
|
567
|
+
// activeId: 'inbox',
|
|
568
|
+
// });
|
|
569
|
+
// });
|
|
570
|
+
// vi.spyOn(topicService, 'createTopic').mockResolvedValue('new-topic');
|
|
571
|
+
//
|
|
572
|
+
// await act(async () => {
|
|
573
|
+
// await result.current.sendMessage({ message: 'test' });
|
|
574
|
+
// });
|
|
575
|
+
//
|
|
576
|
+
// expect(result.current.messagesMap[messageMapKey('inbox')]).toEqual([
|
|
577
|
+
// // { id: '1' },
|
|
578
|
+
// // { id: '2' },
|
|
579
|
+
// // { id: 'temp-id', content: 'test', role: 'user' },
|
|
580
|
+
// ]);
|
|
581
|
+
// // expect(result.current.getMessages('session-id')).toEqual([]);
|
|
582
|
+
// });
|
|
583
|
+
|
|
584
|
+
// it('自动创建主题失败时,正确地处理错误,不会影响后续的消息发送', async () => {
|
|
585
|
+
// const { result } = renderHook(() => useChatStore());
|
|
586
|
+
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 1 });
|
|
587
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
|
588
|
+
// vi.spyOn(topicService, 'createTopic').mockRejectedValue(new Error('create topic error'));
|
|
589
|
+
//
|
|
590
|
+
// await act(async () => {
|
|
591
|
+
// await result.current.sendMessage({ message: 'test' });
|
|
592
|
+
// });
|
|
593
|
+
//
|
|
594
|
+
// expect(result.current.getMessages('session-id')).toEqual([
|
|
595
|
+
// { id: '1' },
|
|
596
|
+
// { id: '2' },
|
|
597
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
|
598
|
+
// ]);
|
|
599
|
+
// });
|
|
600
|
+
|
|
601
|
+
// it('当 activeTopicId 不存在且 autoCreateTopic 为 true,但消息数量未达到阈值时,正确地总结主题标题', async () => {
|
|
602
|
+
// const { result } = renderHook(() => useChatStore());
|
|
603
|
+
// result.current.setAgentConfig({ enableAutoCreateTopic: true, autoCreateTopicThreshold: 10 });
|
|
604
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any);
|
|
605
|
+
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
|
606
|
+
//
|
|
607
|
+
// await act(async () => {
|
|
608
|
+
// await result.current.sendMessage({ message: 'test' });
|
|
609
|
+
// });
|
|
610
|
+
//
|
|
611
|
+
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
|
612
|
+
// { id: '1' },
|
|
613
|
+
// { id: '2' },
|
|
614
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
|
615
|
+
// { id: 'assistant-message', role: 'assistant' },
|
|
616
|
+
// ]);
|
|
617
|
+
// });
|
|
618
|
+
//
|
|
619
|
+
// it('当 activeTopicId 存在且主题标题为空时,正确地总结主题标题', async () => {
|
|
620
|
+
// const { result } = renderHook(() => useChatStore());
|
|
621
|
+
// result.current.setActiveTopic({ id: 'topic-1', title: '' });
|
|
622
|
+
// result.current.setMessages([{ id: '1' }, { id: '2' }] as any, 'session-id', 'topic-1');
|
|
623
|
+
//
|
|
624
|
+
// await act(async () => {
|
|
625
|
+
// await result.current.sendMessage({ message: 'test' });
|
|
626
|
+
// });
|
|
627
|
+
//
|
|
628
|
+
// expect(result.current.summaryTopicTitle).toHaveBeenCalledWith('topic-1', [
|
|
629
|
+
// { id: '1' },
|
|
630
|
+
// { id: '2' },
|
|
631
|
+
// { id: 'new-message-id', content: 'test', role: 'user' },
|
|
632
|
+
// { id: 'assistant-message', role: 'assistant' },
|
|
633
|
+
// ]);
|
|
634
|
+
// });
|
|
360
635
|
});
|
|
361
636
|
|
|
362
637
|
describe('toggleMessageEditing action', () => {
|
|
@@ -391,10 +666,14 @@ describe('chatMessage actions', () => {
|
|
|
391
666
|
|
|
392
667
|
act(() => {
|
|
393
668
|
useChatStore.setState({
|
|
669
|
+
activeId: 'session-id',
|
|
670
|
+
activeTopicId: undefined,
|
|
394
671
|
// Mock the currentChats selector to return a list that includes the message to be resent
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
672
|
+
messagesMap: {
|
|
673
|
+
[messageMapKey('session-id')]: [
|
|
674
|
+
{ id: messageId, role: 'user', content: 'Resend this message' } as ChatMessage,
|
|
675
|
+
],
|
|
676
|
+
},
|
|
398
677
|
});
|
|
399
678
|
});
|
|
400
679
|
|
|
@@ -419,8 +698,12 @@ describe('chatMessage actions', () => {
|
|
|
419
698
|
|
|
420
699
|
act(() => {
|
|
421
700
|
useChatStore.setState({
|
|
701
|
+
activeId: 'session-id',
|
|
702
|
+
activeTopicId: undefined,
|
|
422
703
|
// Mock the currentChats selector to return a list that does not include the message to be resent
|
|
423
|
-
|
|
704
|
+
messagesMap: {
|
|
705
|
+
[messageMapKey('session-id')]: [],
|
|
706
|
+
},
|
|
424
707
|
});
|
|
425
708
|
});
|
|
426
709
|
|
|
@@ -559,6 +842,31 @@ describe('chatMessage actions', () => {
|
|
|
559
842
|
});
|
|
560
843
|
});
|
|
561
844
|
|
|
845
|
+
describe('toggleMessageEditing', () => {
|
|
846
|
+
it('should update messageEditingIds correctly when enabling editing', () => {
|
|
847
|
+
const messageId = 'message-id';
|
|
848
|
+
const { result } = renderHook(() => useChatStore());
|
|
849
|
+
|
|
850
|
+
act(() => {
|
|
851
|
+
result.current.toggleMessageEditing(messageId, true);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
expect(result.current.messageEditingIds).toContain(messageId);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it('should update messageEditingIds correctly when disabling editing', () => {
|
|
858
|
+
const messageId = 'message-id';
|
|
859
|
+
useChatStore.setState({ messageEditingIds: [messageId] });
|
|
860
|
+
const { result } = renderHook(() => useChatStore());
|
|
861
|
+
|
|
862
|
+
act(() => {
|
|
863
|
+
result.current.toggleMessageEditing(messageId, false);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
expect(result.current.messageEditingIds).not.toContain(messageId);
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
562
870
|
describe('refreshMessages action', () => {
|
|
563
871
|
beforeEach(() => {
|
|
564
872
|
vi.mock('swr', async () => {
|
|
@@ -663,6 +971,72 @@ describe('chatMessage actions', () => {
|
|
|
663
971
|
});
|
|
664
972
|
});
|
|
665
973
|
});
|
|
974
|
+
|
|
975
|
+
it('should generate correct contextMessages for "user" role', async () => {
|
|
976
|
+
const messageId = 'message-id';
|
|
977
|
+
const messages = [
|
|
978
|
+
{ id: 'msg-1', role: 'system' },
|
|
979
|
+
{ id: messageId, role: 'user', meta: { avatar: '😀' } },
|
|
980
|
+
{ id: 'msg-3', role: 'assistant' },
|
|
981
|
+
];
|
|
982
|
+
act(() => {
|
|
983
|
+
useChatStore.setState({
|
|
984
|
+
messagesMap: {
|
|
985
|
+
[chatSelectors.currentChatKey(mockState as any)]: messages as ChatMessage[],
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
const { result } = renderHook(() => useChatStore());
|
|
990
|
+
|
|
991
|
+
await act(async () => {
|
|
992
|
+
await result.current.internal_resendMessage(messageId);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
|
996
|
+
messages.slice(0, 2),
|
|
997
|
+
messageId,
|
|
998
|
+
{ traceId: undefined },
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it('should generate correct contextMessages for "assistant" role', async () => {
|
|
1003
|
+
const messageId = 'message-id';
|
|
1004
|
+
const messages = [
|
|
1005
|
+
{ id: 'msg-1', role: 'system' },
|
|
1006
|
+
{ id: 'msg-2', role: 'user', meta: { avatar: '😀' } },
|
|
1007
|
+
{ id: messageId, role: 'assistant', parentId: 'msg-2' },
|
|
1008
|
+
];
|
|
1009
|
+
useChatStore.setState({
|
|
1010
|
+
messagesMap: {
|
|
1011
|
+
[chatSelectors.currentChatKey(mockState as any)]: messages as ChatMessage[],
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
const { result } = renderHook(() => useChatStore());
|
|
1015
|
+
|
|
1016
|
+
await act(async () => {
|
|
1017
|
+
await result.current.internal_resendMessage(messageId);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
|
|
1021
|
+
messages.slice(0, 2),
|
|
1022
|
+
'msg-2',
|
|
1023
|
+
{ traceId: undefined },
|
|
1024
|
+
);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should return early if contextMessages is empty', async () => {
|
|
1028
|
+
const messageId = 'message-id';
|
|
1029
|
+
useChatStore.setState({
|
|
1030
|
+
messagesMap: { [chatSelectors.currentChatKey(mockState as any)]: [] },
|
|
1031
|
+
});
|
|
1032
|
+
const { result } = renderHook(() => useChatStore());
|
|
1033
|
+
|
|
1034
|
+
await act(async () => {
|
|
1035
|
+
await result.current.internal_resendMessage(messageId);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
1039
|
+
});
|
|
666
1040
|
});
|
|
667
1041
|
|
|
668
1042
|
describe('internal_toggleChatLoading', () => {
|
|
@@ -760,4 +1134,101 @@ describe('chatMessage actions', () => {
|
|
|
760
1134
|
expect(result.current.messageLoadingIds).not.toContain(messageId);
|
|
761
1135
|
});
|
|
762
1136
|
});
|
|
1137
|
+
|
|
1138
|
+
describe('stopGenerateMessage', () => {
|
|
1139
|
+
it('should return early if abortController is undefined', () => {
|
|
1140
|
+
act(() => {
|
|
1141
|
+
useChatStore.setState({ abortController: undefined });
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const { result } = renderHook(() => useChatStore());
|
|
1145
|
+
|
|
1146
|
+
const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
|
1147
|
+
|
|
1148
|
+
act(() => {
|
|
1149
|
+
result.current.stopGenerateMessage();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
expect(spy).not.toHaveBeenCalled();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('should call abortController.abort()', () => {
|
|
1156
|
+
const abortMock = vi.fn();
|
|
1157
|
+
const abortController = { abort: abortMock } as unknown as AbortController;
|
|
1158
|
+
act(() => {
|
|
1159
|
+
useChatStore.setState({ abortController });
|
|
1160
|
+
});
|
|
1161
|
+
const { result } = renderHook(() => useChatStore());
|
|
1162
|
+
|
|
1163
|
+
act(() => {
|
|
1164
|
+
result.current.stopGenerateMessage();
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
expect(abortMock).toHaveBeenCalled();
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
it('should call internal_toggleChatLoading with correct parameters', () => {
|
|
1171
|
+
const abortController = new AbortController();
|
|
1172
|
+
act(() => {
|
|
1173
|
+
useChatStore.setState({ abortController });
|
|
1174
|
+
});
|
|
1175
|
+
const { result } = renderHook(() => useChatStore());
|
|
1176
|
+
const spy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
|
1177
|
+
|
|
1178
|
+
act(() => {
|
|
1179
|
+
result.current.stopGenerateMessage();
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
expect(spy).toHaveBeenCalledWith(false, undefined, expect.any(String));
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
describe('updateInputMessage', () => {
|
|
1187
|
+
it('should not update state if message is the same as current inputMessage', () => {
|
|
1188
|
+
const inputMessage = 'Test input message';
|
|
1189
|
+
useChatStore.setState({ inputMessage });
|
|
1190
|
+
const { result } = renderHook(() => useChatStore());
|
|
1191
|
+
|
|
1192
|
+
act(() => {
|
|
1193
|
+
result.current.updateInputMessage(inputMessage);
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
expect(result.current.inputMessage).toBe(inputMessage);
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
describe('modifyMessageContent', () => {
|
|
1201
|
+
it('should call internal_traceMessage with correct parameters before updating', async () => {
|
|
1202
|
+
const messageId = 'message-id';
|
|
1203
|
+
const content = 'Updated content';
|
|
1204
|
+
const { result } = renderHook(() => useChatStore());
|
|
1205
|
+
|
|
1206
|
+
const spy = vi.spyOn(result.current, 'internal_traceMessage');
|
|
1207
|
+
await act(async () => {
|
|
1208
|
+
await result.current.modifyMessageContent(messageId, content);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
expect(spy).toHaveBeenCalledWith(messageId, {
|
|
1212
|
+
eventType: TraceEventType.ModifyMessage,
|
|
1213
|
+
nextContent: content,
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
it('should call internal_updateMessageContent with correct parameters', async () => {
|
|
1218
|
+
const messageId = 'message-id';
|
|
1219
|
+
const content = 'Updated content';
|
|
1220
|
+
const { result } = renderHook(() => useChatStore());
|
|
1221
|
+
|
|
1222
|
+
const spy = vi.spyOn(result.current, 'internal_traceMessage');
|
|
1223
|
+
|
|
1224
|
+
await act(async () => {
|
|
1225
|
+
await result.current.modifyMessageContent(messageId, content);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
expect(spy).toHaveBeenCalledWith(messageId, {
|
|
1229
|
+
eventType: 'Modify Message',
|
|
1230
|
+
nextContent: 'Updated content',
|
|
1231
|
+
});
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
763
1234
|
});
|