basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { describe, it, expect, vi, beforeEach } from 'vitest';8import type { Mock } from 'vitest';9import { executeToolCall } from './nonInteractiveToolExecutor.js';10import type {11 ToolRegistry,12 ToolCallRequestInfo,13 ToolResult,14 Config,15} from '../index.js';16import {17 DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,18 DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,19 ToolErrorType,20 ApprovalMode,21} from '../index.js';22import type { Part } from '@google/genai';23import { MockTool } from '../test-utils/mock-tool.js';24 25describe('executeToolCall', () => {26 let mockToolRegistry: ToolRegistry;27 let mockTool: MockTool;28 let executeFn: Mock;29 let abortController: AbortController;30 let mockConfig: Config;31 32 beforeEach(() => {33 executeFn = vi.fn();34 mockTool = new MockTool({ name: 'testTool', execute: executeFn });35 36 mockToolRegistry = {37 getTool: vi.fn(),38 ensureTool: vi.fn(async (name: string) => mockToolRegistry.getTool(name)),39 getAllToolNames: vi.fn(),40 } as unknown as ToolRegistry;41 42 mockConfig = {43 getToolRegistry: () => mockToolRegistry,44 getApprovalMode: () => ApprovalMode.DEFAULT,45 getAllowedTools: () => [],46 getSessionId: () => 'test-session-id',47 getUsageStatisticsEnabled: () => true,48 getDebugMode: () => false,49 getContentGeneratorConfig: () => ({50 model: 'test-model',51 authType: 'gemini',52 }),53 getShellExecutionConfig: () => ({54 terminalWidth: 90,55 terminalHeight: 30,56 }),57 storage: {58 getProjectTempDir: () => '/tmp',59 },60 getTruncateToolOutputThreshold: () =>61 DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,62 getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,63 getUseModelRouter: () => false,64 getGeminiClient: () => null, // No client needed for these tests65 getChatRecordingService: () => undefined,66 getMessageBus: vi.fn().mockReturnValue(undefined),67 getDisableAllHooks: vi.fn().mockReturnValue(true),68 getHookSystem: vi.fn().mockReturnValue(undefined),69 getDebugLogger: vi.fn().mockReturnValue({70 debug: vi.fn(),71 info: vi.fn(),72 warn: vi.fn(),73 error: vi.fn(),74 }),75 isInteractive: vi.fn().mockReturnValue(false),76 } as unknown as Config;77 78 abortController = new AbortController();79 });80 81 it('should execute a tool successfully', async () => {82 const request: ToolCallRequestInfo = {83 callId: 'call1',84 name: 'testTool',85 args: { param1: 'value1' },86 isClientInitiated: false,87 prompt_id: 'prompt-id-1',88 };89 const toolResult: ToolResult = {90 llmContent: 'Tool executed successfully',91 returnDisplay: 'Success!',92 };93 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);94 executeFn.mockResolvedValue(toolResult);95 96 const response = await executeToolCall(97 mockConfig,98 request,99 abortController.signal,100 );101 102 expect(mockToolRegistry.getTool).toHaveBeenCalledWith('testTool');103 expect(executeFn).toHaveBeenCalledWith(request.args);104 expect(response).toStrictEqual({105 callId: 'call1',106 error: undefined,107 errorType: undefined,108 resultDisplay: 'Success!',109 contentLength:110 typeof toolResult.llmContent === 'string'111 ? toolResult.llmContent.length112 : undefined,113 responseParts: [114 {115 functionResponse: {116 name: 'testTool',117 id: 'call1',118 response: { output: 'Tool executed successfully' },119 },120 },121 ],122 });123 });124 125 it('should return an error if tool is not found', async () => {126 const request: ToolCallRequestInfo = {127 callId: 'call2',128 name: 'nonexistentTool',129 args: {},130 isClientInitiated: false,131 prompt_id: 'prompt-id-2',132 };133 vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);134 vi.mocked(mockToolRegistry.getAllToolNames).mockReturnValue([135 'testTool',136 'anotherTool',137 ]);138 139 const response = await executeToolCall(140 mockConfig,141 request,142 abortController.signal,143 );144 145 const expectedErrorMessage =146 'Tool "nonexistentTool" not found in registry. Tools must use the exact names that are registered. Did you mean one of: "testTool", "anotherTool"?';147 expect(response).toStrictEqual({148 callId: 'call2',149 error: new Error(expectedErrorMessage),150 errorType: ToolErrorType.TOOL_NOT_REGISTERED,151 resultDisplay: expectedErrorMessage,152 contentLength: expectedErrorMessage.length,153 responseParts: [154 {155 functionResponse: {156 name: 'nonexistentTool',157 id: 'call2',158 response: {159 error: expectedErrorMessage,160 },161 },162 },163 ],164 });165 });166 167 it('should return an error if tool validation fails', async () => {168 const request: ToolCallRequestInfo = {169 callId: 'call3',170 name: 'testTool',171 args: { param1: 'invalid' },172 isClientInitiated: false,173 prompt_id: 'prompt-id-3',174 };175 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);176 vi.spyOn(mockTool, 'build').mockImplementation(() => {177 throw new Error('Invalid parameters');178 });179 180 const response = await executeToolCall(181 mockConfig,182 request,183 abortController.signal,184 );185 expect(response).toStrictEqual({186 callId: 'call3',187 error: new Error('Invalid parameters'),188 errorType: ToolErrorType.INVALID_TOOL_PARAMS,189 responseParts: [190 {191 functionResponse: {192 id: 'call3',193 name: 'testTool',194 response: {195 error: 'Invalid parameters',196 },197 },198 },199 ],200 resultDisplay: 'Invalid parameters',201 contentLength: 'Invalid parameters'.length,202 });203 });204 205 it('should return an error if tool execution fails', async () => {206 const request: ToolCallRequestInfo = {207 callId: 'call4',208 name: 'testTool',209 args: { param1: 'value1' },210 isClientInitiated: false,211 prompt_id: 'prompt-id-4',212 };213 const executionErrorResult: ToolResult = {214 llmContent: 'Error: Execution failed',215 returnDisplay: 'Execution failed',216 error: {217 message: 'Execution failed',218 type: ToolErrorType.EXECUTION_FAILED,219 },220 };221 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);222 executeFn.mockResolvedValue(executionErrorResult);223 224 const response = await executeToolCall(225 mockConfig,226 request,227 abortController.signal,228 );229 expect(response).toStrictEqual({230 callId: 'call4',231 error: new Error('Execution failed'),232 errorType: ToolErrorType.EXECUTION_FAILED,233 responseParts: [234 {235 functionResponse: {236 id: 'call4',237 name: 'testTool',238 response: {239 error: 'Execution failed',240 },241 },242 },243 ],244 resultDisplay: 'Execution failed',245 contentLength: 'Execution failed'.length,246 });247 });248 249 it('should return an unhandled exception error if execution throws', async () => {250 const request: ToolCallRequestInfo = {251 callId: 'call5',252 name: 'testTool',253 args: { param1: 'value1' },254 isClientInitiated: false,255 prompt_id: 'prompt-id-5',256 };257 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);258 executeFn.mockRejectedValue(new Error('Something went very wrong'));259 260 const response = await executeToolCall(261 mockConfig,262 request,263 abortController.signal,264 );265 266 expect(response).toStrictEqual({267 callId: 'call5',268 error: new Error('Something went very wrong'),269 errorType: ToolErrorType.UNHANDLED_EXCEPTION,270 resultDisplay: 'Something went very wrong',271 contentLength: 'Something went very wrong'.length,272 responseParts: [273 {274 functionResponse: {275 name: 'testTool',276 id: 'call5',277 response: { error: 'Something went very wrong' },278 },279 },280 ],281 });282 });283 284 it('should correctly format llmContent with inlineData', async () => {285 const request: ToolCallRequestInfo = {286 callId: 'call6',287 name: 'testTool',288 args: {},289 isClientInitiated: false,290 prompt_id: 'prompt-id-6',291 };292 const imageDataPart: Part = {293 inlineData: { mimeType: 'image/png', data: 'base64data' },294 };295 const toolResult: ToolResult = {296 llmContent: [imageDataPart],297 returnDisplay: 'Image processed',298 };299 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);300 executeFn.mockResolvedValue(toolResult);301 302 const response = await executeToolCall(303 mockConfig,304 request,305 abortController.signal,306 );307 308 expect(response).toStrictEqual({309 callId: 'call6',310 error: undefined,311 errorType: undefined,312 resultDisplay: 'Image processed',313 contentLength: undefined,314 responseParts: [315 {316 functionResponse: {317 name: 'testTool',318 id: 'call6',319 response: {320 output: '',321 },322 parts: [323 { inlineData: { mimeType: 'image/png', data: 'base64data' } },324 ],325 },326 },327 ],328 });329 });330 331 it('should calculate contentLength for a string llmContent', async () => {332 const request: ToolCallRequestInfo = {333 callId: 'call7',334 name: 'testTool',335 args: {},336 isClientInitiated: false,337 prompt_id: 'prompt-id-7',338 };339 const toolResult: ToolResult = {340 llmContent: 'This is a test string.',341 returnDisplay: 'String returned',342 };343 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);344 executeFn.mockResolvedValue(toolResult);345 346 const response = await executeToolCall(347 mockConfig,348 request,349 abortController.signal,350 );351 352 expect(response.contentLength).toBe(353 typeof toolResult.llmContent === 'string'354 ? toolResult.llmContent.length355 : undefined,356 );357 });358 359 it('should have undefined contentLength for array llmContent with no string parts', async () => {360 const request: ToolCallRequestInfo = {361 callId: 'call8',362 name: 'testTool',363 args: {},364 isClientInitiated: false,365 prompt_id: 'prompt-id-8',366 };367 const toolResult: ToolResult = {368 llmContent: [{ inlineData: { mimeType: 'image/png', data: 'fakedata' } }],369 returnDisplay: 'Image data returned',370 };371 vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockTool);372 executeFn.mockResolvedValue(toolResult);373 374 const response = await executeToolCall(375 mockConfig,376 request,377 abortController.signal,378 );379 380 expect(response.contentLength).toBeUndefined();381 });382});383 