"""Test suite for shell tools including zsh and smart shell selection.""" import os import platform import shutil import sys from pathlib import Path from unittest.mock import AsyncMock, patch import pytest # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from hanzo_tools.shell.shell_tools import BashTool, ShellTool, ZshTool class MockContext: """Mock MCP context for testing.""" pass class TestBashTool: """Test bash tool functionality.""" def test_bash_tool_properties(self): """Test bash tool basic properties.""" tool = BashTool() assert tool.name == "bash" assert tool.get_tool_name() == "bash" assert "bash" in tool.description.lower() # On Unix-like systems, should always return bash if platform.system() != "Windows": assert tool.get_interpreter() == "bash" @pytest.mark.asyncio async def test_bash_execution(self): """Test bash command execution.""" tool = BashTool() ctx = MockContext() # Mock execute_sync to avoid actual command execution with patch.object(tool, "execute_sync", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "test output" result = await tool.run(ctx, "echo test") assert result == "test output" mock_exec.assert_called_once() def test_bash_flags(self): """Test bash interpreter flags.""" tool = BashTool() if platform.system() == "Windows": # On Windows, might be /c or -c depending on shell flags = tool.get_script_flags() assert flags in [["/c"], ["-c"]] else: assert tool.get_script_flags() == ["-c"] class TestZshTool: """Test zsh tool functionality.""" def test_zsh_tool_properties(self): """Test zsh tool basic properties.""" tool = ZshTool() assert tool.name == "zsh" assert tool.get_tool_name() == "zsh" assert "zsh" in tool.description.lower() assert "enhanced features" in tool.description.lower() def test_zsh_interpreter_detection(self): """Test zsh interpreter detection.""" tool = ZshTool() if platform.system() != "Windows": # Check if zsh is available if shutil.which("zsh"): interpreter = tool.get_interpreter() assert "zsh" in interpreter or interpreter == "bash" else: # Should fall back to bash if zsh not found assert tool.get_interpreter() == "bash" @pytest.mark.asyncio async def test_zsh_execution(self): """Test zsh command execution.""" tool = ZshTool() ctx = MockContext() # Mock execute_sync with patch.object(tool, "execute_sync", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "zsh output" result = await tool.run(ctx, "echo $ZSH_VERSION") assert result == "zsh output" mock_exec.assert_called_once() @pytest.mark.asyncio async def test_zsh_not_installed(self): """Test error when zsh is not installed.""" tool = ZshTool() ctx = MockContext() # Mock shutil.which to return None (zsh not found) with patch("shutil.which", return_value=None): if platform.system() != "Windows": result = await tool.run(ctx, "echo test") assert "not installed" in result.lower() class TestShellTool: """Test smart shell tool functionality.""" def test_shell_tool_properties(self): """Test shell tool basic properties.""" tool = ShellTool() assert tool.name == "shell" assert tool.get_tool_name() == "shell" assert "best available shell" in tool.description.lower() def test_shell_detection(self): """Test smart shell detection.""" tool = ShellTool() # Should have detected a shell assert tool._best_shell is not None assert tool._best_shell in ["zsh", "bash"] or Path(tool._best_shell).exists() def test_shell_preference_order(self): """Test shell preference order.""" # Test with fresh instance each time # Mock different scenarios with patch("shutil.which") as mock_which: with patch.object(Path, "exists") as mock_exists: # Scenario 1: zsh available with .zshrc mock_which.return_value = "/usr/bin/zsh" mock_exists.return_value = True tool = ShellTool() assert "zsh" in tool._best_shell # Scenario 2: zsh not available, use bash mock_which.return_value = None mock_exists.return_value = False tool = ShellTool() assert tool._best_shell == "bash" @pytest.mark.asyncio async def test_shell_execution_with_info(self): """Test shell execution with shell info.""" tool = ShellTool() ctx = MockContext() # Mock execute_sync with patch.object(tool, "execute_sync", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = "" # Empty output result = await tool.run(ctx, "true") # Command with no output # Should mention which shell was used assert "completed successfully" in result.lower() if "zsh" in tool._best_shell: assert "zsh" in result.lower() else: assert "bash" in result.lower() or "shell" in result.lower() def test_shell_description_dynamic(self): """Test that shell description shows current shell.""" tool = ShellTool() description = tool.description shell_name = os.path.basename(tool._best_shell) # Description should mention the current shell assert f"currently: {shell_name}" in description.lower() class TestShellIntegration: """Integration tests for shell tools.""" @pytest.mark.asyncio @pytest.mark.integration async def test_real_shell_execution(self): """Test real shell execution (integration test).""" # This test actually runs commands - skip in CI if os.environ.get("CI"): pytest.skip("Skipping real shell execution in CI") ctx = MockContext() # Test each tool bash_tool = BashTool() result = await bash_tool.run(ctx, "echo 'bash works'") assert "bash works" in result # Test zsh if available if shutil.which("zsh"): zsh_tool = ZshTool() result = await zsh_tool.run(ctx, "echo 'zsh works'") assert "zsh works" in result # Test smart shell shell_tool = ShellTool() result = await shell_tool.run(ctx, "echo 'shell works'") assert "shell works" in result or "completed successfully" in result @pytest.mark.asyncio async def test_shell_tools_registration(self): """Test that shell tools can be registered.""" from hanzo_tools.shell import get_shell_tools from hanzo_mcp.tools.common.permissions import PermissionManager pm = PermissionManager() tools = get_shell_tools(pm) # Should have our core tools tool_names = [tool.name for tool in tools] assert "cmd" in tool_names # Primary command execution assert "zsh" in tool_names assert "bash" in tool_names # cmd should be first (primary) assert tools[0].name == "cmd" def test_shell_tool_ordering(self): """Test that shell tools are returned in correct order.""" from hanzo_tools.shell import get_shell_tools from hanzo_mcp.tools.common.permissions import PermissionManager pm = PermissionManager() tools = get_shell_tools(pm) # Find shell-related tools shell_tools = [t for t in tools if t.name in ["cmd", "zsh", "bash"]] # Order should be: cmd (primary), zsh, bash assert shell_tools[0].name == "cmd" assert shell_tools[1].name == "zsh" assert shell_tools[2].name == "bash" if __name__ == "__main__": pytest.main([__file__, "-v"])