@lobehub/lobehub 2.0.0-next.85 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +25 -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 +9 -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/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  21. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  22. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  23. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  24. package/src/features/Conversation/Messages/index.tsx +3 -3
  25. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  26. package/src/services/search.ts +2 -2
  27. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  28. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  29. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  43. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  44. package/src/store/chat/selectors.ts +1 -0
  45. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  46. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  47. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  49. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  53. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  54. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  55. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  56. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  57. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  58. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  59. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  60. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  61. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  63. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  64. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  65. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  66. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  67. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  68. package/src/store/chat/slices/message/action.test.ts +134 -16
  69. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  70. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  71. package/src/store/chat/slices/message/initialState.ts +0 -10
  72. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  73. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  74. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  75. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  76. package/src/store/chat/slices/operation/actions.ts +218 -11
  77. package/src/store/chat/slices/operation/selectors.ts +135 -6
  78. package/src/store/chat/slices/operation/types.ts +29 -3
  79. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  80. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  81. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  82. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  83. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  84. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  85. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  86. package/src/store/chat/slices/translate/action.ts +54 -41
  87. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  88. 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
+ });