2025-06-13 17:44:14 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-18 16:27:38 -04:00
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
2025-06-13 17:44:14 -07:00
|
|
|
|
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
2026-03-18 16:27:38 -04:00
|
|
|
|
import {
|
|
|
|
|
|
type SerializableConfirmationDetails,
|
|
|
|
|
|
type ToolCallConfirmationDetails,
|
|
|
|
|
|
type Config,
|
|
|
|
|
|
ToolConfirmationOutcome,
|
2025-08-25 17:30:04 -07:00
|
|
|
|
} from '@google/gemini-cli-core';
|
2026-02-03 17:08:10 -08:00
|
|
|
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
|
|
|
|
|
import { createMockSettings } from '../../../test-utils/settings.js';
|
2026-01-21 16:16:30 -05:00
|
|
|
|
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
2026-03-18 16:27:38 -04:00
|
|
|
|
import { act } from 'react';
|
2026-01-21 16:16:30 -05:00
|
|
|
|
|
|
|
|
|
|
vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
|
|
|
|
|
|
const actual =
|
|
|
|
|
|
await importOriginal<
|
|
|
|
|
|
typeof import('../../contexts/ToolActionsContext.js')
|
|
|
|
|
|
>();
|
|
|
|
|
|
return {
|
|
|
|
|
|
...actual,
|
|
|
|
|
|
useToolActions: vi.fn(),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-06-13 17:44:14 -07:00
|
|
|
|
|
|
|
|
|
|
describe('ToolConfirmationMessage', () => {
|
2026-01-21 16:16:30 -05:00
|
|
|
|
const mockConfirm = vi.fn();
|
|
|
|
|
|
vi.mocked(useToolActions).mockReturnValue({
|
|
|
|
|
|
confirm: mockConfirm,
|
|
|
|
|
|
cancel: vi.fn(),
|
2026-01-26 21:24:25 -05:00
|
|
|
|
isDiffingEnabled: false,
|
2026-01-21 16:16:30 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-26 10:02:22 -07:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2025-08-26 10:02:22 -07:00
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should not display urls if prompt and url are the same', async () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
2025-06-13 17:44:14 -07:00
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt: 'https://example.com',
|
|
|
|
|
|
urls: ['https://example.com'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-06-19 20:17:23 +00:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-06-19 20:17:23 +00:00
|
|
|
|
confirmationDetails={confirmationDetails}
|
2025-08-26 10:02:22 -07:00
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-06-19 20:17:23 +00:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
2025-06-13 17:44:14 -07:00
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-06-13 17:44:14 -07:00
|
|
|
|
|
2025-11-22 08:17:29 +05:30
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-06-13 17:44:14 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should display urls if prompt and url are different', async () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
2025-06-13 17:44:14 -07:00
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt:
|
|
|
|
|
|
'fetch https://github.com/google/gemini-react/blob/main/README.md',
|
|
|
|
|
|
urls: [
|
|
|
|
|
|
'https://raw.githubusercontent.com/google/gemini-react/main/README.md',
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-06-19 20:17:23 +00:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-06-19 20:17:23 +00:00
|
|
|
|
confirmationDetails={confirmationDetails}
|
2025-08-26 10:02:22 -07:00
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-06-19 20:17:23 +00:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
2025-06-13 17:44:14 -07:00
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-06-13 17:44:14 -07:00
|
|
|
|
|
2025-11-22 08:17:29 +05:30
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-06-13 17:44:14 -07:00
|
|
|
|
});
|
2025-08-25 17:30:04 -07:00
|
|
|
|
|
2026-02-20 15:21:31 -05:00
|
|
|
|
it('should display WarningMessage for deceptive URLs in info type', async () => {
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt: 'https://täst.com',
|
|
|
|
|
|
urls: ['https://täst.com'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-20 15:21:31 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('Deceptive URL(s) detected');
|
|
|
|
|
|
expect(output).toContain('Original: https://täst.com');
|
|
|
|
|
|
expect(output).toContain(
|
|
|
|
|
|
'Actual Host (Punycode): https://xn--tst-qla.com/',
|
|
|
|
|
|
);
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should display WarningMessage for deceptive URLs in exec type commands', async () => {
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'exec',
|
|
|
|
|
|
title: 'Confirm Execution',
|
|
|
|
|
|
command: 'curl https://еxample.com',
|
|
|
|
|
|
rootCommand: 'curl',
|
|
|
|
|
|
rootCommands: ['curl'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-20 15:21:31 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('Deceptive URL(s) detected');
|
|
|
|
|
|
expect(output).toContain('Original: https://еxample.com/');
|
|
|
|
|
|
expect(output).toContain(
|
|
|
|
|
|
'Actual Host (Punycode): https://xn--xample-2of.com/',
|
|
|
|
|
|
);
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should exclude shell delimiters from extracted URLs in exec type commands', async () => {
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'exec',
|
|
|
|
|
|
title: 'Confirm Execution',
|
|
|
|
|
|
command: 'curl https://еxample.com;ls',
|
|
|
|
|
|
rootCommand: 'curl',
|
|
|
|
|
|
rootCommands: ['curl'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-20 15:21:31 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('Deceptive URL(s) detected');
|
|
|
|
|
|
// It should extract "https://еxample.com" and NOT "https://еxample.com;ls"
|
|
|
|
|
|
expect(output).toContain('Original: https://еxample.com/');
|
|
|
|
|
|
// The command itself still contains 'ls', so we check specifically that 'ls' is not part of the URL line.
|
|
|
|
|
|
expect(output).not.toContain('Original: https://еxample.com/;ls');
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should aggregate multiple deceptive URLs into a single WarningMessage', async () => {
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt: 'Fetch both',
|
|
|
|
|
|
urls: ['https://еxample.com', 'https://täst.com'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-20 15:21:31 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('Deceptive URL(s) detected');
|
|
|
|
|
|
expect(output).toContain('Original: https://еxample.com/');
|
|
|
|
|
|
expect(output).toContain('Original: https://täst.com/');
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should display multiple commands for exec type when provided', async () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
2026-01-16 15:06:52 -08:00
|
|
|
|
type: 'exec',
|
|
|
|
|
|
title: 'Confirm Multiple Commands',
|
|
|
|
|
|
command: 'echo "hello"', // Primary command
|
|
|
|
|
|
rootCommand: 'echo',
|
|
|
|
|
|
rootCommands: ['echo'],
|
|
|
|
|
|
commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2026-01-16 15:06:52 -08:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2026-01-16 15:06:52 -08:00
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-01-16 15:06:52 -08:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2026-01-16 15:06:52 -08:00
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('echo "hello"');
|
|
|
|
|
|
expect(output).toContain('ls -la');
|
|
|
|
|
|
expect(output).toContain('whoami');
|
|
|
|
|
|
expect(output).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2026-01-16 15:06:52 -08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-05 10:39:42 -08:00
|
|
|
|
it('should render multiline shell scripts with correct newlines and syntax highlighting (SVG snapshot)', async () => {
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'exec',
|
|
|
|
|
|
title: 'Confirm Multiline Script',
|
|
|
|
|
|
command: 'echo "hello"\nfor i in 1 2 3; do\n echo $i\ndone',
|
|
|
|
|
|
rootCommand: 'echo',
|
|
|
|
|
|
rootCommands: ['echo'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const result = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
|
|
|
|
|
getPreferredEditor={vi.fn()}
|
|
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
await result.waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = result.lastFrame();
|
|
|
|
|
|
expect(output).toContain('echo "hello"');
|
|
|
|
|
|
expect(output).toContain('for i in 1 2 3; do');
|
|
|
|
|
|
expect(output).toContain('echo $i');
|
|
|
|
|
|
expect(output).toContain('done');
|
|
|
|
|
|
|
|
|
|
|
|
await expect(result).toMatchSvgSnapshot();
|
|
|
|
|
|
result.unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-25 17:30:04 -07:00
|
|
|
|
describe('with folder trust', () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
type: 'edit',
|
|
|
|
|
|
title: 'Confirm Edit',
|
|
|
|
|
|
fileName: 'test.txt',
|
|
|
|
|
|
filePath: '/test.txt',
|
|
|
|
|
|
fileDiff: '...diff...',
|
|
|
|
|
|
originalContent: 'a',
|
|
|
|
|
|
newContent: 'b',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const execConfirmationDetails: SerializableConfirmationDetails = {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
type: 'exec',
|
|
|
|
|
|
title: 'Confirm Execution',
|
|
|
|
|
|
command: 'echo "hello"',
|
|
|
|
|
|
rootCommand: 'echo',
|
2026-01-14 13:50:28 -05:00
|
|
|
|
rootCommands: ['echo'],
|
2025-08-25 17:30:04 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const infoConfirmationDetails: SerializableConfirmationDetails = {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt: 'https://example.com',
|
|
|
|
|
|
urls: ['https://example.com'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const mcpConfirmationDetails: SerializableConfirmationDetails = {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
type: 'mcp',
|
|
|
|
|
|
title: 'Confirm MCP Tool',
|
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
|
toolName: 'test-tool',
|
|
|
|
|
|
toolDisplayName: 'Test Tool',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
describe.each([
|
|
|
|
|
|
{
|
|
|
|
|
|
description: 'for edit confirmations',
|
|
|
|
|
|
details: editConfirmationDetails,
|
2025-12-18 16:38:53 -08:00
|
|
|
|
alwaysAllowText: 'Allow for this session',
|
2025-08-25 17:30:04 -07:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
description: 'for exec confirmations',
|
|
|
|
|
|
details: execConfirmationDetails,
|
2025-12-18 16:38:53 -08:00
|
|
|
|
alwaysAllowText: 'Allow for this session',
|
2025-08-25 17:30:04 -07:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
description: 'for info confirmations',
|
|
|
|
|
|
details: infoConfirmationDetails,
|
2025-12-18 16:38:53 -08:00
|
|
|
|
alwaysAllowText: 'Allow for this session',
|
2025-08-25 17:30:04 -07:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
description: 'for mcp confirmations',
|
|
|
|
|
|
details: mcpConfirmationDetails,
|
|
|
|
|
|
alwaysAllowText: 'always allow',
|
|
|
|
|
|
},
|
2025-11-22 08:17:29 +05:30
|
|
|
|
])('$description', ({ details }) => {
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should show "allow always" when folder is trusted', async () => {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2025-08-25 17:30:04 -07:00
|
|
|
|
} as unknown as Config;
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-08-25 17:30:04 -07:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-08-25 17:30:04 -07:00
|
|
|
|
confirmationDetails={details}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-08-25 17:30:04 -07:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-08-25 17:30:04 -07:00
|
|
|
|
|
2025-11-22 08:17:29 +05:30
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-08-25 17:30:04 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should NOT show "allow always" when folder is untrusted', async () => {
|
2025-08-25 17:30:04 -07:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => false,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2025-08-25 17:30:04 -07:00
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-08-25 17:30:04 -07:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-08-25 17:30:04 -07:00
|
|
|
|
confirmationDetails={details}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-08-25 17:30:04 -07:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-08-25 17:30:04 -07:00
|
|
|
|
|
2025-11-22 08:17:29 +05:30
|
|
|
|
expect(lastFrame()).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-08-25 17:30:04 -07:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-12-19 09:25:23 -08:00
|
|
|
|
|
|
|
|
|
|
describe('enablePermanentToolApproval setting', () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
2025-12-19 09:25:23 -08:00
|
|
|
|
type: 'edit',
|
|
|
|
|
|
title: 'Confirm Edit',
|
|
|
|
|
|
fileName: 'test.txt',
|
|
|
|
|
|
filePath: '/test.txt',
|
|
|
|
|
|
fileDiff: '...diff...',
|
|
|
|
|
|
originalContent: 'a',
|
|
|
|
|
|
newContent: 'b',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should NOT show "Allow for all future sessions" when setting is false (default)', async () => {
|
2025-12-19 09:25:23 -08:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2025-12-19 09:25:23 -08:00
|
|
|
|
} as unknown as Config;
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-12-19 09:25:23 -08:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-12-19 09:25:23 -08:00
|
|
|
|
confirmationDetails={editConfirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-12-19 09:25:23 -08:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
{
|
|
|
|
|
|
settings: createMockSettings({
|
|
|
|
|
|
security: { enablePermanentToolApproval: false },
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-12-19 09:25:23 -08:00
|
|
|
|
|
|
|
|
|
|
expect(lastFrame()).not.toContain('Allow for all future sessions');
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-12-19 09:25:23 -08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-10 13:01:41 -04:00
|
|
|
|
it('should show "Allow for all future sessions" when trusted', async () => {
|
2025-12-19 09:25:23 -08:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2025-12-19 09:25:23 -08:00
|
|
|
|
} as unknown as Config;
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2025-12-19 09:25:23 -08:00
|
|
|
|
<ToolConfirmationMessage
|
2026-01-21 16:16:30 -05:00
|
|
|
|
callId="test-call-id"
|
2025-12-19 09:25:23 -08:00
|
|
|
|
confirmationDetails={editConfirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2025-12-19 09:25:23 -08:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
{
|
|
|
|
|
|
settings: createMockSettings({
|
|
|
|
|
|
security: { enablePermanentToolApproval: true },
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2025-12-19 09:25:23 -08:00
|
|
|
|
|
2026-03-10 13:01:41 -04:00
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('future sessions');
|
|
|
|
|
|
// Verify it is the default selection (matching the indicator in the snapshot)
|
|
|
|
|
|
expect(output).toMatchSnapshot();
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2025-12-19 09:25:23 -08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-01-26 21:24:25 -05:00
|
|
|
|
|
|
|
|
|
|
describe('Modify with external editor option', () => {
|
2026-02-13 11:14:35 +09:00
|
|
|
|
const editConfirmationDetails: SerializableConfirmationDetails = {
|
2026-01-26 21:24:25 -05:00
|
|
|
|
type: 'edit',
|
|
|
|
|
|
title: 'Confirm Edit',
|
|
|
|
|
|
fileName: 'test.txt',
|
|
|
|
|
|
filePath: '/test.txt',
|
|
|
|
|
|
fileDiff: '...diff...',
|
|
|
|
|
|
originalContent: 'a',
|
|
|
|
|
|
newContent: 'b',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should show "Modify with external editor" when NOT in IDE mode', async () => {
|
2026-01-26 21:24:25 -05:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => false,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2026-01-26 21:24:25 -05:00
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
vi.mocked(useToolActions).mockReturnValue({
|
|
|
|
|
|
confirm: vi.fn(),
|
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
|
isDiffingEnabled: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2026-01-26 21:24:25 -05:00
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={editConfirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-01-26 21:24:25 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
|
|
|
|
|
|
expect(lastFrame()).toContain('Modify with external editor');
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', async () => {
|
2026-01-26 21:24:25 -05:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => true,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2026-01-26 21:24:25 -05:00
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
vi.mocked(useToolActions).mockReturnValue({
|
|
|
|
|
|
confirm: vi.fn(),
|
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
|
isDiffingEnabled: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2026-01-26 21:24:25 -05:00
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={editConfirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-01-26 21:24:25 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
|
|
|
|
|
|
expect(lastFrame()).toContain('Modify with external editor');
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', async () => {
|
2026-01-26 21:24:25 -05:00
|
|
|
|
const mockConfig = {
|
|
|
|
|
|
isTrustedFolder: () => true,
|
|
|
|
|
|
getIdeMode: () => true,
|
2026-03-13 16:02:09 -07:00
|
|
|
|
getDisableAlwaysAllow: () => false,
|
2026-01-26 21:24:25 -05:00
|
|
|
|
} as unknown as Config;
|
|
|
|
|
|
vi.mocked(useToolActions).mockReturnValue({
|
|
|
|
|
|
confirm: vi.fn(),
|
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
|
isDiffingEnabled: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-18 16:46:50 -08:00
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
2026-01-26 21:24:25 -05:00
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={editConfirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-01-26 21:24:25 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
2026-02-18 16:46:50 -08:00
|
|
|
|
await waitUntilReady();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
|
|
|
|
|
|
expect(lastFrame()).not.toContain('Modify with external editor');
|
2026-02-18 16:46:50 -08:00
|
|
|
|
unmount();
|
2026-01-26 21:24:25 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-02-20 15:04:32 -05:00
|
|
|
|
|
|
|
|
|
|
it('should strip BiDi characters from MCP tool and server names', async () => {
|
|
|
|
|
|
const confirmationDetails: ToolCallConfirmationDetails = {
|
|
|
|
|
|
type: 'mcp',
|
|
|
|
|
|
title: 'Confirm MCP Tool',
|
|
|
|
|
|
serverName: 'test\u202Eserver',
|
|
|
|
|
|
toolName: 'test\u202Dtool',
|
|
|
|
|
|
toolDisplayName: 'Test Tool',
|
|
|
|
|
|
onConfirm: vi.fn(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-20 15:04:32 -05:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
// BiDi characters \u202E and \u202D should be stripped
|
|
|
|
|
|
expect(output).toContain('MCP Server: testserver');
|
|
|
|
|
|
expect(output).toContain('Tool: testtool');
|
|
|
|
|
|
expect(output).toContain('Allow execution of MCP tool "testtool"');
|
|
|
|
|
|
expect(output).toContain('from server "testserver"?');
|
|
|
|
|
|
expect(output).toMatchSnapshot();
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
2026-02-24 10:45:05 +09:00
|
|
|
|
|
|
|
|
|
|
it('should show MCP tool details expand hint for MCP confirmations', async () => {
|
|
|
|
|
|
const confirmationDetails: ToolCallConfirmationDetails = {
|
|
|
|
|
|
type: 'mcp',
|
|
|
|
|
|
title: 'Confirm MCP Tool',
|
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
|
toolName: 'test-tool',
|
|
|
|
|
|
toolDisplayName: 'Test Tool',
|
|
|
|
|
|
toolArgs: {
|
|
|
|
|
|
url: 'https://www.google.co.jp',
|
|
|
|
|
|
},
|
|
|
|
|
|
toolDescription: 'Navigates browser to a URL.',
|
|
|
|
|
|
toolParameterSchema: {
|
|
|
|
|
|
type: 'object',
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
url: {
|
|
|
|
|
|
type: 'string',
|
|
|
|
|
|
description: 'Destination URL',
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
required: ['url'],
|
|
|
|
|
|
},
|
|
|
|
|
|
onConfirm: vi.fn(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-24 10:45:05 +09:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('MCP Tool Details:');
|
|
|
|
|
|
expect(output).toContain('(press Ctrl+O to expand MCP tool details)');
|
|
|
|
|
|
expect(output).not.toContain('https://www.google.co.jp');
|
|
|
|
|
|
expect(output).not.toContain('Navigates browser to a URL.');
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should omit empty MCP invocation arguments from details', async () => {
|
|
|
|
|
|
const confirmationDetails: ToolCallConfirmationDetails = {
|
|
|
|
|
|
type: 'mcp',
|
|
|
|
|
|
title: 'Confirm MCP Tool',
|
|
|
|
|
|
serverName: 'test-server',
|
|
|
|
|
|
toolName: 'test-tool',
|
|
|
|
|
|
toolDisplayName: 'Test Tool',
|
|
|
|
|
|
toolArgs: {},
|
|
|
|
|
|
toolDescription: 'No arguments required.',
|
|
|
|
|
|
onConfirm: vi.fn(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
2026-02-25 23:38:44 -05:00
|
|
|
|
getPreferredEditor={vi.fn()}
|
2026-02-24 10:45:05 +09:00
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
const output = lastFrame();
|
|
|
|
|
|
expect(output).toContain('MCP Tool Details:');
|
|
|
|
|
|
expect(output).toContain('(press Ctrl+O to expand MCP tool details)');
|
|
|
|
|
|
expect(output).not.toContain('Invocation Arguments:');
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
2026-03-18 16:27:38 -04:00
|
|
|
|
|
|
|
|
|
|
describe('ESCAPE key behavior', () => {
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should call confirm(Cancel) asynchronously via useEffect when ESC is pressed', async () => {
|
|
|
|
|
|
const mockConfirm = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
|
|
vi.mocked(useToolActions).mockReturnValue({
|
|
|
|
|
|
confirm: mockConfirm,
|
|
|
|
|
|
cancel: vi.fn(),
|
|
|
|
|
|
isDiffingEnabled: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const confirmationDetails: SerializableConfirmationDetails = {
|
|
|
|
|
|
type: 'info',
|
|
|
|
|
|
title: 'Confirm Web Fetch',
|
|
|
|
|
|
prompt: 'https://example.com',
|
|
|
|
|
|
urls: ['https://example.com'],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { stdin, waitUntilReady, unmount } = renderWithProviders(
|
|
|
|
|
|
<ToolConfirmationMessage
|
|
|
|
|
|
callId="test-call-id"
|
|
|
|
|
|
confirmationDetails={confirmationDetails}
|
|
|
|
|
|
config={mockConfig}
|
|
|
|
|
|
getPreferredEditor={vi.fn()}
|
|
|
|
|
|
availableTerminalHeight={30}
|
|
|
|
|
|
terminalWidth={80}
|
|
|
|
|
|
/>,
|
|
|
|
|
|
);
|
|
|
|
|
|
await waitUntilReady();
|
|
|
|
|
|
|
|
|
|
|
|
stdin.write('\x1b');
|
|
|
|
|
|
|
|
|
|
|
|
// To assert that the confirmation happens asynchronously (via useEffect) rather than
|
|
|
|
|
|
// synchronously (directly inside the keystroke handler), we must run our assertion
|
|
|
|
|
|
// *inside* the act() block.
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
|
await vi.runAllTimersAsync();
|
|
|
|
|
|
expect(mockConfirm).not.toHaveBeenCalled();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Now that the act() block has returned, React flushes the useEffect, calling handleConfirm.
|
|
|
|
|
|
expect(mockConfirm).toHaveBeenCalledWith(
|
|
|
|
|
|
'test-call-id',
|
|
|
|
|
|
ToolConfirmationOutcome.Cancel,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2025-06-13 17:44:14 -07:00
|
|
|
|
});
|