Files
python-sdk/pkg/hanzo-mcp/tests/test_cli_tools.py
T

377 lines
13 KiB
Python

"""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