mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 19:14:33 -07:00
fix: similar to policy-engine, throw error in case of requiring tool execution confirmation for non-interactive mode (#14702)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,7 @@ export function createMockConfig(
|
|||||||
}),
|
}),
|
||||||
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
|
||||||
getIdeMode: vi.fn().mockReturnValue(false),
|
getIdeMode: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractive: () => true,
|
||||||
getAllowedTools: vi.fn().mockReturnValue([]),
|
getAllowedTools: vi.fn().mockReturnValue([]),
|
||||||
getWorkspaceContext: vi.fn().mockReturnValue({
|
getWorkspaceContext: vi.fn().mockReturnValue({
|
||||||
isPathWithinWorkspace: () => true,
|
isPathWithinWorkspace: () => true,
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ const mockConfig = {
|
|||||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
|
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
|
||||||
mockConfig.getHookSystem = vi.fn().mockReturnValue(new HookSystem(mockConfig));
|
mockConfig.getHookSystem = vi.fn().mockReturnValue(new HookSystem(mockConfig));
|
||||||
|
|
||||||
|
function createMockConfigOverride(overrides: Partial<Config> = {}): Config {
|
||||||
|
return { ...mockConfig, ...overrides } as Config;
|
||||||
|
}
|
||||||
|
|
||||||
const mockTool = new MockTool({
|
const mockTool = new MockTool({
|
||||||
name: 'mockTool',
|
name: 'mockTool',
|
||||||
displayName: 'Mock Tool',
|
displayName: 'Mock Tool',
|
||||||
@@ -262,13 +266,9 @@ describe('useReactToolScheduler', () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderScheduler = () =>
|
const renderScheduler = (config: Config = mockConfig) =>
|
||||||
renderHook(() =>
|
renderHook(() =>
|
||||||
useReactToolScheduler(
|
useReactToolScheduler(onComplete, config, () => undefined),
|
||||||
onComplete,
|
|
||||||
mockConfig as unknown as Config,
|
|
||||||
() => undefined,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
it('initial state should be empty', () => {
|
it('initial state should be empty', () => {
|
||||||
@@ -494,13 +494,16 @@ describe('useReactToolScheduler', () => {
|
|||||||
|
|
||||||
it('should handle tool requiring confirmation - approved', async () => {
|
it('should handle tool requiring confirmation - approved', async () => {
|
||||||
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
||||||
|
const config = createMockConfigOverride({
|
||||||
|
isInteractive: () => true,
|
||||||
|
});
|
||||||
const expectedOutput = 'Confirmed output';
|
const expectedOutput = 'Confirmed output';
|
||||||
(mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({
|
(mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({
|
||||||
llmContent: expectedOutput,
|
llmContent: expectedOutput,
|
||||||
returnDisplay: 'Confirmed display',
|
returnDisplay: 'Confirmed display',
|
||||||
} as ToolResult);
|
} as ToolResult);
|
||||||
|
|
||||||
const { result } = renderScheduler();
|
const { result } = renderScheduler(config);
|
||||||
const schedule = result.current[1];
|
const schedule = result.current[1];
|
||||||
const request: ToolCallRequestInfo = {
|
const request: ToolCallRequestInfo = {
|
||||||
callId: 'callConfirm',
|
callId: 'callConfirm',
|
||||||
@@ -544,7 +547,10 @@ describe('useReactToolScheduler', () => {
|
|||||||
|
|
||||||
it('should handle tool requiring confirmation - cancelled by user', async () => {
|
it('should handle tool requiring confirmation - cancelled by user', async () => {
|
||||||
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
||||||
const { result } = renderScheduler();
|
const config = createMockConfigOverride({
|
||||||
|
isInteractive: () => true,
|
||||||
|
});
|
||||||
|
const { result } = renderScheduler(config);
|
||||||
const schedule = result.current[1];
|
const schedule = result.current[1];
|
||||||
const request: ToolCallRequestInfo = {
|
const request: ToolCallRequestInfo = {
|
||||||
callId: 'callConfirmCancel',
|
callId: 'callConfirmCancel',
|
||||||
|
|||||||
@@ -6,12 +6,16 @@
|
|||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import type { Mock } from 'vitest';
|
import type { Mock } from 'vitest';
|
||||||
import type { ToolCall, WaitingToolCall } from './coreToolScheduler.js';
|
|
||||||
import {
|
import {
|
||||||
CoreToolScheduler,
|
CoreToolScheduler,
|
||||||
convertToFunctionResponse,
|
convertToFunctionResponse,
|
||||||
truncateAndSaveToFile,
|
truncateAndSaveToFile,
|
||||||
} from './coreToolScheduler.js';
|
} from './coreToolScheduler.js';
|
||||||
|
import type {
|
||||||
|
ToolCall,
|
||||||
|
WaitingToolCall,
|
||||||
|
ErroredToolCall,
|
||||||
|
} from './coreToolScheduler.js';
|
||||||
import type {
|
import type {
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolConfirmationPayload,
|
ToolConfirmationPayload,
|
||||||
@@ -232,6 +236,7 @@ function createMockConfig(overrides: Partial<Config> = {}): Config {
|
|||||||
getSessionId: () => 'test-session-id',
|
getSessionId: () => 'test-session-id',
|
||||||
getUsageStatisticsEnabled: () => true,
|
getUsageStatisticsEnabled: () => true,
|
||||||
getDebugMode: () => false,
|
getDebugMode: () => false,
|
||||||
|
isInteractive: () => true,
|
||||||
getApprovalMode: () => ApprovalMode.DEFAULT,
|
getApprovalMode: () => ApprovalMode.DEFAULT,
|
||||||
setApprovalMode: () => {},
|
setApprovalMode: () => {},
|
||||||
getAllowedTools: () => [],
|
getAllowedTools: () => [],
|
||||||
@@ -353,7 +358,6 @@ describe('CoreToolScheduler', () => {
|
|||||||
|
|
||||||
const mockConfig = createMockConfig({
|
const mockConfig = createMockConfig({
|
||||||
getToolRegistry: () => mockToolRegistry,
|
getToolRegistry: () => mockToolRegistry,
|
||||||
isInteractive: () => false,
|
|
||||||
getHookSystem: () => undefined,
|
getHookSystem: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -455,7 +459,6 @@ describe('CoreToolScheduler', () => {
|
|||||||
|
|
||||||
const mockConfig = createMockConfig({
|
const mockConfig = createMockConfig({
|
||||||
getToolRegistry: () => mockToolRegistry,
|
getToolRegistry: () => mockToolRegistry,
|
||||||
isInteractive: () => false,
|
|
||||||
getHookSystem: () => undefined,
|
getHookSystem: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -582,6 +585,67 @@ describe('CoreToolScheduler', () => {
|
|||||||
expect(statuses).not.toContain('error');
|
expect(statuses).not.toContain('error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should error when tool requires confirmation in non-interactive mode', async () => {
|
||||||
|
const mockTool = new MockTool({
|
||||||
|
name: 'mockTool',
|
||||||
|
shouldConfirmExecute: MOCK_TOOL_SHOULD_CONFIRM_EXECUTE,
|
||||||
|
});
|
||||||
|
const declarativeTool = mockTool;
|
||||||
|
const mockToolRegistry = {
|
||||||
|
getTool: () => declarativeTool,
|
||||||
|
getFunctionDeclarations: () => [],
|
||||||
|
tools: new Map(),
|
||||||
|
discovery: {},
|
||||||
|
registerTool: () => {},
|
||||||
|
getToolByName: () => declarativeTool,
|
||||||
|
getToolByDisplayName: () => declarativeTool,
|
||||||
|
getTools: () => [],
|
||||||
|
discoverTools: async () => {},
|
||||||
|
getAllTools: () => [],
|
||||||
|
getToolsByServer: () => [],
|
||||||
|
} as unknown as ToolRegistry;
|
||||||
|
|
||||||
|
const onAllToolCallsComplete = vi.fn();
|
||||||
|
const onToolCallsUpdate = vi.fn();
|
||||||
|
|
||||||
|
const mockConfig = createMockConfig({
|
||||||
|
getToolRegistry: () => mockToolRegistry,
|
||||||
|
isInteractive: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduler = new CoreToolScheduler({
|
||||||
|
config: mockConfig,
|
||||||
|
onAllToolCallsComplete,
|
||||||
|
onToolCallsUpdate,
|
||||||
|
getPreferredEditor: () => 'vscode',
|
||||||
|
});
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const request = {
|
||||||
|
callId: '1',
|
||||||
|
name: 'mockTool',
|
||||||
|
args: {},
|
||||||
|
isClientInitiated: false,
|
||||||
|
prompt_id: 'prompt-id-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
await scheduler.schedule([request], abortController.signal);
|
||||||
|
|
||||||
|
expect(onAllToolCallsComplete).toHaveBeenCalled();
|
||||||
|
const completedCalls = onAllToolCallsComplete.mock
|
||||||
|
.calls[0][0] as ToolCall[];
|
||||||
|
expect(completedCalls[0].status).toBe('error');
|
||||||
|
|
||||||
|
const erroredCall = completedCalls[0] as ErroredToolCall;
|
||||||
|
const errorResponse = erroredCall.response;
|
||||||
|
const errorParts = errorResponse.responseParts;
|
||||||
|
// @ts-expect-error - accessing internal structure of FunctionResponsePart
|
||||||
|
const errorMessage = errorParts[0].functionResponse.response.error;
|
||||||
|
expect(errorMessage).toContain(
|
||||||
|
'Tool execution for "mockTool" requires user confirmation, which is not supported in non-interactive mode.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getToolSuggestion', () => {
|
describe('getToolSuggestion', () => {
|
||||||
it('should suggest the top N closest tool names for a typo', () => {
|
it('should suggest the top N closest tool names for a typo', () => {
|
||||||
// Create mocked tool registry
|
// Create mocked tool registry
|
||||||
@@ -643,7 +707,6 @@ describe('CoreToolScheduler with payload', () => {
|
|||||||
|
|
||||||
const mockConfig = createMockConfig({
|
const mockConfig = createMockConfig({
|
||||||
getToolRegistry: () => mockToolRegistry,
|
getToolRegistry: () => mockToolRegistry,
|
||||||
isInteractive: () => false,
|
|
||||||
});
|
});
|
||||||
const mockMessageBus = createMockMessageBus();
|
const mockMessageBus = createMockMessageBus();
|
||||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
||||||
@@ -950,7 +1013,6 @@ describe('CoreToolScheduler edit cancellation', () => {
|
|||||||
|
|
||||||
const mockConfig = createMockConfig({
|
const mockConfig = createMockConfig({
|
||||||
getToolRegistry: () => mockToolRegistry,
|
getToolRegistry: () => mockToolRegistry,
|
||||||
isInteractive: () => false,
|
|
||||||
});
|
});
|
||||||
const mockMessageBus = createMockMessageBus();
|
const mockMessageBus = createMockMessageBus();
|
||||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
||||||
@@ -1366,7 +1428,6 @@ describe('CoreToolScheduler request queueing', () => {
|
|||||||
terminalHeight: 24,
|
terminalHeight: 24,
|
||||||
}),
|
}),
|
||||||
getToolRegistry: () => toolRegistry,
|
getToolRegistry: () => toolRegistry,
|
||||||
isInteractive: () => false,
|
|
||||||
getHookSystem: () => undefined,
|
getHookSystem: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1423,7 +1484,6 @@ describe('CoreToolScheduler request queueing', () => {
|
|||||||
const mockConfig = createMockConfig({
|
const mockConfig = createMockConfig({
|
||||||
getToolRegistry: () => mockToolRegistry,
|
getToolRegistry: () => mockToolRegistry,
|
||||||
getApprovalMode: () => ApprovalMode.YOLO,
|
getApprovalMode: () => ApprovalMode.YOLO,
|
||||||
isInteractive: () => false,
|
|
||||||
});
|
});
|
||||||
const mockMessageBus = createMockMessageBus();
|
const mockMessageBus = createMockMessageBus();
|
||||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
||||||
@@ -1484,7 +1544,6 @@ describe('CoreToolScheduler request queueing', () => {
|
|||||||
setApprovalMode: (mode: ApprovalMode) => {
|
setApprovalMode: (mode: ApprovalMode) => {
|
||||||
approvalMode = mode;
|
approvalMode = mode;
|
||||||
},
|
},
|
||||||
isInteractive: () => false,
|
|
||||||
});
|
});
|
||||||
const mockMessageBus = createMockMessageBus();
|
const mockMessageBus = createMockMessageBus();
|
||||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
mockConfig.getMessageBus = vi.fn().mockReturnValue(mockMessageBus);
|
||||||
|
|||||||
@@ -870,6 +870,13 @@ export class CoreToolScheduler {
|
|||||||
);
|
);
|
||||||
this.setStatusInternal(reqInfo.callId, 'scheduled', signal);
|
this.setStatusInternal(reqInfo.callId, 'scheduled', signal);
|
||||||
} else {
|
} else {
|
||||||
|
if (!this.config.isInteractive()) {
|
||||||
|
throw new Error(
|
||||||
|
`Tool execution for "${
|
||||||
|
toolCall.tool.displayName || toolCall.tool.name
|
||||||
|
}" requires user confirmation, which is not supported in non-interactive mode.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Fire Notification hook before showing confirmation to user
|
// Fire Notification hook before showing confirmation to user
|
||||||
const messageBus = this.config.getMessageBus();
|
const messageBus = this.config.getMessageBus();
|
||||||
const hooksEnabled = this.config.getEnableHooks();
|
const hooksEnabled = this.config.getEnableHooks();
|
||||||
|
|||||||
Reference in New Issue
Block a user