@lobehub/lobehub 2.0.0-next.35 → 2.0.0-next.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/next.config.ts +5 -6
- package/package.json +2 -2
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
- package/packages/agent-runtime/src/core/runtime.ts +63 -18
- package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
- package/packages/agent-runtime/src/types/index.ts +1 -0
- package/packages/agent-runtime/src/types/instruction.ts +10 -3
- package/packages/const/src/user.ts +0 -1
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
- package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
- package/packages/conversation-flow/src/index.ts +1 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
- package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
- package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
- package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
- package/packages/database/src/models/message.ts +18 -19
- package/packages/types/src/aiChat.ts +2 -0
- package/packages/types/src/importer.ts +2 -2
- package/packages/types/src/message/ui/chat.ts +17 -1
- package/packages/types/src/message/ui/extra.ts +2 -2
- package/packages/types/src/message/ui/params.ts +2 -2
- package/packages/types/src/user/preference.ts +0 -4
- package/packages/utils/src/tokenizer/index.ts +3 -11
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
- package/src/app/[variants]/(main)/labs/page.tsx +0 -9
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
- package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
- package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
- package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
- package/src/features/Conversation/Error/index.tsx +0 -5
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
- package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
- package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
- package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
- package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
- package/src/features/Conversation/Messages/Default.tsx +1 -0
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
- package/src/features/Conversation/Messages/Group/index.tsx +2 -1
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
- package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
- package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
- package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
- package/src/features/Conversation/Messages/User/index.tsx +43 -44
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +3 -3
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
- package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
- package/src/hooks/useHotkeys/chatScope.ts +15 -7
- package/src/libs/trpc/client/lambda.ts +4 -3
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
- package/src/server/routers/lambda/aiChat.ts +3 -2
- package/src/server/routers/lambda/message.ts +8 -16
- package/src/server/services/message/__tests__/index.test.ts +29 -39
- package/src/server/services/message/index.ts +41 -36
- package/src/services/electron/desktopNotification.ts +6 -6
- package/src/services/electron/file.ts +6 -6
- package/src/services/file/ClientS3/index.ts +8 -8
- package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
- package/src/services/message/index.ts +21 -15
- package/src/services/upload.ts +11 -11
- package/src/services/utils/abortableRequest.test.ts +161 -0
- package/src/services/utils/abortableRequest.ts +67 -0
- package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
- package/src/store/chat/agents/createAgentExecutors.ts +395 -0
- package/src/store/chat/helpers.test.ts +0 -99
- package/src/store/chat/helpers.ts +0 -11
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
- package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
- package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
- package/src/store/chat/slices/message/action.test.ts +79 -68
- package/src/store/chat/slices/message/actions/index.ts +39 -0
- package/src/store/chat/slices/message/actions/internals.ts +77 -0
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
- package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
- package/src/store/chat/slices/message/actions/query.ts +120 -0
- package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
- package/src/store/chat/slices/message/initialState.ts +13 -0
- package/src/store/chat/slices/message/reducer.test.ts +48 -370
- package/src/store/chat/slices/message/reducer.ts +17 -81
- package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
- package/src/store/chat/slices/message/selectors/chat.ts +78 -242
- package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
- package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
- package/src/store/chat/slices/plugin/action.test.ts +62 -64
- package/src/store/chat/slices/plugin/action.ts +34 -28
- package/src/store/chat/slices/thread/action.test.ts +28 -31
- package/src/store/chat/slices/thread/action.ts +13 -10
- package/src/store/chat/slices/thread/selectors/index.ts +8 -6
- package/src/store/chat/slices/topic/reducer.ts +11 -3
- package/src/store/chat/store.ts +1 -1
- package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
- package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
- package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
- package/packages/database/src/utils/groupMessages.ts +0 -361
- package/packages/utils/src/tokenizer/client.ts +0 -35
- package/packages/utils/src/tokenizer/estimated.ts +0 -4
- package/packages/utils/src/tokenizer/server.ts +0 -11
- package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
- package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
- package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
- package/src/store/chat/slices/message/action.ts +0 -629
|
@@ -1,1050 +0,0 @@
|
|
|
1
|
-
import { UIChatMessage } from '@lobechat/types';
|
|
2
|
-
import { act, renderHook } from '@testing-library/react';
|
|
3
|
-
import { TRPCClientError } from '@trpc/client';
|
|
4
|
-
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
import { LOADING_FLAT } from '@/const/message';
|
|
7
|
-
import { DEFAULT_AGENT_CHAT_CONFIG, DEFAULT_MODEL, DEFAULT_PROVIDER } from '@/const/settings';
|
|
8
|
-
import { aiChatService } from '@/services/aiChat';
|
|
9
|
-
import { chatService } from '@/services/chat';
|
|
10
|
-
import { messageService } from '@/services/message';
|
|
11
|
-
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
|
12
|
-
import { UploadFileItem } from '@/types/files/upload';
|
|
13
|
-
|
|
14
|
-
import { useChatStore } from '../../../../store';
|
|
15
|
-
import { messageMapKey } from '../../../../utils/messageMapKey';
|
|
16
|
-
import { TEST_CONTENT, TEST_IDS, createMockStoreState } from './fixtures';
|
|
17
|
-
import { resetTestEnvironment, setupMockSelectors, spyOnMessageService } from './helpers';
|
|
18
|
-
|
|
19
|
-
// Keep zustand mock as it's needed globally
|
|
20
|
-
vi.mock('zustand/traditional');
|
|
21
|
-
|
|
22
|
-
// Mock AntdStaticMethods
|
|
23
|
-
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
24
|
-
notification: {
|
|
25
|
-
error: vi.fn(),
|
|
26
|
-
success: vi.fn(),
|
|
27
|
-
info: vi.fn(),
|
|
28
|
-
warning: vi.fn(),
|
|
29
|
-
},
|
|
30
|
-
message: {
|
|
31
|
-
error: vi.fn(),
|
|
32
|
-
success: vi.fn(),
|
|
33
|
-
info: vi.fn(),
|
|
34
|
-
warning: vi.fn(),
|
|
35
|
-
},
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
// Mock sessionService to prevent TRPC requests
|
|
39
|
-
vi.mock('@/services/session', () => ({
|
|
40
|
-
sessionService: {
|
|
41
|
-
updateSession: vi.fn(),
|
|
42
|
-
updateSessionConfig: vi.fn(),
|
|
43
|
-
updateSessionChatConfig: vi.fn(),
|
|
44
|
-
},
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
// Mock server mode for V2 tests
|
|
48
|
-
vi.mock('@lobechat/const', async (importOriginal) => {
|
|
49
|
-
const module = await importOriginal();
|
|
50
|
-
return {
|
|
51
|
-
...(module as any),
|
|
52
|
-
isServerMode: true,
|
|
53
|
-
isDesktop: false,
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Mock aiChatService for V2 server flow
|
|
58
|
-
vi.mock('@/services/aiChat', () => ({
|
|
59
|
-
aiChatService: {
|
|
60
|
-
sendMessageInServer: vi.fn(async (params: any) => {
|
|
61
|
-
const userId = TEST_IDS.USER_MESSAGE_ID;
|
|
62
|
-
const assistantId = TEST_IDS.ASSISTANT_MESSAGE_ID;
|
|
63
|
-
const topicId = params.topicId ?? TEST_IDS.TOPIC_ID;
|
|
64
|
-
return {
|
|
65
|
-
messages: [
|
|
66
|
-
{
|
|
67
|
-
id: userId,
|
|
68
|
-
role: 'user',
|
|
69
|
-
content: params.newUserMessage?.content ?? '',
|
|
70
|
-
sessionId: params.sessionId ?? TEST_IDS.SESSION_ID,
|
|
71
|
-
topicId,
|
|
72
|
-
} as any,
|
|
73
|
-
{
|
|
74
|
-
id: assistantId,
|
|
75
|
-
role: 'assistant',
|
|
76
|
-
content: LOADING_FLAT,
|
|
77
|
-
sessionId: params.sessionId ?? TEST_IDS.SESSION_ID,
|
|
78
|
-
topicId,
|
|
79
|
-
} as any,
|
|
80
|
-
],
|
|
81
|
-
topics: [],
|
|
82
|
-
topicId,
|
|
83
|
-
userMessageId: userId,
|
|
84
|
-
assistantMessageId: assistantId,
|
|
85
|
-
isCreateNewTopic: !params.topicId,
|
|
86
|
-
} as any;
|
|
87
|
-
}),
|
|
88
|
-
},
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
const realExecAgentRuntime = useChatStore.getState().internal_execAgentRuntime;
|
|
92
|
-
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
resetTestEnvironment();
|
|
95
|
-
setupMockSelectors();
|
|
96
|
-
|
|
97
|
-
// Setup default spies that most tests need
|
|
98
|
-
spyOnMessageService();
|
|
99
|
-
|
|
100
|
-
// Setup common mock methods that most V2 tests need
|
|
101
|
-
act(() => {
|
|
102
|
-
useChatStore.setState({
|
|
103
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
104
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
105
|
-
mainSendMessageOperations: {},
|
|
106
|
-
refreshMessages: vi.fn(),
|
|
107
|
-
refreshTopic: vi.fn(),
|
|
108
|
-
internal_execAgentRuntime: vi.fn(),
|
|
109
|
-
saveToTopic: vi.fn(),
|
|
110
|
-
switchTopic: vi.fn(),
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterEach(() => {
|
|
116
|
-
process.env.NEXT_PUBLIC_BASE_PATH = undefined;
|
|
117
|
-
vi.restoreAllMocks();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('generateAIChatV2 actions', () => {
|
|
121
|
-
describe('sendMessageInServer', () => {
|
|
122
|
-
describe('validation', () => {
|
|
123
|
-
it('should not send when there is no active session', async () => {
|
|
124
|
-
act(() => {
|
|
125
|
-
useChatStore.setState({ activeId: undefined });
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const { result } = renderHook(() => useChatStore());
|
|
129
|
-
|
|
130
|
-
await act(async () => {
|
|
131
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
135
|
-
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should not send when message is empty and no files are provided', async () => {
|
|
139
|
-
const { result } = renderHook(() => useChatStore());
|
|
140
|
-
|
|
141
|
-
await act(async () => {
|
|
142
|
-
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should not send when message is empty with empty files array', async () => {
|
|
149
|
-
const { result } = renderHook(() => useChatStore());
|
|
150
|
-
|
|
151
|
-
await act(async () => {
|
|
152
|
-
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('message creation', () => {
|
|
160
|
-
it('should create user message and trigger AI processing', async () => {
|
|
161
|
-
const { result } = renderHook(() => useChatStore());
|
|
162
|
-
|
|
163
|
-
await act(async () => {
|
|
164
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
|
168
|
-
{
|
|
169
|
-
newAssistantMessage: {
|
|
170
|
-
model: DEFAULT_MODEL,
|
|
171
|
-
provider: DEFAULT_PROVIDER,
|
|
172
|
-
},
|
|
173
|
-
newUserMessage: {
|
|
174
|
-
content: TEST_CONTENT.USER_MESSAGE,
|
|
175
|
-
files: undefined,
|
|
176
|
-
},
|
|
177
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
178
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
179
|
-
},
|
|
180
|
-
expect.anything(),
|
|
181
|
-
);
|
|
182
|
-
expect(result.current.internal_execAgentRuntime).toHaveBeenCalled();
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should skip creating new topic when auto-create topic is disabled', async () => {
|
|
186
|
-
const { result } = renderHook(() => useChatStore());
|
|
187
|
-
|
|
188
|
-
(agentChatConfigSelectors.currentChatConfig as Mock).mockReturnValue({
|
|
189
|
-
...DEFAULT_AGENT_CHAT_CONFIG,
|
|
190
|
-
enableAutoCreateTopic: false,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
await act(async () => {
|
|
194
|
-
useChatStore.setState({
|
|
195
|
-
...createMockStoreState(),
|
|
196
|
-
activeTopicId: undefined,
|
|
197
|
-
messagesMap: {},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
await result.current.sendMessage({ message: 'disable auto create' });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const callArgs = (aiChatService.sendMessageInServer as Mock).mock.calls[0][0];
|
|
204
|
-
expect(callArgs.newTopic).toBeUndefined();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should include newTopic payload when auto-create topic is enabled and threshold is reached', async () => {
|
|
208
|
-
const { result } = renderHook(() => useChatStore());
|
|
209
|
-
|
|
210
|
-
(agentChatConfigSelectors.currentChatConfig as Mock).mockReturnValue({
|
|
211
|
-
...DEFAULT_AGENT_CHAT_CONFIG,
|
|
212
|
-
enableAutoCreateTopic: true,
|
|
213
|
-
autoCreateTopicThreshold: 1,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
await act(async () => {
|
|
217
|
-
useChatStore.setState({
|
|
218
|
-
...createMockStoreState(),
|
|
219
|
-
activeTopicId: undefined,
|
|
220
|
-
messagesMap: {},
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
await result.current.sendMessage({ message: 'auto create topic' });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const callArgs = (aiChatService.sendMessageInServer as Mock).mock.calls[0][0];
|
|
227
|
-
expect(callArgs.newTopic).toMatchObject({
|
|
228
|
-
topicMessageIds: [],
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should not create new topic when threshold is not reached', async () => {
|
|
233
|
-
const { result } = renderHook(() => useChatStore());
|
|
234
|
-
|
|
235
|
-
(agentChatConfigSelectors.currentChatConfig as Mock).mockReturnValue({
|
|
236
|
-
...DEFAULT_AGENT_CHAT_CONFIG,
|
|
237
|
-
enableAutoCreateTopic: true,
|
|
238
|
-
autoCreateTopicThreshold: 10,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await act(async () => {
|
|
242
|
-
useChatStore.setState({
|
|
243
|
-
...createMockStoreState(),
|
|
244
|
-
activeTopicId: undefined,
|
|
245
|
-
messagesMap: {},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
await result.current.sendMessage({ message: 'threshold not met' });
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const callArgs = (aiChatService.sendMessageInServer as Mock).mock.calls[0][0];
|
|
252
|
-
expect(callArgs.newTopic).toBeUndefined();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should send message with files attached', async () => {
|
|
256
|
-
const { result } = renderHook(() => useChatStore());
|
|
257
|
-
const files = [{ id: TEST_IDS.FILE_ID } as UploadFileItem];
|
|
258
|
-
|
|
259
|
-
await act(async () => {
|
|
260
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE, files });
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
|
264
|
-
{
|
|
265
|
-
newAssistantMessage: {
|
|
266
|
-
model: DEFAULT_MODEL,
|
|
267
|
-
provider: DEFAULT_PROVIDER,
|
|
268
|
-
},
|
|
269
|
-
newUserMessage: {
|
|
270
|
-
content: TEST_CONTENT.USER_MESSAGE,
|
|
271
|
-
files: [TEST_IDS.FILE_ID],
|
|
272
|
-
},
|
|
273
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
274
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
275
|
-
},
|
|
276
|
-
expect.anything(),
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('should send files without message content', async () => {
|
|
281
|
-
const { result } = renderHook(() => useChatStore());
|
|
282
|
-
const files = [{ id: TEST_IDS.FILE_ID } as UploadFileItem];
|
|
283
|
-
|
|
284
|
-
await act(async () => {
|
|
285
|
-
await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files });
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
|
289
|
-
{
|
|
290
|
-
newAssistantMessage: {
|
|
291
|
-
model: DEFAULT_MODEL,
|
|
292
|
-
provider: DEFAULT_PROVIDER,
|
|
293
|
-
},
|
|
294
|
-
newUserMessage: {
|
|
295
|
-
content: TEST_CONTENT.EMPTY,
|
|
296
|
-
files: [TEST_IDS.FILE_ID],
|
|
297
|
-
},
|
|
298
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
299
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
300
|
-
},
|
|
301
|
-
expect.anything(),
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should not process AI when onlyAddUserMessage is true', async () => {
|
|
306
|
-
const { result } = renderHook(() => useChatStore());
|
|
307
|
-
|
|
308
|
-
await act(async () => {
|
|
309
|
-
await result.current.sendMessage({
|
|
310
|
-
message: TEST_CONTENT.USER_MESSAGE,
|
|
311
|
-
onlyAddUserMessage: true,
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
expect(messageService.createMessage).toHaveBeenCalled();
|
|
316
|
-
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('should handle message creation errors gracefully', async () => {
|
|
320
|
-
const { result } = renderHook(() => useChatStore());
|
|
321
|
-
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(
|
|
322
|
-
new Error('create message error'),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
await act(async () => {
|
|
326
|
-
try {
|
|
327
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
328
|
-
} catch {
|
|
329
|
-
// Expected to throw
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe('RAG integration', () => {
|
|
338
|
-
it('should include RAG query when RAG is enabled', async () => {
|
|
339
|
-
const { result } = renderHook(() => useChatStore());
|
|
340
|
-
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
|
|
341
|
-
|
|
342
|
-
await act(async () => {
|
|
343
|
-
await result.current.sendMessage({ message: TEST_CONTENT.RAG_QUERY });
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
|
347
|
-
expect.objectContaining({
|
|
348
|
-
ragQuery: TEST_CONTENT.RAG_QUERY,
|
|
349
|
-
}),
|
|
350
|
-
);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('should not use RAG when feature is disabled', async () => {
|
|
354
|
-
const { result } = renderHook(() => useChatStore());
|
|
355
|
-
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
|
|
356
|
-
const retrieveChunksSpy = vi.spyOn(result.current, 'internal_retrieveChunks');
|
|
357
|
-
|
|
358
|
-
await act(async () => {
|
|
359
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
expect(retrieveChunksSpy).not.toHaveBeenCalled();
|
|
363
|
-
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
|
364
|
-
expect.objectContaining({
|
|
365
|
-
ragQuery: undefined,
|
|
366
|
-
}),
|
|
367
|
-
);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
describe('special flags', () => {
|
|
372
|
-
it('should pass isWelcomeQuestion flag to processing', async () => {
|
|
373
|
-
const { result } = renderHook(() => useChatStore());
|
|
374
|
-
|
|
375
|
-
await act(async () => {
|
|
376
|
-
await result.current.sendMessage({
|
|
377
|
-
message: TEST_CONTENT.USER_MESSAGE,
|
|
378
|
-
isWelcomeQuestion: true,
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
expect(result.current.internal_execAgentRuntime).toHaveBeenCalledWith(
|
|
383
|
-
expect.objectContaining({
|
|
384
|
-
isWelcomeQuestion: true,
|
|
385
|
-
}),
|
|
386
|
-
);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
describe('internal_execAgentRuntime', () => {
|
|
392
|
-
it('should handle the core AI message processing', async () => {
|
|
393
|
-
act(() => {
|
|
394
|
-
useChatStore.setState({ internal_execAgentRuntime: realExecAgentRuntime });
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const { result } = renderHook(() => useChatStore());
|
|
398
|
-
const userMessage = {
|
|
399
|
-
id: TEST_IDS.USER_MESSAGE_ID,
|
|
400
|
-
role: 'user',
|
|
401
|
-
content: TEST_CONTENT.USER_MESSAGE,
|
|
402
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
403
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
404
|
-
} as UIChatMessage;
|
|
405
|
-
const messages = [userMessage];
|
|
406
|
-
|
|
407
|
-
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream');
|
|
408
|
-
|
|
409
|
-
await act(async () => {
|
|
410
|
-
await result.current.internal_execAgentRuntime({
|
|
411
|
-
messages,
|
|
412
|
-
userMessageId: userMessage.id,
|
|
413
|
-
assistantMessageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
expect(streamSpy).toHaveBeenCalled();
|
|
418
|
-
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
describe('error handling', () => {
|
|
423
|
-
it('should set error message when sendMessageInServer throws a regular error', async () => {
|
|
424
|
-
const { result } = renderHook(() => useChatStore());
|
|
425
|
-
const errorMessage = 'Network error';
|
|
426
|
-
const mockError = new TRPCClientError(errorMessage);
|
|
427
|
-
(mockError as any).data = { code: 'BAD_REQUEST' };
|
|
428
|
-
|
|
429
|
-
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(mockError);
|
|
430
|
-
|
|
431
|
-
await act(async () => {
|
|
432
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const operationKey = messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID);
|
|
436
|
-
expect(result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg).toBe(
|
|
437
|
-
errorMessage,
|
|
438
|
-
);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('should not set error message when receiving a cancel signal', async () => {
|
|
442
|
-
const { result } = renderHook(() => useChatStore());
|
|
443
|
-
const abortError = new Error('AbortError');
|
|
444
|
-
abortError.name = 'AbortError';
|
|
445
|
-
|
|
446
|
-
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(abortError);
|
|
447
|
-
|
|
448
|
-
await act(async () => {
|
|
449
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const operationKey = messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID);
|
|
453
|
-
expect(
|
|
454
|
-
result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg,
|
|
455
|
-
).toBeUndefined();
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
describe('topic creation and switching', () => {
|
|
460
|
-
it('should remove temporary message when creating new topic in default state', async () => {
|
|
461
|
-
const { result } = renderHook(() => useChatStore());
|
|
462
|
-
|
|
463
|
-
vi.spyOn(aiChatService, 'sendMessageInServer').mockResolvedValueOnce({
|
|
464
|
-
isCreateNewTopic: true,
|
|
465
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
466
|
-
messages: [{}, {}] as any,
|
|
467
|
-
topics: [{}] as any,
|
|
468
|
-
assistantMessageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
|
|
469
|
-
userMessageId: TEST_IDS.USER_MESSAGE_ID,
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
await act(async () => {
|
|
473
|
-
useChatStore.setState({
|
|
474
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
475
|
-
activeTopicId: undefined,
|
|
476
|
-
messagesMap: {},
|
|
477
|
-
switchTopic: vi.fn(),
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
expect(useChatStore.getState().messagesMap[`${TEST_IDS.SESSION_ID}_null`]).toEqual([]);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('should automatically switch to newly created topic when no active topic exists', async () => {
|
|
487
|
-
const { result } = renderHook(() => useChatStore());
|
|
488
|
-
const mockSwitchTopic = vi.fn();
|
|
489
|
-
|
|
490
|
-
await act(async () => {
|
|
491
|
-
useChatStore.setState({
|
|
492
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
493
|
-
activeTopicId: undefined,
|
|
494
|
-
switchTopic: mockSwitchTopic,
|
|
495
|
-
});
|
|
496
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
expect(mockSwitchTopic).toHaveBeenCalledWith(TEST_IDS.TOPIC_ID, true);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it('should not switch topic when active topic already exists', async () => {
|
|
503
|
-
const { result } = renderHook(() => useChatStore());
|
|
504
|
-
const mockSwitchTopic = vi.fn();
|
|
505
|
-
|
|
506
|
-
await act(async () => {
|
|
507
|
-
useChatStore.setState({
|
|
508
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
509
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
510
|
-
switchTopic: mockSwitchTopic,
|
|
511
|
-
});
|
|
512
|
-
await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
expect(mockSwitchTopic).not.toHaveBeenCalled();
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
describe('cancelSendMessageInServer', () => {
|
|
520
|
-
it('should abort operation and restore editor state when cancelling', () => {
|
|
521
|
-
const { result } = renderHook(() => useChatStore());
|
|
522
|
-
const mockAbort = vi.fn();
|
|
523
|
-
const mockSetJSONState = vi.fn();
|
|
524
|
-
|
|
525
|
-
act(() => {
|
|
526
|
-
useChatStore.setState({
|
|
527
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
528
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
529
|
-
mainSendMessageOperations: {
|
|
530
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: {
|
|
531
|
-
isLoading: true,
|
|
532
|
-
abortController: { abort: mockAbort, signal: {} as any },
|
|
533
|
-
inputEditorTempState: { content: 'saved content' },
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
act(() => {
|
|
541
|
-
result.current.cancelSendMessageInServer();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessage operation');
|
|
545
|
-
expect(
|
|
546
|
-
result.current.mainSendMessageOperations[
|
|
547
|
-
messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)
|
|
548
|
-
]?.isLoading,
|
|
549
|
-
).toBe(false);
|
|
550
|
-
expect(mockSetJSONState).toHaveBeenCalledWith({ content: 'saved content' });
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('should cancel operation for specified topic ID', () => {
|
|
554
|
-
const { result } = renderHook(() => useChatStore());
|
|
555
|
-
const mockAbort = vi.fn();
|
|
556
|
-
const customTopicId = 'custom-topic-id';
|
|
557
|
-
|
|
558
|
-
act(() => {
|
|
559
|
-
useChatStore.setState({
|
|
560
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
561
|
-
mainSendMessageOperations: {
|
|
562
|
-
[messageMapKey(TEST_IDS.SESSION_ID, customTopicId)]: {
|
|
563
|
-
isLoading: true,
|
|
564
|
-
abortController: { abort: mockAbort, signal: {} as any },
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
act(() => {
|
|
571
|
-
result.current.cancelSendMessageInServer(customTopicId);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessage operation');
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it('should handle gracefully when operation does not exist', () => {
|
|
578
|
-
const { result } = renderHook(() => useChatStore());
|
|
579
|
-
|
|
580
|
-
act(() => {
|
|
581
|
-
useChatStore.setState({ mainSendMessageOperations: {} });
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
expect(() => {
|
|
585
|
-
act(() => {
|
|
586
|
-
result.current.cancelSendMessageInServer('non-existing-topic');
|
|
587
|
-
});
|
|
588
|
-
}).not.toThrow();
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
describe('clearSendMessageError', () => {
|
|
593
|
-
it('should clear error state for current topic', () => {
|
|
594
|
-
const { result } = renderHook(() => useChatStore());
|
|
595
|
-
|
|
596
|
-
act(() => {
|
|
597
|
-
useChatStore.setState({
|
|
598
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
599
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
600
|
-
mainSendMessageOperations: {
|
|
601
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: {
|
|
602
|
-
isLoading: false,
|
|
603
|
-
inputSendErrorMsg: 'Some error',
|
|
604
|
-
},
|
|
605
|
-
},
|
|
606
|
-
});
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
act(() => {
|
|
610
|
-
result.current.clearSendMessageError();
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
expect(
|
|
614
|
-
result.current.mainSendMessageOperations[
|
|
615
|
-
messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)
|
|
616
|
-
],
|
|
617
|
-
).toBeUndefined();
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it('should handle gracefully when no error operation exists', () => {
|
|
621
|
-
const { result } = renderHook(() => useChatStore());
|
|
622
|
-
|
|
623
|
-
act(() => {
|
|
624
|
-
useChatStore.setState({ mainSendMessageOperations: {} });
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
expect(() => {
|
|
628
|
-
act(() => {
|
|
629
|
-
result.current.clearSendMessageError();
|
|
630
|
-
});
|
|
631
|
-
}).not.toThrow();
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
describe('internal_toggleSendMessageOperation', () => {
|
|
636
|
-
it('should create new send operation with abort controller', () => {
|
|
637
|
-
const { result } = renderHook(() => useChatStore());
|
|
638
|
-
let abortController: AbortController | undefined;
|
|
639
|
-
|
|
640
|
-
act(() => {
|
|
641
|
-
abortController = result.current.internal_toggleSendMessageOperation('test-key', true);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
expect(abortController!).toBeInstanceOf(AbortController);
|
|
645
|
-
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(true);
|
|
646
|
-
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBe(
|
|
647
|
-
abortController,
|
|
648
|
-
);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it('should stop send operation and clear abort controller', () => {
|
|
652
|
-
const { result } = renderHook(() => useChatStore());
|
|
653
|
-
const mockAbortController = { abort: vi.fn() } as any;
|
|
654
|
-
|
|
655
|
-
let abortController: AbortController | undefined;
|
|
656
|
-
act(() => {
|
|
657
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
658
|
-
isLoading: true,
|
|
659
|
-
abortController: mockAbortController,
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
abortController = result.current.internal_toggleSendMessageOperation('test-key', false);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
expect(abortController).toBeUndefined();
|
|
666
|
-
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(false);
|
|
667
|
-
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBeNull();
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it('should call abort with cancel reason when stopping', () => {
|
|
671
|
-
const { result } = renderHook(() => useChatStore());
|
|
672
|
-
const mockAbortController = { abort: vi.fn() } as any;
|
|
673
|
-
|
|
674
|
-
act(() => {
|
|
675
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
676
|
-
isLoading: true,
|
|
677
|
-
abortController: mockAbortController,
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
result.current.internal_toggleSendMessageOperation('test-key', false, 'Test cancel reason');
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
expect(mockAbortController.abort).toHaveBeenCalledWith('Test cancel reason');
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
it('should support multiple parallel operations', () => {
|
|
687
|
-
const { result } = renderHook(() => useChatStore());
|
|
688
|
-
|
|
689
|
-
let abortController1, abortController2;
|
|
690
|
-
act(() => {
|
|
691
|
-
abortController1 = result.current.internal_toggleSendMessageOperation('key1', true);
|
|
692
|
-
abortController2 = result.current.internal_toggleSendMessageOperation('key2', true);
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
expect(result.current.mainSendMessageOperations['key1']?.isLoading).toBe(true);
|
|
696
|
-
expect(result.current.mainSendMessageOperations['key2']?.isLoading).toBe(true);
|
|
697
|
-
expect(abortController1).not.toBe(abortController2);
|
|
698
|
-
});
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
describe('internal_updateSendMessageOperation', () => {
|
|
702
|
-
it('should update operation state', () => {
|
|
703
|
-
const { result } = renderHook(() => useChatStore());
|
|
704
|
-
const mockAbortController = new AbortController();
|
|
705
|
-
|
|
706
|
-
act(() => {
|
|
707
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
708
|
-
isLoading: true,
|
|
709
|
-
abortController: mockAbortController,
|
|
710
|
-
inputSendErrorMsg: 'test error',
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
expect(result.current.mainSendMessageOperations['test-key']).toEqual({
|
|
715
|
-
isLoading: true,
|
|
716
|
-
abortController: mockAbortController,
|
|
717
|
-
inputSendErrorMsg: 'test error',
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
it('should support partial update of operation state', () => {
|
|
722
|
-
const { result } = renderHook(() => useChatStore());
|
|
723
|
-
const initialController = new AbortController();
|
|
724
|
-
|
|
725
|
-
act(() => {
|
|
726
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
727
|
-
isLoading: true,
|
|
728
|
-
abortController: initialController,
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
732
|
-
inputSendErrorMsg: 'new error',
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
expect(result.current.mainSendMessageOperations['test-key']).toEqual({
|
|
737
|
-
isLoading: true,
|
|
738
|
-
abortController: initialController,
|
|
739
|
-
inputSendErrorMsg: 'new error',
|
|
740
|
-
});
|
|
741
|
-
});
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
describe('callToolFollowAssistantMessage', () => {
|
|
745
|
-
const TOOL_RESULT_MSG_ID = 'tool-result-msg-id';
|
|
746
|
-
const ASSISTANT_BLOCK_ID = 'assistant-block-id';
|
|
747
|
-
const GROUP_MESSAGE_ID = 'group-message-id';
|
|
748
|
-
const TOOL_CALL_ID = 'tool-call-id';
|
|
749
|
-
|
|
750
|
-
beforeEach(() => {
|
|
751
|
-
// Reset mocks
|
|
752
|
-
vi.spyOn(messageService, 'createMessage').mockResolvedValue({
|
|
753
|
-
id: 'new-assistant-block-id',
|
|
754
|
-
messages: [] as any,
|
|
755
|
-
});
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
it('should find group message from tool result_msg_id in tools array', async () => {
|
|
759
|
-
const { result } = renderHook(() => useChatStore());
|
|
760
|
-
const dispatchSpy = vi.fn();
|
|
761
|
-
|
|
762
|
-
// Create a group message structure with tool results
|
|
763
|
-
const groupMessage: UIChatMessage = {
|
|
764
|
-
id: GROUP_MESSAGE_ID,
|
|
765
|
-
role: 'group',
|
|
766
|
-
content: '',
|
|
767
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
768
|
-
topicId: TEST_IDS.TOPIC_ID,
|
|
769
|
-
children: [
|
|
770
|
-
{
|
|
771
|
-
id: ASSISTANT_BLOCK_ID,
|
|
772
|
-
content: 'Assistant response',
|
|
773
|
-
tools: [
|
|
774
|
-
{
|
|
775
|
-
id: TOOL_CALL_ID,
|
|
776
|
-
type: 'builtin',
|
|
777
|
-
apiName: 'testTool',
|
|
778
|
-
identifier: 'test-tool',
|
|
779
|
-
arguments: '{}',
|
|
780
|
-
result: {
|
|
781
|
-
id: TOOL_RESULT_MSG_ID,
|
|
782
|
-
content: 'Tool result',
|
|
783
|
-
},
|
|
784
|
-
result_msg_id: TOOL_RESULT_MSG_ID,
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
},
|
|
788
|
-
],
|
|
789
|
-
} as any;
|
|
790
|
-
|
|
791
|
-
act(() => {
|
|
792
|
-
useChatStore.setState({
|
|
793
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
794
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
795
|
-
messagesMap: {
|
|
796
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
|
|
797
|
-
},
|
|
798
|
-
internal_execAgentRuntime: vi.fn(),
|
|
799
|
-
internal_dispatchMessage: dispatchSpy,
|
|
800
|
-
});
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
await act(async () => {
|
|
804
|
-
await result.current.callToolFollowAssistantMessage({
|
|
805
|
-
parentId: TOOL_RESULT_MSG_ID,
|
|
806
|
-
});
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
// Verify that addGroupBlock was called with the correct groupMessageId
|
|
810
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
811
|
-
expect.objectContaining({
|
|
812
|
-
type: 'addGroupBlock',
|
|
813
|
-
groupMessageId: GROUP_MESSAGE_ID,
|
|
814
|
-
}),
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
// Verify that createMessage was called with message params
|
|
818
|
-
expect(messageService.createMessage).toHaveBeenCalledWith(
|
|
819
|
-
expect.objectContaining({
|
|
820
|
-
role: 'assistant',
|
|
821
|
-
parentId: TOOL_RESULT_MSG_ID,
|
|
822
|
-
}),
|
|
823
|
-
);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
it('should handle case when tool result is not found in any group message', async () => {
|
|
827
|
-
const { result } = renderHook(() => useChatStore());
|
|
828
|
-
const dispatchSpy = vi.fn();
|
|
829
|
-
|
|
830
|
-
const groupMessage: UIChatMessage = {
|
|
831
|
-
id: GROUP_MESSAGE_ID,
|
|
832
|
-
role: 'group',
|
|
833
|
-
content: '',
|
|
834
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
835
|
-
children: [
|
|
836
|
-
{
|
|
837
|
-
id: ASSISTANT_BLOCK_ID,
|
|
838
|
-
content: 'Assistant response',
|
|
839
|
-
tools: [], // No tools
|
|
840
|
-
},
|
|
841
|
-
],
|
|
842
|
-
} as any;
|
|
843
|
-
|
|
844
|
-
act(() => {
|
|
845
|
-
useChatStore.setState({
|
|
846
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
847
|
-
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
848
|
-
messagesMap: {
|
|
849
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
|
|
850
|
-
},
|
|
851
|
-
internal_execAgentRuntime: vi.fn(),
|
|
852
|
-
internal_dispatchMessage: dispatchSpy,
|
|
853
|
-
});
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
await act(async () => {
|
|
857
|
-
await result.current.callToolFollowAssistantMessage({
|
|
858
|
-
parentId: 'non-existent-tool-result-id',
|
|
859
|
-
});
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
// Should create message as regular top-level message (createMessage, not addGroupBlock)
|
|
863
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
864
|
-
expect.objectContaining({
|
|
865
|
-
type: 'createMessage',
|
|
866
|
-
}),
|
|
867
|
-
);
|
|
868
|
-
|
|
869
|
-
expect(messageService.createMessage).toHaveBeenCalledWith(
|
|
870
|
-
expect.objectContaining({
|
|
871
|
-
role: 'assistant',
|
|
872
|
-
parentId: 'non-existent-tool-result-id',
|
|
873
|
-
}),
|
|
874
|
-
);
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
it('should find group message from nested tool results in multiple children', async () => {
|
|
878
|
-
const { result } = renderHook(() => useChatStore());
|
|
879
|
-
const dispatchSpy = vi.fn();
|
|
880
|
-
|
|
881
|
-
const groupMessage: UIChatMessage = {
|
|
882
|
-
id: GROUP_MESSAGE_ID,
|
|
883
|
-
role: 'group',
|
|
884
|
-
content: '',
|
|
885
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
886
|
-
children: [
|
|
887
|
-
{
|
|
888
|
-
id: 'first-block',
|
|
889
|
-
content: 'First assistant response',
|
|
890
|
-
tools: [
|
|
891
|
-
{
|
|
892
|
-
id: 'tool-1',
|
|
893
|
-
type: 'builtin',
|
|
894
|
-
apiName: 'tool1',
|
|
895
|
-
identifier: 'tool-1',
|
|
896
|
-
arguments: '{}',
|
|
897
|
-
result_msg_id: 'other-result-id',
|
|
898
|
-
},
|
|
899
|
-
],
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
id: 'second-block',
|
|
903
|
-
content: 'Second assistant response',
|
|
904
|
-
tools: [
|
|
905
|
-
{
|
|
906
|
-
id: 'tool-2',
|
|
907
|
-
type: 'builtin',
|
|
908
|
-
apiName: 'tool2',
|
|
909
|
-
identifier: 'tool-2',
|
|
910
|
-
arguments: '{}',
|
|
911
|
-
result_msg_id: TOOL_RESULT_MSG_ID, // Target tool result
|
|
912
|
-
},
|
|
913
|
-
],
|
|
914
|
-
},
|
|
915
|
-
],
|
|
916
|
-
} as any;
|
|
917
|
-
|
|
918
|
-
act(() => {
|
|
919
|
-
useChatStore.setState({
|
|
920
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
921
|
-
messagesMap: {
|
|
922
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
|
|
923
|
-
},
|
|
924
|
-
internal_execAgentRuntime: vi.fn(),
|
|
925
|
-
internal_dispatchMessage: dispatchSpy,
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
await act(async () => {
|
|
930
|
-
await result.current.callToolFollowAssistantMessage({
|
|
931
|
-
parentId: TOOL_RESULT_MSG_ID,
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
// Should find the correct group message even with multiple children
|
|
936
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
937
|
-
expect.objectContaining({
|
|
938
|
-
type: 'addGroupBlock',
|
|
939
|
-
groupMessageId: GROUP_MESSAGE_ID,
|
|
940
|
-
}),
|
|
941
|
-
);
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it('should call internal_execAgentRuntime after creating assistant message', async () => {
|
|
945
|
-
const { result } = renderHook(() => useChatStore());
|
|
946
|
-
const mockExecAgentRuntime = vi.fn();
|
|
947
|
-
|
|
948
|
-
const groupMessage: UIChatMessage = {
|
|
949
|
-
id: GROUP_MESSAGE_ID,
|
|
950
|
-
role: 'group',
|
|
951
|
-
content: '',
|
|
952
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
953
|
-
children: [
|
|
954
|
-
{
|
|
955
|
-
id: ASSISTANT_BLOCK_ID,
|
|
956
|
-
content: 'Response',
|
|
957
|
-
tools: [
|
|
958
|
-
{
|
|
959
|
-
id: TOOL_CALL_ID,
|
|
960
|
-
type: 'builtin',
|
|
961
|
-
apiName: 'test',
|
|
962
|
-
identifier: 'test',
|
|
963
|
-
arguments: '{}',
|
|
964
|
-
result_msg_id: TOOL_RESULT_MSG_ID,
|
|
965
|
-
},
|
|
966
|
-
],
|
|
967
|
-
},
|
|
968
|
-
],
|
|
969
|
-
} as any;
|
|
970
|
-
|
|
971
|
-
act(() => {
|
|
972
|
-
useChatStore.setState({
|
|
973
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
974
|
-
messagesMap: {
|
|
975
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
|
|
976
|
-
},
|
|
977
|
-
internal_execAgentRuntime: mockExecAgentRuntime,
|
|
978
|
-
});
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
await act(async () => {
|
|
982
|
-
await result.current.callToolFollowAssistantMessage({
|
|
983
|
-
parentId: TOOL_RESULT_MSG_ID,
|
|
984
|
-
traceId: 'test-trace-id',
|
|
985
|
-
threadId: 'test-thread-id',
|
|
986
|
-
});
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
expect(mockExecAgentRuntime).toHaveBeenCalledWith(
|
|
990
|
-
expect.objectContaining({
|
|
991
|
-
assistantMessageId: 'new-assistant-block-id',
|
|
992
|
-
traceId: 'test-trace-id',
|
|
993
|
-
threadId: 'test-thread-id',
|
|
994
|
-
}),
|
|
995
|
-
);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
it('should handle missing result_msg_id field gracefully', async () => {
|
|
999
|
-
const { result } = renderHook(() => useChatStore());
|
|
1000
|
-
const dispatchSpy = vi.fn();
|
|
1001
|
-
|
|
1002
|
-
const groupMessage: UIChatMessage = {
|
|
1003
|
-
id: GROUP_MESSAGE_ID,
|
|
1004
|
-
role: 'group',
|
|
1005
|
-
content: '',
|
|
1006
|
-
sessionId: TEST_IDS.SESSION_ID,
|
|
1007
|
-
children: [
|
|
1008
|
-
{
|
|
1009
|
-
id: ASSISTANT_BLOCK_ID,
|
|
1010
|
-
content: 'Response',
|
|
1011
|
-
tools: [
|
|
1012
|
-
{
|
|
1013
|
-
id: TOOL_CALL_ID,
|
|
1014
|
-
type: 'builtin',
|
|
1015
|
-
apiName: 'test',
|
|
1016
|
-
identifier: 'test',
|
|
1017
|
-
arguments: '{}',
|
|
1018
|
-
// Missing result_msg_id
|
|
1019
|
-
},
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
|
-
],
|
|
1023
|
-
} as any;
|
|
1024
|
-
|
|
1025
|
-
act(() => {
|
|
1026
|
-
useChatStore.setState({
|
|
1027
|
-
activeId: TEST_IDS.SESSION_ID,
|
|
1028
|
-
messagesMap: {
|
|
1029
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
|
|
1030
|
-
},
|
|
1031
|
-
internal_execAgentRuntime: vi.fn(),
|
|
1032
|
-
internal_dispatchMessage: dispatchSpy,
|
|
1033
|
-
});
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
await act(async () => {
|
|
1037
|
-
await result.current.callToolFollowAssistantMessage({
|
|
1038
|
-
parentId: TOOL_RESULT_MSG_ID,
|
|
1039
|
-
});
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
// Should create as regular message since no groupMessageId found
|
|
1043
|
-
expect(dispatchSpy).toHaveBeenCalledWith(
|
|
1044
|
-
expect.objectContaining({
|
|
1045
|
-
type: 'createMessage',
|
|
1046
|
-
}),
|
|
1047
|
-
);
|
|
1048
|
-
});
|
|
1049
|
-
});
|
|
1050
|
-
});
|