Files
python-sdk/pkg/hanzo-mcp/tests/test_shell_tools.py
T
zeekay 95d8461196 Fix CI Quality Gate: exclude vscode-extension from ruff, relax lint rules
- Update quality-gate.yml to only lint hanzo_mcp and tests directories
- Add ruff config to pyproject.toml:
  - Exclude vscode-extension and node_modules
  - Ignore lint rules that conflict with test patterns:
    B023 (loop variable binding), B904 (raise from),
    E741 (ambiguous vars), F401/F821/F841 (unused/undefined in tests)
  - Apply per-file ignores for tests/ and __init__.py
- Apply ruff fixes for import sorting and format
2026-01-17 21:49:47 -08:00

255 lines
8.1 KiB
Python

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