ClaudeFlow ported, needs cleanup
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Test suite for CleverClaude."""
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Pytest configuration and shared fixtures for CleverClaude tests."""
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from cleverclaude.config.settings import Settings
|
||||
from cleverclaude.database.models import Base
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir() -> Generator[Path]:
|
||||
"""Create a temporary directory for test files."""
|
||||
with tempfile.TemporaryDirectory(prefix="cleverclaude_test_") as temp_path:
|
||||
yield Path(temp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_settings(temp_dir: Path) -> Settings:
|
||||
"""Create test settings with temporary directories."""
|
||||
config_dir = temp_dir / ".cleverclaude"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
return Settings(
|
||||
app=Settings.AppConfig(name="CleverClaude Test", version="2.0.0-test", environment="testing", debug=True),
|
||||
database=Settings.DatabaseConfig(url=f"sqlite+aiosqlite:///{config_dir}/test.db", echo=False),
|
||||
redis=Settings.RedisConfig(
|
||||
url="redis://localhost:6379/15" # Test database
|
||||
),
|
||||
agents=Settings.AgentsConfig(max_agents=10, default_timeout=30, health_check_interval=5),
|
||||
swarm=Settings.SwarmConfig(default_topology="mesh", max_swarm_size=5, coordination_timeout=30),
|
||||
api=Settings.APIConfig(
|
||||
host="127.0.0.1",
|
||||
port=8080, # Different port for testing
|
||||
docs_enabled=False,
|
||||
),
|
||||
monitoring=Settings.MonitoringConfig(metrics_enabled=False, log_level="DEBUG", log_format="json"),
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_engine(test_settings: Settings):
|
||||
"""Create async SQLAlchemy engine for testing."""
|
||||
engine = create_async_engine(
|
||||
test_settings.database.url,
|
||||
echo=test_settings.database.echo,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
|
||||
# Create all tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
yield engine
|
||||
|
||||
# Clean up
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def async_session(async_engine) -> AsyncGenerator[AsyncSession]:
|
||||
"""Create async database session for testing."""
|
||||
async with AsyncSession(async_engine, expire_on_commit=False) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Create a mock Redis client."""
|
||||
mock = MagicMock()
|
||||
mock.get = AsyncMock()
|
||||
mock.set = AsyncMock()
|
||||
mock.delete = AsyncMock()
|
||||
mock.exists = AsyncMock()
|
||||
mock.keys = AsyncMock()
|
||||
mock.expire = AsyncMock()
|
||||
mock.ping = AsyncMock(return_value=True)
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent():
|
||||
"""Create a mock agent for testing."""
|
||||
mock = AsyncMock()
|
||||
mock.agent_id = "test_agent_123"
|
||||
mock.name = "Test Agent"
|
||||
mock.agent_type = "researcher"
|
||||
mock.status = "active"
|
||||
mock.capabilities = {"research", "analysis"}
|
||||
mock.created_at = "2024-01-01T12:00:00Z"
|
||||
mock.health_status = {"cpu_usage": 25.5, "memory_usage": 150.2, "task_count": 3, "status": "healthy"}
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_swarm():
|
||||
"""Create a mock swarm for testing."""
|
||||
mock = AsyncMock()
|
||||
mock.swarm_id = "test_swarm_456"
|
||||
mock.name = "Test Swarm"
|
||||
mock.topology = "mesh"
|
||||
mock.state = "active"
|
||||
mock.agents = []
|
||||
mock.tasks = []
|
||||
mock.created_at = "2024-01-01T12:00:00Z"
|
||||
mock.metrics = {"total_agents": 3, "active_agents": 2, "completed_tasks": 15, "efficiency_score": 85.5}
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mcp_client():
|
||||
"""Create a mock MCP client for testing."""
|
||||
mock = AsyncMock()
|
||||
mock.is_connected = True
|
||||
mock.available_tools = [
|
||||
"swarm_init",
|
||||
"agent_spawn",
|
||||
"task_orchestrate",
|
||||
"memory_usage",
|
||||
"neural_train",
|
||||
"performance_report",
|
||||
]
|
||||
mock.execute_tool = AsyncMock()
|
||||
mock.get_tool_info = AsyncMock()
|
||||
mock.connect = AsyncMock()
|
||||
mock.disconnect = AsyncMock()
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_task():
|
||||
"""Create a mock task for testing."""
|
||||
return {
|
||||
"task_id": "test_task_789",
|
||||
"type": "research_query",
|
||||
"status": "pending",
|
||||
"priority": "medium",
|
||||
"data": {"query": "Test research query", "scope": "general", "depth": "standard"},
|
||||
"created_at": "2024-01-01T12:00:00Z",
|
||||
"assigned_agent": None,
|
||||
"result": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_test_environment(test_settings: Settings, monkeypatch):
|
||||
"""Set up test environment variables."""
|
||||
monkeypatch.setenv("CLEVERCLAUDE_ENVIRONMENT", "testing")
|
||||
monkeypatch.setenv("CLEVERCLAUDE_DEBUG", "true")
|
||||
monkeypatch.setenv("CLEVERCLAUDE_CONFIG_DIR", str(test_settings.database.url.split("///")[1].rsplit("/", 1)[0]))
|
||||
|
||||
# Override settings
|
||||
monkeypatch.setattr("cleverclaude.config.settings.get_settings", lambda: test_settings)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup_tasks():
|
||||
"""Cleanup any remaining asyncio tasks after tests."""
|
||||
yield
|
||||
|
||||
# Cancel any remaining tasks
|
||||
tasks = [task for task in asyncio.all_tasks() if not task.done()]
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
if tasks:
|
||||
asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
# Marker definitions
|
||||
pytest.mark.unit = pytest.mark.unit
|
||||
pytest.mark.integration = pytest.mark.integration
|
||||
pytest.mark.async_test = pytest.mark.asyncio
|
||||
pytest.mark.slow = pytest.mark.slow
|
||||
@@ -0,0 +1 @@
|
||||
"""Integration tests for CleverClaude components."""
|
||||
@@ -0,0 +1,506 @@
|
||||
"""Integration tests for MCP (Model Context Protocol) system."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cleverclaude.agents.manager import AgentManager
|
||||
from cleverclaude.config.settings import Settings
|
||||
from cleverclaude.coordination.swarm import SwarmCoordinator
|
||||
from cleverclaude.core.app import CleverClaudeApp
|
||||
from cleverclaude.mcp.client import MCPClient
|
||||
from cleverclaude.mcp.types import MCPToolExecutionResult
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.async_test
|
||||
class TestMCPIntegration:
|
||||
"""Integration tests for MCP system with other components."""
|
||||
|
||||
async def test_mcp_with_agent_manager(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test MCP integration with AgentManager."""
|
||||
# Initialize MCP client and agent manager
|
||||
mcp_client = MCPClient(test_settings)
|
||||
agent_manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
|
||||
await mcp_client.initialize()
|
||||
await agent_manager.initialize()
|
||||
|
||||
# Mock MCP tool for agent creation
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(
|
||||
success=True, result={"agent_id": "mcp_agent_123", "type": "researcher", "status": "created"}
|
||||
)
|
||||
|
||||
# Execute MCP tool to create agent
|
||||
result = await mcp_client.execute_tool(
|
||||
"agent_spawn",
|
||||
{"type": "researcher", "name": "MCP Test Agent", "capabilities": ["research", "analysis"]},
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.result["agent_id"] == "mcp_agent_123"
|
||||
|
||||
await mcp_client.disconnect()
|
||||
await agent_manager.shutdown()
|
||||
|
||||
async def test_mcp_with_swarm_coordinator(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test MCP integration with SwarmCoordinator."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
agent_manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
swarm_coordinator = SwarmCoordinator(test_settings.swarm, async_session, agent_manager, mock_redis)
|
||||
|
||||
await mcp_client.initialize()
|
||||
await agent_manager.initialize()
|
||||
await swarm_coordinator.initialize()
|
||||
|
||||
# Test swarm creation via MCP
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(
|
||||
success=True, result={"swarm_id": "mcp_swarm_456", "topology": "mesh", "status": "created"}
|
||||
)
|
||||
|
||||
result = await mcp_client.execute_tool(
|
||||
"swarm_init", {"topology": "mesh", "maxAgents": 10, "strategy": "balanced"}
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.result["swarm_id"] == "mcp_swarm_456"
|
||||
|
||||
await swarm_coordinator.shutdown()
|
||||
await agent_manager.shutdown()
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_mcp_end_to_end_workflow(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test complete end-to-end workflow using MCP tools."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Mock a complete workflow: swarm -> agents -> tasks -> results
|
||||
workflow_steps = [
|
||||
("swarm_init", {"topology": "hierarchical"}, {"swarm_id": "workflow_swarm"}),
|
||||
("agent_spawn", {"type": "researcher"}, {"agent_id": "workflow_agent_1"}),
|
||||
("agent_spawn", {"type": "coder"}, {"agent_id": "workflow_agent_2"}),
|
||||
("task_orchestrate", {"task": "Complex analysis task"}, {"task_id": "workflow_task_1"}),
|
||||
("task_status", {"taskId": "workflow_task_1"}, {"status": "completed"}),
|
||||
("performance_report", {"format": "detailed"}, {"metrics": {"efficiency": 92.5}}),
|
||||
]
|
||||
|
||||
results = []
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result=expected_result) for _, _, expected_result in workflow_steps
|
||||
]
|
||||
|
||||
for tool_name, params, expected_result in workflow_steps:
|
||||
result = await mcp_client.execute_tool(tool_name, params)
|
||||
results.append(result)
|
||||
|
||||
assert result.success is True
|
||||
assert result.result == expected_result
|
||||
|
||||
# Verify workflow completion
|
||||
assert len(results) == 6
|
||||
assert all(r.success for r in results)
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_mcp_error_recovery(self, test_settings: Settings):
|
||||
"""Test MCP error recovery and resilience."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test recovery from tool execution errors
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
# First call fails, second succeeds
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=False, result=None, error="Temporary network error"),
|
||||
MCPToolExecutionResult(success=True, result={"swarm_id": "recovered_swarm"}),
|
||||
]
|
||||
|
||||
# First attempt should fail
|
||||
result1 = await mcp_client.execute_tool("swarm_init", {"topology": "mesh"})
|
||||
assert result1.success is False
|
||||
assert "network error" in result1.error.lower()
|
||||
|
||||
# Second attempt should succeed (simulating retry)
|
||||
result2 = await mcp_client.execute_tool("swarm_init", {"topology": "mesh"})
|
||||
assert result2.success is True
|
||||
assert result2.result["swarm_id"] == "recovered_swarm"
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_mcp_concurrent_operations(self, test_settings: Settings):
|
||||
"""Test concurrent MCP operations."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Define concurrent operations
|
||||
concurrent_ops = [
|
||||
("swarm_status", {}, {"active_swarms": 2}),
|
||||
("agent_metrics", {"agentId": "agent_1"}, {"performance": 85.5}),
|
||||
("memory_usage", {"action": "list"}, {"total_keys": 42}),
|
||||
("neural_status", {"modelId": "model_1"}, {"status": "trained"}),
|
||||
("performance_report", {"format": "summary"}, {"uptime": "24h"}),
|
||||
]
|
||||
|
||||
async def execute_operation(tool_name, params, expected_result):
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(success=True, result=expected_result)
|
||||
return await mcp_client.execute_tool(tool_name, params)
|
||||
|
||||
# Execute operations concurrently
|
||||
tasks = [execute_operation(tool_name, params, expected) for tool_name, params, expected in concurrent_ops]
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Verify all operations completed successfully
|
||||
assert len(results) == 5
|
||||
assert all(r.success for r in results)
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_mcp_with_full_application(self, test_settings: Settings, temp_dir):
|
||||
"""Test MCP integration with full CleverClaude application."""
|
||||
# Create config directory
|
||||
config_dir = temp_dir / ".cleverclaude"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp._initialize_mcp") as mock_init_mcp:
|
||||
mock_mcp_client = AsyncMock()
|
||||
mock_mcp_client.get_available_tools.return_value = {
|
||||
"swarm_init": {"description": "Initialize swarm"},
|
||||
"agent_spawn": {"description": "Spawn agent"},
|
||||
"task_orchestrate": {"description": "Orchestrate task"},
|
||||
}
|
||||
mock_init_mcp.return_value = mock_mcp_client
|
||||
|
||||
# Initialize application
|
||||
app = CleverClaudeApp(config_dir)
|
||||
|
||||
with (
|
||||
patch.object(app, "_initialize_database"),
|
||||
patch.object(app, "_initialize_redis"),
|
||||
patch.object(app, "_initialize_agents"),
|
||||
patch.object(app, "_initialize_swarm"),
|
||||
):
|
||||
await app.initialize()
|
||||
|
||||
# Verify MCP client was initialized
|
||||
assert app.mcp_client is not None
|
||||
mock_init_mcp.assert_called_once()
|
||||
|
||||
await app.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.async_test
|
||||
class TestMCPTools:
|
||||
"""Integration tests for specific MCP tools."""
|
||||
|
||||
async def test_neural_tools_integration(self, test_settings: Settings):
|
||||
"""Test neural network tools integration."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test neural training workflow
|
||||
training_workflow = [
|
||||
(
|
||||
"neural_train",
|
||||
{"pattern_type": "coordination", "training_data": "sample_coordination_data", "epochs": 10},
|
||||
),
|
||||
("neural_status", {"modelId": "coordination_model"}),
|
||||
("neural_predict", {"modelId": "coordination_model", "input": "test_coordination_scenario"}),
|
||||
]
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result={"training_id": "train_123", "status": "started"}),
|
||||
MCPToolExecutionResult(success=True, result={"status": "trained", "accuracy": 0.92}),
|
||||
MCPToolExecutionResult(success=True, result={"prediction": "optimal_coordination", "confidence": 0.88}),
|
||||
]
|
||||
|
||||
results = []
|
||||
for tool_name, params in training_workflow:
|
||||
result = await mcp_client.execute_tool(tool_name, params)
|
||||
results.append(result)
|
||||
|
||||
assert len(results) == 3
|
||||
assert all(r.success for r in results)
|
||||
assert results[0].result["status"] == "started"
|
||||
assert results[1].result["accuracy"] == 0.92
|
||||
assert results[2].result["confidence"] == 0.88
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_memory_tools_integration(self, test_settings: Settings):
|
||||
"""Test memory management tools integration."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test memory operations workflow
|
||||
memory_ops = [
|
||||
(
|
||||
"memory_usage",
|
||||
{"action": "store", "key": "test_key", "value": "test_value", "namespace": "integration_test"},
|
||||
),
|
||||
("memory_usage", {"action": "retrieve", "key": "test_key", "namespace": "integration_test"}),
|
||||
("memory_search", {"pattern": "test_*", "namespace": "integration_test"}),
|
||||
("memory_usage", {"action": "delete", "key": "test_key", "namespace": "integration_test"}),
|
||||
]
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result={"action": "store", "status": "success"}),
|
||||
MCPToolExecutionResult(success=True, result={"value": "test_value", "found": True}),
|
||||
MCPToolExecutionResult(success=True, result={"matches": ["test_key"], "count": 1}),
|
||||
MCPToolExecutionResult(success=True, result={"action": "delete", "status": "success"}),
|
||||
]
|
||||
|
||||
results = []
|
||||
for tool_name, params in memory_ops:
|
||||
result = await mcp_client.execute_tool(tool_name, params)
|
||||
results.append(result)
|
||||
|
||||
assert len(results) == 4
|
||||
assert all(r.success for r in results)
|
||||
assert results[1].result["value"] == "test_value"
|
||||
assert results[2].result["count"] == 1
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
async def test_workflow_tools_integration(self, test_settings: Settings):
|
||||
"""Test workflow automation tools integration."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test workflow creation and execution
|
||||
workflow_definition = {
|
||||
"name": "Integration Test Workflow",
|
||||
"steps": [
|
||||
{"action": "create_agents", "count": 3},
|
||||
{"action": "create_swarm", "topology": "mesh"},
|
||||
{"action": "assign_tasks", "task_count": 5},
|
||||
{"action": "monitor_execution"},
|
||||
{"action": "collect_results"},
|
||||
],
|
||||
"triggers": ["on_demand"],
|
||||
}
|
||||
|
||||
workflow_ops = [
|
||||
("workflow_create", workflow_definition),
|
||||
("workflow_execute", {"workflowId": "workflow_123"}),
|
||||
("workflow_status", {"workflowId": "workflow_123"}),
|
||||
("workflow_results", {"workflowId": "workflow_123"}),
|
||||
]
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result={"workflow_id": "workflow_123", "status": "created"}),
|
||||
MCPToolExecutionResult(success=True, result={"execution_id": "exec_456", "status": "running"}),
|
||||
MCPToolExecutionResult(success=True, result={"status": "completed", "progress": 100}),
|
||||
MCPToolExecutionResult(success=True, result={"results": {"tasks_completed": 5, "success_rate": 100}}),
|
||||
]
|
||||
|
||||
results = []
|
||||
for tool_name, params in workflow_ops:
|
||||
result = await mcp_client.execute_tool(tool_name, params)
|
||||
results.append(result)
|
||||
|
||||
assert len(results) == 4
|
||||
assert all(r.success for r in results)
|
||||
assert results[0].result["workflow_id"] == "workflow_123"
|
||||
assert results[2].result["progress"] == 100
|
||||
assert results[3].result["results"]["success_rate"] == 100
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.slow
|
||||
class TestMCPPerformance:
|
||||
"""Performance and stress tests for MCP system."""
|
||||
|
||||
@pytest.mark.async_test
|
||||
async def test_mcp_high_throughput(self, test_settings: Settings):
|
||||
"""Test MCP system under high throughput."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Execute many operations rapidly
|
||||
num_operations = 100
|
||||
operations = []
|
||||
|
||||
for _i in range(num_operations):
|
||||
operations.append(("swarm_status", {"detailed": False}))
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(success=True, result={"status": "running", "swarms": 2})
|
||||
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Execute operations in batches to avoid overwhelming
|
||||
batch_size = 20
|
||||
results = []
|
||||
for i in range(0, num_operations, batch_size):
|
||||
batch = operations[i : i + batch_size]
|
||||
batch_tasks = [mcp_client.execute_tool(tool_name, params) for tool_name, params in batch]
|
||||
batch_results = await asyncio.gather(*batch_tasks)
|
||||
results.extend(batch_results)
|
||||
|
||||
end_time = asyncio.get_event_loop().time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
# Verify performance
|
||||
assert len(results) == num_operations
|
||||
assert all(r.success for r in results)
|
||||
assert execution_time < 10.0 # Should complete within 10 seconds
|
||||
|
||||
throughput = num_operations / execution_time
|
||||
assert throughput > 10 # Should handle more than 10 ops/second
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
@pytest.mark.async_test
|
||||
async def test_mcp_connection_resilience(self, test_settings: Settings):
|
||||
"""Test MCP connection resilience under stress."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Simulate connection failures and recoveries
|
||||
failure_count = 0
|
||||
success_count = 0
|
||||
|
||||
def mock_execute_with_failures(tool_name, params):
|
||||
nonlocal failure_count, success_count
|
||||
|
||||
# Simulate intermittent failures (20% failure rate)
|
||||
if (success_count + failure_count) % 5 == 0:
|
||||
failure_count += 1
|
||||
return MCPToolExecutionResult(success=False, result=None, error="Connection temporarily unavailable")
|
||||
else:
|
||||
success_count += 1
|
||||
return MCPToolExecutionResult(success=True, result={"status": "success", "operation": tool_name})
|
||||
|
||||
with patch.object(mcp_client, "execute_tool", side_effect=mock_execute_with_failures):
|
||||
# Execute operations with expected failures
|
||||
num_operations = 50
|
||||
results = []
|
||||
|
||||
for _i in range(num_operations):
|
||||
result = await mcp_client.execute_tool("health_check", {})
|
||||
results.append(result)
|
||||
|
||||
# Verify resilience
|
||||
total_results = len(results)
|
||||
successful_results = len([r for r in results if r.success])
|
||||
failed_results = len([r for r in results if not r.success])
|
||||
|
||||
assert total_results == num_operations
|
||||
assert successful_results > 0 # Should have some successes
|
||||
assert failed_results > 0 # Should have some expected failures
|
||||
assert successful_results >= failed_results # More successes than failures
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
@pytest.mark.async_test
|
||||
async def test_mcp_memory_efficiency(self, test_settings: Settings):
|
||||
"""Test MCP memory efficiency during extended operations."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Execute long-running sequence of operations
|
||||
import gc
|
||||
|
||||
initial_objects = len(gc.get_objects())
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(
|
||||
success=True,
|
||||
result={"data": "test" * 100}, # Some data payload
|
||||
)
|
||||
|
||||
# Execute many operations
|
||||
for i in range(200):
|
||||
result = await mcp_client.execute_tool("memory_usage", {"action": "list"})
|
||||
assert result.success
|
||||
|
||||
# Force garbage collection periodically
|
||||
if i % 50 == 0:
|
||||
gc.collect()
|
||||
|
||||
# Final garbage collection
|
||||
gc.collect()
|
||||
|
||||
final_objects = len(gc.get_objects())
|
||||
|
||||
# Verify no significant memory growth
|
||||
object_growth = final_objects - initial_objects
|
||||
assert object_growth < 1000 # Should not have excessive object growth
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestMCPToolValidation:
|
||||
"""Test MCP tool parameter validation and error handling."""
|
||||
|
||||
@pytest.mark.async_test
|
||||
async def test_tool_parameter_validation(self, test_settings: Settings):
|
||||
"""Test comprehensive tool parameter validation."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test various invalid parameter scenarios
|
||||
invalid_scenarios = [
|
||||
("swarm_init", {"topology": "invalid_topology"}, "Invalid topology"),
|
||||
("agent_spawn", {"type": "invalid_type"}, "Invalid agent type"),
|
||||
("task_orchestrate", {"priority": "invalid_priority"}, "Invalid priority"),
|
||||
("memory_usage", {"action": "invalid_action"}, "Invalid action"),
|
||||
("neural_train", {"epochs": -1}, "Invalid epochs"),
|
||||
]
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
for tool_name, invalid_params, expected_error in invalid_scenarios:
|
||||
mock_execute.return_value = MCPToolExecutionResult(success=False, result=None, error=expected_error)
|
||||
|
||||
result = await mcp_client.execute_tool(tool_name, invalid_params)
|
||||
|
||||
assert result.success is False
|
||||
assert expected_error.lower() in result.error.lower()
|
||||
|
||||
await mcp_client.disconnect()
|
||||
|
||||
@pytest.mark.async_test
|
||||
async def test_tool_response_validation(self, test_settings: Settings):
|
||||
"""Test MCP tool response validation."""
|
||||
mcp_client = MCPClient(test_settings)
|
||||
await mcp_client.initialize()
|
||||
|
||||
# Test various response formats
|
||||
response_scenarios = [
|
||||
("swarm_init", {"swarm_id": "test", "topology": "mesh", "status": "created"}),
|
||||
("agent_spawn", {"agent_id": "test", "type": "researcher", "status": "active"}),
|
||||
("task_status", {"task_id": "test", "status": "completed", "progress": 100}),
|
||||
("performance_report", {"metrics": {"cpu": 50, "memory": 200}, "timestamp": "2024-01-01T12:00:00Z"}),
|
||||
]
|
||||
|
||||
with patch.object(mcp_client, "execute_tool") as mock_execute:
|
||||
for tool_name, expected_response in response_scenarios:
|
||||
mock_execute.return_value = MCPToolExecutionResult(success=True, result=expected_response)
|
||||
|
||||
result = await mcp_client.execute_tool(tool_name, {})
|
||||
|
||||
assert result.success is True
|
||||
assert result.result == expected_response
|
||||
|
||||
# Verify required fields are present
|
||||
if tool_name == "swarm_init":
|
||||
assert "swarm_id" in result.result
|
||||
assert "topology" in result.result
|
||||
elif tool_name == "agent_spawn":
|
||||
assert "agent_id" in result.result
|
||||
assert "type" in result.result
|
||||
|
||||
await mcp_client.disconnect()
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for CleverClaude modules."""
|
||||
@@ -0,0 +1,383 @@
|
||||
"""Unit tests for CleverClaude agent management."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cleverclaude.agents.agent import BaseAgent
|
||||
from cleverclaude.agents.manager import AgentManager
|
||||
from cleverclaude.agents.types import AgentConfig, AgentStatus, AgentType
|
||||
from cleverclaude.config.settings import Settings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestAgentManager:
|
||||
"""Test suite for AgentManager class."""
|
||||
|
||||
async def test_initialization(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test AgentManager initialization."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
|
||||
assert manager.config == test_settings.agents
|
||||
assert manager.session == async_session
|
||||
assert manager.redis == mock_redis
|
||||
assert manager.agents == {}
|
||||
assert manager._initialized is False
|
||||
|
||||
async def test_initialize_manager(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test manager initialization process."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
|
||||
with patch("cleverclaude.agents.manager.structlog.get_logger") as mock_logger:
|
||||
await manager.initialize()
|
||||
|
||||
assert manager._initialized is True
|
||||
mock_logger.assert_called_once()
|
||||
|
||||
async def test_create_agent_success(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test successful agent creation."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
with patch("cleverclaude.agents.factory.AgentFactory.create_agent") as mock_factory:
|
||||
mock_agent = AsyncMock()
|
||||
mock_agent.agent_id = "test_agent_123"
|
||||
mock_agent.name = "Test Agent"
|
||||
mock_agent.agent_type = AgentType.RESEARCHER
|
||||
mock_agent.status = AgentStatus.ACTIVE
|
||||
mock_factory.return_value = mock_agent
|
||||
|
||||
agent_id = await manager.create_agent(
|
||||
agent_type=AgentType.RESEARCHER, name="Test Agent", capabilities={"research", "analysis"}
|
||||
)
|
||||
|
||||
assert agent_id == "test_agent_123"
|
||||
assert agent_id in manager.agents
|
||||
mock_factory.assert_called_once()
|
||||
|
||||
async def test_create_agent_max_limit(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test agent creation with max limit exceeded."""
|
||||
# Set very low limit for testing
|
||||
test_settings.agents.max_agents = 1
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Create first agent (should succeed)
|
||||
with patch("cleverclaude.agents.factory.AgentFactory.create_agent") as mock_factory:
|
||||
mock_agent = AsyncMock()
|
||||
mock_agent.agent_id = "test_agent_1"
|
||||
mock_factory.return_value = mock_agent
|
||||
|
||||
agent_id_1 = await manager.create_agent(AgentType.RESEARCHER, "Agent 1")
|
||||
assert agent_id_1 == "test_agent_1"
|
||||
|
||||
# Try to create second agent (should fail)
|
||||
with pytest.raises(ValueError, match="Maximum number of agents reached"):
|
||||
await manager.create_agent(AgentType.RESEARCHER, "Agent 2")
|
||||
|
||||
async def test_get_agent_status(self, test_settings: Settings, async_session, mock_redis, mock_agent):
|
||||
"""Test getting agent status."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add mock agent to manager
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
|
||||
status = await manager.get_agent_status(mock_agent.agent_id)
|
||||
|
||||
assert status["agent_id"] == mock_agent.agent_id
|
||||
assert status["name"] == mock_agent.name
|
||||
assert status["type"] == mock_agent.agent_type
|
||||
assert status["status"] == mock_agent.status
|
||||
|
||||
async def test_get_agent_status_not_found(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test getting status for non-existent agent."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
with pytest.raises(ValueError, match="Agent not found"):
|
||||
await manager.get_agent_status("non_existent_agent")
|
||||
|
||||
async def test_list_agents(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test listing all agents."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add multiple mock agents
|
||||
mock_agents = []
|
||||
for i in range(3):
|
||||
mock_agent = AsyncMock()
|
||||
mock_agent.agent_id = f"agent_{i}"
|
||||
mock_agent.name = f"Agent {i}"
|
||||
mock_agent.agent_type = AgentType.RESEARCHER
|
||||
mock_agent.status = AgentStatus.ACTIVE
|
||||
mock_agents.append(mock_agent)
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
|
||||
agents_list = await manager.list_agents()
|
||||
|
||||
assert len(agents_list) == 3
|
||||
for i, agent_info in enumerate(agents_list):
|
||||
assert agent_info["agent_id"] == f"agent_{i}"
|
||||
assert agent_info["name"] == f"Agent {i}"
|
||||
|
||||
async def test_execute_task(self, test_settings: Settings, async_session, mock_redis, mock_agent, mock_task):
|
||||
"""Test task execution on agent."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Set up mock agent
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
mock_agent.execute_task.return_value = {"status": "completed", "result": "task completed"}
|
||||
|
||||
result = await manager.execute_task(mock_task, agent_id=mock_agent.agent_id)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
mock_agent.execute_task.assert_called_once_with(mock_task)
|
||||
|
||||
async def test_execute_task_auto_assign(self, test_settings: Settings, async_session, mock_redis, mock_task):
|
||||
"""Test task execution with automatic agent assignment."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add multiple mock agents
|
||||
for i in range(2):
|
||||
mock_agent = AsyncMock()
|
||||
mock_agent.agent_id = f"agent_{i}"
|
||||
mock_agent.agent_type = AgentType.RESEARCHER
|
||||
mock_agent.status = AgentStatus.ACTIVE
|
||||
mock_agent.capabilities = {"research", "analysis"}
|
||||
mock_agent.execute_task.return_value = {"status": "completed"}
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
|
||||
with patch.object(manager, "_find_suitable_agent") as mock_find:
|
||||
mock_find.return_value = "agent_0"
|
||||
|
||||
result = await manager.execute_task(mock_task)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
mock_find.assert_called_once_with(mock_task)
|
||||
|
||||
async def test_destroy_agent(self, test_settings: Settings, async_session, mock_redis, mock_agent):
|
||||
"""Test agent destruction."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add mock agent
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
mock_agent.shutdown = AsyncMock()
|
||||
|
||||
await manager.destroy_agent(mock_agent.agent_id)
|
||||
|
||||
assert mock_agent.agent_id not in manager.agents
|
||||
mock_agent.shutdown.assert_called_once()
|
||||
|
||||
async def test_health_check(self, test_settings: Settings, async_session, mock_redis, mock_agent):
|
||||
"""Test agent health check."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add mock agent
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
mock_agent.get_health_status.return_value = mock_agent.health_status
|
||||
|
||||
health_report = await manager.health_check()
|
||||
|
||||
assert len(health_report["agents"]) == 1
|
||||
assert health_report["agents"][0]["agent_id"] == mock_agent.agent_id
|
||||
assert health_report["total_agents"] == 1
|
||||
assert health_report["healthy_agents"] == 1
|
||||
|
||||
async def test_shutdown(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test manager shutdown."""
|
||||
manager = AgentManager(test_settings.agents, async_session, mock_redis)
|
||||
await manager.initialize()
|
||||
|
||||
# Add mock agents
|
||||
mock_agents = []
|
||||
for i in range(2):
|
||||
mock_agent = AsyncMock()
|
||||
mock_agent.agent_id = f"agent_{i}"
|
||||
mock_agent.shutdown = AsyncMock()
|
||||
mock_agents.append(mock_agent)
|
||||
manager.agents[mock_agent.agent_id] = mock_agent
|
||||
|
||||
await manager.shutdown()
|
||||
|
||||
# Verify all agents were shut down
|
||||
for mock_agent in mock_agents:
|
||||
mock_agent.shutdown.assert_called_once()
|
||||
|
||||
assert len(manager.agents) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestBaseAgent:
|
||||
"""Test suite for BaseAgent class."""
|
||||
|
||||
def test_agent_initialization(self):
|
||||
"""Test agent initialization with config."""
|
||||
config = AgentConfig(
|
||||
agent_id="test_agent",
|
||||
name="Test Agent",
|
||||
agent_type=AgentType.RESEARCHER,
|
||||
capabilities={"research", "analysis"},
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
|
||||
assert agent.agent_id == "test_agent"
|
||||
assert agent.name == "Test Agent"
|
||||
assert agent.agent_type == AgentType.RESEARCHER
|
||||
assert agent.capabilities == {"research", "analysis"}
|
||||
assert agent.status == AgentStatus.INITIALIZING
|
||||
assert agent.timeout == 300
|
||||
|
||||
async def test_agent_startup(self):
|
||||
"""Test agent startup process."""
|
||||
config = AgentConfig(agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
await agent.startup()
|
||||
|
||||
assert agent.status == AgentStatus.ACTIVE
|
||||
assert agent.created_at is not None
|
||||
|
||||
async def test_agent_execute_task(self, mock_task):
|
||||
"""Test basic task execution."""
|
||||
config = AgentConfig(
|
||||
agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER, capabilities={"research"}
|
||||
)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
await agent.startup()
|
||||
|
||||
with patch.object(agent, "_process_task") as mock_process:
|
||||
mock_process.return_value = {"status": "completed", "result": "processed"}
|
||||
|
||||
result = await agent.execute_task(mock_task)
|
||||
|
||||
assert result["status"] == "completed"
|
||||
assert agent.task_count == 1
|
||||
mock_process.assert_called_once_with(mock_task)
|
||||
|
||||
async def test_agent_execute_task_timeout(self, mock_task):
|
||||
"""Test task execution with timeout."""
|
||||
config = AgentConfig(
|
||||
agent_id="test_agent",
|
||||
name="Test Agent",
|
||||
agent_type=AgentType.RESEARCHER,
|
||||
timeout=1, # Very short timeout
|
||||
)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
await agent.startup()
|
||||
|
||||
# Mock a slow task
|
||||
async def slow_task(task):
|
||||
import asyncio
|
||||
|
||||
await asyncio.sleep(2) # Longer than timeout
|
||||
return {"status": "completed"}
|
||||
|
||||
with patch.object(agent, "_process_task", side_effect=slow_task):
|
||||
result = await agent.execute_task(mock_task)
|
||||
|
||||
assert result["status"] == "error"
|
||||
assert "timeout" in result["error"].lower()
|
||||
|
||||
def test_agent_get_health_status(self):
|
||||
"""Test agent health status reporting."""
|
||||
config = AgentConfig(agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
agent.task_count = 5
|
||||
agent.error_count = 1
|
||||
|
||||
health = agent.get_health_status()
|
||||
|
||||
assert health["agent_id"] == "test_agent"
|
||||
assert health["status"] == AgentStatus.INITIALIZING
|
||||
assert health["task_count"] == 5
|
||||
assert health["error_count"] == 1
|
||||
assert "cpu_usage" in health
|
||||
assert "memory_usage" in health
|
||||
|
||||
async def test_agent_pause_resume(self):
|
||||
"""Test agent pause and resume functionality."""
|
||||
config = AgentConfig(agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
await agent.startup()
|
||||
|
||||
# Test pause
|
||||
await agent.pause()
|
||||
assert agent.status == AgentStatus.PAUSED
|
||||
|
||||
# Test resume
|
||||
await agent.resume()
|
||||
assert agent.status == AgentStatus.ACTIVE
|
||||
|
||||
async def test_agent_shutdown(self):
|
||||
"""Test agent shutdown process."""
|
||||
config = AgentConfig(agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER)
|
||||
|
||||
agent = BaseAgent(config)
|
||||
await agent.startup()
|
||||
|
||||
await agent.shutdown()
|
||||
|
||||
assert agent.status == AgentStatus.TERMINATED
|
||||
assert agent.shutdown_at is not None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentTypes:
|
||||
"""Test suite for agent type definitions."""
|
||||
|
||||
def test_agent_type_enum(self):
|
||||
"""Test AgentType enumeration."""
|
||||
assert AgentType.RESEARCHER == "researcher"
|
||||
assert AgentType.CODER == "coder"
|
||||
assert AgentType.ANALYST == "analyst"
|
||||
assert AgentType.COORDINATOR == "coordinator"
|
||||
assert AgentType.REVIEWER == "reviewer"
|
||||
assert AgentType.TESTER == "tester"
|
||||
|
||||
def test_agent_status_enum(self):
|
||||
"""Test AgentStatus enumeration."""
|
||||
assert AgentStatus.INITIALIZING == "initializing"
|
||||
assert AgentStatus.ACTIVE == "active"
|
||||
assert AgentStatus.BUSY == "busy"
|
||||
assert AgentStatus.PAUSED == "paused"
|
||||
assert AgentStatus.ERROR == "error"
|
||||
assert AgentStatus.TERMINATED == "terminated"
|
||||
|
||||
def test_agent_config_creation(self):
|
||||
"""Test AgentConfig creation and validation."""
|
||||
config = AgentConfig(
|
||||
agent_id="test_agent",
|
||||
name="Test Agent",
|
||||
agent_type=AgentType.RESEARCHER,
|
||||
capabilities={"research", "analysis"},
|
||||
timeout=300,
|
||||
max_concurrent_tasks=5,
|
||||
)
|
||||
|
||||
assert config.agent_id == "test_agent"
|
||||
assert config.name == "Test Agent"
|
||||
assert config.agent_type == AgentType.RESEARCHER
|
||||
assert config.capabilities == {"research", "analysis"}
|
||||
assert config.timeout == 300
|
||||
assert config.max_concurrent_tasks == 5
|
||||
|
||||
def test_agent_config_defaults(self):
|
||||
"""Test AgentConfig default values."""
|
||||
config = AgentConfig(agent_id="test_agent", name="Test Agent", agent_type=AgentType.RESEARCHER)
|
||||
|
||||
assert config.capabilities == set()
|
||||
assert config.timeout == 300 # Default timeout
|
||||
assert config.max_concurrent_tasks == 1 # Default concurrency
|
||||
@@ -0,0 +1,452 @@
|
||||
"""Unit tests for CleverClaude CLI interface."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from cleverclaude.cli.commands.init import InitCommand
|
||||
from cleverclaude.cli.commands.start import StartCommand
|
||||
from cleverclaude.cli.commands.status import StatusCommand
|
||||
from cleverclaude.cli.main import app
|
||||
from cleverclaude.config.settings import Settings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCLIMain:
|
||||
"""Test suite for main CLI application."""
|
||||
|
||||
def test_cli_app_creation(self):
|
||||
"""Test CLI app initialization."""
|
||||
assert app.name == "cleverclaude"
|
||||
assert "Advanced AI Agent Orchestration System" in app.help
|
||||
|
||||
def test_version_command(self):
|
||||
"""Test --version command."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["--version"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "CleverClaude Python v" in result.output
|
||||
|
||||
def test_help_command(self):
|
||||
"""Test --help command."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "CleverClaude" in result.output
|
||||
assert "init" in result.output
|
||||
assert "start" in result.output
|
||||
assert "status" in result.output
|
||||
|
||||
def test_subcommand_help(self):
|
||||
"""Test help for subcommands."""
|
||||
runner = CliRunner()
|
||||
|
||||
# Test init command help
|
||||
result = runner.invoke(app, ["init", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Initialize" in result.output
|
||||
|
||||
# Test start command help
|
||||
result = runner.invoke(app, ["start", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Start" in result.output
|
||||
|
||||
# Test status command help
|
||||
result = runner.invoke(app, ["status", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Display" in result.output
|
||||
|
||||
def test_invalid_command(self):
|
||||
"""Test invalid command handling."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["invalid_command"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "No such command" in result.output
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestInitCommand:
|
||||
"""Test suite for init command."""
|
||||
|
||||
async def test_init_command_basic(self, temp_dir: Path, test_settings: Settings):
|
||||
"""Test basic init command execution."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
await init_cmd.execute(directory=temp_dir, template="default", force=False)
|
||||
|
||||
# Verify directory structure was created
|
||||
assert (temp_dir / ".cleverclaude").exists()
|
||||
assert (temp_dir / ".cleverclaude" / "config.yaml").exists()
|
||||
assert (temp_dir / "examples").exists()
|
||||
assert (temp_dir / ".env.example").exists()
|
||||
|
||||
async def test_init_command_with_template(self, temp_dir: Path):
|
||||
"""Test init command with production template."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
await init_cmd.execute(directory=temp_dir, template="production", force=False)
|
||||
|
||||
# Verify production-specific files
|
||||
assert (temp_dir / "docker-compose.yml").exists()
|
||||
|
||||
# Check config contains production settings
|
||||
config_content = (temp_dir / ".cleverclaude" / "config.yaml").read_text()
|
||||
assert "production" in config_content
|
||||
|
||||
async def test_init_command_force_overwrite(self, temp_dir: Path):
|
||||
"""Test init command with force overwrite."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Create existing files
|
||||
existing_file = temp_dir / "existing.txt"
|
||||
existing_file.write_text("existing content")
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
# Should succeed with force=True
|
||||
await init_cmd.execute(directory=temp_dir, template="default", force=True)
|
||||
|
||||
assert (temp_dir / ".cleverclaude").exists()
|
||||
|
||||
async def test_init_command_non_empty_directory_without_force(self, temp_dir: Path):
|
||||
"""Test init command fails on non-empty directory without force."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Create existing file
|
||||
(temp_dir / "existing.txt").write_text("content")
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
# Should raise error without force
|
||||
with pytest.raises(RuntimeError, match="not empty"):
|
||||
await init_cmd.execute(directory=temp_dir, template="default", force=False)
|
||||
|
||||
def test_init_config_templates(self):
|
||||
"""Test configuration template generation."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
# Test default template
|
||||
default_config = init_cmd._get_config_template("default")
|
||||
assert "development" in default_config
|
||||
assert "debug: true" in default_config
|
||||
|
||||
# Test production template
|
||||
prod_config = init_cmd._get_config_template("production")
|
||||
assert "production" in prod_config
|
||||
assert "debug: false" in prod_config
|
||||
|
||||
def test_init_example_generation(self):
|
||||
"""Test example file generation."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
init_cmd = InitCommand(console, logger)
|
||||
|
||||
# Test agent example
|
||||
agent_example = init_cmd._get_agent_example()
|
||||
assert "AgentManager" in agent_example
|
||||
assert "create_agent" in agent_example
|
||||
|
||||
# Test swarm example
|
||||
swarm_example = init_cmd._get_swarm_example()
|
||||
assert "SwarmCoordinator" in swarm_example
|
||||
assert "add_agent" in swarm_example
|
||||
|
||||
# Test task example
|
||||
task_example = init_cmd._get_task_example()
|
||||
assert "TaskOrchestrator" in task_example
|
||||
assert "execute_workflow" in task_example
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestStartCommand:
|
||||
"""Test suite for start command."""
|
||||
|
||||
async def test_start_command_basic(self, test_settings: Settings):
|
||||
"""Test basic start command execution."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp") as mock_app_class:
|
||||
mock_app = AsyncMock()
|
||||
mock_app_class.return_value = mock_app
|
||||
|
||||
start_cmd = StartCommand(console, logger)
|
||||
|
||||
await start_cmd.execute(config_dir=None, port=8000, host="127.0.0.1", daemon=False)
|
||||
|
||||
mock_app.initialize.assert_called_once()
|
||||
mock_app.start.assert_called_once()
|
||||
|
||||
async def test_start_command_with_config_dir(self, temp_dir: Path):
|
||||
"""Test start command with custom config directory."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Create config directory
|
||||
config_dir = temp_dir / ".cleverclaude"
|
||||
config_dir.mkdir()
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp") as mock_app_class:
|
||||
mock_app = AsyncMock()
|
||||
mock_app_class.return_value = mock_app
|
||||
|
||||
start_cmd = StartCommand(console, logger)
|
||||
|
||||
await start_cmd.execute(config_dir=config_dir, port=8080, host="0.0.0.0", daemon=False)
|
||||
|
||||
mock_app.initialize.assert_called_once()
|
||||
# Verify config directory was set
|
||||
call_args = mock_app_class.call_args
|
||||
assert call_args is not None
|
||||
|
||||
async def test_start_command_daemon_mode(self):
|
||||
"""Test start command in daemon mode."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp") as mock_app_class:
|
||||
mock_app = AsyncMock()
|
||||
mock_app_class.return_value = mock_app
|
||||
|
||||
with patch("cleverclaude.cli.commands.start.start_daemon") as mock_daemon:
|
||||
start_cmd = StartCommand(console, logger)
|
||||
|
||||
await start_cmd.execute(config_dir=None, port=8000, host="127.0.0.1", daemon=True)
|
||||
|
||||
mock_daemon.assert_called_once()
|
||||
|
||||
async def test_start_command_error_handling(self):
|
||||
"""Test start command error handling."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp") as mock_app_class:
|
||||
mock_app = AsyncMock()
|
||||
mock_app.initialize.side_effect = Exception("Initialization failed")
|
||||
mock_app_class.return_value = mock_app
|
||||
|
||||
start_cmd = StartCommand(console, logger)
|
||||
|
||||
with pytest.raises(Exception, match="Initialization failed"):
|
||||
await start_cmd.execute(config_dir=None, port=8000, host="127.0.0.1", daemon=False)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestStatusCommand:
|
||||
"""Test suite for status command."""
|
||||
|
||||
async def test_status_command_basic(self):
|
||||
"""Test basic status command execution."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
mock_status = {
|
||||
"system": {"status": "running", "uptime": "00:15:23", "version": "2.0.0"},
|
||||
"agents": {"total": 5, "active": 4, "busy": 1},
|
||||
"swarms": {"total": 2, "active": 2},
|
||||
"performance": {"cpu_usage": 25.5, "memory_usage": 512.3, "tasks_completed": 142},
|
||||
}
|
||||
|
||||
with patch("cleverclaude.cli.commands.status.get_system_status") as mock_get_status:
|
||||
mock_get_status.return_value = mock_status
|
||||
|
||||
status_cmd = StatusCommand(console, logger)
|
||||
|
||||
result = await status_cmd.execute(format="table", watch=False, interval=5)
|
||||
|
||||
assert result is not None
|
||||
mock_get_status.assert_called_once()
|
||||
|
||||
async def test_status_command_json_format(self):
|
||||
"""Test status command with JSON format."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
mock_status = {"system": {"status": "running"}, "agents": {"total": 3}, "swarms": {"total": 1}}
|
||||
|
||||
with patch("cleverclaude.cli.commands.status.get_system_status") as mock_get_status:
|
||||
mock_get_status.return_value = mock_status
|
||||
|
||||
status_cmd = StatusCommand(console, logger)
|
||||
|
||||
result = await status_cmd.execute(format="json", watch=False, interval=5)
|
||||
|
||||
assert result == mock_status
|
||||
|
||||
async def test_status_command_watch_mode(self):
|
||||
"""Test status command in watch mode."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
mock_status = {"system": {"status": "running"}}
|
||||
|
||||
call_count = 0
|
||||
|
||||
def mock_get_status():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count >= 3: # Stop after 3 calls
|
||||
raise KeyboardInterrupt()
|
||||
return mock_status
|
||||
|
||||
with (
|
||||
patch("cleverclaude.cli.commands.status.get_system_status", side_effect=mock_get_status),
|
||||
patch("asyncio.sleep") as mock_sleep,
|
||||
):
|
||||
status_cmd = StatusCommand(console, logger)
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
await status_cmd.execute(format="table", watch=True, interval=1)
|
||||
|
||||
assert call_count == 3
|
||||
assert mock_sleep.call_count >= 2 # Should have slept between calls
|
||||
|
||||
async def test_status_command_service_unavailable(self):
|
||||
"""Test status command when service is unavailable."""
|
||||
import structlog
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
logger = structlog.get_logger()
|
||||
|
||||
with patch("cleverclaude.cli.commands.status.get_system_status") as mock_get_status:
|
||||
mock_get_status.side_effect = ConnectionError("Service unavailable")
|
||||
|
||||
status_cmd = StatusCommand(console, logger)
|
||||
|
||||
result = await status_cmd.execute(format="table", watch=False, interval=5)
|
||||
|
||||
assert result["system"]["status"] == "unavailable"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCLIIntegration:
|
||||
"""Integration tests for CLI commands."""
|
||||
|
||||
def test_full_cli_workflow(self, temp_dir):
|
||||
"""Test full CLI workflow: init -> start -> status."""
|
||||
runner = CliRunner()
|
||||
|
||||
# Test init command
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(app, ["init", "--dir", str(temp_dir), "--template", "default"])
|
||||
assert result.exit_code == 0
|
||||
assert "initialized successfully" in result.output
|
||||
|
||||
# Mock the start command to avoid actually starting services
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp"):
|
||||
result = runner.invoke(app, ["start", "--config-dir", str(temp_dir / ".cleverclaude")])
|
||||
# This would normally start the app, but we're mocking it
|
||||
|
||||
def test_cli_error_handling(self):
|
||||
"""Test CLI error handling for invalid arguments."""
|
||||
runner = CliRunner()
|
||||
|
||||
# Test init with invalid template
|
||||
result = runner.invoke(app, ["init", "--template", "invalid_template"])
|
||||
# Should handle gracefully
|
||||
|
||||
# Test start with invalid port
|
||||
result = runner.invoke(app, ["start", "--port", "invalid_port"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
# Test status with invalid format
|
||||
result = runner.invoke(app, ["status", "--format", "invalid_format"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_cli_with_environment_variables(self, monkeypatch):
|
||||
"""Test CLI behavior with environment variables."""
|
||||
runner = CliRunner()
|
||||
|
||||
# Set environment variables
|
||||
monkeypatch.setenv("CLEVERCLAUDE_API_HOST", "192.168.1.100")
|
||||
monkeypatch.setenv("CLEVERCLAUDE_API_PORT", "9000")
|
||||
|
||||
# Test that environment variables are respected
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp"):
|
||||
runner.invoke(app, ["start"])
|
||||
|
||||
# Should use environment variables for config
|
||||
# This would be verified in the actual app initialization
|
||||
|
||||
def test_cli_config_file_precedence(self, temp_dir):
|
||||
"""Test configuration file precedence over defaults."""
|
||||
runner = CliRunner()
|
||||
|
||||
# Create config file
|
||||
config_dir = temp_dir / ".cleverclaude"
|
||||
config_dir.mkdir()
|
||||
config_file = config_dir / "config.yaml"
|
||||
config_file.write_text("""
|
||||
app:
|
||||
name: "Custom CleverClaude"
|
||||
environment: "testing"
|
||||
api:
|
||||
host: "custom.host.com"
|
||||
port: 7777
|
||||
""")
|
||||
|
||||
with patch("cleverclaude.core.app.CleverClaudeApp"):
|
||||
runner.invoke(app, ["start", "--config-dir", str(config_dir)])
|
||||
|
||||
# Should use config file values
|
||||
# This would be verified in the settings loading
|
||||
@@ -0,0 +1,398 @@
|
||||
"""Unit tests for CleverClaude MCP (Model Context Protocol) client."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cleverclaude.config.settings import Settings
|
||||
from cleverclaude.mcp.client import MCPClient
|
||||
from cleverclaude.mcp.protocol import MCPError, MCPMessage, MCPProtocol, MCPToolInfo
|
||||
from cleverclaude.mcp.types import MCPToolExecutionResult
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestMCPClient:
|
||||
"""Test suite for MCPClient class."""
|
||||
|
||||
async def test_initialization(self, test_settings: Settings):
|
||||
"""Test MCPClient initialization."""
|
||||
client = MCPClient(test_settings)
|
||||
|
||||
assert client.config == test_settings
|
||||
assert client.protocol is not None
|
||||
assert client.available_tools == {}
|
||||
assert client.connected_servers == []
|
||||
assert client._initialized is False
|
||||
|
||||
async def test_initialize_client(self, test_settings: Settings):
|
||||
"""Test client initialization process."""
|
||||
client = MCPClient(test_settings)
|
||||
|
||||
with patch.object(client, "_connect_to_servers") as mock_connect:
|
||||
mock_connect.return_value = None
|
||||
|
||||
await client.initialize()
|
||||
|
||||
assert client._initialized is True
|
||||
mock_connect.assert_called_once()
|
||||
|
||||
async def test_connect_to_servers(self, test_settings: Settings):
|
||||
"""Test connection to MCP servers."""
|
||||
client = MCPClient(test_settings)
|
||||
|
||||
mock_server_configs = [
|
||||
{"name": "claude-flow-server", "url": "http://localhost:8001/mcp", "enabled": True},
|
||||
{"name": "neural-server", "url": "http://localhost:8002/mcp", "enabled": True},
|
||||
]
|
||||
|
||||
with patch.object(client, "_connect_server") as mock_connect_server:
|
||||
mock_connect_server.return_value = {
|
||||
"swarm_init": MCPToolInfo(name="swarm_init", description="Initialize swarm"),
|
||||
"agent_spawn": MCPToolInfo(name="agent_spawn", description="Spawn agent"),
|
||||
}
|
||||
|
||||
with patch.object(client.config, "mcp_servers", mock_server_configs):
|
||||
await client._connect_to_servers()
|
||||
|
||||
assert len(client.connected_servers) == 2
|
||||
assert "swarm_init" in client.available_tools
|
||||
assert "agent_spawn" in client.available_tools
|
||||
|
||||
async def test_execute_tool_success(self, test_settings: Settings):
|
||||
"""Test successful tool execution."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
# Mock tool availability
|
||||
client.available_tools["swarm_init"] = MCPToolInfo(
|
||||
name="swarm_init",
|
||||
description="Initialize swarm",
|
||||
parameters={"topology": {"type": "string"}, "maxAgents": {"type": "integer"}},
|
||||
)
|
||||
|
||||
expected_result = {"swarm_id": "swarm_123", "topology": "mesh", "status": "created"}
|
||||
|
||||
with patch.object(client.protocol, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(success=True, result=expected_result, error=None)
|
||||
|
||||
result = await client.execute_tool("swarm_init", {"topology": "mesh", "maxAgents": 5})
|
||||
|
||||
assert result.success is True
|
||||
assert result.result == expected_result
|
||||
mock_execute.assert_called_once_with("swarm_init", {"topology": "mesh", "maxAgents": 5})
|
||||
|
||||
async def test_execute_tool_not_found(self, test_settings: Settings):
|
||||
"""Test executing non-existent tool."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
with pytest.raises(ValueError, match="Tool 'nonexistent_tool' not found"):
|
||||
await client.execute_tool("nonexistent_tool", {})
|
||||
|
||||
async def test_execute_tool_with_error(self, test_settings: Settings):
|
||||
"""Test tool execution with error."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
# Mock tool availability
|
||||
client.available_tools["failing_tool"] = MCPToolInfo(name="failing_tool", description="Tool that fails")
|
||||
|
||||
with patch.object(client.protocol, "execute_tool") as mock_execute:
|
||||
mock_execute.return_value = MCPToolExecutionResult(
|
||||
success=False, result=None, error="Tool execution failed"
|
||||
)
|
||||
|
||||
result = await client.execute_tool("failing_tool", {})
|
||||
|
||||
assert result.success is False
|
||||
assert result.error == "Tool execution failed"
|
||||
|
||||
async def test_get_available_tools(self, test_settings: Settings):
|
||||
"""Test getting list of available tools."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
# Add mock tools
|
||||
client.available_tools = {
|
||||
"swarm_init": MCPToolInfo(name="swarm_init", description="Initialize swarm"),
|
||||
"agent_spawn": MCPToolInfo(name="agent_spawn", description="Spawn agent"),
|
||||
"task_orchestrate": MCPToolInfo(name="task_orchestrate", description="Orchestrate task"),
|
||||
}
|
||||
|
||||
tools = await client.get_available_tools()
|
||||
|
||||
assert len(tools) == 3
|
||||
assert "swarm_init" in tools
|
||||
assert "agent_spawn" in tools
|
||||
assert "task_orchestrate" in tools
|
||||
|
||||
async def test_get_tool_info(self, test_settings: Settings):
|
||||
"""Test getting tool information."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
tool_info = MCPToolInfo(
|
||||
name="swarm_init",
|
||||
description="Initialize swarm topology",
|
||||
parameters={
|
||||
"topology": {"type": "string", "enum": ["mesh", "hierarchical", "star", "ring"]},
|
||||
"maxAgents": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
},
|
||||
)
|
||||
client.available_tools["swarm_init"] = tool_info
|
||||
|
||||
retrieved_info = await client.get_tool_info("swarm_init")
|
||||
|
||||
assert retrieved_info == tool_info
|
||||
assert retrieved_info.name == "swarm_init"
|
||||
assert retrieved_info.description == "Initialize swarm topology"
|
||||
assert "topology" in retrieved_info.parameters
|
||||
assert "maxAgents" in retrieved_info.parameters
|
||||
|
||||
async def test_execute_multiple_tools(self, test_settings: Settings):
|
||||
"""Test executing multiple tools in sequence."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
# Mock multiple tools
|
||||
tools = ["swarm_init", "agent_spawn", "task_orchestrate"]
|
||||
for tool in tools:
|
||||
client.available_tools[tool] = MCPToolInfo(name=tool, description=f"Execute {tool}")
|
||||
|
||||
execution_sequence = [
|
||||
("swarm_init", {"topology": "mesh"}),
|
||||
("agent_spawn", {"type": "researcher"}),
|
||||
("task_orchestrate", {"task": "test task"}),
|
||||
]
|
||||
|
||||
results = []
|
||||
with patch.object(client.protocol, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result={"swarm_id": "swarm_1"}),
|
||||
MCPToolExecutionResult(success=True, result={"agent_id": "agent_1"}),
|
||||
MCPToolExecutionResult(success=True, result={"task_id": "task_1"}),
|
||||
]
|
||||
|
||||
for tool_name, params in execution_sequence:
|
||||
result = await client.execute_tool(tool_name, params)
|
||||
results.append(result)
|
||||
|
||||
assert len(results) == 3
|
||||
assert all(r.success for r in results)
|
||||
assert results[0].result["swarm_id"] == "swarm_1"
|
||||
assert results[1].result["agent_id"] == "agent_1"
|
||||
assert results[2].result["task_id"] == "task_1"
|
||||
|
||||
async def test_batch_execute_tools(self, test_settings: Settings):
|
||||
"""Test batch execution of tools."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
# Mock tools
|
||||
for tool in ["tool_1", "tool_2", "tool_3"]:
|
||||
client.available_tools[tool] = MCPToolInfo(name=tool, description=f"Tool {tool}")
|
||||
|
||||
batch_requests = [
|
||||
{"tool": "tool_1", "params": {"param": "value1"}},
|
||||
{"tool": "tool_2", "params": {"param": "value2"}},
|
||||
{"tool": "tool_3", "params": {"param": "value3"}},
|
||||
]
|
||||
|
||||
with patch.object(client.protocol, "execute_tool") as mock_execute:
|
||||
mock_execute.side_effect = [
|
||||
MCPToolExecutionResult(success=True, result={"result": f"result_{i}"}) for i in range(3)
|
||||
]
|
||||
|
||||
results = await client.batch_execute(batch_requests)
|
||||
|
||||
assert len(results) == 3
|
||||
assert all(r.success for r in results)
|
||||
assert mock_execute.call_count == 3
|
||||
|
||||
async def test_health_check(self, test_settings: Settings):
|
||||
"""Test MCP client health check."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
client.connected_servers = ["server_1", "server_2"]
|
||||
client.available_tools = {
|
||||
"tool_1": MCPToolInfo(name="tool_1", description="Tool 1"),
|
||||
"tool_2": MCPToolInfo(name="tool_2", description="Tool 2"),
|
||||
}
|
||||
|
||||
with patch.object(client.protocol, "ping_server") as mock_ping:
|
||||
mock_ping.return_value = True
|
||||
|
||||
health = await client.health_check()
|
||||
|
||||
assert health["status"] == "healthy"
|
||||
assert health["connected_servers"] == 2
|
||||
assert health["available_tools"] == 2
|
||||
assert health["server_connectivity"]["server_1"] is True
|
||||
assert health["server_connectivity"]["server_2"] is True
|
||||
|
||||
async def test_disconnect(self, test_settings: Settings):
|
||||
"""Test client disconnection and cleanup."""
|
||||
client = MCPClient(test_settings)
|
||||
await client.initialize()
|
||||
|
||||
client.connected_servers = ["server_1", "server_2"]
|
||||
|
||||
with patch.object(client.protocol, "disconnect") as mock_disconnect:
|
||||
await client.disconnect()
|
||||
|
||||
assert len(client.connected_servers) == 0
|
||||
assert len(client.available_tools) == 0
|
||||
assert client._initialized is False
|
||||
mock_disconnect.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestMCPProtocol:
|
||||
"""Test suite for MCPProtocol class."""
|
||||
|
||||
def test_create_message(self):
|
||||
"""Test MCP message creation."""
|
||||
protocol = MCPProtocol()
|
||||
|
||||
message = protocol.create_message(
|
||||
method="tools/execute", params={"tool": "swarm_init", "arguments": {"topology": "mesh"}}
|
||||
)
|
||||
|
||||
assert isinstance(message, MCPMessage)
|
||||
assert message.method == "tools/execute"
|
||||
assert message.params["tool"] == "swarm_init"
|
||||
assert "id" in message.dict() # Should have generated ID
|
||||
|
||||
def test_parse_response_success(self):
|
||||
"""Test parsing successful MCP response."""
|
||||
protocol = MCPProtocol()
|
||||
|
||||
response_data = {
|
||||
"id": "request_123",
|
||||
"result": {"success": True, "data": {"swarm_id": "swarm_456", "status": "created"}},
|
||||
}
|
||||
|
||||
result = protocol.parse_response(response_data)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["data"]["swarm_id"] == "swarm_456"
|
||||
|
||||
def test_parse_response_error(self):
|
||||
"""Test parsing MCP error response."""
|
||||
protocol = MCPProtocol()
|
||||
|
||||
response_data = {
|
||||
"id": "request_123",
|
||||
"error": {"code": -32601, "message": "Method not found", "data": {"method": "unknown_method"}},
|
||||
}
|
||||
|
||||
with pytest.raises(MCPError, match="Method not found"):
|
||||
protocol.parse_response(response_data)
|
||||
|
||||
def test_validate_tool_parameters(self):
|
||||
"""Test tool parameter validation."""
|
||||
protocol = MCPProtocol()
|
||||
|
||||
tool_info = MCPToolInfo(
|
||||
name="swarm_init",
|
||||
description="Initialize swarm",
|
||||
parameters={
|
||||
"topology": {"type": "string", "enum": ["mesh", "hierarchical"]},
|
||||
"maxAgents": {"type": "integer", "minimum": 1, "maximum": 100},
|
||||
},
|
||||
)
|
||||
|
||||
# Valid parameters
|
||||
valid_params = {"topology": "mesh", "maxAgents": 10}
|
||||
assert protocol.validate_parameters(tool_info, valid_params) is True
|
||||
|
||||
# Invalid topology
|
||||
invalid_params = {"topology": "invalid", "maxAgents": 10}
|
||||
assert protocol.validate_parameters(tool_info, invalid_params) is False
|
||||
|
||||
# Out of range maxAgents
|
||||
invalid_params = {"topology": "mesh", "maxAgents": 200}
|
||||
assert protocol.validate_parameters(tool_info, invalid_params) is False
|
||||
|
||||
async def test_execute_tool_with_retry(self):
|
||||
"""Test tool execution with retry logic."""
|
||||
protocol = MCPProtocol()
|
||||
|
||||
# Mock connection that fails first time, succeeds second time
|
||||
call_count = 0
|
||||
|
||||
async def mock_send_request(message):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise ConnectionError("Network error")
|
||||
return {"id": message.id, "result": {"success": True, "data": "success"}}
|
||||
|
||||
with patch.object(protocol, "_send_request", side_effect=mock_send_request):
|
||||
result = await protocol.execute_tool("test_tool", {}, max_retries=2)
|
||||
|
||||
assert result.success is True
|
||||
assert call_count == 2 # Should have retried once
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMCPTypes:
|
||||
"""Test suite for MCP type definitions."""
|
||||
|
||||
def test_mcp_message_creation(self):
|
||||
"""Test MCPMessage creation."""
|
||||
message = MCPMessage(method="tools/list", params={"category": "swarm"}, id="msg_123")
|
||||
|
||||
assert message.method == "tools/list"
|
||||
assert message.params == {"category": "swarm"}
|
||||
assert message.id == "msg_123"
|
||||
|
||||
def test_mcp_tool_info_creation(self):
|
||||
"""Test MCPToolInfo creation."""
|
||||
tool_info = MCPToolInfo(
|
||||
name="agent_spawn",
|
||||
description="Spawn a new agent",
|
||||
parameters={
|
||||
"type": {"type": "string", "enum": ["researcher", "coder", "analyst"]},
|
||||
"name": {"type": "string"},
|
||||
"capabilities": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
)
|
||||
|
||||
assert tool_info.name == "agent_spawn"
|
||||
assert tool_info.description == "Spawn a new agent"
|
||||
assert "type" in tool_info.parameters
|
||||
assert "capabilities" in tool_info.parameters
|
||||
|
||||
def test_mcp_tool_execution_result(self):
|
||||
"""Test MCPToolExecutionResult creation."""
|
||||
# Successful result
|
||||
success_result = MCPToolExecutionResult(
|
||||
success=True, result={"agent_id": "agent_123", "status": "active"}, error=None, execution_time=0.5
|
||||
)
|
||||
|
||||
assert success_result.success is True
|
||||
assert success_result.result["agent_id"] == "agent_123"
|
||||
assert success_result.error is None
|
||||
assert success_result.execution_time == 0.5
|
||||
|
||||
# Error result
|
||||
error_result = MCPToolExecutionResult(
|
||||
success=False, result=None, error="Agent creation failed", execution_time=0.1
|
||||
)
|
||||
|
||||
assert error_result.success is False
|
||||
assert error_result.result is None
|
||||
assert error_result.error == "Agent creation failed"
|
||||
|
||||
def test_mcp_error_creation(self):
|
||||
"""Test MCPError exception creation."""
|
||||
error = MCPError(code=-32601, message="Method not found", data={"method": "unknown_method"})
|
||||
|
||||
assert error.code == -32601
|
||||
assert error.message == "Method not found"
|
||||
assert error.data["method"] == "unknown_method"
|
||||
assert "Method not found" in str(error)
|
||||
@@ -0,0 +1,414 @@
|
||||
"""Unit tests for CleverClaude swarm coordination."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cleverclaude.agents.types import AgentType
|
||||
from cleverclaude.config.settings import Settings
|
||||
from cleverclaude.coordination.swarm import SwarmCoordinator
|
||||
from cleverclaude.coordination.types import SwarmConfig, SwarmState, SwarmTask, SwarmTopology, TaskPriority
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.async_test
|
||||
class TestSwarmCoordinator:
|
||||
"""Test suite for SwarmCoordinator class."""
|
||||
|
||||
async def test_initialization(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test SwarmCoordinator initialization."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
|
||||
assert coordinator.config == test_settings.swarm
|
||||
assert coordinator.session == async_session
|
||||
assert coordinator.agent_manager == mock_agent_manager
|
||||
assert coordinator.redis == mock_redis
|
||||
assert coordinator.swarms == {}
|
||||
assert coordinator._initialized is False
|
||||
|
||||
async def test_initialize_coordinator(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test coordinator initialization process."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
|
||||
with patch("cleverclaude.coordination.swarm.structlog.get_logger") as mock_logger:
|
||||
await coordinator.initialize()
|
||||
|
||||
assert coordinator._initialized is True
|
||||
mock_logger.assert_called_once()
|
||||
|
||||
async def test_create_swarm_success(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test successful swarm creation."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
swarm_id = await coordinator.create_swarm(name="Test Swarm", topology=SwarmTopology.MESH, max_agents=10)
|
||||
|
||||
assert swarm_id in coordinator.swarms
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert swarm["name"] == "Test Swarm"
|
||||
assert swarm["topology"] == SwarmTopology.MESH
|
||||
assert swarm["state"] == SwarmState.ACTIVE
|
||||
assert swarm["max_agents"] == 10
|
||||
|
||||
async def test_create_swarm_with_agents(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test swarm creation with initial agents."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Mock agent creation
|
||||
mock_agent_ids = ["agent_1", "agent_2", "agent_3"]
|
||||
mock_agent_manager.create_agent.side_effect = mock_agent_ids
|
||||
|
||||
swarm_id = await coordinator.create_swarm(
|
||||
name="Test Swarm",
|
||||
topology=SwarmTopology.HIERARCHICAL,
|
||||
initial_agents=[
|
||||
{"type": AgentType.COORDINATOR, "name": "coordinator"},
|
||||
{"type": AgentType.RESEARCHER, "name": "researcher_1"},
|
||||
{"type": AgentType.ANALYST, "name": "analyst_1"},
|
||||
],
|
||||
)
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert len(swarm["agents"]) == 3
|
||||
assert mock_agent_manager.create_agent.call_count == 3
|
||||
|
||||
async def test_add_agent_to_swarm(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test adding an agent to an existing swarm."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm
|
||||
swarm_id = await coordinator.create_swarm("Test Swarm", SwarmTopology.MESH)
|
||||
|
||||
# Add agent
|
||||
agent_id = "test_agent_123"
|
||||
await coordinator.add_agent(swarm_id, agent_id, role="worker")
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert len(swarm["agents"]) == 1
|
||||
assert swarm["agents"][0]["agent_id"] == agent_id
|
||||
assert swarm["agents"][0]["role"] == "worker"
|
||||
|
||||
async def test_add_agent_max_limit(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test adding agent when max limit is reached."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with low max limit
|
||||
swarm_id = await coordinator.create_swarm("Test Swarm", SwarmTopology.MESH, max_agents=1)
|
||||
|
||||
# Add first agent (should succeed)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
|
||||
# Try to add second agent (should fail)
|
||||
with pytest.raises(ValueError, match="Maximum number of agents reached"):
|
||||
await coordinator.add_agent(swarm_id, "agent_2", role="worker")
|
||||
|
||||
async def test_remove_agent_from_swarm(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test removing an agent from swarm."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm and add agent
|
||||
swarm_id = await coordinator.create_swarm("Test Swarm", SwarmTopology.MESH)
|
||||
agent_id = "test_agent_123"
|
||||
await coordinator.add_agent(swarm_id, agent_id, role="worker")
|
||||
|
||||
# Remove agent
|
||||
await coordinator.remove_agent(swarm_id, agent_id)
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert len(swarm["agents"]) == 0
|
||||
|
||||
async def test_submit_task_to_swarm(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test submitting a task to swarm."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
mock_agent_manager.execute_task.return_value = {"status": "completed", "result": "task done"}
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with agents
|
||||
swarm_id = await coordinator.create_swarm("Test Swarm", SwarmTopology.MESH)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
|
||||
# Submit task
|
||||
task = SwarmTask(
|
||||
task_type="analysis",
|
||||
priority=TaskPriority.NORMAL,
|
||||
data={"analysis_type": "data_analysis", "dataset": {"records": ["data_1", "data_2"]}},
|
||||
)
|
||||
|
||||
task_id = await coordinator.submit_task(swarm_id, task)
|
||||
|
||||
assert task_id in coordinator.swarms[swarm_id]["tasks"]
|
||||
task_info = coordinator.swarms[swarm_id]["tasks"][task_id]
|
||||
assert task_info["task_type"] == "analysis"
|
||||
assert task_info["status"] == "submitted"
|
||||
|
||||
async def test_task_distribution_mesh(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test task distribution in mesh topology."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
mock_agent_manager.execute_task.return_value = {"status": "completed"}
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create mesh swarm with multiple agents
|
||||
swarm_id = await coordinator.create_swarm("Mesh Swarm", SwarmTopology.MESH)
|
||||
for i in range(3):
|
||||
await coordinator.add_agent(swarm_id, f"agent_{i}", role="worker")
|
||||
|
||||
# Submit multiple tasks
|
||||
tasks = []
|
||||
for i in range(5):
|
||||
task = SwarmTask(task_type="processing", priority=TaskPriority.NORMAL, data={"task_id": i})
|
||||
task_id = await coordinator.submit_task(swarm_id, task)
|
||||
tasks.append(task_id)
|
||||
|
||||
# Process tasks
|
||||
await coordinator._process_pending_tasks(swarm_id)
|
||||
|
||||
# Verify tasks were distributed
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
completed_tasks = [t for t in swarm["tasks"].values() if t["status"] == "completed"]
|
||||
assert len(completed_tasks) == 5
|
||||
|
||||
async def test_task_distribution_hierarchical(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test task distribution in hierarchical topology."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
mock_agent_manager.execute_task.return_value = {"status": "completed"}
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create hierarchical swarm
|
||||
swarm_id = await coordinator.create_swarm("Hierarchical Swarm", SwarmTopology.HIERARCHICAL)
|
||||
|
||||
# Add agents with hierarchy
|
||||
await coordinator.add_agent(swarm_id, "coordinator", role="coordinator", level=0)
|
||||
await coordinator.add_agent(swarm_id, "team_lead_1", role="team_lead", level=1)
|
||||
await coordinator.add_agent(swarm_id, "worker_1", role="worker", level=2)
|
||||
await coordinator.add_agent(swarm_id, "worker_2", role="worker", level=2)
|
||||
|
||||
# Submit complex task
|
||||
task = SwarmTask(
|
||||
task_type="complex_analysis",
|
||||
priority=TaskPriority.HIGH,
|
||||
data={"requires_coordination": True, "subtasks": 3},
|
||||
)
|
||||
|
||||
task_id = await coordinator.submit_task(swarm_id, task)
|
||||
|
||||
# Process task
|
||||
await coordinator._process_pending_tasks(swarm_id)
|
||||
|
||||
# Verify hierarchical processing
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
task_info = swarm["tasks"][task_id]
|
||||
assert task_info["assigned_coordinator"] == "coordinator"
|
||||
|
||||
async def test_get_swarm_metrics(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test swarm metrics collection."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with agents and tasks
|
||||
swarm_id = await coordinator.create_swarm("Metrics Swarm", SwarmTopology.MESH)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
await coordinator.add_agent(swarm_id, "agent_2", role="worker")
|
||||
|
||||
# Simulate completed tasks
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
swarm["tasks"]["task_1"] = {"status": "completed", "completed_at": datetime.utcnow() - timedelta(minutes=5)}
|
||||
swarm["tasks"]["task_2"] = {"status": "completed", "completed_at": datetime.utcnow() - timedelta(minutes=3)}
|
||||
swarm["tasks"]["task_3"] = {"status": "running", "started_at": datetime.utcnow() - timedelta(minutes=1)}
|
||||
|
||||
metrics = await coordinator.get_swarm_metrics(swarm_id)
|
||||
|
||||
assert metrics.total_agents == 2
|
||||
assert metrics.active_agents == 2
|
||||
assert metrics.completed_tasks == 2
|
||||
assert metrics.running_tasks == 1
|
||||
assert metrics.efficiency_score > 0
|
||||
|
||||
async def test_scale_swarm_up(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test scaling swarm up (adding agents)."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
mock_agent_manager.create_agent.return_value = "new_agent_123"
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with initial agents
|
||||
swarm_id = await coordinator.create_swarm("Scalable Swarm", SwarmTopology.MESH, max_agents=10)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
|
||||
# Scale up
|
||||
await coordinator.scale_swarm(swarm_id, target_size=3)
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert len(swarm["agents"]) == 3
|
||||
assert mock_agent_manager.create_agent.call_count == 2 # 2 new agents created
|
||||
|
||||
async def test_scale_swarm_down(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test scaling swarm down (removing agents)."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with multiple agents
|
||||
swarm_id = await coordinator.create_swarm("Scalable Swarm", SwarmTopology.MESH)
|
||||
for i in range(5):
|
||||
await coordinator.add_agent(swarm_id, f"agent_{i}", role="worker")
|
||||
|
||||
# Scale down
|
||||
await coordinator.scale_swarm(swarm_id, target_size=2)
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
assert len(swarm["agents"]) == 2
|
||||
|
||||
async def test_swarm_health_monitoring(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test swarm health monitoring."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
mock_agent_manager.get_agent_status.return_value = {
|
||||
"status": "active",
|
||||
"health": {"cpu_usage": 50, "memory_usage": 200, "task_count": 2},
|
||||
}
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with agents
|
||||
swarm_id = await coordinator.create_swarm("Health Swarm", SwarmTopology.MESH)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
await coordinator.add_agent(swarm_id, "agent_2", role="worker")
|
||||
|
||||
# Check health
|
||||
health_report = await coordinator.check_swarm_health(swarm_id)
|
||||
|
||||
assert health_report["swarm_id"] == swarm_id
|
||||
assert health_report["total_agents"] == 2
|
||||
assert len(health_report["agent_health"]) == 2
|
||||
assert health_report["overall_health"] in ["healthy", "degraded", "critical"]
|
||||
|
||||
async def test_handle_agent_failure(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test handling agent failure in swarm."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with agents
|
||||
swarm_id = await coordinator.create_swarm("Resilient Swarm", SwarmTopology.MESH)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
await coordinator.add_agent(swarm_id, "agent_2", role="worker")
|
||||
|
||||
# Simulate agent failure
|
||||
await coordinator.handle_agent_failure(swarm_id, "agent_1")
|
||||
|
||||
swarm = coordinator.swarms[swarm_id]
|
||||
failed_agent = next((a for a in swarm["agents"] if a["agent_id"] == "agent_1"), None)
|
||||
assert failed_agent["status"] == "failed"
|
||||
|
||||
# Verify tasks are redistributed
|
||||
# This would check that tasks assigned to failed agent are reassigned
|
||||
|
||||
async def test_destroy_swarm(self, test_settings: Settings, async_session, mock_redis):
|
||||
"""Test swarm destruction and cleanup."""
|
||||
mock_agent_manager = AsyncMock()
|
||||
|
||||
coordinator = SwarmCoordinator(test_settings.swarm, async_session, mock_agent_manager, mock_redis)
|
||||
await coordinator.initialize()
|
||||
|
||||
# Create swarm with agents
|
||||
swarm_id = await coordinator.create_swarm("Doomed Swarm", SwarmTopology.MESH)
|
||||
await coordinator.add_agent(swarm_id, "agent_1", role="worker")
|
||||
await coordinator.add_agent(swarm_id, "agent_2", role="worker")
|
||||
|
||||
# Destroy swarm
|
||||
await coordinator.destroy_swarm(swarm_id)
|
||||
|
||||
# Verify cleanup
|
||||
assert swarm_id not in coordinator.swarms
|
||||
# Verify agents were destroyed/removed
|
||||
assert mock_agent_manager.destroy_agent.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSwarmTypes:
|
||||
"""Test suite for swarm type definitions."""
|
||||
|
||||
def test_swarm_topology_enum(self):
|
||||
"""Test SwarmTopology enumeration."""
|
||||
assert SwarmTopology.MESH == "mesh"
|
||||
assert SwarmTopology.HIERARCHICAL == "hierarchical"
|
||||
assert SwarmTopology.STAR == "star"
|
||||
assert SwarmTopology.RING == "ring"
|
||||
|
||||
def test_swarm_state_enum(self):
|
||||
"""Test SwarmState enumeration."""
|
||||
assert SwarmState.INITIALIZING == "initializing"
|
||||
assert SwarmState.ACTIVE == "active"
|
||||
assert SwarmState.IDLE == "idle"
|
||||
assert SwarmState.PAUSED == "paused"
|
||||
assert SwarmState.SCALING == "scaling"
|
||||
assert SwarmState.TERMINATED == "terminated"
|
||||
|
||||
def test_task_priority_enum(self):
|
||||
"""Test TaskPriority enumeration."""
|
||||
assert TaskPriority.LOW == "low"
|
||||
assert TaskPriority.NORMAL == "normal"
|
||||
assert TaskPriority.HIGH == "high"
|
||||
assert TaskPriority.CRITICAL == "critical"
|
||||
|
||||
def test_swarm_task_creation(self):
|
||||
"""Test SwarmTask creation and validation."""
|
||||
task = SwarmTask(
|
||||
task_type="analysis",
|
||||
priority=TaskPriority.HIGH,
|
||||
data={"analysis_type": "sentiment", "dataset": "customer_reviews"},
|
||||
timeout=600,
|
||||
dependencies=["task_1", "task_2"],
|
||||
)
|
||||
|
||||
assert task.task_type == "analysis"
|
||||
assert task.priority == TaskPriority.HIGH
|
||||
assert task.data["analysis_type"] == "sentiment"
|
||||
assert task.timeout == 600
|
||||
assert task.dependencies == ["task_1", "task_2"]
|
||||
|
||||
def test_swarm_config_creation(self):
|
||||
"""Test SwarmConfig creation and validation."""
|
||||
config = SwarmConfig(
|
||||
name="Test Swarm",
|
||||
topology=SwarmTopology.HIERARCHICAL,
|
||||
max_agents=20,
|
||||
coordination_timeout=120,
|
||||
load_balancing=True,
|
||||
)
|
||||
|
||||
assert config.name == "Test Swarm"
|
||||
assert config.topology == SwarmTopology.HIERARCHICAL
|
||||
assert config.max_agents == 20
|
||||
assert config.coordination_timeout == 120
|
||||
assert config.load_balancing is True
|
||||
|
||||
def test_swarm_config_defaults(self):
|
||||
"""Test SwarmConfig default values."""
|
||||
config = SwarmConfig(name="Default Swarm", topology=SwarmTopology.MESH)
|
||||
|
||||
assert config.max_agents == 50 # Default
|
||||
assert config.coordination_timeout == 60 # Default
|
||||
assert config.load_balancing is True # Default
|
||||
Reference in New Issue
Block a user