"""Test suite for CLI tools in batch operations.""" import os import time import shutil import asyncio from typing import Any, Dict from unittest.mock import Mock, AsyncMock, MagicMock, patch import pytest from hanzo_tools.agent.cli_tools import ( GrokCLITool, AiderCLITool, ClineCLITool, CodexCLITool, ClaudeCLITool, GeminiCLITool, HanzoDevCLITool, OpenHandsCLITool, ClaudeCodeCLITool, 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.""" import time # 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