"""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