mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-17 17:41:24 -07:00
Co-authored-by: Abhi <abhipatel@google.com> Co-authored-by: Adam Weidman <adamfweidman@google.com>
656 lines
21 KiB
TypeScript
656 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
import { Task } from './task.js';
|
|
import {
|
|
type Config,
|
|
MessageBusType,
|
|
ToolConfirmationOutcome,
|
|
ApprovalMode,
|
|
Scheduler,
|
|
type MessageBus,
|
|
} from '@google/gemini-cli-core';
|
|
import { createMockConfig } from '../utils/testing_utils.js';
|
|
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
|
|
describe('Task Event-Driven Scheduler', () => {
|
|
let mockConfig: Config;
|
|
let mockEventBus: ExecutionEventBus;
|
|
let messageBus: MessageBus;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockConfig = createMockConfig({
|
|
isEventDrivenSchedulerEnabled: () => true,
|
|
}) as Config;
|
|
messageBus = mockConfig.getMessageBus();
|
|
mockEventBus = {
|
|
publish: vi.fn(),
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
once: vi.fn(),
|
|
removeAllListeners: vi.fn(),
|
|
finished: vi.fn(),
|
|
};
|
|
});
|
|
|
|
it('should instantiate Scheduler when enabled', () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
expect(task.scheduler).toBeInstanceOf(Scheduler);
|
|
});
|
|
|
|
it('should subscribe to TOOL_CALLS_UPDATE and map status changes', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'executing',
|
|
};
|
|
|
|
// Simulate MessageBus event
|
|
// Simulate MessageBus event
|
|
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
|
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
|
)?.[1];
|
|
|
|
if (!handler) {
|
|
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
}
|
|
|
|
handler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
toolCalls: [toolCall],
|
|
});
|
|
|
|
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: expect.objectContaining({
|
|
state: 'submitted', // initial task state
|
|
}),
|
|
metadata: expect.objectContaining({
|
|
coderAgent: expect.objectContaining({
|
|
kind: 'tool-call-update',
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle tool confirmations by publishing to MessageBus', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-1',
|
|
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
};
|
|
|
|
// Simulate MessageBus event to stash the correlationId
|
|
// Simulate MessageBus event
|
|
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
|
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
|
)?.[1];
|
|
|
|
if (!handler) {
|
|
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
}
|
|
|
|
handler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
toolCalls: [toolCall],
|
|
});
|
|
|
|
// Simulate A2A client confirmation
|
|
const part = {
|
|
kind: 'data',
|
|
data: {
|
|
callId: '1',
|
|
outcome: 'proceed_once',
|
|
},
|
|
};
|
|
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart(part);
|
|
expect(handled).toBe(true);
|
|
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-1',
|
|
confirmed: true,
|
|
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle Rejection (Cancel) and Modification (ModifyWithEditor)', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-1',
|
|
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
};
|
|
|
|
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] });
|
|
|
|
// Simulate Rejection (Cancel)
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'cancel' },
|
|
});
|
|
expect(handled).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-1',
|
|
confirmed: false,
|
|
}),
|
|
);
|
|
|
|
const toolCall2 = {
|
|
request: { callId: '2', name: 'ls', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-2',
|
|
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
};
|
|
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
|
|
|
|
// Simulate ModifyWithEditor
|
|
const handled2 = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '2', outcome: 'modify_with_editor' },
|
|
});
|
|
expect(handled2).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-2',
|
|
confirmed: false,
|
|
outcome: ToolConfirmationOutcome.ModifyWithEditor,
|
|
payload: undefined,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle MCP Server tool operations correctly', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-mcp-1',
|
|
confirmationDetails: {
|
|
type: 'mcp',
|
|
title: 'MCP Server Operation',
|
|
prompt: 'test_mcp',
|
|
},
|
|
};
|
|
|
|
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] });
|
|
|
|
// Simulate ProceedOnce for MCP
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'proceed_once' },
|
|
});
|
|
expect(handled).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-mcp-1',
|
|
confirmed: true,
|
|
outcome: ToolConfirmationOutcome.ProceedOnce,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle MCP Server tool ProceedAlwaysServer outcome', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-mcp-2',
|
|
confirmationDetails: {
|
|
type: 'mcp',
|
|
title: 'MCP Server Operation',
|
|
prompt: 'test_mcp',
|
|
},
|
|
};
|
|
|
|
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] });
|
|
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'proceed_always_server' },
|
|
});
|
|
expect(handled).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-mcp-2',
|
|
confirmed: true,
|
|
outcome: ToolConfirmationOutcome.ProceedAlwaysServer,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle MCP Server tool ProceedAlwaysTool outcome', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-mcp-3',
|
|
confirmationDetails: {
|
|
type: 'mcp',
|
|
title: 'MCP Server Operation',
|
|
prompt: 'test_mcp',
|
|
},
|
|
};
|
|
|
|
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] });
|
|
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'proceed_always_tool' },
|
|
});
|
|
expect(handled).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-mcp-3',
|
|
confirmed: true,
|
|
outcome: ToolConfirmationOutcome.ProceedAlwaysTool,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle MCP Server tool ProceedAlwaysAndSave outcome', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-mcp-4',
|
|
confirmationDetails: {
|
|
type: 'mcp',
|
|
title: 'MCP Server Operation',
|
|
prompt: 'test_mcp',
|
|
},
|
|
};
|
|
|
|
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] });
|
|
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'proceed_always_and_save' },
|
|
});
|
|
expect(handled).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-mcp-4',
|
|
confirmed: true,
|
|
outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should execute without confirmation in YOLO mode and not transition to input-required', async () => {
|
|
// Enable YOLO mode
|
|
const yoloConfig = createMockConfig({
|
|
isEventDrivenSchedulerEnabled: () => true,
|
|
getApprovalMode: () => ApprovalMode.YOLO,
|
|
}) as Config;
|
|
const yoloMessageBus = yoloConfig.getMessageBus();
|
|
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);
|
|
task.setTaskStateAndPublishUpdate = vi.fn();
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-1',
|
|
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
|
};
|
|
|
|
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] });
|
|
|
|
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
|
|
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
}),
|
|
);
|
|
|
|
// Should NOT transition to input-required since it was auto-approved
|
|
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
|
'input-required',
|
|
expect.anything(),
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should handle output updates via the message bus', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'executing',
|
|
liveOutput: 'chunk1',
|
|
};
|
|
|
|
// Simulate MessageBus event
|
|
// Simulate MessageBus event
|
|
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
|
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
|
)?.[1];
|
|
|
|
if (!handler) {
|
|
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
|
}
|
|
|
|
handler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
toolCalls: [toolCall],
|
|
});
|
|
|
|
// Should publish artifact update for output
|
|
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
kind: 'artifact-update',
|
|
artifact: expect.objectContaining({
|
|
artifactId: 'tool-1-output',
|
|
parts: [{ kind: 'text', text: 'chunk1' }],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should complete artifact creation without hanging', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCallId = 'create-file-123';
|
|
task['_registerToolCall'](toolCallId, 'executing');
|
|
|
|
const toolCall = {
|
|
request: {
|
|
callId: toolCallId,
|
|
name: 'writeFile',
|
|
args: { path: 'test.sh' },
|
|
},
|
|
status: 'success',
|
|
result: { ok: true },
|
|
};
|
|
|
|
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] });
|
|
|
|
// The tool should be complete and registered appropriately, eventually
|
|
// triggering the toolCompletionPromise resolution when all clear.
|
|
const internalTask = task as unknown as {
|
|
completedToolCalls: unknown[];
|
|
pendingToolCalls: Map<string, string>;
|
|
};
|
|
expect(internalTask.completedToolCalls.length).toBe(1);
|
|
expect(internalTask.pendingToolCalls.size).toBe(0);
|
|
});
|
|
|
|
it('should preserve messageId across multiple text chunks to prevent UI duplication', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
// Initialize the ID for the first turn (happens internally upon LLM stream)
|
|
task.currentAgentMessageId = 'test-id-123';
|
|
|
|
// Simulate sending multiple text chunks
|
|
task._sendTextContent('chunk 1');
|
|
task._sendTextContent('chunk 2');
|
|
|
|
// Both text contents should have been published with the same messageId
|
|
const textCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
|
(call) => call[0].status?.message?.kind === 'message',
|
|
);
|
|
expect(textCalls.length).toBe(2);
|
|
expect(textCalls[0][0].status.message.messageId).toBe('test-id-123');
|
|
expect(textCalls[1][0].status.message.messageId).toBe('test-id-123');
|
|
|
|
// Simulate starting a new turn by calling getAndClearCompletedTools
|
|
// (which precedes sendCompletedToolsToLlm where a new ID is minted)
|
|
task.getAndClearCompletedTools();
|
|
|
|
// sendCompletedToolsToLlm internally rolls the ID forward.
|
|
// Simulate what sendCompletedToolsToLlm does:
|
|
const internalTask = task as unknown as {
|
|
setTaskStateAndPublishUpdate: (state: string, change: unknown) => void;
|
|
};
|
|
internalTask.setTaskStateAndPublishUpdate('working', {});
|
|
|
|
// Simulate what sendCompletedToolsToLlm does: generate a new UUID for the next turn
|
|
task.currentAgentMessageId = 'test-id-456';
|
|
|
|
task._sendTextContent('chunk 3');
|
|
|
|
const secondTurnCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
|
(call) => call[0].status?.message?.messageId === 'test-id-456',
|
|
);
|
|
expect(secondTurnCalls.length).toBe(1);
|
|
expect(secondTurnCalls[0][0].status.message.parts[0].text).toBe('chunk 3');
|
|
});
|
|
|
|
it('should handle parallel tool calls correctly', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const toolCall1 = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-1',
|
|
confirmationDetails: { type: 'info', title: 'test 1', prompt: 'test 1' },
|
|
};
|
|
|
|
const toolCall2 = {
|
|
request: { callId: '2', name: 'pwd', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-2',
|
|
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
|
};
|
|
|
|
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
|
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
|
)?.[1];
|
|
|
|
// Publish update for both tool calls simultaneously
|
|
handler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
toolCalls: [toolCall1, toolCall2],
|
|
});
|
|
|
|
// Confirm first tool call
|
|
const handled1 = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '1', outcome: 'proceed_once' },
|
|
});
|
|
expect(handled1).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-1',
|
|
confirmed: true,
|
|
}),
|
|
);
|
|
|
|
// Confirm second tool call
|
|
const handled2 = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: '2', outcome: 'cancel' },
|
|
});
|
|
expect(handled2).toBe(true);
|
|
expect(messageBus.publish).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
|
correlationId: 'corr-2',
|
|
confirmed: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should wait for executing tools before transitioning to input-required state', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
task.setTaskStateAndPublishUpdate = vi.fn();
|
|
|
|
// Register tool 1 as executing
|
|
task['_registerToolCall']('1', 'executing');
|
|
|
|
const toolCall1 = {
|
|
request: { callId: '1', name: 'ls', args: {} },
|
|
status: 'executing',
|
|
};
|
|
|
|
const toolCall2 = {
|
|
request: { callId: '2', name: 'pwd', args: {} },
|
|
status: 'awaiting_approval',
|
|
correlationId: 'corr-2',
|
|
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
|
};
|
|
|
|
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: [toolCall1, toolCall2],
|
|
});
|
|
|
|
// Should NOT transition to input-required yet
|
|
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
|
'input-required',
|
|
expect.anything(),
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
// Complete tool 1
|
|
const toolCall1Complete = {
|
|
...toolCall1,
|
|
status: 'success',
|
|
result: { ok: true },
|
|
};
|
|
|
|
handler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
toolCalls: [toolCall1Complete, toolCall2],
|
|
});
|
|
|
|
// Now it should transition
|
|
expect(task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith(
|
|
'input-required',
|
|
expect.anything(),
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('should ignore confirmations for unknown tool calls', async () => {
|
|
// @ts-expect-error - Calling private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
|
|
|
const handled = await (
|
|
task as unknown as {
|
|
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
|
}
|
|
)._handleToolConfirmationPart({
|
|
kind: 'data',
|
|
data: { callId: 'unknown-id', outcome: 'proceed_once' },
|
|
});
|
|
|
|
// Should return false for unhandled tool call
|
|
expect(handled).toBe(false);
|
|
|
|
// Should not publish anything to the message bus
|
|
expect(messageBus.publish).not.toHaveBeenCalled();
|
|
});
|
|
});
|