@lobehub/lobehub 2.0.0-next.84 → 2.0.0-next.86
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/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- 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/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/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,686 @@
|
|
|
1
|
+
import type { AgentEventDone } from '@lobechat/agent-runtime';
|
|
2
|
+
import type { ChatToolPayload } from '@lobechat/types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createAssistantMessage,
|
|
7
|
+
createMockStore,
|
|
8
|
+
createResolveAbortedToolsInstruction,
|
|
9
|
+
} from './fixtures';
|
|
10
|
+
import {
|
|
11
|
+
createInitialState,
|
|
12
|
+
createTestContext,
|
|
13
|
+
executeWithMockContext,
|
|
14
|
+
expectMessageCreated,
|
|
15
|
+
} from './helpers';
|
|
16
|
+
|
|
17
|
+
describe('resolve_aborted_tools executor', () => {
|
|
18
|
+
describe('Basic Behavior', () => {
|
|
19
|
+
it('should create tool messages with aborted status', async () => {
|
|
20
|
+
// Given
|
|
21
|
+
const mockStore = createMockStore();
|
|
22
|
+
const context = createTestContext({ sessionId: 'test-session', topicId: 'test-topic' });
|
|
23
|
+
|
|
24
|
+
const toolCalls: ChatToolPayload[] = [
|
|
25
|
+
{
|
|
26
|
+
id: 'tool_1',
|
|
27
|
+
identifier: 'lobe-web-browsing',
|
|
28
|
+
apiName: 'search',
|
|
29
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
30
|
+
type: 'default',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const parentMessage = createAssistantMessage();
|
|
35
|
+
const instruction = createResolveAbortedToolsInstruction(toolCalls, parentMessage.id);
|
|
36
|
+
const state = createInitialState({ sessionId: 'test-session' });
|
|
37
|
+
|
|
38
|
+
// When
|
|
39
|
+
const result = await executeWithMockContext({
|
|
40
|
+
executor: 'resolve_aborted_tools',
|
|
41
|
+
instruction,
|
|
42
|
+
state,
|
|
43
|
+
mockStore,
|
|
44
|
+
context,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Then
|
|
48
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
role: 'tool',
|
|
52
|
+
content: 'Tool execution was aborted by user.',
|
|
53
|
+
plugin: toolCalls[0],
|
|
54
|
+
pluginIntervention: { status: 'aborted' },
|
|
55
|
+
tool_call_id: 'tool_1',
|
|
56
|
+
sessionId: 'test-session',
|
|
57
|
+
topicId: 'test-topic',
|
|
58
|
+
parentId: parentMessage.id,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle multiple aborted tools', async () => {
|
|
64
|
+
// Given
|
|
65
|
+
const mockStore = createMockStore();
|
|
66
|
+
const context = createTestContext({ sessionId: 'test-session' });
|
|
67
|
+
|
|
68
|
+
const toolCalls: ChatToolPayload[] = [
|
|
69
|
+
{
|
|
70
|
+
id: 'tool_1',
|
|
71
|
+
identifier: 'lobe-web-browsing',
|
|
72
|
+
apiName: 'search',
|
|
73
|
+
arguments: JSON.stringify({ query: 'test1' }),
|
|
74
|
+
type: 'default',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'tool_2',
|
|
78
|
+
identifier: 'lobe-web-browsing',
|
|
79
|
+
apiName: 'craw',
|
|
80
|
+
arguments: JSON.stringify({ url: 'https://example.com' }),
|
|
81
|
+
type: 'default',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'tool_3',
|
|
85
|
+
identifier: 'lobe-image-generator',
|
|
86
|
+
apiName: 'generate',
|
|
87
|
+
arguments: JSON.stringify({ prompt: 'test prompt' }),
|
|
88
|
+
type: 'default',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const instruction = createResolveAbortedToolsInstruction(toolCalls);
|
|
93
|
+
const state = createInitialState();
|
|
94
|
+
|
|
95
|
+
// When
|
|
96
|
+
const result = await executeWithMockContext({
|
|
97
|
+
executor: 'resolve_aborted_tools',
|
|
98
|
+
instruction,
|
|
99
|
+
state,
|
|
100
|
+
mockStore,
|
|
101
|
+
context,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Then
|
|
105
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(3);
|
|
106
|
+
|
|
107
|
+
// Verify each tool message
|
|
108
|
+
toolCalls.forEach((toolCall) => {
|
|
109
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
role: 'tool',
|
|
112
|
+
content: 'Tool execution was aborted by user.',
|
|
113
|
+
plugin: toolCall,
|
|
114
|
+
pluginIntervention: { status: 'aborted' },
|
|
115
|
+
tool_call_id: toolCall.id,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should mark state as done', async () => {
|
|
122
|
+
// Given
|
|
123
|
+
const mockStore = createMockStore();
|
|
124
|
+
const context = createTestContext();
|
|
125
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
126
|
+
const state = createInitialState({ status: 'waiting_for_human' });
|
|
127
|
+
|
|
128
|
+
// When
|
|
129
|
+
const result = await executeWithMockContext({
|
|
130
|
+
executor: 'resolve_aborted_tools',
|
|
131
|
+
instruction,
|
|
132
|
+
state,
|
|
133
|
+
mockStore,
|
|
134
|
+
context,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Then
|
|
138
|
+
expect(result.newState.status).toBe('done');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should emit done event with user_aborted reason', async () => {
|
|
142
|
+
// Given
|
|
143
|
+
const mockStore = createMockStore();
|
|
144
|
+
const context = createTestContext();
|
|
145
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
146
|
+
const state = createInitialState();
|
|
147
|
+
|
|
148
|
+
// When
|
|
149
|
+
const result = await executeWithMockContext({
|
|
150
|
+
executor: 'resolve_aborted_tools',
|
|
151
|
+
instruction,
|
|
152
|
+
state,
|
|
153
|
+
mockStore,
|
|
154
|
+
context,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Then
|
|
158
|
+
expect(result.events).toHaveLength(1);
|
|
159
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
160
|
+
expect(doneEvent).toMatchObject({
|
|
161
|
+
type: 'done',
|
|
162
|
+
reason: 'user_aborted',
|
|
163
|
+
reasonDetail: 'User aborted operation with pending tool calls',
|
|
164
|
+
finalState: result.newState,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Tool Message Creation', () => {
|
|
170
|
+
it('should create tool messages with correct structure', async () => {
|
|
171
|
+
// Given
|
|
172
|
+
const mockStore = createMockStore();
|
|
173
|
+
const context = createTestContext({ sessionId: 'sess_123', topicId: 'topic_456' });
|
|
174
|
+
|
|
175
|
+
const toolCall: ChatToolPayload = {
|
|
176
|
+
id: 'tool_abc',
|
|
177
|
+
identifier: 'lobe-web-browsing',
|
|
178
|
+
apiName: 'search',
|
|
179
|
+
arguments: JSON.stringify({ query: 'AI news' }),
|
|
180
|
+
type: 'default',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const instruction = createResolveAbortedToolsInstruction([toolCall], 'msg_parent');
|
|
184
|
+
const state = createInitialState();
|
|
185
|
+
|
|
186
|
+
// When
|
|
187
|
+
await executeWithMockContext({
|
|
188
|
+
executor: 'resolve_aborted_tools',
|
|
189
|
+
instruction,
|
|
190
|
+
state,
|
|
191
|
+
mockStore,
|
|
192
|
+
context: { ...context, sessionId: 'sess_123', topicId: 'topic_456' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Then
|
|
196
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({
|
|
197
|
+
role: 'tool',
|
|
198
|
+
content: 'Tool execution was aborted by user.',
|
|
199
|
+
plugin: toolCall,
|
|
200
|
+
pluginIntervention: { status: 'aborted' },
|
|
201
|
+
tool_call_id: 'tool_abc',
|
|
202
|
+
parentId: 'msg_parent',
|
|
203
|
+
sessionId: 'sess_123',
|
|
204
|
+
topicId: 'topic_456',
|
|
205
|
+
threadId: undefined,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should preserve tool payload details', async () => {
|
|
210
|
+
// Given
|
|
211
|
+
const mockStore = createMockStore();
|
|
212
|
+
const context = createTestContext();
|
|
213
|
+
|
|
214
|
+
const toolCall: ChatToolPayload = {
|
|
215
|
+
id: 'tool_complex',
|
|
216
|
+
identifier: 'custom-plugin',
|
|
217
|
+
apiName: 'complexApi',
|
|
218
|
+
arguments: JSON.stringify({
|
|
219
|
+
param1: 'value1',
|
|
220
|
+
param2: { nested: 'value2' },
|
|
221
|
+
param3: [1, 2, 3],
|
|
222
|
+
}),
|
|
223
|
+
type: 'builtin',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const instruction = createResolveAbortedToolsInstruction([toolCall]);
|
|
227
|
+
const state = createInitialState();
|
|
228
|
+
|
|
229
|
+
// When
|
|
230
|
+
await executeWithMockContext({
|
|
231
|
+
executor: 'resolve_aborted_tools',
|
|
232
|
+
instruction,
|
|
233
|
+
state,
|
|
234
|
+
mockStore,
|
|
235
|
+
context,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Then
|
|
239
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
plugin: toolCall,
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle tool without topicId', async () => {
|
|
247
|
+
// Given
|
|
248
|
+
const mockStore = createMockStore();
|
|
249
|
+
const context = createTestContext({ sessionId: 'test-session', topicId: null });
|
|
250
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
251
|
+
const state = createInitialState();
|
|
252
|
+
|
|
253
|
+
// When
|
|
254
|
+
await executeWithMockContext({
|
|
255
|
+
executor: 'resolve_aborted_tools',
|
|
256
|
+
instruction,
|
|
257
|
+
state,
|
|
258
|
+
mockStore,
|
|
259
|
+
context: { ...context, sessionId: 'test-session', topicId: null },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Then
|
|
263
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
topicId: undefined,
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('State Management', () => {
|
|
272
|
+
it('should update lastModified timestamp', async () => {
|
|
273
|
+
// Given
|
|
274
|
+
const mockStore = createMockStore();
|
|
275
|
+
const context = createTestContext();
|
|
276
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
277
|
+
const oldTimestamp = new Date('2024-01-01').toISOString();
|
|
278
|
+
const state = createInitialState({ lastModified: oldTimestamp });
|
|
279
|
+
|
|
280
|
+
// When
|
|
281
|
+
const result = await executeWithMockContext({
|
|
282
|
+
executor: 'resolve_aborted_tools',
|
|
283
|
+
instruction,
|
|
284
|
+
state,
|
|
285
|
+
mockStore,
|
|
286
|
+
context,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Then
|
|
290
|
+
expect(result.newState.lastModified).not.toBe(oldTimestamp);
|
|
291
|
+
expect(new Date(result.newState.lastModified).getTime()).toBeGreaterThan(
|
|
292
|
+
new Date(oldTimestamp).getTime(),
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should preserve other state fields', async () => {
|
|
297
|
+
// Given
|
|
298
|
+
const mockStore = createMockStore();
|
|
299
|
+
const context = createTestContext();
|
|
300
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
301
|
+
const state = createInitialState({
|
|
302
|
+
sessionId: 'test-session',
|
|
303
|
+
stepCount: 10,
|
|
304
|
+
messages: [{ role: 'user', content: 'test' } as any],
|
|
305
|
+
cost: {
|
|
306
|
+
total: 0.05,
|
|
307
|
+
calculatedAt: new Date().toISOString(),
|
|
308
|
+
currency: 'USD',
|
|
309
|
+
llm: { total: 0.04, currency: 'USD', byModel: [] },
|
|
310
|
+
tools: { total: 0.01, currency: 'USD', byTool: [] },
|
|
311
|
+
},
|
|
312
|
+
usage: {
|
|
313
|
+
humanInteraction: {
|
|
314
|
+
approvalRequests: 0,
|
|
315
|
+
promptRequests: 0,
|
|
316
|
+
selectRequests: 0,
|
|
317
|
+
totalWaitingTimeMs: 0,
|
|
318
|
+
},
|
|
319
|
+
llm: {
|
|
320
|
+
apiCalls: 2,
|
|
321
|
+
processingTimeMs: 100,
|
|
322
|
+
tokens: {
|
|
323
|
+
input: 100,
|
|
324
|
+
output: 200,
|
|
325
|
+
total: 300,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
tools: {
|
|
329
|
+
totalCalls: 2,
|
|
330
|
+
totalTimeMs: 500,
|
|
331
|
+
byTool: [],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// When
|
|
337
|
+
const result = await executeWithMockContext({
|
|
338
|
+
executor: 'resolve_aborted_tools',
|
|
339
|
+
instruction,
|
|
340
|
+
state,
|
|
341
|
+
mockStore,
|
|
342
|
+
context,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Then
|
|
346
|
+
expect(result.newState.sessionId).toBe(state.sessionId);
|
|
347
|
+
expect(result.newState.stepCount).toBe(state.stepCount);
|
|
348
|
+
expect(result.newState.messages).toEqual(state.messages);
|
|
349
|
+
expect(result.newState.usage).toEqual(state.usage);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should not mutate original state', async () => {
|
|
353
|
+
// Given
|
|
354
|
+
const mockStore = createMockStore();
|
|
355
|
+
const context = createTestContext();
|
|
356
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
357
|
+
const state = createInitialState({ status: 'waiting_for_human' });
|
|
358
|
+
const originalState = JSON.parse(JSON.stringify(state));
|
|
359
|
+
|
|
360
|
+
// When
|
|
361
|
+
const result = await executeWithMockContext({
|
|
362
|
+
executor: 'resolve_aborted_tools',
|
|
363
|
+
instruction,
|
|
364
|
+
state,
|
|
365
|
+
mockStore,
|
|
366
|
+
context,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Then
|
|
370
|
+
expect(state).toEqual(originalState);
|
|
371
|
+
expect(result.newState).not.toBe(state);
|
|
372
|
+
expect(result.newState.status).toBe('done');
|
|
373
|
+
expect(state.status).toBe('waiting_for_human');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('Event Handling', () => {
|
|
378
|
+
it('should emit single done event', async () => {
|
|
379
|
+
// Given
|
|
380
|
+
const mockStore = createMockStore();
|
|
381
|
+
const context = createTestContext();
|
|
382
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
383
|
+
const state = createInitialState();
|
|
384
|
+
|
|
385
|
+
// When
|
|
386
|
+
const result = await executeWithMockContext({
|
|
387
|
+
executor: 'resolve_aborted_tools',
|
|
388
|
+
instruction,
|
|
389
|
+
state,
|
|
390
|
+
mockStore,
|
|
391
|
+
context,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Then
|
|
395
|
+
expect(result.events).toHaveLength(1);
|
|
396
|
+
expect(result.events[0].type).toBe('done');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should include finalState in event', async () => {
|
|
400
|
+
// Given
|
|
401
|
+
const mockStore = createMockStore();
|
|
402
|
+
const context = createTestContext();
|
|
403
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
404
|
+
const state = createInitialState();
|
|
405
|
+
|
|
406
|
+
// When
|
|
407
|
+
const result = await executeWithMockContext({
|
|
408
|
+
executor: 'resolve_aborted_tools',
|
|
409
|
+
instruction,
|
|
410
|
+
state,
|
|
411
|
+
mockStore,
|
|
412
|
+
context,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Then
|
|
416
|
+
const doneEvent = result.events[0] as AgentEventDone;
|
|
417
|
+
expect(doneEvent.finalState).toEqual(result.newState);
|
|
418
|
+
expect(doneEvent.finalState.status).toBe('done');
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('Edge Cases', () => {
|
|
423
|
+
it('should handle empty toolsCalling array', async () => {
|
|
424
|
+
// Given
|
|
425
|
+
const mockStore = createMockStore();
|
|
426
|
+
const context = createTestContext();
|
|
427
|
+
// Manually construct instruction with truly empty array
|
|
428
|
+
const instruction: any = {
|
|
429
|
+
type: 'resolve_aborted_tools',
|
|
430
|
+
payload: {
|
|
431
|
+
toolsCalling: [],
|
|
432
|
+
parentMessageId: 'msg_parent',
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
const state = createInitialState();
|
|
436
|
+
|
|
437
|
+
// When
|
|
438
|
+
const result = await executeWithMockContext({
|
|
439
|
+
executor: 'resolve_aborted_tools',
|
|
440
|
+
instruction,
|
|
441
|
+
state,
|
|
442
|
+
mockStore,
|
|
443
|
+
context,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Then
|
|
447
|
+
expect(mockStore.optimisticCreateMessage).not.toHaveBeenCalled();
|
|
448
|
+
expect(result.newState.status).toBe('done');
|
|
449
|
+
expect(result.events).toHaveLength(1);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should handle tools with special characters in arguments', async () => {
|
|
453
|
+
// Given
|
|
454
|
+
const mockStore = createMockStore();
|
|
455
|
+
const context = createTestContext();
|
|
456
|
+
|
|
457
|
+
const toolCall: ChatToolPayload = {
|
|
458
|
+
id: 'tool_special',
|
|
459
|
+
identifier: 'lobe-web-browsing',
|
|
460
|
+
apiName: 'search',
|
|
461
|
+
arguments: JSON.stringify({
|
|
462
|
+
query: 'Test with "quotes" and \'apostrophes\' and <tags>',
|
|
463
|
+
}),
|
|
464
|
+
type: 'default',
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const instruction = createResolveAbortedToolsInstruction([toolCall]);
|
|
468
|
+
const state = createInitialState();
|
|
469
|
+
|
|
470
|
+
// When
|
|
471
|
+
const result = await executeWithMockContext({
|
|
472
|
+
executor: 'resolve_aborted_tools',
|
|
473
|
+
instruction,
|
|
474
|
+
state,
|
|
475
|
+
mockStore,
|
|
476
|
+
context,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Then
|
|
480
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
481
|
+
expect.objectContaining({
|
|
482
|
+
plugin: toolCall,
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should handle very large toolsCalling array', async () => {
|
|
488
|
+
// Given
|
|
489
|
+
const mockStore = createMockStore();
|
|
490
|
+
const context = createTestContext();
|
|
491
|
+
|
|
492
|
+
const toolCalls: ChatToolPayload[] = Array.from({ length: 50 }, (_, i) => ({
|
|
493
|
+
id: `tool_${i}`,
|
|
494
|
+
identifier: 'lobe-web-browsing',
|
|
495
|
+
apiName: 'search',
|
|
496
|
+
arguments: JSON.stringify({ query: `query_${i}` }),
|
|
497
|
+
type: 'default' as const,
|
|
498
|
+
}));
|
|
499
|
+
|
|
500
|
+
const instruction = createResolveAbortedToolsInstruction(toolCalls);
|
|
501
|
+
const state = createInitialState();
|
|
502
|
+
|
|
503
|
+
// When
|
|
504
|
+
const result = await executeWithMockContext({
|
|
505
|
+
executor: 'resolve_aborted_tools',
|
|
506
|
+
instruction,
|
|
507
|
+
state,
|
|
508
|
+
mockStore,
|
|
509
|
+
context,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Then
|
|
513
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(50);
|
|
514
|
+
expect(result.newState.status).toBe('done');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should handle failed message creation gracefully', async () => {
|
|
518
|
+
// Given
|
|
519
|
+
const mockStore = createMockStore({
|
|
520
|
+
optimisticCreateMessage: vi.fn().mockResolvedValue(null),
|
|
521
|
+
});
|
|
522
|
+
const context = createTestContext();
|
|
523
|
+
const instruction = createResolveAbortedToolsInstruction();
|
|
524
|
+
const state = createInitialState();
|
|
525
|
+
|
|
526
|
+
// When
|
|
527
|
+
const result = await executeWithMockContext({
|
|
528
|
+
executor: 'resolve_aborted_tools',
|
|
529
|
+
instruction,
|
|
530
|
+
state,
|
|
531
|
+
mockStore,
|
|
532
|
+
context,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Then - should complete despite message creation failure
|
|
536
|
+
expect(result.newState.status).toBe('done');
|
|
537
|
+
expect(result.events).toHaveLength(1);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('Different Tool Types', () => {
|
|
542
|
+
it('should handle builtin tools', async () => {
|
|
543
|
+
// Given
|
|
544
|
+
const mockStore = createMockStore();
|
|
545
|
+
const context = createTestContext();
|
|
546
|
+
|
|
547
|
+
const toolCall: ChatToolPayload = {
|
|
548
|
+
id: 'tool_builtin',
|
|
549
|
+
identifier: 'builtin-search',
|
|
550
|
+
apiName: 'vectorSearch',
|
|
551
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
552
|
+
type: 'builtin',
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const instruction = createResolveAbortedToolsInstruction([toolCall]);
|
|
556
|
+
const state = createInitialState();
|
|
557
|
+
|
|
558
|
+
// When
|
|
559
|
+
await executeWithMockContext({
|
|
560
|
+
executor: 'resolve_aborted_tools',
|
|
561
|
+
instruction,
|
|
562
|
+
state,
|
|
563
|
+
mockStore,
|
|
564
|
+
context,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Then
|
|
568
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
569
|
+
expect.objectContaining({
|
|
570
|
+
plugin: expect.objectContaining({
|
|
571
|
+
type: 'builtin',
|
|
572
|
+
}),
|
|
573
|
+
}),
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should handle default/plugin tools', async () => {
|
|
578
|
+
// Given
|
|
579
|
+
const mockStore = createMockStore();
|
|
580
|
+
const context = createTestContext();
|
|
581
|
+
|
|
582
|
+
const toolCall: ChatToolPayload = {
|
|
583
|
+
id: 'tool_plugin',
|
|
584
|
+
identifier: 'lobe-web-browsing',
|
|
585
|
+
apiName: 'search',
|
|
586
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
587
|
+
type: 'default',
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const instruction = createResolveAbortedToolsInstruction([toolCall]);
|
|
591
|
+
const state = createInitialState();
|
|
592
|
+
|
|
593
|
+
// When
|
|
594
|
+
await executeWithMockContext({
|
|
595
|
+
executor: 'resolve_aborted_tools',
|
|
596
|
+
instruction,
|
|
597
|
+
state,
|
|
598
|
+
mockStore,
|
|
599
|
+
context,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Then
|
|
603
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
604
|
+
expect.objectContaining({
|
|
605
|
+
plugin: expect.objectContaining({
|
|
606
|
+
type: 'default',
|
|
607
|
+
}),
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should handle mixed tool types', async () => {
|
|
613
|
+
// Given
|
|
614
|
+
const mockStore = createMockStore();
|
|
615
|
+
const context = createTestContext();
|
|
616
|
+
|
|
617
|
+
const toolCalls: ChatToolPayload[] = [
|
|
618
|
+
{
|
|
619
|
+
id: 'tool_1',
|
|
620
|
+
identifier: 'lobe-web-browsing',
|
|
621
|
+
apiName: 'search',
|
|
622
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
623
|
+
type: 'default',
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: 'tool_2',
|
|
627
|
+
identifier: 'builtin-search',
|
|
628
|
+
apiName: 'vectorSearch',
|
|
629
|
+
arguments: JSON.stringify({ query: 'test' }),
|
|
630
|
+
type: 'builtin',
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
const instruction = createResolveAbortedToolsInstruction(toolCalls);
|
|
635
|
+
const state = createInitialState();
|
|
636
|
+
|
|
637
|
+
// When
|
|
638
|
+
await executeWithMockContext({
|
|
639
|
+
executor: 'resolve_aborted_tools',
|
|
640
|
+
instruction,
|
|
641
|
+
state,
|
|
642
|
+
mockStore,
|
|
643
|
+
context,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Then
|
|
647
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(2);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
describe('Concurrent Tool Message Creation', () => {
|
|
652
|
+
it('should create all tool messages concurrently', async () => {
|
|
653
|
+
// Given
|
|
654
|
+
const mockStore = createMockStore();
|
|
655
|
+
const context = createTestContext();
|
|
656
|
+
|
|
657
|
+
const toolCalls: ChatToolPayload[] = Array.from({ length: 5 }, (_, i) => ({
|
|
658
|
+
id: `tool_${i}`,
|
|
659
|
+
identifier: 'lobe-web-browsing',
|
|
660
|
+
apiName: 'search',
|
|
661
|
+
arguments: JSON.stringify({ query: `query_${i}` }),
|
|
662
|
+
type: 'default' as const,
|
|
663
|
+
}));
|
|
664
|
+
|
|
665
|
+
const instruction = createResolveAbortedToolsInstruction(toolCalls);
|
|
666
|
+
const state = createInitialState();
|
|
667
|
+
|
|
668
|
+
const startTime = Date.now();
|
|
669
|
+
|
|
670
|
+
// When
|
|
671
|
+
await executeWithMockContext({
|
|
672
|
+
executor: 'resolve_aborted_tools',
|
|
673
|
+
instruction,
|
|
674
|
+
state,
|
|
675
|
+
mockStore,
|
|
676
|
+
context,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const duration = Date.now() - startTime;
|
|
680
|
+
|
|
681
|
+
// Then - should complete quickly (concurrent execution)
|
|
682
|
+
expect(duration).toBeLessThan(100); // Should be fast since mocked
|
|
683
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledTimes(5);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
});
|