diff --git a/packages/a2a-server/src/agent/task-event-driven.test.ts b/packages/a2a-server/src/agent/task-event-driven.test.ts index 86436fa811..5fc548a8f4 100644 --- a/packages/a2a-server/src/agent/task-event-driven.test.ts +++ b/packages/a2a-server/src/agent/task-event-driven.test.ts @@ -66,6 +66,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); expect(mockEventBus.publish).toHaveBeenCalledWith( @@ -106,6 +107,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); // Simulate A2A client confirmation @@ -148,7 +150,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Simulate Rejection (Cancel) const handled = await ( @@ -174,7 +180,11 @@ describe('Task Event-Driven Scheduler', () => { correlationId: 'corr-2', confirmationDetails: { type: 'info', title: 'test', prompt: 'test' }, }; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall2], + schedulerId: 'task-id', + }); // Simulate ModifyWithEditor const handled2 = await ( @@ -215,7 +225,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Simulate ProceedOnce for MCP const handled = await ( @@ -255,7 +269,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -294,7 +312,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -333,7 +355,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -376,7 +402,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly expect(yoloMessageBus.publish).not.toHaveBeenCalledWith( @@ -419,6 +449,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); // Should publish artifact update for output @@ -453,7 +484,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // The tool should be complete and registered appropriately, eventually // triggering the toolCompletionPromise resolution when all clear. @@ -533,6 +568,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1, toolCall2], + schedulerId: 'task-id', }); // Confirm first tool call @@ -600,6 +636,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1, toolCall2], + schedulerId: 'task-id', }); // Should NOT transition to input-required yet @@ -621,6 +658,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1Complete, toolCall2], + schedulerId: 'task-id', }); // Now it should transition diff --git a/packages/a2a-server/src/agent/task.test.ts b/packages/a2a-server/src/agent/task.test.ts index 26039ae3aa..2a3a20ebf4 100644 --- a/packages/a2a-server/src/agent/task.test.ts +++ b/packages/a2a-server/src/agent/task.test.ts @@ -12,6 +12,9 @@ import { type ToolCallRequestInfo, type GitService, type CompletedToolCall, + type ToolCall, + type ToolCallsUpdateMessage, + MessageBusType, } from '@google/gemini-cli-core'; import { createMockConfig } from '../utils/testing_utils.js'; import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server'; @@ -460,4 +463,204 @@ describe('Task', () => { expect(task.currentPromptId).toBe(expectedPromptId2); }); }); + + describe('Race Condition Fix', () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should NOT transition to input-required if a tool is still validating', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + // Manually register two tool calls + task['_registerToolCall']('tool-1', 'awaiting_approval'); + task['_registerToolCall']('tool-2', 'validating'); + + // Call checkInputRequiredState (private) + task['checkInputRequiredState'](); + + // Verify task state did NOT change to input-required + expect(task.taskState).not.toBe('input-required'); + expect(mockEventBus.publish).not.toHaveBeenCalledWith( + expect.objectContaining({ + status: expect.objectContaining({ state: 'input-required' }), + }), + ); + }); + + it('should transition to input-required if all active tools are awaiting approval', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + // Transition from submitted to working first to simulate normal flow + task.taskState = 'working'; + + // Manually register tool calls + task['_registerToolCall']('tool-1', 'awaiting_approval'); + + // Call checkInputRequiredState + task['checkInputRequiredState'](); + + // Verify task state changed to input-required + expect(task.taskState).toBe('input-required'); + expect(mockEventBus.publish).toHaveBeenCalledWith( + expect.objectContaining({ + status: expect.objectContaining({ state: 'input-required' }), + }), + ); + }); + + it('handleEventDrivenToolCallsUpdate should ignore events for other schedulers', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const handleEventDrivenToolCallSpy = vi.spyOn( + task as unknown as { + handleEventDrivenToolCall: Task['handleEventDrivenToolCall']; + }, + 'handleEventDrivenToolCall', + ); + + const otherEvent: ToolCallsUpdateMessage = { + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [ + { request: { callId: '1' }, status: 'executing' } as ToolCall, + ], + schedulerId: 'other-task-id', + }; + + task['handleEventDrivenToolCallsUpdate'](otherEvent); + + expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled(); + + const ownEvent: ToolCallsUpdateMessage = { + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [ + { request: { callId: '1' }, status: 'executing' } as ToolCall, + ], + schedulerId: 'task-id', + }; + + task['handleEventDrivenToolCallsUpdate'](ownEvent); + + expect(handleEventDrivenToolCallSpy).toHaveBeenCalled(); + }); + }); + + describe('Serialization and Mapping', () => { + it('should map internal "validating" status to "scheduled" for the client and include outcome', async () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const mockToolCall = { + request: { callId: 'tool-1' }, + status: 'validating', + outcome: 'accepted', + tool: { name: 'test-tool' }, + }; + + const message = task['toolStatusMessage']( + mockToolCall as unknown as ToolCall, + 'task-id', + 'context-id', + ); + const serialized = ( + message.parts![0] as { + data: { status: string; outcome: string }; + } + ).data; + + expect(serialized.status).toBe('scheduled'); + expect(serialized.outcome).toBe('accepted'); + }); + + it('should correctly detect changes when status or outcome changes', async () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const toolCall1 = { + request: { callId: 'tool-1' }, + status: 'awaiting_approval', + }; + + // First update - should trigger change + const changed1 = task['handleEventDrivenToolCall']( + toolCall1 as unknown as ToolCall, + ); + expect(changed1).toBe(true); + + // Second update with same status - should NOT trigger change + const changed2 = task['handleEventDrivenToolCall']( + toolCall1 as unknown as ToolCall, + ); + expect(changed2).toBe(false); + + // Update with new outcome - SHOULD trigger change + const toolCall2 = { + request: { callId: 'tool-1' }, + status: 'awaiting_approval', + outcome: 'accepted', + }; + const changed3 = task['handleEventDrivenToolCall']( + toolCall2 as unknown as ToolCall, + ); + expect(changed3).toBe(true); + }); + }); }); diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index a76054263f..3fcb5c3ef5 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -11,6 +11,7 @@ import { GeminiEventType, ToolConfirmationOutcome, ApprovalMode, + CoreToolCallStatus, getAllMCPServerStatuses, MCPServerStatus, isNodeError, @@ -95,6 +96,8 @@ export class Task { // For tool waiting logic private pendingToolCalls: Map = new Map(); //toolCallId --> status + private pendingOutcomes: Map = + new Map(); // toolCallId --> outcome private toolsAlreadyConfirmed: Set = new Set(); private toolCompletionPromise?: Promise; private toolCompletionNotifier?: { @@ -413,7 +416,10 @@ export class Task { private handleEventDrivenToolCallsUpdate( event: ToolCallsUpdateMessage, ): void { - if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) { + if ( + event.type !== MessageBusType.TOOL_CALLS_UPDATE || + event.schedulerId !== this.id + ) { return; } @@ -426,7 +432,7 @@ export class Task { this.checkInputRequiredState(); } - private handleEventDrivenToolCall(tc: ToolCall): void { + private handleEventDrivenToolCall(tc: ToolCall): boolean { const callId = tc.request.callId; // Do not process events for tools that have already been finalized. @@ -436,11 +442,16 @@ export class Task { this.processedToolCallIds.has(callId) || this.completedToolCalls.some((c) => c.request.callId === callId) ) { - return; + return false; } const previousStatus = this.pendingToolCalls.get(callId); - const hasChanged = previousStatus !== tc.status; + const previousOutcome = this.pendingOutcomes.get(callId); + const hasChanged = + previousStatus !== tc.status || previousOutcome !== tc.outcome; + + // Update outcome tracking + this.pendingOutcomes.set(callId, tc.outcome); // 1. Handle Output if (tc.status === 'executing' && tc.liveOutput) { @@ -454,6 +465,7 @@ export class Task { tc.status === 'cancelled' ) { this.toolsAlreadyConfirmed.delete(callId); + this.pendingOutcomes.delete(callId); if (hasChanged) { logger.info( `[Task] Tool call ${callId} completed with status: ${tc.status}`, @@ -496,6 +508,8 @@ export class Task { ); this.eventBus?.publish(statusUpdate); } + + return hasChanged; } private checkInputRequiredState(): void { @@ -508,12 +522,14 @@ export class Task { let isExecuting = false; for (const [callId, status] of this.pendingToolCalls.entries()) { - if (status === 'executing' || status === 'scheduled') { - isExecuting = true; - } else if ( - status === 'awaiting_approval' && - !this.toolsAlreadyConfirmed.has(callId) + if ( + status === CoreToolCallStatus.Executing || + status === CoreToolCallStatus.Scheduled || + status === CoreToolCallStatus.Validating || + this.toolsAlreadyConfirmed.has(callId) ) { + isExecuting = true; + } else if (status === CoreToolCallStatus.AwaitingApproval) { isAwaitingApproval = true; } } @@ -574,8 +590,14 @@ export class Task { 'confirmationDetails', 'liveOutput', 'response', + 'outcome', ); + // Map internal 'validating' status to 'scheduled' for the client + if (serializableToolCall.status === CoreToolCallStatus.Validating) { + serializableToolCall.status = CoreToolCallStatus.Scheduled; + } + if (tc.tool) { const toolFields = this._pickFields( tc.tool, diff --git a/packages/a2a-server/src/http/app.test.ts b/packages/a2a-server/src/http/app.test.ts index 4a883992b5..7534ad66a8 100644 --- a/packages/a2a-server/src/http/app.test.ts +++ b/packages/a2a-server/src/http/app.test.ts @@ -228,7 +228,7 @@ describe('E2E Tests', () => { expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id' }, }, }, @@ -330,7 +330,7 @@ describe('E2E Tests', () => { expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-1' }, }, }, @@ -352,7 +352,7 @@ describe('E2E Tests', () => { kind: 'state-change', }); - // 4. Tool 1 is validating. + // 4. Tool 1 is scheduled. const toolCallUpdate1 = events[3].result as TaskStatusUpdateEvent; expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', @@ -361,12 +361,12 @@ describe('E2E Tests', () => { { data: { request: { callId: 'test-call-id-1' }, - status: 'validating', + status: 'scheduled', }, }, ]); - // 5. Tool 2 is validating. + // 5. Tool 2 is scheduled. const toolCallUpdate2 = events[4].result as TaskStatusUpdateEvent; expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', @@ -375,17 +375,17 @@ describe('E2E Tests', () => { { data: { request: { callId: 'test-call-id-2' }, - status: 'validating', + status: 'scheduled', }, }, ]); // 6. Tool 1 is awaiting approval. - const toolCallAwaitEvent = events[5].result as TaskStatusUpdateEvent; - expect(toolCallAwaitEvent.metadata?.['coderAgent']).toMatchObject({ + const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-confirmation', }); - expect(toolCallAwaitEvent.status.message?.parts).toMatchObject([ + expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([ { data: { request: { callId: 'test-call-id-1' }, @@ -394,14 +394,28 @@ describe('E2E Tests', () => { }, ]); - // 7. The final event is "input-required". - const finalEvent = events[6].result as TaskStatusUpdateEvent; + // 7. Tool 2 is awaiting approval. + const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([ + { + data: { + request: { callId: 'test-call-id-2' }, + status: 'awaiting_approval', + }, + }, + ]); + + // 8. The final event is "input-required". + const finalEvent = events[7].result as TaskStatusUpdateEvent; expect(finalEvent.final).toBe(true); expect(finalEvent.status.state).toBe('input-required'); // The scheduler now waits for approval, so no more events are sent. assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(7); + expect(events.length).toBe(8); }); it('should handle multiple tool calls sequentially in YOLO mode', async () => { @@ -499,7 +513,7 @@ describe('E2E Tests', () => { // Tool 1 Lifecycle { kind: 'tool-call-update', - status: 'validating', + status: 'scheduled', callId: 'test-call-id-1', }, { @@ -520,7 +534,7 @@ describe('E2E Tests', () => { // Tool 2 Lifecycle { kind: 'tool-call-update', - status: 'validating', + status: 'scheduled', callId: 'test-call-id-2', }, { @@ -603,26 +617,40 @@ describe('E2E Tests', () => { expect(workingEvent2.kind).toBe('status-update'); expect(workingEvent2.status.state).toBe('working'); - // Status update: tool-call-update (validating) - const validatingEvent = events[3].result as TaskStatusUpdateEvent; - expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + // Status update: tool-call-update (scheduled) + const scheduledEvent1 = events[3].result as TaskStatusUpdateEvent; + expect(scheduledEvent1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(validatingEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent1.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-no-approval' }, }, }, ]); // Status update: tool-call-update (scheduled) - const scheduledEvent = events[4].result as TaskStatusUpdateEvent; - expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ + const scheduledEvent2 = events[4].result as TaskStatusUpdateEvent; + expect(scheduledEvent2.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(scheduledEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent; + expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent3.status.message?.parts).toMatchObject([ { data: { status: 'scheduled', @@ -632,7 +660,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (executing) - const executingEvent = events[5].result as TaskStatusUpdateEvent; + const executingEvent = events[6].result as TaskStatusUpdateEvent; expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -646,7 +674,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (success) - const successEvent = events[6].result as TaskStatusUpdateEvent; + const successEvent = events[7].result as TaskStatusUpdateEvent; expect(successEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -660,12 +688,12 @@ describe('E2E Tests', () => { ]); // Status update: working (before sending tool result to LLM) - const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + const workingEvent3 = events[8].result as TaskStatusUpdateEvent; expect(workingEvent3.kind).toBe('status-update'); expect(workingEvent3.status.state).toBe('working'); // Status update: text-content (final LLM response) - const textContentEvent = events[8].result as TaskStatusUpdateEvent; + const textContentEvent = events[9].result as TaskStatusUpdateEvent; expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'text-content', }); @@ -674,7 +702,7 @@ describe('E2E Tests', () => { ]); assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(10); + expect(events.length).toBe(11); }); it('should bypass tool approval in YOLO mode', async () => { @@ -734,15 +762,15 @@ describe('E2E Tests', () => { expect(workingEvent2.kind).toBe('status-update'); expect(workingEvent2.status.state).toBe('working'); - // Status update: tool-call-update (validating) - const validatingEvent = events[3].result as TaskStatusUpdateEvent; - expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + // Status update: tool-call-update (scheduled) + const scheduledEvent = events[3].result as TaskStatusUpdateEvent; + expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(validatingEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-yolo' }, }, }, @@ -762,8 +790,22 @@ describe('E2E Tests', () => { }, ]); + // Status update: tool-call-update (scheduled) + const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent; + expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent3.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + // Status update: tool-call-update (executing) - const executingEvent = events[5].result as TaskStatusUpdateEvent; + const executingEvent = events[6].result as TaskStatusUpdateEvent; expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -777,7 +819,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (success) - const successEvent = events[6].result as TaskStatusUpdateEvent; + const successEvent = events[7].result as TaskStatusUpdateEvent; expect(successEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -791,12 +833,12 @@ describe('E2E Tests', () => { ]); // Status update: working (before sending tool result to LLM) - const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + const workingEvent3 = events[8].result as TaskStatusUpdateEvent; expect(workingEvent3.kind).toBe('status-update'); expect(workingEvent3.status.state).toBe('working'); // Status update: text-content (final LLM response) - const textContentEvent = events[8].result as TaskStatusUpdateEvent; + const textContentEvent = events[9].result as TaskStatusUpdateEvent; expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'text-content', }); @@ -805,7 +847,7 @@ describe('E2E Tests', () => { ]); assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(10); + expect(events.length).toBe(11); }); it('should include traceId in status updates when available', async () => {