Download packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
-
curl -L -o ToolConfirmationMessage.test.tsx https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
32.1 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
| import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; | |
| import { | |
| type SerializableConfirmationDetails, | |
| type ToolCallConfirmationDetails, | |
| type Config, | |
| ToolConfirmationOutcome, | |
| } from '@google/gemini-cli-core'; | |
| import { renderWithProviders } from '../../../test-utils/render.js'; | |
| import { createMockSettings } from '../../../test-utils/settings.js'; | |
| import { useToolActions } from '../../contexts/ToolActionsContext.js'; | |
| import { act } from 'react'; | |
| vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => { | |
| const actual = | |
| await importOriginal< | |
| typeof import('../../contexts/ToolActionsContext.js') | |
| >(); | |
| return { | |
| ...actual, | |
| useToolActions: vi.fn(), | |
| }; | |
| }); | |
| describe('ToolConfirmationMessage', () => { | |
| const mockConfirm = vi.fn(); | |
| beforeEach(() => { | |
| vi.mocked(useToolActions).mockReturnValue({ | |
| confirm: mockConfirm, | |
| cancel: vi.fn(), | |
| isDiffingEnabled: false, | |
| isExpanded: vi.fn().mockReturnValue(false), | |
| toggleExpansion: vi.fn(), | |
| toggleAllExpansion: vi.fn(), | |
| }); | |
| }); | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| it('should not display urls if prompt and url are the same', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'info', | |
| title: 'Confirm Web Fetch', | |
| prompt: 'https://example.com', | |
| urls: ['https://example.com'], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| it('should display urls if prompt and url are different', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| 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', | |
| ], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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(); | |
| }); | |
| it('should display multiple commands for exec type when provided', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Multiple Commands', | |
| command: 'echo "hello"', // Primary command | |
| rootCommand: 'echo', | |
| rootCommands: ['echo'], | |
| commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| const output = lastFrame(); | |
| expect(output).toContain('echo "hello"'); | |
| expect(output).toContain('ls -la'); | |
| expect(output).toContain('whoami'); | |
| expect(output).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| it('should render multiline shell scripts with correct newlines and syntax highlighting', 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 = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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(); | |
| }); | |
| it('should use the tool name for display in the confirmation question', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Execution', | |
| command: '# This is a comment\necho "hello"', | |
| rootCommand: 'echo', | |
| rootCommands: ['echo'], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| const output = lastFrame(); | |
| expect(output).toContain('Allow execution of [Shell]?'); | |
| unmount(); | |
| }); | |
| describe('tool name humanization', () => { | |
| const cases = [ | |
| { | |
| toolName: 'run_shell_command', | |
| expected: 'Allow execution of [Shell]?', | |
| desc: 'humanize run_shell_command to Shell', | |
| }, | |
| { | |
| toolName: 'shell', | |
| expected: 'Allow execution of [Shell]?', | |
| desc: 'humanize shell to Shell', | |
| }, | |
| { | |
| toolName: 'grep_search', | |
| expected: 'Allow execution of [grep_search]?', | |
| desc: 'keep raw name for non-shell tools', | |
| }, | |
| ]; | |
| for (const { toolName, expected, desc } of cases) { | |
| it(`should ${desc}`, async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm', | |
| command: 'ls', | |
| rootCommand: 'ls', | |
| rootCommands: ['ls'], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName={toolName} | |
| />, | |
| ); | |
| expect(lastFrame()).toContain(expected); | |
| unmount(); | |
| }); | |
| } | |
| it('should humanize shell tool in sandbox expansion prompt', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'sandbox_expansion', | |
| title: 'Confirm', | |
| command: 'ls', | |
| rootCommand: 'ls', | |
| additionalPermissions: { | |
| network: true, | |
| }, | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="run_shell_command" | |
| />, | |
| ); | |
| expect(lastFrame()).toContain( | |
| 'To run [Shell], allow access to the following?', | |
| ); | |
| unmount(); | |
| }); | |
| }); | |
| describe('with folder trust', () => { | |
| const editConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'edit', | |
| title: 'Confirm Edit', | |
| fileName: 'test.txt', | |
| filePath: '/test.txt', | |
| fileDiff: '...diff...', | |
| originalContent: 'a', | |
| newContent: 'b', | |
| }; | |
| const execConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Execution', | |
| command: 'echo "hello"', | |
| rootCommand: 'echo', | |
| rootCommands: ['echo'], | |
| }; | |
| const infoConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'info', | |
| title: 'Confirm Web Fetch', | |
| prompt: 'https://example.com', | |
| urls: ['https://example.com'], | |
| }; | |
| const mcpConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'mcp', | |
| title: 'Confirm MCP Tool', | |
| serverName: 'test-server', | |
| toolName: 'test-tool', | |
| toolDisplayName: 'Test Tool', | |
| }; | |
| describe.each([ | |
| { | |
| description: 'for edit confirmations', | |
| details: editConfirmationDetails, | |
| alwaysAllowText: 'Allow for this session', | |
| }, | |
| { | |
| description: 'for exec confirmations', | |
| details: execConfirmationDetails, | |
| alwaysAllowText: 'Allow for this session', | |
| }, | |
| { | |
| description: 'for info confirmations', | |
| details: infoConfirmationDetails, | |
| alwaysAllowText: 'Allow for this session', | |
| }, | |
| { | |
| description: 'for mcp confirmations', | |
| details: mcpConfirmationDetails, | |
| alwaysAllowText: 'always allow', | |
| }, | |
| ])('$description', ({ details }) => { | |
| it('should show "allow always" when folder is trusted', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={details} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| it('should NOT show "allow always" when folder is untrusted', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => false, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={details} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| }); | |
| }); | |
| describe('enablePermanentToolApproval setting', () => { | |
| const editConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'edit', | |
| title: 'Confirm Edit', | |
| fileName: 'test.txt', | |
| filePath: '/test.txt', | |
| fileDiff: '...diff...', | |
| originalContent: 'a', | |
| newContent: 'b', | |
| }; | |
| it('should NOT show "Allow for all future sessions" when setting is false (default)', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={editConfirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| { | |
| settings: createMockSettings({ | |
| security: { enablePermanentToolApproval: false }, | |
| }), | |
| }, | |
| ); | |
| expect(lastFrame()).not.toContain('Allow for all future sessions'); | |
| unmount(); | |
| }); | |
| it('should show "Allow for all future sessions" when trusted', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={editConfirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| { | |
| settings: createMockSettings({ | |
| security: { enablePermanentToolApproval: true }, | |
| }), | |
| }, | |
| ); | |
| const output = lastFrame(); | |
| expect(output).toContain('future sessions'); | |
| // Verify it is the default selection (matching the indicator in the snapshot) | |
| expect(output).toMatchSnapshot(); | |
| unmount(); | |
| }); | |
| }); | |
| describe('Modify with external editor option', () => { | |
| const editConfirmationDetails: SerializableConfirmationDetails = { | |
| type: 'edit', | |
| title: 'Confirm Edit', | |
| fileName: 'test.txt', | |
| filePath: '/test.txt', | |
| fileDiff: '...diff...', | |
| originalContent: 'a', | |
| newContent: 'b', | |
| }; | |
| it('should show "Modify with external editor" when NOT in IDE mode', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => false, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| vi.mocked(useToolActions).mockReturnValue({ | |
| confirm: vi.fn(), | |
| cancel: vi.fn(), | |
| isDiffingEnabled: false, | |
| isExpanded: vi.fn().mockReturnValue(false), | |
| toggleExpansion: vi.fn(), | |
| toggleAllExpansion: vi.fn(), | |
| }); | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={editConfirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toContain('Modify with external editor'); | |
| unmount(); | |
| }); | |
| it('should show "Modify with external editor" when in IDE mode but diffing is NOT enabled', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => true, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| vi.mocked(useToolActions).mockReturnValue({ | |
| confirm: vi.fn(), | |
| cancel: vi.fn(), | |
| isDiffingEnabled: false, | |
| isExpanded: vi.fn().mockReturnValue(false), | |
| toggleExpansion: vi.fn(), | |
| toggleAllExpansion: vi.fn(), | |
| }); | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={editConfirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).toContain('Modify with external editor'); | |
| unmount(); | |
| }); | |
| it('should NOT show "Modify with external editor" when in IDE mode AND diffing is enabled', async () => { | |
| const mockConfig = { | |
| isTrustedFolder: () => true, | |
| getIdeMode: () => true, | |
| getDisableAlwaysAllow: () => false, | |
| getApprovalMode: () => 'default', | |
| } as unknown as Config; | |
| vi.mocked(useToolActions).mockReturnValue({ | |
| confirm: vi.fn(), | |
| cancel: vi.fn(), | |
| isDiffingEnabled: true, | |
| isExpanded: vi.fn().mockReturnValue(false), | |
| toggleExpansion: vi.fn(), | |
| toggleAllExpansion: vi.fn(), | |
| }); | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={editConfirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| expect(lastFrame()).not.toContain('Modify with external editor'); | |
| unmount(); | |
| }); | |
| }); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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(); | |
| }); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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(); | |
| }); | |
| describe('height allocation and layout', () => { | |
| it('should expand to available height for large exec commands', async () => { | |
| let largeCommand = ''; | |
| for (let i = 1; i <= 50; i++) { | |
| largeCommand += `echo "Line ${i}"\n`; | |
| } | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Execution', | |
| command: largeCommand.trimEnd(), | |
| rootCommand: 'echo', | |
| rootCommands: ['echo'], | |
| }; | |
| const { waitUntilReady, lastFrame, generateSvg, unmount } = | |
| await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={40} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| await waitUntilReady(); | |
| const outputLines = lastFrame().split('\n'); | |
| // Should use the entire terminal height | |
| expect(outputLines.length).toBe(40); | |
| await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot(); | |
| unmount(); | |
| }); | |
| it('should expand to available height for large edit diffs', async () => { | |
| // Create a large diff string | |
| let largeDiff = '--- a/file.ts\n+++ b/file.ts\n@@ -1,10 +1,15 @@\n'; | |
| for (let i = 1; i <= 20; i++) { | |
| largeDiff += `-const oldLine${i} = true;\n`; | |
| largeDiff += `+const newLine${i} = true;\n`; | |
| } | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'edit', | |
| title: 'Confirm Edit', | |
| fileName: 'file.ts', | |
| filePath: '/file.ts', | |
| fileDiff: largeDiff, | |
| originalContent: 'old', | |
| newContent: 'new', | |
| isModifying: false, | |
| }; | |
| const { waitUntilReady, lastFrame, generateSvg, unmount } = | |
| await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={40} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| await waitUntilReady(); | |
| const outputLines = lastFrame().split('\n'); | |
| // Should use the entire terminal height | |
| expect(outputLines.length).toBe(40); | |
| await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot(); | |
| unmount(); | |
| }); | |
| }); | |
| 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, | |
| isExpanded: vi.fn().mockReturnValue(false), | |
| toggleExpansion: vi.fn(), | |
| toggleAllExpansion: vi.fn(), | |
| }); | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'info', | |
| title: 'Confirm Web Fetch', | |
| prompt: 'https://example.com', | |
| urls: ['https://example.com'], | |
| }; | |
| const { stdin, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-call-id" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="shell" | |
| />, | |
| ); | |
| 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(); | |
| }); | |
| }); | |
| describe('Security Warnings & Build File Protection', () => { | |
| it('should display critical security warning and suppress persistent approvals for build files', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'edit', | |
| title: 'Confirm Edit: BUILD', | |
| fileName: 'BUILD', | |
| filePath: '/workspace/BUILD', | |
| fileDiff: | |
| '--- BUILD\n+++ BUILD\n@@ -1,3 +1,3 @@\n-cc_library(name = "a")\n+cc_library(name = "b")', | |
| originalContent: 'cc_library(name = "a")', | |
| newContent: 'cc_library(name = "b")', | |
| isBuildFile: true, | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-build-edit" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="replace" | |
| />, | |
| ); | |
| const frame = lastFrame(); | |
| expect(frame).toContain( | |
| 'CRITICAL SECURITY WARNING: Build File Modification', | |
| ); | |
| expect(frame).toContain('Target: BUILD'); | |
| expect(frame).toContain('Allow once'); | |
| expect(frame).not.toContain('Allow for this session'); | |
| expect(frame).not.toContain('Allow for this file in all future sessions'); | |
| unmount(); | |
| }); | |
| it('should display critical security warning and suppress persistent approvals for untrusted command flags', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Shell Command', | |
| command: 'blaze test //target:all --test_arg=malicious_flag', | |
| rootCommand: 'blaze', | |
| rootCommands: ['blaze'], | |
| untrustedFlags: ['--test_arg=malicious_flag'], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-untrusted-exec" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="run_shell_command" | |
| />, | |
| ); | |
| const frame = lastFrame(); | |
| expect(frame).toContain( | |
| 'CRITICAL SECURITY WARNING: Untrusted Command Flags Detected', | |
| ); | |
| expect(frame).toContain('--test_arg=malicious_flag'); | |
| expect(frame).toContain('Allow once'); | |
| expect(frame).not.toContain('Allow for this session'); | |
| expect(frame).not.toContain('Allow this command for all future sessions'); | |
| unmount(); | |
| }); | |
| it('should display caution warning when build files were modified earlier in session', async () => { | |
| const confirmationDetails: SerializableConfirmationDetails = { | |
| type: 'exec', | |
| title: 'Confirm Shell Command', | |
| command: 'blaze test //target:all', | |
| rootCommand: 'blaze', | |
| rootCommands: ['blaze'], | |
| modifiedBuildFiles: ['/workspace/pkg/BUILD'], | |
| }; | |
| const { lastFrame, unmount } = await renderWithProviders( | |
| <ToolConfirmationMessage | |
| callId="test-build-mod-exec" | |
| confirmationDetails={confirmationDetails} | |
| config={mockConfig} | |
| getPreferredEditor={vi.fn()} | |
| availableTerminalHeight={30} | |
| terminalWidth={80} | |
| toolName="run_shell_command" | |
| />, | |
| ); | |
| const frame = lastFrame(); | |
| expect(frame).toContain( | |
| 'CAUTION: Build Configuration Was Recently Modified', | |
| ); | |
| expect(frame).toContain('BUILD'); | |
| unmount(); | |
| }); | |
| }); | |
| }); | |