Files
cleverclaude-core/tests/unit/test_cli.py
T
2025-08-10 12:00:13 -04:00

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