ClaudeFlow ported, needs cleanup
This commit is contained in:
@@ -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