@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
  3. package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
  4. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
  5. package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
  6. package/changelog/v1.json +18 -0
  7. package/package.json +1 -1
  8. package/packages/agent-runtime/src/core/runtime.ts +36 -1
  9. package/packages/agent-runtime/src/types/event.ts +1 -0
  10. package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
  11. package/packages/agent-runtime/src/types/instruction.ts +30 -0
  12. package/packages/agent-runtime/src/types/runtime.ts +7 -0
  13. package/packages/types/src/message/common/metadata.ts +3 -0
  14. package/packages/types/src/message/common/tools.ts +2 -2
  15. package/packages/types/src/tool/search/index.ts +8 -2
  16. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  17. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
  18. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
  19. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
  20. package/src/components/Analytics/MainInterfaceTracker.tsx +2 -2
  21. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  22. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  23. package/src/features/Conversation/MarkdownElements/LobeThinking/Render.tsx +3 -3
  24. package/src/features/Conversation/MarkdownElements/Thinking/Render.tsx +3 -3
  25. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  26. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  27. package/src/features/Conversation/Messages/index.tsx +3 -3
  28. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  29. package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +3 -3
  30. package/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx +3 -3
  31. package/src/features/ShareModal/ShareText/index.tsx +3 -3
  32. package/src/services/search.ts +2 -2
  33. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  34. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  43. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  44. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  45. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  46. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  47. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  48. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  49. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  50. package/src/store/chat/selectors.ts +1 -0
  51. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  52. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  53. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  54. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  55. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  56. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  57. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  58. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  59. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  60. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  61. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  62. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  63. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  64. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  65. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  66. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  67. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  68. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  69. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  70. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  71. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  72. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  73. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  74. package/src/store/chat/slices/message/action.test.ts +134 -16
  75. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  76. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  77. package/src/store/chat/slices/message/initialState.ts +0 -10
  78. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  79. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  80. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  81. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  82. package/src/store/chat/slices/operation/actions.ts +218 -11
  83. package/src/store/chat/slices/operation/selectors.ts +135 -6
  84. package/src/store/chat/slices/operation/types.ts +29 -3
  85. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  86. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  87. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  88. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  89. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  90. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  91. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  92. package/src/store/chat/slices/topic/action.ts +3 -3
  93. package/src/store/chat/slices/translate/action.ts +54 -41
  94. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  95. 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
+ });