diff options
| author | Arya Gummadi <[email protected]> | 2025-08-12 15:10:22 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-12 22:10:22 +0000 |
| commit | 8d6eb8c322890b5cdf20d4a30dd17afb1541f5aa (patch) | |
| tree | 34c8c86d69aa96405c2c1024c45cef76d1675ece /packages/cli/src/config/config.test.ts | |
| parent | 11377915dbf42064ed9a8d9e31b46adc565ae021 (diff) | |
feat: add --approval-mode parameter (#6024)
Co-authored-by: Jacob Richman <[email protected]>
Diffstat (limited to 'packages/cli/src/config/config.test.ts')
| -rw-r--r-- | packages/cli/src/config/config.test.ts | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 178980eb..fc4d24bd 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -156,6 +156,93 @@ describe('parseArguments', () => { expect(argv.promptInteractive).toBe('interactive prompt'); expect(argv.prompt).toBeUndefined(); }); + + it('should throw an error when both --yolo and --approval-mode are used together', async () => { + process.argv = [ + 'node', + 'script.js', + '--yolo', + '--approval-mode', + 'default', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should throw an error when using short flags -y and --approval-mode together', async () => { + process.argv = ['node', 'script.js', '-y', '--approval-mode', 'yolo']; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should allow --approval-mode without --yolo', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; + const argv = await parseArguments(); + expect(argv.approvalMode).toBe('auto_edit'); + expect(argv.yolo).toBe(false); + }); + + it('should allow --yolo without --approval-mode', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments(); + expect(argv.yolo).toBe(true); + expect(argv.approvalMode).toBeUndefined(); + }); + + it('should reject invalid --approval-mode values', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'invalid']; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining('Invalid values:'), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); }); describe('loadCliConfig', () => { @@ -760,6 +847,211 @@ describe('mergeExcludeTools', () => { }); }); +describe('Approval mode tool exclusion logic', () => { + const originalIsTTY = process.stdin.isTTY; + + beforeEach(() => { + process.stdin.isTTY = false; // Ensure non-interactive mode + }); + + afterEach(() => { + process.stdin.isTTY = originalIsTTY; + }); + + it('should exclude all interactive tools in non-interactive mode with default approval mode', async () => { + process.argv = ['node', 'script.js', '-p', 'test']; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).toContain(ShellTool.Name); + expect(excludedTools).toContain(EditTool.Name); + expect(excludedTools).toContain(WriteFileTool.Name); + }); + + it('should exclude all interactive tools in non-interactive mode with explicit default approval mode', async () => { + process.argv = [ + 'node', + 'script.js', + '--approval-mode', + 'default', + '-p', + 'test', + ]; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).toContain(ShellTool.Name); + expect(excludedTools).toContain(EditTool.Name); + expect(excludedTools).toContain(WriteFileTool.Name); + }); + + it('should exclude only shell tools in non-interactive mode with auto_edit approval mode', async () => { + process.argv = [ + 'node', + 'script.js', + '--approval-mode', + 'auto_edit', + '-p', + 'test', + ]; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).toContain(ShellTool.Name); + expect(excludedTools).not.toContain(EditTool.Name); + expect(excludedTools).not.toContain(WriteFileTool.Name); + }); + + it('should exclude no interactive tools in non-interactive mode with yolo approval mode', async () => { + process.argv = [ + 'node', + 'script.js', + '--approval-mode', + 'yolo', + '-p', + 'test', + ]; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).not.toContain(ShellTool.Name); + expect(excludedTools).not.toContain(EditTool.Name); + expect(excludedTools).not.toContain(WriteFileTool.Name); + }); + + it('should exclude no interactive tools in non-interactive mode with legacy yolo flag', async () => { + process.argv = ['node', 'script.js', '--yolo', '-p', 'test']; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).not.toContain(ShellTool.Name); + expect(excludedTools).not.toContain(EditTool.Name); + expect(excludedTools).not.toContain(WriteFileTool.Name); + }); + + it('should not exclude interactive tools in interactive mode regardless of approval mode', async () => { + process.stdin.isTTY = true; // Interactive mode + + const testCases = [ + { args: ['node', 'script.js'] }, // default + { args: ['node', 'script.js', '--approval-mode', 'default'] }, + { args: ['node', 'script.js', '--approval-mode', 'auto_edit'] }, + { args: ['node', 'script.js', '--approval-mode', 'yolo'] }, + { args: ['node', 'script.js', '--yolo'] }, + ]; + + for (const testCase of testCases) { + process.argv = testCase.args; + const argv = await parseArguments(); + const settings: Settings = {}; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).not.toContain(ShellTool.Name); + expect(excludedTools).not.toContain(EditTool.Name); + expect(excludedTools).not.toContain(WriteFileTool.Name); + } + }); + + it('should merge approval mode exclusions with settings exclusions in auto_edit mode', async () => { + process.argv = [ + 'node', + 'script.js', + '--approval-mode', + 'auto_edit', + '-p', + 'test', + ]; + const argv = await parseArguments(); + const settings: Settings = { excludeTools: ['custom_tool'] }; + const extensions: Extension[] = []; + + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); + + const excludedTools = config.getExcludeTools(); + expect(excludedTools).toContain('custom_tool'); // From settings + expect(excludedTools).toContain(ShellTool.Name); // From approval mode + expect(excludedTools).not.toContain(EditTool.Name); // Should be allowed in auto_edit + expect(excludedTools).not.toContain(WriteFileTool.Name); // Should be allowed in auto_edit + }); + + it('should throw an error for invalid approval mode values in loadCliConfig', async () => { + // Create a mock argv with an invalid approval mode that bypasses argument parsing validation + const invalidArgv: Partial<CliArgs> & { approvalMode: string } = { + approvalMode: 'invalid_mode', + promptInteractive: '', + prompt: '', + yolo: false, + }; + + const settings: Settings = {}; + const extensions: Extension[] = []; + + await expect( + loadCliConfig(settings, extensions, 'test-session', invalidArgv), + ).rejects.toThrow( + 'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, default', + ); + }); +}); + describe('loadCliConfig with allowed-mcp-server-names', () => { const originalArgv = process.argv; const originalEnv = { ...process.env }; @@ -1327,3 +1619,80 @@ describe('loadCliConfig interactive', () => { expect(config.isInteractive()).toBe(false); }); }); + +describe('loadCliConfig approval mode', () => { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + process.env.GEMINI_API_KEY = 'test-api-key'; + }); + + afterEach(() => { + process.argv = originalArgv; + process.env = originalEnv; + vi.restoreAllMocks(); + }); + + it('should default to DEFAULT approval mode when no flags are set', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should set YOLO approval mode when --yolo flag is used', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + }); + + it('should set YOLO approval mode when -y flag is used', async () => { + process.argv = ['node', 'script.js', '-y']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + }); + + it('should set DEFAULT approval mode when --approval-mode=default', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'default']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'auto_edit']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT); + }); + + it('should set YOLO approval mode when --approval-mode=yolo', async () => { + process.argv = ['node', 'script.js', '--approval-mode', 'yolo']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + }); + + it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => { + // Note: This test documents the intended behavior, but in practice the validation + // prevents both flags from being used together + process.argv = ['node', 'script.js', '--approval-mode', 'default']; + const argv = await parseArguments(); + // Manually set yolo to true to simulate what would happen if validation didn't prevent it + argv.yolo = true; + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT); + }); + + it('should fall back to --yolo behavior when --approval-mode is not set', async () => { + process.argv = ['node', 'script.js', '--yolo']; + const argv = await parseArguments(); + const config = await loadCliConfig({}, [], 'test-session', argv); + expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO); + }); +}); |
