mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
280 lines
8.6 KiB
Python
280 lines
8.6 KiB
Python
"""Test shell features including auto-backgrounding and shell detection."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from hanzo_tools.shell.base_process import ProcessManager
|
|
from hanzo_tools.shell.ps_tool import PsTool
|
|
from hanzo_tools.shell.shell_detect import (
|
|
SUPPORTED_SHELLS,
|
|
clear_shell_cache,
|
|
detect_shells,
|
|
get_active_shell,
|
|
get_shell_tool_class,
|
|
)
|
|
from hanzo_tools.shell.shell_tools import (
|
|
BashTool,
|
|
DashTool,
|
|
FishTool,
|
|
ShellTool,
|
|
ZshTool,
|
|
)
|
|
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ctx():
|
|
"""Create a mock MCP context."""
|
|
ctx = MagicMock()
|
|
return ctx
|
|
|
|
|
|
@pytest.fixture
|
|
def permission_manager():
|
|
"""Create a permission manager."""
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path("/tmp")
|
|
pm.add_allowed_path(str(Path.home()))
|
|
return pm
|
|
|
|
|
|
@pytest.fixture
|
|
def bash_tool(permission_manager):
|
|
"""Create a bash tool instance."""
|
|
tool = BashTool()
|
|
tool.permission_manager = permission_manager
|
|
return tool
|
|
|
|
|
|
@pytest.fixture
|
|
def zsh_tool(permission_manager):
|
|
"""Create a zsh tool instance."""
|
|
tool = ZshTool()
|
|
tool.permission_manager = permission_manager
|
|
return tool
|
|
|
|
|
|
@pytest.fixture
|
|
def shell_tool(permission_manager):
|
|
"""Create a shell tool instance (smart shell detection)."""
|
|
tool = ShellTool()
|
|
tool.permission_manager = permission_manager
|
|
return tool
|
|
|
|
|
|
@pytest.fixture
|
|
def ps_tool_instance():
|
|
"""Create a ps tool instance."""
|
|
return PsTool()
|
|
|
|
|
|
class TestShellDetection:
|
|
"""Test shell detection functionality."""
|
|
|
|
def test_detect_shells_returns_shell_info(self):
|
|
"""Test detect_shells returns proper ShellInfo."""
|
|
info = detect_shells()
|
|
assert hasattr(info, "login_shell")
|
|
assert hasattr(info, "invoking_shell")
|
|
assert hasattr(info, "env_shell")
|
|
assert hasattr(info, "evidence")
|
|
|
|
def test_get_active_shell_returns_tuple(self):
|
|
"""Test get_active_shell returns (name, path) tuple."""
|
|
name, path = get_active_shell()
|
|
assert isinstance(name, str)
|
|
assert isinstance(path, str)
|
|
assert name in SUPPORTED_SHELLS or name == "sh"
|
|
|
|
def test_supported_shells_contains_expected(self):
|
|
"""Test SUPPORTED_SHELLS contains expected shells."""
|
|
assert "zsh" in SUPPORTED_SHELLS
|
|
assert "bash" in SUPPORTED_SHELLS
|
|
assert "fish" in SUPPORTED_SHELLS
|
|
assert "dash" in SUPPORTED_SHELLS
|
|
|
|
def test_get_shell_tool_class_returns_correct_class(self):
|
|
"""Test get_shell_tool_class returns the right tool class."""
|
|
assert get_shell_tool_class("zsh") == ZshTool
|
|
assert get_shell_tool_class("bash") == BashTool
|
|
assert get_shell_tool_class("fish") == FishTool
|
|
assert get_shell_tool_class("dash") == DashTool
|
|
assert get_shell_tool_class("unknown") is None
|
|
|
|
def test_shell_env_override(self):
|
|
"""Test HANZO_MCP_SHELL environment variable override."""
|
|
clear_shell_cache()
|
|
with patch.dict(os.environ, {"HANZO_MCP_SHELL": "bash"}, clear=False):
|
|
name, path = get_active_shell()
|
|
assert name == "bash"
|
|
clear_shell_cache()
|
|
|
|
def test_force_shell_path_override(self):
|
|
"""Test HANZO_MCP_FORCE_SHELL environment variable override."""
|
|
clear_shell_cache()
|
|
with patch.dict(
|
|
os.environ, {"HANZO_MCP_FORCE_SHELL": "/bin/bash"}, clear=False
|
|
):
|
|
name, path = get_active_shell()
|
|
assert name == "bash"
|
|
assert path == "/bin/bash"
|
|
clear_shell_cache()
|
|
|
|
def test_bash_tool_name(self, bash_tool):
|
|
"""Test BashTool has correct name."""
|
|
assert bash_tool.name == "bash"
|
|
|
|
def test_zsh_tool_name(self, zsh_tool):
|
|
"""Test ZshTool has correct name."""
|
|
assert zsh_tool.name == "zsh"
|
|
|
|
def test_shell_tool_name(self, shell_tool):
|
|
"""Test ShellTool has correct name."""
|
|
assert shell_tool.name == "shell"
|
|
|
|
|
|
class TestAutoBackgrounding:
|
|
"""Test auto-backgrounding functionality."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quick_command_completes(self, bash_tool, mock_ctx):
|
|
"""Test that quick commands complete normally."""
|
|
result = await bash_tool.call(mock_ctx, command="echo 'Hello World'")
|
|
assert "Hello World" in result
|
|
assert "backgrounded" not in result.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_output(self, zsh_tool, mock_ctx):
|
|
"""Test command output is captured."""
|
|
result = await zsh_tool.call(mock_ctx, command="echo 'test output'")
|
|
assert "test output" in result
|
|
|
|
|
|
class TestProcessManagement:
|
|
"""Test process management functionality."""
|
|
|
|
@pytest.fixture
|
|
def process_manager(self):
|
|
"""Get process manager instance."""
|
|
return ProcessManager()
|
|
|
|
def test_process_tracking(self, process_manager):
|
|
"""Test process tracking functionality."""
|
|
# Create mock process (asyncio.subprocess.Process uses returncode, not poll())
|
|
mock_process = MagicMock()
|
|
mock_process.pid = 12345
|
|
mock_process.returncode = (
|
|
None # Still running (asyncio.subprocess.Process style)
|
|
)
|
|
|
|
# Add process
|
|
process_manager.add_process("test_123", mock_process, "/tmp/test.log")
|
|
|
|
# Check it's tracked
|
|
assert process_manager.get_process("test_123") == mock_process
|
|
|
|
# List processes
|
|
processes = process_manager.list_processes()
|
|
assert "test_123" in processes
|
|
assert processes["test_123"]["pid"] == 12345
|
|
assert processes["test_123"]["running"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ps_list_command(self, ps_tool_instance, mock_ctx):
|
|
"""Test ps list command."""
|
|
result = await ps_tool_instance.call(mock_ctx)
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestToolsListWithShellDetection:
|
|
"""Test that TOOLS list respects shell detection."""
|
|
|
|
def test_default_tools_list_has_one_shell(self):
|
|
"""Test default TOOLS list only includes detected shell."""
|
|
clear_shell_cache()
|
|
# Re-import to get fresh TOOLS list
|
|
from hanzo_tools.shell import TOOLS, get_cached_active_shell
|
|
|
|
shell_name, _ = get_cached_active_shell()
|
|
|
|
# Count shell tools
|
|
shell_tools = [
|
|
t for t in TOOLS if hasattr(t, "name") and t.name in SUPPORTED_SHELLS
|
|
]
|
|
|
|
# Should have exactly one shell tool (the detected one)
|
|
assert len(shell_tools) == 1
|
|
assert shell_tools[0].name == shell_name
|
|
|
|
def test_all_shells_mode(self):
|
|
"""Test HANZO_MCP_ALL_SHELLS=1 exposes all shells."""
|
|
clear_shell_cache()
|
|
with patch.dict(os.environ, {"HANZO_MCP_ALL_SHELLS": "1"}, clear=False):
|
|
# Re-import to get fresh TOOLS list
|
|
import importlib
|
|
|
|
import hanzo_tools.shell
|
|
|
|
importlib.reload(hanzo_tools.shell)
|
|
from hanzo_tools.shell import TOOLS
|
|
|
|
# Count shell tools
|
|
shell_tools = [
|
|
t for t in TOOLS if hasattr(t, "name") and t.name in SUPPORTED_SHELLS
|
|
]
|
|
|
|
# Should have all 4 shell tools
|
|
assert len(shell_tools) == 4
|
|
shell_names = {t.name for t in shell_tools}
|
|
assert shell_names == SUPPORTED_SHELLS
|
|
|
|
# Cleanup
|
|
clear_shell_cache()
|
|
if "HANZO_MCP_ALL_SHELLS" in os.environ:
|
|
del os.environ["HANZO_MCP_ALL_SHELLS"]
|
|
import importlib
|
|
|
|
import hanzo_tools.shell
|
|
|
|
importlib.reload(hanzo_tools.shell)
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for shell features."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bash_command_execution(self, bash_tool, mock_ctx):
|
|
"""Test bash command execution."""
|
|
result = await bash_tool.call(mock_ctx, command="echo 'integration test'")
|
|
assert "integration test" in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_zsh_command_execution(self, zsh_tool, mock_ctx):
|
|
"""Test zsh command execution."""
|
|
result = await zsh_tool.call(mock_ctx, command="echo 'zsh test'")
|
|
assert "zsh test" in result
|
|
|
|
def test_tool_names_are_correct(self, bash_tool, zsh_tool, shell_tool):
|
|
"""Test all shell tools have correct names."""
|
|
assert bash_tool.name == "bash"
|
|
assert zsh_tool.name == "zsh"
|
|
assert shell_tool.name == "shell"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_end_to_end_workflow():
|
|
"""Test complete workflow with real commands."""
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path("/tmp")
|
|
|
|
bash = BashTool()
|
|
bash.permission_manager = pm
|
|
|
|
ps = PsTool()
|
|
|
|
assert bash.name == "bash"
|
|
assert ps.name == "ps"
|