453 lines
15 KiB
Python
453 lines
15 KiB
Python
"""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
|