@google/gemini-cli-a2a-server 0.33.0-preview.13 → 0.33.0-preview.14

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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { CoderAgentExecutor } from './executor.js';
8
+ import { EventEmitter } from 'node:events';
9
+ import { requestStorage } from '../http/requestStorage.js';
10
+ // Mocks for constructor dependencies
11
+ vi.mock('../config/config.js', () => ({
12
+ loadConfig: vi.fn().mockReturnValue({
13
+ getSessionId: () => 'test-session',
14
+ getTargetDir: () => '/tmp',
15
+ getCheckpointingEnabled: () => false,
16
+ }),
17
+ loadEnvironment: vi.fn(),
18
+ setTargetDir: vi.fn().mockReturnValue('/tmp'),
19
+ }));
20
+ vi.mock('../config/settings.js', () => ({
21
+ loadSettings: vi.fn().mockReturnValue({}),
22
+ }));
23
+ vi.mock('../config/extension.js', () => ({
24
+ loadExtensions: vi.fn().mockReturnValue([]),
25
+ }));
26
+ vi.mock('../http/requestStorage.js', () => ({
27
+ requestStorage: {
28
+ getStore: vi.fn(),
29
+ },
30
+ }));
31
+ vi.mock('./task.js', () => {
32
+ const mockTaskInstance = (taskId, contextId) => ({
33
+ id: taskId,
34
+ contextId,
35
+ taskState: 'working',
36
+ acceptUserMessage: vi
37
+ .fn()
38
+ .mockImplementation(async function* (context, aborted) {
39
+ const isConfirmation = context.userMessage.parts.some((p) => p.kind === 'confirmation');
40
+ // Hang only for main user messages (text), allow confirmations to finish quickly
41
+ if (!isConfirmation && aborted) {
42
+ await new Promise((resolve) => {
43
+ aborted.addEventListener('abort', resolve, { once: true });
44
+ });
45
+ }
46
+ yield { type: 'content', value: 'hello' };
47
+ }),
48
+ acceptAgentMessage: vi.fn().mockResolvedValue(undefined),
49
+ scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
50
+ waitForPendingTools: vi.fn().mockResolvedValue(undefined),
51
+ getAndClearCompletedTools: vi.fn().mockReturnValue([]),
52
+ addToolResponsesToHistory: vi.fn(),
53
+ sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () { }),
54
+ cancelPendingTools: vi.fn(),
55
+ setTaskStateAndPublishUpdate: vi.fn(),
56
+ dispose: vi.fn(),
57
+ getMetadata: vi.fn().mockResolvedValue({}),
58
+ geminiClient: {
59
+ initialize: vi.fn().mockResolvedValue(undefined),
60
+ },
61
+ toSDKTask: () => ({
62
+ id: taskId,
63
+ contextId,
64
+ kind: 'task',
65
+ status: { state: 'working', timestamp: new Date().toISOString() },
66
+ metadata: {},
67
+ history: [],
68
+ artifacts: [],
69
+ }),
70
+ });
71
+ const MockTask = vi.fn().mockImplementation(mockTaskInstance);
72
+ MockTask.create = vi
73
+ .fn()
74
+ .mockImplementation(async (taskId, contextId) => mockTaskInstance(taskId, contextId));
75
+ return { Task: MockTask };
76
+ });
77
+ describe('CoderAgentExecutor', () => {
78
+ let executor;
79
+ let mockTaskStore;
80
+ let mockEventBus;
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ mockTaskStore = {
84
+ save: vi.fn().mockResolvedValue(undefined),
85
+ load: vi.fn().mockResolvedValue(undefined),
86
+ delete: vi.fn().mockResolvedValue(undefined),
87
+ list: vi.fn().mockResolvedValue([]),
88
+ };
89
+ mockEventBus = new EventEmitter();
90
+ mockEventBus.publish = vi.fn();
91
+ mockEventBus.finished = vi.fn();
92
+ executor = new CoderAgentExecutor(mockTaskStore);
93
+ });
94
+ it('should distinguish between primary and secondary execution', async () => {
95
+ const taskId = 'test-task';
96
+ const contextId = 'test-context';
97
+ const mockSocket = new EventEmitter();
98
+ const requestContext = {
99
+ userMessage: {
100
+ messageId: 'msg-1',
101
+ taskId,
102
+ contextId,
103
+ parts: [{ kind: 'text', text: 'hi' }],
104
+ metadata: {
105
+ coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
106
+ },
107
+ },
108
+ };
109
+ // Mock requestStorage for primary
110
+ requestStorage.getStore.mockReturnValue({
111
+ req: { socket: mockSocket },
112
+ });
113
+ // First execution (Primary)
114
+ const primaryPromise = executor.execute(requestContext, mockEventBus);
115
+ // Give it enough time to reach line 490 in executor.ts
116
+ await new Promise((resolve) => setTimeout(resolve, 50));
117
+ expect(executor.executingTasks.has(taskId)).toBe(true);
118
+ const wrapper = executor.getTask(taskId);
119
+ expect(wrapper).toBeDefined();
120
+ // Mock requestStorage for secondary
121
+ const secondarySocket = new EventEmitter();
122
+ requestStorage.getStore.mockReturnValue({
123
+ req: { socket: secondarySocket },
124
+ });
125
+ const secondaryRequestContext = {
126
+ userMessage: {
127
+ messageId: 'msg-2',
128
+ taskId,
129
+ contextId,
130
+ parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
131
+ metadata: {
132
+ coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
133
+ },
134
+ },
135
+ };
136
+ const secondaryPromise = executor.execute(secondaryRequestContext, mockEventBus);
137
+ // Secondary execution should NOT add to executingTasks (already there)
138
+ // and should return early after its loop
139
+ await secondaryPromise;
140
+ // Task should still be in executingTasks and NOT disposed
141
+ expect(executor.executingTasks.has(taskId)).toBe(true);
142
+ expect(wrapper?.task.dispose).not.toHaveBeenCalled();
143
+ // Now simulate secondary socket closure - it should NOT affect primary
144
+ secondarySocket.emit('end');
145
+ expect(executor.executingTasks.has(taskId)).toBe(true);
146
+ expect(wrapper?.task.dispose).not.toHaveBeenCalled();
147
+ // Set to terminal state to verify disposal on finish
148
+ wrapper.task.taskState = 'completed';
149
+ // Now close primary socket
150
+ mockSocket.emit('end');
151
+ await primaryPromise;
152
+ expect(executor.executingTasks.has(taskId)).toBe(false);
153
+ expect(wrapper?.task.dispose).toHaveBeenCalled();
154
+ });
155
+ it('should evict task from cache when it reaches terminal state', async () => {
156
+ const taskId = 'test-task-terminal';
157
+ const contextId = 'test-context';
158
+ const mockSocket = new EventEmitter();
159
+ requestStorage.getStore.mockReturnValue({
160
+ req: { socket: mockSocket },
161
+ });
162
+ const requestContext = {
163
+ userMessage: {
164
+ messageId: 'msg-1',
165
+ taskId,
166
+ contextId,
167
+ parts: [{ kind: 'text', text: 'hi' }],
168
+ metadata: {
169
+ coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
170
+ },
171
+ },
172
+ };
173
+ const primaryPromise = executor.execute(requestContext, mockEventBus);
174
+ await new Promise((resolve) => setTimeout(resolve, 50));
175
+ const wrapper = executor.getTask(taskId);
176
+ expect(wrapper).toBeDefined();
177
+ // Simulate terminal state
178
+ wrapper.task.taskState = 'completed';
179
+ // Finish primary execution
180
+ mockSocket.emit('end');
181
+ await primaryPromise;
182
+ expect(executor.getTask(taskId)).toBeUndefined();
183
+ expect(wrapper.task.dispose).toHaveBeenCalled();
184
+ });
185
+ });
186
+ //# sourceMappingURL=executor.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.test.js","sourceRoot":"","sources":["../../../src/agent/executor.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,446 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { Task } from './task.js';
8
+ import { MessageBusType, ToolConfirmationOutcome, ApprovalMode, Scheduler, } from '@google/gemini-cli-core';
9
+ import { createMockConfig } from '../utils/testing_utils.js';
10
+ describe('Task Event-Driven Scheduler', () => {
11
+ let mockConfig;
12
+ let mockEventBus;
13
+ let messageBus;
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ mockConfig = createMockConfig({
17
+ isEventDrivenSchedulerEnabled: () => true,
18
+ });
19
+ messageBus = mockConfig.getMessageBus();
20
+ mockEventBus = {
21
+ publish: vi.fn(),
22
+ on: vi.fn(),
23
+ off: vi.fn(),
24
+ once: vi.fn(),
25
+ removeAllListeners: vi.fn(),
26
+ finished: vi.fn(),
27
+ };
28
+ });
29
+ it('should instantiate Scheduler when enabled', () => {
30
+ // @ts-expect-error - Calling private constructor
31
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
32
+ expect(task.scheduler).toBeInstanceOf(Scheduler);
33
+ });
34
+ it('should subscribe to TOOL_CALLS_UPDATE and map status changes', async () => {
35
+ // @ts-expect-error - Calling private constructor
36
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
37
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
38
+ const toolCall = {
39
+ request: { callId: '1', name: 'ls', args: {} },
40
+ status: 'executing',
41
+ };
42
+ // Simulate MessageBus event
43
+ // Simulate MessageBus event
44
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
45
+ if (!handler) {
46
+ throw new Error('TOOL_CALLS_UPDATE handler not found');
47
+ }
48
+ handler({
49
+ type: MessageBusType.TOOL_CALLS_UPDATE,
50
+ toolCalls: [toolCall],
51
+ });
52
+ expect(mockEventBus.publish).toHaveBeenCalledWith(expect.objectContaining({
53
+ status: expect.objectContaining({
54
+ state: 'submitted', // initial task state
55
+ }),
56
+ metadata: expect.objectContaining({
57
+ coderAgent: expect.objectContaining({
58
+ kind: 'tool-call-update',
59
+ }),
60
+ }),
61
+ }));
62
+ });
63
+ it('should handle tool confirmations by publishing to MessageBus', async () => {
64
+ // @ts-expect-error - Calling private constructor
65
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
66
+ const toolCall = {
67
+ request: { callId: '1', name: 'ls', args: {} },
68
+ status: 'awaiting_approval',
69
+ correlationId: 'corr-1',
70
+ confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
71
+ };
72
+ // Simulate MessageBus event to stash the correlationId
73
+ // Simulate MessageBus event
74
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
75
+ if (!handler) {
76
+ throw new Error('TOOL_CALLS_UPDATE handler not found');
77
+ }
78
+ handler({
79
+ type: MessageBusType.TOOL_CALLS_UPDATE,
80
+ toolCalls: [toolCall],
81
+ });
82
+ // Simulate A2A client confirmation
83
+ const part = {
84
+ kind: 'data',
85
+ data: {
86
+ callId: '1',
87
+ outcome: 'proceed_once',
88
+ },
89
+ };
90
+ const handled = await task._handleToolConfirmationPart(part);
91
+ expect(handled).toBe(true);
92
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
93
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
94
+ correlationId: 'corr-1',
95
+ confirmed: true,
96
+ outcome: ToolConfirmationOutcome.ProceedOnce,
97
+ }));
98
+ });
99
+ it('should handle Rejection (Cancel) and Modification (ModifyWithEditor)', async () => {
100
+ // @ts-expect-error - Calling private constructor
101
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
102
+ const toolCall = {
103
+ request: { callId: '1', name: 'ls', args: {} },
104
+ status: 'awaiting_approval',
105
+ correlationId: 'corr-1',
106
+ confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
107
+ };
108
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
109
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
110
+ // Simulate Rejection (Cancel)
111
+ const handled = await task._handleToolConfirmationPart({
112
+ kind: 'data',
113
+ data: { callId: '1', outcome: 'cancel' },
114
+ });
115
+ expect(handled).toBe(true);
116
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
117
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
118
+ correlationId: 'corr-1',
119
+ confirmed: false,
120
+ }));
121
+ const toolCall2 = {
122
+ request: { callId: '2', name: 'ls', args: {} },
123
+ status: 'awaiting_approval',
124
+ correlationId: 'corr-2',
125
+ confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
126
+ };
127
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
128
+ // Simulate ModifyWithEditor
129
+ const handled2 = await task._handleToolConfirmationPart({
130
+ kind: 'data',
131
+ data: { callId: '2', outcome: 'modify_with_editor' },
132
+ });
133
+ expect(handled2).toBe(true);
134
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
135
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
136
+ correlationId: 'corr-2',
137
+ confirmed: false,
138
+ outcome: ToolConfirmationOutcome.ModifyWithEditor,
139
+ payload: undefined,
140
+ }));
141
+ });
142
+ it('should handle MCP Server tool operations correctly', async () => {
143
+ // @ts-expect-error - Calling private constructor
144
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
145
+ const toolCall = {
146
+ request: { callId: '1', name: 'call_mcp_tool', args: {} },
147
+ status: 'awaiting_approval',
148
+ correlationId: 'corr-mcp-1',
149
+ confirmationDetails: {
150
+ type: 'mcp',
151
+ title: 'MCP Server Operation',
152
+ prompt: 'test_mcp',
153
+ },
154
+ };
155
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
156
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
157
+ // Simulate ProceedOnce for MCP
158
+ const handled = await task._handleToolConfirmationPart({
159
+ kind: 'data',
160
+ data: { callId: '1', outcome: 'proceed_once' },
161
+ });
162
+ expect(handled).toBe(true);
163
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
164
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
165
+ correlationId: 'corr-mcp-1',
166
+ confirmed: true,
167
+ outcome: ToolConfirmationOutcome.ProceedOnce,
168
+ }));
169
+ });
170
+ it('should handle MCP Server tool ProceedAlwaysServer outcome', async () => {
171
+ // @ts-expect-error - Calling private constructor
172
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
173
+ const toolCall = {
174
+ request: { callId: '1', name: 'call_mcp_tool', args: {} },
175
+ status: 'awaiting_approval',
176
+ correlationId: 'corr-mcp-2',
177
+ confirmationDetails: {
178
+ type: 'mcp',
179
+ title: 'MCP Server Operation',
180
+ prompt: 'test_mcp',
181
+ },
182
+ };
183
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
184
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
185
+ const handled = await task._handleToolConfirmationPart({
186
+ kind: 'data',
187
+ data: { callId: '1', outcome: 'proceed_always_server' },
188
+ });
189
+ expect(handled).toBe(true);
190
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
191
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
192
+ correlationId: 'corr-mcp-2',
193
+ confirmed: true,
194
+ outcome: ToolConfirmationOutcome.ProceedAlwaysServer,
195
+ }));
196
+ });
197
+ it('should handle MCP Server tool ProceedAlwaysTool outcome', async () => {
198
+ // @ts-expect-error - Calling private constructor
199
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
200
+ const toolCall = {
201
+ request: { callId: '1', name: 'call_mcp_tool', args: {} },
202
+ status: 'awaiting_approval',
203
+ correlationId: 'corr-mcp-3',
204
+ confirmationDetails: {
205
+ type: 'mcp',
206
+ title: 'MCP Server Operation',
207
+ prompt: 'test_mcp',
208
+ },
209
+ };
210
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
211
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
212
+ const handled = await task._handleToolConfirmationPart({
213
+ kind: 'data',
214
+ data: { callId: '1', outcome: 'proceed_always_tool' },
215
+ });
216
+ expect(handled).toBe(true);
217
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
218
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
219
+ correlationId: 'corr-mcp-3',
220
+ confirmed: true,
221
+ outcome: ToolConfirmationOutcome.ProceedAlwaysTool,
222
+ }));
223
+ });
224
+ it('should handle MCP Server tool ProceedAlwaysAndSave outcome', async () => {
225
+ // @ts-expect-error - Calling private constructor
226
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
227
+ const toolCall = {
228
+ request: { callId: '1', name: 'call_mcp_tool', args: {} },
229
+ status: 'awaiting_approval',
230
+ correlationId: 'corr-mcp-4',
231
+ confirmationDetails: {
232
+ type: 'mcp',
233
+ title: 'MCP Server Operation',
234
+ prompt: 'test_mcp',
235
+ },
236
+ };
237
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
238
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
239
+ const handled = await task._handleToolConfirmationPart({
240
+ kind: 'data',
241
+ data: { callId: '1', outcome: 'proceed_always_and_save' },
242
+ });
243
+ expect(handled).toBe(true);
244
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
245
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
246
+ correlationId: 'corr-mcp-4',
247
+ confirmed: true,
248
+ outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
249
+ }));
250
+ });
251
+ it('should execute without confirmation in YOLO mode and not transition to input-required', async () => {
252
+ // Enable YOLO mode
253
+ const yoloConfig = createMockConfig({
254
+ isEventDrivenSchedulerEnabled: () => true,
255
+ getApprovalMode: () => ApprovalMode.YOLO,
256
+ });
257
+ const yoloMessageBus = yoloConfig.getMessageBus();
258
+ // @ts-expect-error - Calling private constructor
259
+ const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);
260
+ task.setTaskStateAndPublishUpdate = vi.fn();
261
+ const toolCall = {
262
+ request: { callId: '1', name: 'ls', args: {} },
263
+ status: 'awaiting_approval',
264
+ correlationId: 'corr-1',
265
+ confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
266
+ };
267
+ const handler = yoloMessageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
268
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
269
+ // Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
270
+ expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(expect.objectContaining({
271
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
272
+ }));
273
+ // Should NOT transition to input-required since it was auto-approved
274
+ expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
275
+ });
276
+ it('should handle output updates via the message bus', async () => {
277
+ // @ts-expect-error - Calling private constructor
278
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
279
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
280
+ const toolCall = {
281
+ request: { callId: '1', name: 'ls', args: {} },
282
+ status: 'executing',
283
+ liveOutput: 'chunk1',
284
+ };
285
+ // Simulate MessageBus event
286
+ // Simulate MessageBus event
287
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
288
+ if (!handler) {
289
+ throw new Error('TOOL_CALLS_UPDATE handler not found');
290
+ }
291
+ handler({
292
+ type: MessageBusType.TOOL_CALLS_UPDATE,
293
+ toolCalls: [toolCall],
294
+ });
295
+ // Should publish artifact update for output
296
+ expect(mockEventBus.publish).toHaveBeenCalledWith(expect.objectContaining({
297
+ kind: 'artifact-update',
298
+ artifact: expect.objectContaining({
299
+ artifactId: 'tool-1-output',
300
+ parts: [{ kind: 'text', text: 'chunk1' }],
301
+ }),
302
+ }));
303
+ });
304
+ it('should complete artifact creation without hanging', async () => {
305
+ // @ts-expect-error - Calling private constructor
306
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
307
+ const toolCallId = 'create-file-123';
308
+ task['_registerToolCall'](toolCallId, 'executing');
309
+ const toolCall = {
310
+ request: {
311
+ callId: toolCallId,
312
+ name: 'writeFile',
313
+ args: { path: 'test.sh' },
314
+ },
315
+ status: 'success',
316
+ result: { ok: true },
317
+ };
318
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
319
+ handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
320
+ // The tool should be complete and registered appropriately, eventually
321
+ // triggering the toolCompletionPromise resolution when all clear.
322
+ const internalTask = task;
323
+ expect(internalTask.completedToolCalls.length).toBe(1);
324
+ expect(internalTask.pendingToolCalls.size).toBe(0);
325
+ });
326
+ it('should preserve messageId across multiple text chunks to prevent UI duplication', async () => {
327
+ // @ts-expect-error - Calling private constructor
328
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
329
+ // Initialize the ID for the first turn (happens internally upon LLM stream)
330
+ task.currentAgentMessageId = 'test-id-123';
331
+ // Simulate sending multiple text chunks
332
+ task._sendTextContent('chunk 1');
333
+ task._sendTextContent('chunk 2');
334
+ // Both text contents should have been published with the same messageId
335
+ const textCalls = mockEventBus.publish.mock.calls.filter((call) => call[0].status?.message?.kind === 'message');
336
+ expect(textCalls.length).toBe(2);
337
+ expect(textCalls[0][0].status.message.messageId).toBe('test-id-123');
338
+ expect(textCalls[1][0].status.message.messageId).toBe('test-id-123');
339
+ // Simulate starting a new turn by calling getAndClearCompletedTools
340
+ // (which precedes sendCompletedToolsToLlm where a new ID is minted)
341
+ task.getAndClearCompletedTools();
342
+ // sendCompletedToolsToLlm internally rolls the ID forward.
343
+ // Simulate what sendCompletedToolsToLlm does:
344
+ const internalTask = task;
345
+ internalTask.setTaskStateAndPublishUpdate('working', {});
346
+ // Simulate what sendCompletedToolsToLlm does: generate a new UUID for the next turn
347
+ task.currentAgentMessageId = 'test-id-456';
348
+ task._sendTextContent('chunk 3');
349
+ const secondTurnCalls = mockEventBus.publish.mock.calls.filter((call) => call[0].status?.message?.messageId === 'test-id-456');
350
+ expect(secondTurnCalls.length).toBe(1);
351
+ expect(secondTurnCalls[0][0].status.message.parts[0].text).toBe('chunk 3');
352
+ });
353
+ it('should handle parallel tool calls correctly', async () => {
354
+ // @ts-expect-error - Calling private constructor
355
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
356
+ const toolCall1 = {
357
+ request: { callId: '1', name: 'ls', args: {} },
358
+ status: 'awaiting_approval',
359
+ correlationId: 'corr-1',
360
+ confirmationDetails: { type: 'info', title: 'test 1', prompt: 'test 1' },
361
+ };
362
+ const toolCall2 = {
363
+ request: { callId: '2', name: 'pwd', args: {} },
364
+ status: 'awaiting_approval',
365
+ correlationId: 'corr-2',
366
+ confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
367
+ };
368
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
369
+ // Publish update for both tool calls simultaneously
370
+ handler({
371
+ type: MessageBusType.TOOL_CALLS_UPDATE,
372
+ toolCalls: [toolCall1, toolCall2],
373
+ });
374
+ // Confirm first tool call
375
+ const handled1 = await task._handleToolConfirmationPart({
376
+ kind: 'data',
377
+ data: { callId: '1', outcome: 'proceed_once' },
378
+ });
379
+ expect(handled1).toBe(true);
380
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
381
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
382
+ correlationId: 'corr-1',
383
+ confirmed: true,
384
+ }));
385
+ // Confirm second tool call
386
+ const handled2 = await task._handleToolConfirmationPart({
387
+ kind: 'data',
388
+ data: { callId: '2', outcome: 'cancel' },
389
+ });
390
+ expect(handled2).toBe(true);
391
+ expect(messageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
392
+ type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
393
+ correlationId: 'corr-2',
394
+ confirmed: false,
395
+ }));
396
+ });
397
+ it('should wait for executing tools before transitioning to input-required state', async () => {
398
+ // @ts-expect-error - Calling private constructor
399
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
400
+ task.setTaskStateAndPublishUpdate = vi.fn();
401
+ // Register tool 1 as executing
402
+ task['_registerToolCall']('1', 'executing');
403
+ const toolCall1 = {
404
+ request: { callId: '1', name: 'ls', args: {} },
405
+ status: 'executing',
406
+ };
407
+ const toolCall2 = {
408
+ request: { callId: '2', name: 'pwd', args: {} },
409
+ status: 'awaiting_approval',
410
+ correlationId: 'corr-2',
411
+ confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
412
+ };
413
+ const handler = messageBus.subscribe.mock.calls.find((call) => call[0] === MessageBusType.TOOL_CALLS_UPDATE)?.[1];
414
+ handler({
415
+ type: MessageBusType.TOOL_CALLS_UPDATE,
416
+ toolCalls: [toolCall1, toolCall2],
417
+ });
418
+ // Should NOT transition to input-required yet
419
+ expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
420
+ // Complete tool 1
421
+ const toolCall1Complete = {
422
+ ...toolCall1,
423
+ status: 'success',
424
+ result: { ok: true },
425
+ };
426
+ handler({
427
+ type: MessageBusType.TOOL_CALLS_UPDATE,
428
+ toolCalls: [toolCall1Complete, toolCall2],
429
+ });
430
+ // Now it should transition
431
+ expect(task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith('input-required', expect.anything(), undefined, undefined, true);
432
+ });
433
+ it('should ignore confirmations for unknown tool calls', async () => {
434
+ // @ts-expect-error - Calling private constructor
435
+ const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
436
+ const handled = await task._handleToolConfirmationPart({
437
+ kind: 'data',
438
+ data: { callId: 'unknown-id', outcome: 'proceed_once' },
439
+ });
440
+ // Should return false for unhandled tool call
441
+ expect(handled).toBe(false);
442
+ // Should not publish anything to the message bus
443
+ expect(messageBus.publish).not.toHaveBeenCalled();
444
+ });
445
+ });
446
+ //# sourceMappingURL=task-event-driven.test.js.map