mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
245 lines
8.5 KiB
Python
245 lines
8.5 KiB
Python
"""Basic swarm tool tests with success and failure cases."""
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
# Add the parent directory to the path for imports
|
|
sys.path.insert(
|
|
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
)
|
|
|
|
from hanzo_tools.agent.swarm_tool import SwarmTool
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
|
|
class TestSwarmBasic:
|
|
"""Basic tests for swarm tool functionality."""
|
|
|
|
def test_swarm_defaults_to_claude_sonnet(self):
|
|
"""Test that swarm tool defaults to Claude Sonnet."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
|
|
assert swarm.model == "anthropic/claude-3-5-sonnet-20241022"
|
|
print("✓ Swarm tool correctly defaults to Claude 3.5 Sonnet")
|
|
|
|
def test_swarm_detects_api_keys(self):
|
|
"""Test that swarm tool detects API keys from environment."""
|
|
pm = PermissionManager()
|
|
|
|
# Test with ANTHROPIC_API_KEY
|
|
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key-123"}):
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
assert swarm.api_key == "test-key-123"
|
|
print("✓ Swarm detects ANTHROPIC_API_KEY")
|
|
|
|
# Test with CLAUDE_API_KEY
|
|
with patch.dict(os.environ, {"CLAUDE_API_KEY": "test-key-456"}, clear=True):
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
print(f"Expected: test-key-456, Got: {swarm.api_key}")
|
|
assert swarm.api_key == "test-key-456"
|
|
print("✓ Swarm detects CLAUDE_API_KEY")
|
|
|
|
# Test priority (ANTHROPIC_API_KEY takes precedence)
|
|
with patch.dict(
|
|
os.environ,
|
|
{"ANTHROPIC_API_KEY": "primary-key", "CLAUDE_API_KEY": "fallback-key"},
|
|
):
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
assert swarm.api_key == "primary-key"
|
|
print("✓ ANTHROPIC_API_KEY takes precedence over CLAUDE_API_KEY")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swarm_fails_without_agents(self):
|
|
"""Test that swarm fails gracefully without agents."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
ctx = Mock(spec=MCPContext)
|
|
ctx.meta = {"tool_manager": Mock()}
|
|
|
|
# Mock tool context
|
|
mock_tool_ctx = Mock()
|
|
mock_tool_ctx.set_tool_info = AsyncMock()
|
|
mock_tool_ctx.error = AsyncMock()
|
|
mock_tool_ctx.info = AsyncMock()
|
|
|
|
with patch(
|
|
"hanzo_mcp.tools.common.context.create_tool_context",
|
|
return_value=mock_tool_ctx,
|
|
):
|
|
# Call with no agents in config
|
|
result = await swarm.call(ctx, config={"agents": {}})
|
|
|
|
assert "Error:" in result
|
|
assert "at least one agent" in result.lower()
|
|
print("✓ Swarm correctly fails when no agents provided")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swarm_validates_agent_format(self):
|
|
"""Test that swarm validates agent format."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
ctx = Mock(spec=MCPContext)
|
|
ctx.meta = {"tool_manager": Mock()}
|
|
|
|
# Mock tool context
|
|
mock_tool_ctx = Mock()
|
|
mock_tool_ctx.set_tool_info = AsyncMock()
|
|
mock_tool_ctx.error = AsyncMock()
|
|
mock_tool_ctx.info = AsyncMock()
|
|
|
|
with patch(
|
|
"hanzo_mcp.tools.common.context.create_tool_context",
|
|
return_value=mock_tool_ctx,
|
|
):
|
|
# Valid agent format - v2 interface uses agents with query
|
|
result = await swarm.call(
|
|
ctx,
|
|
config={
|
|
"agents": {
|
|
"agent1": {"query": "Analyze the code", "role": "Code analyzer"}
|
|
}
|
|
},
|
|
query="Analyze this project",
|
|
)
|
|
|
|
# Should handle gracefully
|
|
assert isinstance(result, str)
|
|
print("✓ Swarm handles agent format")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swarm_respects_max_concurrent(self):
|
|
"""Test that swarm respects max_concurrent setting."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
ctx = Mock(spec=MCPContext)
|
|
ctx.meta = {"tool_manager": Mock()}
|
|
|
|
# Create dummy agents with v2 config
|
|
test_dir = tempfile.mkdtemp()
|
|
try:
|
|
# Create test files
|
|
for i in range(5):
|
|
with open(os.path.join(test_dir, f"file{i}.txt"), "w") as f:
|
|
f.write(f"Content {i}")
|
|
|
|
# Allow access
|
|
pm.add_allowed_path(test_dir)
|
|
|
|
# Create multiple agents
|
|
agents = {}
|
|
for i in range(5):
|
|
agents[f"agent{i}"] = {
|
|
"query": f"Analyze file{i}.txt",
|
|
"file_path": os.path.join(test_dir, f"file{i}.txt"),
|
|
"role": f"Analyzer {i}",
|
|
}
|
|
|
|
# Mock execute to track concurrency
|
|
concurrent_count = 0
|
|
max_observed = 0
|
|
|
|
async def mock_execute(*args, **kwargs):
|
|
nonlocal concurrent_count, max_observed
|
|
concurrent_count += 1
|
|
max_observed = max(max_observed, concurrent_count)
|
|
# Simulate work
|
|
import asyncio
|
|
|
|
await asyncio.sleep(0.01)
|
|
concurrent_count -= 1
|
|
return "Mock result"
|
|
|
|
# Test with max_concurrent=2
|
|
with patch.object(swarm, "_execute_agent", mock_execute):
|
|
await swarm.call(
|
|
ctx,
|
|
config={"agents": agents},
|
|
query="Analyze all files",
|
|
max_concurrent=2,
|
|
)
|
|
|
|
# Max observed should not exceed 2
|
|
assert max_observed <= 2
|
|
print(f"✓ Swarm respects max_concurrent (observed max: {max_observed})")
|
|
|
|
finally:
|
|
shutil.rmtree(test_dir)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swarm_handles_agent_failures(self):
|
|
"""Test that swarm handles individual agent failures gracefully."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
ctx = Mock(spec=MCPContext)
|
|
ctx.meta = {"tool_manager": Mock()}
|
|
|
|
# Create agents where one will fail
|
|
agents = {
|
|
"agent1": {"query": "Task 1", "role": "Worker 1"},
|
|
"agent2": {"query": "Task 2", "role": "Worker 2"},
|
|
"agent3": {"query": "Task 3", "role": "Worker 3"},
|
|
}
|
|
|
|
call_count = 0
|
|
|
|
async def mock_execute(agent_id, *args, **kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if agent_id == "agent2":
|
|
raise Exception("Agent 2 failed!")
|
|
return f"Result from {agent_id}"
|
|
|
|
# Mock the execution
|
|
with patch.object(swarm, "_execute_agent", mock_execute):
|
|
result = await swarm.call(
|
|
ctx, config={"agents": agents}, query="Execute all tasks"
|
|
)
|
|
|
|
# Should complete despite one failure
|
|
assert isinstance(result, str)
|
|
assert call_count == 3 # All agents should be attempted
|
|
print("✓ Swarm handles agent failures gracefully")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swarm_summary_format(self):
|
|
"""Test that swarm returns properly formatted summary."""
|
|
pm = PermissionManager()
|
|
swarm = SwarmTool(permission_manager=pm)
|
|
ctx = Mock(spec=MCPContext)
|
|
ctx.meta = {"tool_manager": Mock()}
|
|
|
|
# Create simple agents
|
|
agents = {
|
|
"analyzer": {
|
|
"query": "Analyze the code structure",
|
|
"role": "Code Analyzer",
|
|
},
|
|
"reviewer": {
|
|
"query": "Review the analysis",
|
|
"role": "Code Reviewer",
|
|
"receives_from": ["analyzer"],
|
|
},
|
|
}
|
|
|
|
async def mock_execute(agent_id, *args, **kwargs):
|
|
return f"Mock result from {agent_id}"
|
|
|
|
with patch.object(swarm, "_execute_agent", mock_execute):
|
|
result = await swarm.call(
|
|
ctx,
|
|
config={"agents": agents, "entry_point": "analyzer"},
|
|
query="Analyze and review code",
|
|
)
|
|
|
|
# Should return a string result
|
|
assert isinstance(result, str)
|
|
print("✓ Swarm returns formatted summary")
|