"""Test suite for CLI tools in batch operations.""" import asyncio import time from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from hanzo_tools.agent.cli_tools import ( AiderCLITool, ClaudeCLITool, ClaudeCodeCLITool, ClineCLITool, CodexCLITool, GeminiCLITool, GrokCLITool, HanzoDevCLITool, OpenHandsCLITool, OpenHandsShortCLITool, ) from hanzo_mcp.tools.common.batch_tool import BatchTool class TestCLITools: """Test CLI tool implementations.""" @pytest.fixture def mock_context(self): """Create a mock MCP context.""" context = MagicMock() context.session = MagicMock() context.session.send_log_message = AsyncMock() context.session.send_progress = AsyncMock() return context @pytest.fixture def mock_permission_manager(self): """Create a mock permission manager.""" pm = MagicMock() pm.check_permission = Mock(return_value=True) return pm @pytest.mark.asyncio async def test_claude_cli_tool(self, mock_context, mock_permission_manager): """Test Claude CLI tool execution.""" tool = ClaudeCLITool(mock_permission_manager) # Test name and description assert tool.name == "claude" assert "Claude CLI" in tool.description # Mock subprocess execution with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Claude response" result = await tool.call( mock_context, prompt="Test prompt", model="claude-3-opus-20240229", timeout=300, ) assert result == "Claude response" mock_exec.assert_called_once() @pytest.mark.asyncio async def test_claude_code_alias(self, mock_context, mock_permission_manager): """Test Claude Code (cc) alias tool.""" tool = ClaudeCodeCLITool(mock_permission_manager) # Test alias name assert tool.name == "cc" assert "Claude Code" in tool.description @pytest.mark.asyncio async def test_codex_cli_tool(self, mock_context, mock_permission_manager): """Test Codex/GPT-4 CLI tool execution.""" tool = CodexCLITool(mock_permission_manager) assert tool.name == "codex" assert "OpenAI" in tool.description or "GPT" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "GPT-4 response" result = await tool.call( mock_context, prompt="Generate code", model="gpt-4-turbo" ) assert result == "GPT-4 response" @pytest.mark.asyncio async def test_gemini_cli_tool(self, mock_context, mock_permission_manager): """Test Gemini CLI tool execution.""" tool = GeminiCLITool(mock_permission_manager) assert tool.name == "gemini" assert "Gemini" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Gemini response" result = await tool.call( mock_context, prompt="Analyze image", model="gemini-1.5-pro" ) assert result == "Gemini response" @pytest.mark.asyncio async def test_grok_cli_tool(self, mock_context, mock_permission_manager): """Test Grok CLI tool execution.""" tool = GrokCLITool(mock_permission_manager) assert tool.name == "grok" assert "Grok" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Grok response" result = await tool.call( mock_context, prompt="Real-time analysis", model="grok-2" ) assert result == "Grok response" @pytest.mark.asyncio async def test_openhands_cli_tool(self, mock_context, mock_permission_manager): """Test OpenHands CLI tool execution.""" tool = OpenHandsCLITool(mock_permission_manager) assert tool.name == "openhands" assert "OpenHands" in tool.description or "OpenDevin" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "OpenHands execution complete" result = await tool.call( mock_context, prompt="Build feature", working_dir="/project" ) assert result == "OpenHands execution complete" @pytest.mark.asyncio async def test_openhands_alias(self, mock_context, mock_permission_manager): """Test OpenHands (oh) alias tool.""" tool = OpenHandsShortCLITool(mock_permission_manager) assert tool.name == "oh" assert "OpenHands" in tool.description @pytest.mark.asyncio async def test_hanzo_dev_cli_tool(self, mock_context, mock_permission_manager): """Test Hanzo Dev CLI tool execution.""" tool = HanzoDevCLITool(mock_permission_manager) assert tool.name == "hanzo_dev" assert "Hanzo Dev" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Hanzo Dev completed task" result = await tool.call( mock_context, prompt="Implement feature", model="claude-3-5-sonnet-20241022", ) assert result == "Hanzo Dev completed task" @pytest.mark.asyncio async def test_cline_cli_tool(self, mock_context, mock_permission_manager): """Test Cline CLI tool execution.""" tool = ClineCLITool(mock_permission_manager) assert tool.name == "cline" assert "Cline" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Cline autonomous coding complete" result = await tool.call( mock_context, prompt="Fix bugs", working_dir="/src" ) assert result == "Cline autonomous coding complete" @pytest.mark.asyncio async def test_aider_cli_tool(self, mock_context, mock_permission_manager): """Test Aider CLI tool execution.""" tool = AiderCLITool(mock_permission_manager) assert tool.name == "aider" assert "Aider" in tool.description with patch.object(tool, "execute_cli", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "Aider pair programming complete" result = await tool.call( mock_context, prompt="Refactor module", model="gpt-4-turbo" ) assert result == "Aider pair programming complete" @pytest.mark.asyncio async def test_cli_tool_auth_env(self, mock_permission_manager): """Test that CLI tools properly set authentication environment.""" # Test Claude with Anthropic key with patch.dict("os.environ", {"ANTHROPIC_API_KEY": "test-anthropic-key"}): tool = ClaudeCLITool(mock_permission_manager) env = tool.get_auth_env() assert env["ANTHROPIC_API_KEY"] == "test-anthropic-key" # Test Codex with OpenAI key with patch.dict("os.environ", {"OPENAI_API_KEY": "test-openai-key"}): tool = CodexCLITool(mock_permission_manager) env = tool.get_auth_env() assert env["OPENAI_API_KEY"] == "test-openai-key" # Test unified Hanzo auth with patch.dict("os.environ", {"HANZO_API_KEY": "test-hanzo-key"}): tool = OpenHandsCLITool(mock_permission_manager) env = tool.get_auth_env() assert env["HANZO_API_KEY"] == "test-hanzo-key" @pytest.mark.asyncio async def test_cli_tool_timeout(self, mock_context, mock_permission_manager): """Test CLI tool timeout handling.""" tool = ClaudeCLITool(mock_permission_manager) # Mock a timeout with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = MagicMock() mock_process.communicate = AsyncMock(side_effect=asyncio.TimeoutError) mock_subprocess.return_value = mock_process result = await tool.call(mock_context, prompt="Long task", timeout=1) assert "timed out" in result.lower() @pytest.mark.asyncio async def test_cli_tool_error_handling(self, mock_context, mock_permission_manager): """Test CLI tool error handling.""" tool = CodexCLITool(mock_permission_manager) # Mock a subprocess error with patch("asyncio.create_subprocess_exec") as mock_subprocess: mock_process = MagicMock() mock_process.communicate = AsyncMock( return_value=(b"", b"Error: API key not found") ) mock_process.returncode = 1 mock_subprocess.return_value = mock_process result = await tool.call(mock_context, prompt="Generate code") assert "Error" in result assert "API key" in result class TestBatchWithCLITools: """Test batch tool with CLI tools.""" @pytest.fixture def mock_permission_manager(self): """Create a mock permission manager.""" pm = MagicMock() pm.check_permission = Mock(return_value=True) return pm @pytest.fixture def mock_tools(self, mock_permission_manager): """Create mock CLI tools for batch testing.""" tools = { "claude": ClaudeCLITool(mock_permission_manager), "cc": ClaudeCodeCLITool(mock_permission_manager), "codex": CodexCLITool(mock_permission_manager), "gemini": GeminiCLITool(mock_permission_manager), "grok": GrokCLITool(mock_permission_manager), "openhands": OpenHandsCLITool(mock_permission_manager), "oh": OpenHandsShortCLITool(mock_permission_manager), "cline": ClineCLITool(mock_permission_manager), "aider": AiderCLITool(mock_permission_manager), } # Mock execute_cli for all tools for tool in tools.values(): tool.execute_cli = AsyncMock(return_value=f"{tool.name} response") return tools @pytest.fixture def batch_tool(self, mock_tools): """Create batch tool with CLI tools.""" return BatchTool(mock_tools) @pytest.fixture def mock_context(self): """Create a mock MCP context.""" context = MagicMock() context.session = MagicMock() context.session.send_log_message = AsyncMock() context.session.send_progress = AsyncMock() return context @pytest.mark.asyncio async def test_batch_with_multiple_cli_tools(self, batch_tool, mock_context): """Test batch execution with multiple CLI tools.""" invocations = [ {"tool_name": "claude", "input": {"prompt": "Analyze architecture"}}, {"tool_name": "codex", "input": {"prompt": "Generate implementation"}}, {"tool_name": "gemini", "input": {"prompt": "Review code"}}, ] result = await batch_tool.call( mock_context, description="Multi-AI analysis", invocations=invocations ) # Check that all tools were called assert "claude response" in result assert "codex response" in result assert "gemini response" in result @pytest.mark.asyncio async def test_batch_with_cli_aliases(self, batch_tool, mock_context): """Test batch execution with CLI tool aliases.""" invocations = [ { "tool_name": "cc", # Claude Code alias "input": {"prompt": "Quick analysis"}, }, { "tool_name": "oh", # OpenHands alias "input": {"prompt": "Build feature"}, }, ] result = await batch_tool.call( mock_context, description="Test aliases", invocations=invocations ) assert "cc response" in result assert "oh response" in result @pytest.mark.asyncio async def test_batch_cli_tools_parallel_execution(self, batch_tool, mock_context): """Test that CLI tools execute in parallel in batch.""" # Add delay to mock executions for tool in batch_tool.tools.values(): async def delayed_response(cmd, **kwargs): await asyncio.sleep(0.1) # 100ms delay return f"{tool.name} response" tool.execute_cli = delayed_response invocations = [ {"tool_name": "claude", "input": {"prompt": "Task 1"}}, {"tool_name": "codex", "input": {"prompt": "Task 2"}}, {"tool_name": "gemini", "input": {"prompt": "Task 3"}}, {"tool_name": "grok", "input": {"prompt": "Task 4"}}, ] start = time.time() result = await batch_tool.call( mock_context, description="Parallel test", invocations=invocations ) duration = time.time() - start # If executed in parallel, should take ~100ms, not 400ms assert duration < 0.3 # Allow some overhead assert all(tool in result for tool in ["claude", "codex", "gemini", "grok"]) # Integration tests for CLI tools have been moved to tests/e2e/test_cli_tools_integration.py # These tests require the actual CLI tools to be installed and API keys to be set