@lobehub/lobehub 2.0.0-next.85 → 2.0.0-next.87
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/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/components/Analytics/MainInterfaceTracker.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/MarkdownElements/LobeThinking/Render.tsx +3 -3
- package/src/features/Conversation/MarkdownElements/Thinking/Render.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +3 -3
- package/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx +3 -3
- package/src/features/ShareModal/ShareText/index.tsx +3 -3
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/topic/action.ts +3 -3
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import type { ChatToolPayload } from '@lobechat/types';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createAssistantMessage,
|
|
6
|
+
createMockStore,
|
|
7
|
+
createRequestHumanApproveInstruction,
|
|
8
|
+
} from './fixtures';
|
|
9
|
+
import { createInitialState, createTestContext, executeWithMockContext } from './helpers';
|
|
10
|
+
|
|
11
|
+
describe('request_human_approve executor', () => {
|
|
12
|
+
describe('Basic Behavior', () => {
|
|
13
|
+
it('should create tool messages with pending intervention status', async () => {
|
|
14
|
+
// Given
|
|
15
|
+
const mockStore = createMockStore();
|
|
16
|
+
const assistantMessage = createAssistantMessage({ id: 'msg_assistant' });
|
|
17
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
18
|
+
|
|
19
|
+
const context = createTestContext();
|
|
20
|
+
const toolCalls: ChatToolPayload[] = [
|
|
21
|
+
{
|
|
22
|
+
id: 'tool_1',
|
|
23
|
+
identifier: 'lobe-web-browsing',
|
|
24
|
+
apiName: 'search',
|
|
25
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
26
|
+
type: 'default',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls);
|
|
31
|
+
const state = createInitialState();
|
|
32
|
+
|
|
33
|
+
// When
|
|
34
|
+
const result = await executeWithMockContext({
|
|
35
|
+
executor: 'request_human_approve',
|
|
36
|
+
instruction,
|
|
37
|
+
state,
|
|
38
|
+
mockStore,
|
|
39
|
+
context,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Then
|
|
43
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
role: 'tool',
|
|
47
|
+
content: '',
|
|
48
|
+
plugin: toolCalls[0],
|
|
49
|
+
pluginIntervention: { status: 'pending' },
|
|
50
|
+
tool_call_id: 'tool_1',
|
|
51
|
+
parentId: 'msg_assistant',
|
|
52
|
+
groupId: assistantMessage.groupId,
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should update state to waiting_for_human', async () => {
|
|
58
|
+
// Given
|
|
59
|
+
const mockStore = createMockStore();
|
|
60
|
+
const assistantMessage = createAssistantMessage();
|
|
61
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
62
|
+
|
|
63
|
+
const context = createTestContext();
|
|
64
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
65
|
+
const state = createInitialState();
|
|
66
|
+
|
|
67
|
+
// When
|
|
68
|
+
const result = await executeWithMockContext({
|
|
69
|
+
executor: 'request_human_approve',
|
|
70
|
+
instruction,
|
|
71
|
+
state,
|
|
72
|
+
mockStore,
|
|
73
|
+
context,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Then
|
|
77
|
+
expect(result.newState.status).toBe('waiting_for_human');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should store pendingToolsCalling in state', async () => {
|
|
81
|
+
// Given
|
|
82
|
+
const mockStore = createMockStore();
|
|
83
|
+
const assistantMessage = createAssistantMessage();
|
|
84
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
85
|
+
|
|
86
|
+
const context = createTestContext();
|
|
87
|
+
const toolCalls: ChatToolPayload[] = [
|
|
88
|
+
{
|
|
89
|
+
id: 'tool_1',
|
|
90
|
+
identifier: 'lobe-web-browsing',
|
|
91
|
+
apiName: 'search',
|
|
92
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
93
|
+
type: 'default',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'tool_2',
|
|
97
|
+
identifier: 'lobe-web-browsing',
|
|
98
|
+
apiName: 'craw',
|
|
99
|
+
arguments: JSON.stringify({ url: 'https://example.com' }),
|
|
100
|
+
type: 'default',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls);
|
|
105
|
+
const state = createInitialState();
|
|
106
|
+
|
|
107
|
+
// When
|
|
108
|
+
const result = await executeWithMockContext({
|
|
109
|
+
executor: 'request_human_approve',
|
|
110
|
+
instruction,
|
|
111
|
+
state,
|
|
112
|
+
mockStore,
|
|
113
|
+
context,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Then
|
|
117
|
+
expect(result.newState.pendingToolsCalling).toEqual(toolCalls);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should emit human_approve_required event', async () => {
|
|
121
|
+
// Given
|
|
122
|
+
const mockStore = createMockStore();
|
|
123
|
+
const assistantMessage = createAssistantMessage();
|
|
124
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
125
|
+
|
|
126
|
+
const context = createTestContext();
|
|
127
|
+
const toolCalls: ChatToolPayload[] = [
|
|
128
|
+
{
|
|
129
|
+
id: 'tool_1',
|
|
130
|
+
identifier: 'lobe-web-browsing',
|
|
131
|
+
apiName: 'search',
|
|
132
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
133
|
+
type: 'default',
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls);
|
|
138
|
+
const state = createInitialState({ sessionId: 'test-session' });
|
|
139
|
+
|
|
140
|
+
// When
|
|
141
|
+
const result = await executeWithMockContext({
|
|
142
|
+
executor: 'request_human_approve',
|
|
143
|
+
instruction,
|
|
144
|
+
state,
|
|
145
|
+
mockStore,
|
|
146
|
+
context,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Then
|
|
150
|
+
expect(result.events).toHaveLength(1);
|
|
151
|
+
expect(result.events[0]).toMatchObject({
|
|
152
|
+
type: 'human_approve_required',
|
|
153
|
+
pendingToolsCalling: toolCalls,
|
|
154
|
+
sessionId: 'test-session',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Assistant Message Handling', () => {
|
|
160
|
+
it('should throw error if no assistant message found', async () => {
|
|
161
|
+
// Given
|
|
162
|
+
const mockStore = createMockStore();
|
|
163
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = []; // No messages
|
|
164
|
+
|
|
165
|
+
const context = createTestContext();
|
|
166
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
167
|
+
const state = createInitialState();
|
|
168
|
+
|
|
169
|
+
// When/Then
|
|
170
|
+
await expect(
|
|
171
|
+
executeWithMockContext({
|
|
172
|
+
executor: 'request_human_approve',
|
|
173
|
+
instruction,
|
|
174
|
+
state,
|
|
175
|
+
mockStore,
|
|
176
|
+
context,
|
|
177
|
+
}),
|
|
178
|
+
).rejects.toThrow('No assistant message found for intervention');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should use groupId from assistant message', async () => {
|
|
182
|
+
// Given
|
|
183
|
+
const mockStore = createMockStore();
|
|
184
|
+
const assistantMessage = createAssistantMessage({
|
|
185
|
+
id: 'msg_assistant',
|
|
186
|
+
groupId: 'group_123',
|
|
187
|
+
});
|
|
188
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
189
|
+
|
|
190
|
+
const context = createTestContext();
|
|
191
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
192
|
+
const state = createInitialState();
|
|
193
|
+
|
|
194
|
+
// When
|
|
195
|
+
await executeWithMockContext({
|
|
196
|
+
executor: 'request_human_approve',
|
|
197
|
+
instruction,
|
|
198
|
+
state,
|
|
199
|
+
mockStore,
|
|
200
|
+
context,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Then
|
|
204
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
205
|
+
expect.objectContaining({
|
|
206
|
+
groupId: 'group_123',
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should use assistant message id as parentId', async () => {
|
|
212
|
+
// Given
|
|
213
|
+
const mockStore = createMockStore();
|
|
214
|
+
const assistantMessage = createAssistantMessage({ id: 'msg_assistant_456' });
|
|
215
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
216
|
+
|
|
217
|
+
const context = createTestContext();
|
|
218
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
219
|
+
const state = createInitialState();
|
|
220
|
+
|
|
221
|
+
// When
|
|
222
|
+
await executeWithMockContext({
|
|
223
|
+
executor: 'request_human_approve',
|
|
224
|
+
instruction,
|
|
225
|
+
state,
|
|
226
|
+
mockStore,
|
|
227
|
+
context,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Then
|
|
231
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
parentId: 'msg_assistant_456',
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('Skip Create Tool Message Mode', () => {
|
|
240
|
+
it('should skip message creation when skipCreateToolMessage is true', async () => {
|
|
241
|
+
// Given
|
|
242
|
+
const mockStore = createMockStore();
|
|
243
|
+
const assistantMessage = createAssistantMessage();
|
|
244
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
245
|
+
|
|
246
|
+
const context = createTestContext();
|
|
247
|
+
const toolCalls: ChatToolPayload[] = [
|
|
248
|
+
{
|
|
249
|
+
id: 'tool_1',
|
|
250
|
+
identifier: 'lobe-web-browsing',
|
|
251
|
+
apiName: 'search',
|
|
252
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
253
|
+
type: 'default',
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls, {
|
|
258
|
+
skipCreateToolMessage: true,
|
|
259
|
+
});
|
|
260
|
+
const state = createInitialState();
|
|
261
|
+
|
|
262
|
+
// When
|
|
263
|
+
const result = await executeWithMockContext({
|
|
264
|
+
executor: 'request_human_approve',
|
|
265
|
+
instruction,
|
|
266
|
+
state,
|
|
267
|
+
mockStore,
|
|
268
|
+
context,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Then
|
|
272
|
+
expect(mockStore.optimisticCreateMessage).not.toHaveBeenCalled();
|
|
273
|
+
expect(result.newState.status).toBe('waiting_for_human');
|
|
274
|
+
expect(result.events).toHaveLength(1);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('Multiple Tool Messages', () => {
|
|
279
|
+
it('should create multiple tool messages for multiple pending tools', async () => {
|
|
280
|
+
// Given
|
|
281
|
+
const mockStore = createMockStore();
|
|
282
|
+
const assistantMessage = createAssistantMessage();
|
|
283
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
284
|
+
|
|
285
|
+
const context = createTestContext();
|
|
286
|
+
const toolCalls: ChatToolPayload[] = [
|
|
287
|
+
{
|
|
288
|
+
id: 'tool_1',
|
|
289
|
+
identifier: 'lobe-web-browsing',
|
|
290
|
+
apiName: 'search',
|
|
291
|
+
arguments: JSON.stringify({ query: 'test1' }),
|
|
292
|
+
type: 'default',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: 'tool_2',
|
|
296
|
+
identifier: 'lobe-web-browsing',
|
|
297
|
+
apiName: 'craw',
|
|
298
|
+
arguments: JSON.stringify({ url: 'https://example.com' }),
|
|
299
|
+
type: 'default',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'tool_3',
|
|
303
|
+
identifier: 'lobe-image-generator',
|
|
304
|
+
apiName: 'generate',
|
|
305
|
+
arguments: JSON.stringify({ prompt: 'test' }),
|
|
306
|
+
type: 'default',
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls);
|
|
311
|
+
const state = createInitialState();
|
|
312
|
+
|
|
313
|
+
// When
|
|
314
|
+
await executeWithMockContext({
|
|
315
|
+
executor: 'request_human_approve',
|
|
316
|
+
instruction,
|
|
317
|
+
state,
|
|
318
|
+
mockStore,
|
|
319
|
+
context,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Then
|
|
323
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(3);
|
|
324
|
+
toolCalls.forEach((toolCall) => {
|
|
325
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
326
|
+
expect.objectContaining({
|
|
327
|
+
plugin: expect.objectContaining({
|
|
328
|
+
id: toolCall.id,
|
|
329
|
+
}),
|
|
330
|
+
tool_call_id: toolCall.id,
|
|
331
|
+
pluginIntervention: { status: 'pending' },
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('State Management', () => {
|
|
339
|
+
it('should update lastModified timestamp', async () => {
|
|
340
|
+
// Given
|
|
341
|
+
const mockStore = createMockStore();
|
|
342
|
+
const assistantMessage = createAssistantMessage();
|
|
343
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
344
|
+
|
|
345
|
+
const context = createTestContext();
|
|
346
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
347
|
+
const oldTimestamp = new Date('2024-01-01').toISOString();
|
|
348
|
+
const state = createInitialState({ lastModified: oldTimestamp });
|
|
349
|
+
|
|
350
|
+
// When
|
|
351
|
+
const result = await executeWithMockContext({
|
|
352
|
+
executor: 'request_human_approve',
|
|
353
|
+
instruction,
|
|
354
|
+
state,
|
|
355
|
+
mockStore,
|
|
356
|
+
context,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Then
|
|
360
|
+
expect(result.newState.lastModified).not.toBe(oldTimestamp);
|
|
361
|
+
expect(new Date(result.newState.lastModified).getTime()).toBeGreaterThan(
|
|
362
|
+
new Date(oldTimestamp).getTime(),
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should preserve other state fields', async () => {
|
|
367
|
+
// Given
|
|
368
|
+
const mockStore = createMockStore();
|
|
369
|
+
const assistantMessage = createAssistantMessage();
|
|
370
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
371
|
+
|
|
372
|
+
const context = createTestContext();
|
|
373
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
374
|
+
const state = createInitialState({
|
|
375
|
+
sessionId: 'test-session',
|
|
376
|
+
stepCount: 10,
|
|
377
|
+
messages: [{ role: 'user', content: 'test' } as any],
|
|
378
|
+
cost: {
|
|
379
|
+
total: 0.05,
|
|
380
|
+
calculatedAt: new Date().toISOString(),
|
|
381
|
+
currency: 'USD',
|
|
382
|
+
llm: { total: 0.04, currency: 'USD', byModel: [] },
|
|
383
|
+
tools: { total: 0.01, currency: 'USD', byTool: [] },
|
|
384
|
+
},
|
|
385
|
+
usage: {
|
|
386
|
+
humanInteraction: {
|
|
387
|
+
approvalRequests: 0,
|
|
388
|
+
promptRequests: 0,
|
|
389
|
+
selectRequests: 0,
|
|
390
|
+
totalWaitingTimeMs: 0,
|
|
391
|
+
},
|
|
392
|
+
llm: {
|
|
393
|
+
apiCalls: 2,
|
|
394
|
+
processingTimeMs: 100,
|
|
395
|
+
tokens: {
|
|
396
|
+
input: 100,
|
|
397
|
+
output: 200,
|
|
398
|
+
total: 300,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
tools: {
|
|
402
|
+
totalCalls: 2,
|
|
403
|
+
totalTimeMs: 500,
|
|
404
|
+
byTool: [],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// When
|
|
410
|
+
const result = await executeWithMockContext({
|
|
411
|
+
executor: 'request_human_approve',
|
|
412
|
+
instruction,
|
|
413
|
+
state,
|
|
414
|
+
mockStore,
|
|
415
|
+
context,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Then
|
|
419
|
+
expect(result.newState.sessionId).toBe(state.sessionId);
|
|
420
|
+
expect(result.newState.stepCount).toBe(state.stepCount);
|
|
421
|
+
expect(result.newState.messages).toEqual(state.messages);
|
|
422
|
+
expect(result.newState.usage).toEqual(state.usage);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should not mutate original state', async () => {
|
|
426
|
+
// Given
|
|
427
|
+
const mockStore = createMockStore();
|
|
428
|
+
const assistantMessage = createAssistantMessage();
|
|
429
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
430
|
+
|
|
431
|
+
const context = createTestContext();
|
|
432
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
433
|
+
const state = createInitialState({ status: 'running' });
|
|
434
|
+
const originalState = JSON.parse(JSON.stringify(state));
|
|
435
|
+
|
|
436
|
+
// When
|
|
437
|
+
const result = await executeWithMockContext({
|
|
438
|
+
executor: 'request_human_approve',
|
|
439
|
+
instruction,
|
|
440
|
+
state,
|
|
441
|
+
mockStore,
|
|
442
|
+
context,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Then
|
|
446
|
+
expect(state).toEqual(originalState);
|
|
447
|
+
expect(result.newState).not.toBe(state);
|
|
448
|
+
expect(result.newState.status).toBe('waiting_for_human');
|
|
449
|
+
expect(state.status).toBe('running');
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('Error Handling', () => {
|
|
454
|
+
it('should throw error if message creation fails', async () => {
|
|
455
|
+
// Given
|
|
456
|
+
const mockStore = createMockStore({
|
|
457
|
+
optimisticCreateMessage: vi.fn().mockResolvedValue(null),
|
|
458
|
+
});
|
|
459
|
+
const assistantMessage = createAssistantMessage();
|
|
460
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
461
|
+
|
|
462
|
+
const context = createTestContext();
|
|
463
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
464
|
+
const state = createInitialState();
|
|
465
|
+
|
|
466
|
+
// When/Then
|
|
467
|
+
await expect(
|
|
468
|
+
executeWithMockContext({
|
|
469
|
+
executor: 'request_human_approve',
|
|
470
|
+
instruction,
|
|
471
|
+
state,
|
|
472
|
+
mockStore,
|
|
473
|
+
context,
|
|
474
|
+
}),
|
|
475
|
+
).rejects.toThrow('Failed to create tool message');
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe('Edge Cases', () => {
|
|
480
|
+
it('should handle very large number of pending tools', async () => {
|
|
481
|
+
// Given
|
|
482
|
+
const mockStore = createMockStore();
|
|
483
|
+
const assistantMessage = createAssistantMessage();
|
|
484
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = [assistantMessage];
|
|
485
|
+
|
|
486
|
+
const context = createTestContext();
|
|
487
|
+
const toolCalls: ChatToolPayload[] = Array.from({ length: 50 }, (_, i) => ({
|
|
488
|
+
id: `tool_${i}`,
|
|
489
|
+
identifier: 'lobe-web-browsing',
|
|
490
|
+
apiName: 'search',
|
|
491
|
+
arguments: JSON.stringify({ query: `query_${i}` }),
|
|
492
|
+
type: 'default' as const,
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
const instruction = createRequestHumanApproveInstruction(toolCalls);
|
|
496
|
+
const state = createInitialState();
|
|
497
|
+
|
|
498
|
+
// When
|
|
499
|
+
const result = await executeWithMockContext({
|
|
500
|
+
executor: 'request_human_approve',
|
|
501
|
+
instruction,
|
|
502
|
+
state,
|
|
503
|
+
mockStore,
|
|
504
|
+
context,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Then
|
|
508
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(50);
|
|
509
|
+
expect(result.newState.pendingToolsCalling).toHaveLength(50);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should find last assistant message in conversation with multiple messages', async () => {
|
|
513
|
+
// Given
|
|
514
|
+
const mockStore = createMockStore();
|
|
515
|
+
const messages = [
|
|
516
|
+
createAssistantMessage({ id: 'msg_1' }),
|
|
517
|
+
{ id: 'msg_user_1', role: 'user', content: 'Hello' } as any,
|
|
518
|
+
createAssistantMessage({ id: 'msg_2' }),
|
|
519
|
+
{ id: 'msg_user_2', role: 'user', content: 'Follow up' } as any,
|
|
520
|
+
createAssistantMessage({ id: 'msg_3_last' }),
|
|
521
|
+
];
|
|
522
|
+
mockStore.dbMessagesMap['test-session_test-topic'] = messages;
|
|
523
|
+
|
|
524
|
+
const context = createTestContext();
|
|
525
|
+
const instruction = createRequestHumanApproveInstruction();
|
|
526
|
+
const state = createInitialState();
|
|
527
|
+
|
|
528
|
+
// When
|
|
529
|
+
await executeWithMockContext({
|
|
530
|
+
executor: 'request_human_approve',
|
|
531
|
+
instruction,
|
|
532
|
+
state,
|
|
533
|
+
mockStore,
|
|
534
|
+
context,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Then
|
|
538
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
539
|
+
expect.objectContaining({
|
|
540
|
+
parentId: 'msg_3_last',
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|