mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
- 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
255 lines
8.1 KiB
Python
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"])
|