mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
436 lines
15 KiB
Python
436 lines
15 KiB
Python
"""Tests for new tools added to Hanzo AI."""
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
|
|
from hanzo_mcp.tools.mcp.mcp_remove import McpRemoveTool
|
|
from hanzo_mcp.tools.mcp.mcp_stats import McpStatsTool
|
|
from hanzo_tools.database.database_manager import DatabaseManager
|
|
from hanzo_tools.database.graph_add import GraphAddTool
|
|
from hanzo_tools.database.graph_query import GraphQueryTool
|
|
from hanzo_tools.database.graph_remove import GraphRemoveTool
|
|
from hanzo_tools.database.graph_search import GraphSearchTool
|
|
from hanzo_tools.database.graph_stats import GraphStatsTool
|
|
from hanzo_tools.filesystem.find_files import FindFilesTool
|
|
from hanzo_tools.shell.npx import NpxTool
|
|
from hanzo_tools.shell.uvx import UvxTool
|
|
from hanzo_tools.shell.uvx_background import UvxBackgroundTool
|
|
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
from hanzo_mcp.tools.common.stats import StatsTool
|
|
from hanzo_mcp.tools.common.tool_disable import ToolDisableTool
|
|
from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ctx():
|
|
"""Create a mock MCP context."""
|
|
ctx = Mock()
|
|
ctx.client = Mock()
|
|
ctx.client.notify_progress = Mock()
|
|
return ctx
|
|
|
|
|
|
@pytest.fixture
|
|
def permission_manager():
|
|
"""Create a permission manager."""
|
|
pm = PermissionManager()
|
|
pm.add_allowed_path(os.getcwd())
|
|
return pm
|
|
|
|
|
|
@pytest.fixture
|
|
def db_manager(permission_manager):
|
|
"""Create a database manager."""
|
|
return DatabaseManager(permission_manager)
|
|
|
|
|
|
class TestGraphTools:
|
|
"""Test graph database tools."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_add_node(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test adding a node to the graph."""
|
|
tool = GraphAddTool(permission_manager, db_manager)
|
|
|
|
result = await tool_helper.call_tool(
|
|
tool,
|
|
mock_ctx,
|
|
node_id="test_node",
|
|
node_type="file",
|
|
properties={"size": 1024},
|
|
)
|
|
|
|
tool_helper.assert_in_result("Successfully added node 'test_node'", result)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_add_edge(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test adding an edge to the graph."""
|
|
tool = GraphAddTool(permission_manager, db_manager)
|
|
|
|
# Add nodes first
|
|
await tool.call(mock_ctx, node_id="node1", node_type="file")
|
|
await tool.call(mock_ctx, node_id="node2", node_type="file")
|
|
|
|
# Add edge
|
|
result = await tool.call(
|
|
mock_ctx, source="node1", target="node2", relationship="imports"
|
|
)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Successfully added edge", result)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_remove_node(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test removing a node from the graph."""
|
|
add_tool = GraphAddTool(permission_manager, db_manager)
|
|
remove_tool = GraphRemoveTool(permission_manager, db_manager)
|
|
|
|
# Add a node
|
|
await add_tool.call(mock_ctx, node_id="test_node", node_type="file")
|
|
|
|
# Remove it
|
|
result = await remove_tool.call(mock_ctx, node_id="test_node")
|
|
|
|
tool_helper.assert_in_result("Successfully removed node 'test_node'", result)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_query_neighbors(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test querying neighbors in the graph."""
|
|
add_tool = GraphAddTool(permission_manager, db_manager)
|
|
query_tool = GraphQueryTool(permission_manager, db_manager)
|
|
|
|
# Create a simple graph
|
|
await add_tool.call(mock_ctx, node_id="A", node_type="file")
|
|
await add_tool.call(mock_ctx, node_id="B", node_type="file")
|
|
await add_tool.call(mock_ctx, node_id="C", node_type="file")
|
|
await add_tool.call(mock_ctx, source="A", target="B", relationship="imports")
|
|
await add_tool.call(mock_ctx, source="A", target="C", relationship="imports")
|
|
|
|
# Query neighbors
|
|
result = await query_tool.call(mock_ctx, query="neighbors", node_id="A")
|
|
|
|
tool_helper.assert_in_result("Neighbors of 'A'", result)
|
|
tool_helper.assert_in_result("B", result)
|
|
tool_helper.assert_in_result("C", result)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_search(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test searching in the graph."""
|
|
add_tool = GraphAddTool(permission_manager, db_manager)
|
|
search_tool = GraphSearchTool(permission_manager, db_manager)
|
|
|
|
# Add nodes with properties
|
|
await add_tool.call(
|
|
mock_ctx,
|
|
node_id="main.py",
|
|
node_type="file",
|
|
properties={"description": "Main entry point"},
|
|
)
|
|
|
|
# Search
|
|
result = await search_tool.call(mock_ctx, pattern="main%")
|
|
|
|
tool_helper.assert_in_result("Found", result)
|
|
tool_helper.assert_in_result("main.py", result)
|
|
|
|
@pytest.mark.skip(
|
|
reason="Graph stats test needs database isolation - stale data causes false failures"
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_graph_stats(
|
|
self, tool_helper, mock_ctx, permission_manager, db_manager
|
|
):
|
|
"""Test graph statistics."""
|
|
add_tool = GraphAddTool(permission_manager, db_manager)
|
|
stats_tool = GraphStatsTool(permission_manager, db_manager)
|
|
|
|
# Add some data
|
|
await add_tool.call(mock_ctx, node_id="A", node_type="file")
|
|
await add_tool.call(mock_ctx, node_id="B", node_type="class")
|
|
await add_tool.call(mock_ctx, source="A", target="B", relationship="contains")
|
|
|
|
# Get stats
|
|
result = await stats_tool.call(mock_ctx)
|
|
|
|
tool_helper.assert_in_result("Total Nodes: 2", result)
|
|
tool_helper.assert_in_result("Total Edges: 1", result)
|
|
|
|
|
|
class TestFindFilesTool:
|
|
"""Test find files tool."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_files_basic(self, tool_helper, mock_ctx, permission_manager):
|
|
"""Test basic file finding."""
|
|
tool = FindFilesTool(permission_manager)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create test files
|
|
Path(tmpdir, "test1.py").touch()
|
|
Path(tmpdir, "test2.py").touch()
|
|
Path(tmpdir, "data.txt").touch()
|
|
|
|
# Allow access
|
|
permission_manager.add_allowed_path(tmpdir)
|
|
|
|
# Find Python files
|
|
result = await tool.call(mock_ctx, pattern="*.py", path=tmpdir)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Found 2 file(s)", result)
|
|
tool_helper.assert_in_result("test1.py", result)
|
|
tool_helper.assert_in_result("test2.py", result)
|
|
assert "data.txt" not in result
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_files_recursive(
|
|
self, tool_helper, mock_ctx, permission_manager
|
|
):
|
|
"""Test recursive file finding."""
|
|
tool = FindFilesTool(permission_manager)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create nested structure
|
|
subdir = Path(tmpdir, "subdir")
|
|
subdir.mkdir()
|
|
Path(tmpdir, "top.txt").touch()
|
|
Path(subdir, "nested.txt").touch()
|
|
|
|
permission_manager.add_allowed_path(tmpdir)
|
|
|
|
# Find all txt files
|
|
result = await tool.call(
|
|
mock_ctx, pattern="*.txt", path=tmpdir, recursive=True
|
|
)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Found 2 file(s)", result)
|
|
tool_helper.assert_in_result("top.txt", result)
|
|
tool_helper.assert_in_result("subdir/nested.txt", result)
|
|
|
|
|
|
class TestPackageRunnerTools:
|
|
"""Test uvx and npx tools."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_uvx_basic(self, tool_helper, mock_ctx, permission_manager):
|
|
"""Test basic uvx execution."""
|
|
tool = UvxTool(permission_manager)
|
|
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="Package output", stderr="", returncode=0
|
|
)
|
|
|
|
with patch("shutil.which", return_value="/usr/bin/uvx"):
|
|
result = await tool.call(mock_ctx, package="ruff", args="check .")
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Package output", result)
|
|
mock_run.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_npx_basic(self, tool_helper, mock_ctx, permission_manager):
|
|
"""Test basic npx execution."""
|
|
tool = NpxTool(permission_manager)
|
|
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="Package output", stderr="", returncode=0
|
|
)
|
|
|
|
with patch("shutil.which", return_value="/usr/bin/npx"):
|
|
result = await tool.call(mock_ctx, package="eslint", args="--version")
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Package output", result)
|
|
mock_run.assert_called_once()
|
|
|
|
@pytest.mark.skip(reason="UvxBackgroundTool is deprecated - use UvxTool instead")
|
|
@pytest.mark.asyncio
|
|
async def test_uvx_background(self, tool_helper, mock_ctx, permission_manager):
|
|
"""Test uvx background execution."""
|
|
tool = UvxBackgroundTool(permission_manager)
|
|
|
|
with patch("subprocess.Popen") as mock_popen:
|
|
mock_process = MagicMock()
|
|
mock_process.pid = 12345
|
|
mock_popen.return_value = mock_process
|
|
|
|
with patch("shutil.which", return_value="/usr/bin/uvx"):
|
|
result = await tool.call(
|
|
mock_ctx, package="streamlit", args="run app.py", name="test-app"
|
|
)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Started uvx background process", result)
|
|
tool_helper.assert_in_result("PID: 12345", result)
|
|
mock_popen.assert_called_once()
|
|
|
|
|
|
class TestMcpManagementTools:
|
|
"""Test MCP management tools."""
|
|
|
|
@pytest.mark.skip(
|
|
reason="McpAddTool test modifies user's MCP config - run manually"
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_add(self, tool_helper, mock_ctx):
|
|
"""Test adding an MCP server."""
|
|
tool = McpAddTool()
|
|
|
|
result = await tool.call(
|
|
mock_ctx, command="uvx mcp-server-git", name="git-server"
|
|
)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result(
|
|
"Successfully added MCP server 'git-server'", result
|
|
)
|
|
|
|
# Check it was saved
|
|
servers = McpAddTool.get_servers()
|
|
assert "git-server" in servers
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_remove(self, tool_helper, mock_ctx):
|
|
"""Test removing an MCP server."""
|
|
# First add a server
|
|
add_tool = McpAddTool()
|
|
await add_tool.call(mock_ctx, command="uvx test-server", name="test-server")
|
|
|
|
# Then remove it
|
|
remove_tool = McpRemoveTool()
|
|
result = await remove_tool.call(mock_ctx, name="test-server")
|
|
|
|
tool_helper.assert_in_result(
|
|
"Successfully removed MCP server 'test-server'", result
|
|
)
|
|
|
|
# Check it was removed
|
|
servers = McpAddTool.get_servers()
|
|
assert "test-server" not in servers
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_stats(self, tool_helper, mock_ctx):
|
|
"""Test MCP stats."""
|
|
tool = McpStatsTool()
|
|
|
|
# Add some servers first
|
|
McpAddTool._mcp_servers = {
|
|
"server1": {
|
|
"type": "python",
|
|
"status": "running",
|
|
"command": ["uvx", "server1"],
|
|
"tools": ["tool1", "tool2"],
|
|
},
|
|
"server2": {
|
|
"type": "node",
|
|
"status": "stopped",
|
|
"command": ["npx", "server2"],
|
|
"tools": [],
|
|
},
|
|
}
|
|
|
|
result = await tool.call(mock_ctx)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Total Servers: 2", result)
|
|
tool_helper.assert_in_result("server1", result)
|
|
tool_helper.assert_in_result("server2", result)
|
|
|
|
|
|
class TestSystemTools:
|
|
"""Test system management tools."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stats_tool(self, tool_helper, mock_ctx, db_manager):
|
|
"""Test comprehensive stats tool."""
|
|
tool = StatsTool(db_manager)
|
|
|
|
with patch("psutil.cpu_percent", return_value=50.0):
|
|
with patch("psutil.cpu_count", return_value=4):
|
|
with patch("psutil.virtual_memory") as mock_mem:
|
|
mock_mem.return_value = MagicMock(
|
|
total=8 * 1024**3, used=4 * 1024**3, percent=50.0
|
|
)
|
|
|
|
with patch("psutil.disk_usage") as mock_disk:
|
|
mock_disk.return_value = MagicMock(
|
|
total=100 * 1024**3,
|
|
used=50 * 1024**3,
|
|
free=50 * 1024**3,
|
|
percent=50.0,
|
|
)
|
|
|
|
result = await tool.call(mock_ctx)
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("CPU Usage: 50.0%", result)
|
|
tool_helper.assert_in_result("Memory: 4.0/8.0 GB", result)
|
|
tool_helper.assert_in_result("Disk: 50.0/100.0 GB", result)
|
|
tool_helper.assert_in_result("System resources are healthy", result)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_enable_disable(self, tool_helper, mock_ctx):
|
|
"""Test tool enable/disable functionality."""
|
|
enable_tool = ToolEnableTool()
|
|
disable_tool = ToolDisableTool()
|
|
|
|
# Disable a tool
|
|
result = await disable_tool.call(mock_ctx, tool="vector_search")
|
|
tool_helper.assert_in_result(
|
|
"Successfully disabled tool 'vector_search'", result
|
|
)
|
|
|
|
# Check it's disabled
|
|
assert not ToolEnableTool.is_tool_enabled("vector_search")
|
|
|
|
# Re-enable it
|
|
result = await enable_tool.call(mock_ctx, tool="vector_search")
|
|
tool_helper.assert_in_result(
|
|
"Successfully enabled tool 'vector_search'", result
|
|
)
|
|
|
|
# Check it's enabled
|
|
assert ToolEnableTool.is_tool_enabled("vector_search")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_disable_critical(self, tool_helper, mock_ctx):
|
|
"""Test that critical tools cannot be disabled."""
|
|
tool = ToolDisableTool()
|
|
|
|
result = await tool.call(mock_ctx, tool="tool_enable")
|
|
if isinstance(result, dict) and "output" in result:
|
|
result = result["output"]
|
|
|
|
tool_helper.assert_in_result("Error: Cannot disable critical tool", result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|