@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.
@@ -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({ messages: [{ id: messageId } as ChatMessage] });
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).toBeUndefined();
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
- messages: Array.from({ length: autoCreateTopicThreshold + 1 }, (_, i) => ({
269
- id: `msg-${i}`,
270
- })) as any,
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 saveToTopicMock = vi.fn(() => Promise.resolve('new-topic-id'));
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
- messages: Array.from({ length: autoCreateTopicThreshold }, (_, i) => ({
306
- id: `msg-${i}`,
307
- })) as any,
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
- saveToTopic: saveToTopicMock,
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(saveToTopicMock).toHaveBeenCalled();
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 saveToTopicMock = vi.fn();
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
- // Mock the currentChats selector to return a list that does not reach the threshold
345
- messages: Array.from({ length: autoCreateTopicThreshold - 2 }, (_, i) => ({
346
- id: `msg-${i}`,
347
- })) as any,
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
- saveToTopic: saveToTopicMock,
473
+ createTopic: createTopicMock,
350
474
  switchTopic: switchTopicMock,
351
475
  });
352
476
 
353
477
  await result.current.sendMessage({ message });
354
478
  });
355
479
 
356
- expect(saveToTopicMock).not.toHaveBeenCalled();
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
- messages: [
396
- { id: messageId, role: 'user', content: 'Resend this message' } as ChatMessage,
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
- messages: [],
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
  });