"""Tests for the main module - CLI entry point.""" import argparse import logging import sys from pathlib import Path from unittest.mock import MagicMock, Mock, call, patch import pytest from nodupe.core.main import CLIHandler, main class TestCLIHandlerInitialization: """Test CLIHandler initialization and setup.""" def test_cli_handler_creation(self): """Test CLIHandler instance creation.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) assert cli is not None assert cli.loader is mock_loader assert cli.parser is not None def test_cli_handler_with_tool_registry(self): """Test CLIHandler with tool registry.""" mock_loader = Mock() mock_registry = Mock() mock_registry.get_tools.return_value = [] mock_loader.tool_registry = mock_registry cli = CLIHandler(mock_loader) assert cli.loader.tool_registry is mock_registry def test_create_parser_structure(self): """Test parser has expected structure.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) parser = cli._create_parser() assert parser is not None assert parser.description is not None def test_parser_has_verbose_flag(self): """Test parser has verbose flag.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--verbose']) assert args.verbose is True def test_parser_has_debug_flag(self): """Test parser has debug flag.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--debug']) assert args.debug is True def test_parser_has_speed_option(self): """Test parser has speed option.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--speed', 'fast']) assert args.speed == 'fast' def test_parser_has_cores_option(self): """Test parser has cores option.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--cores', '8']) assert args.cores == 8 def test_parser_has_max_workers_option(self): """Test parser has max_workers option.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--max-workers', '16']) assert args.max_workers == 16 def test_parser_has_batch_size_option(self): """Test parser has batch_size option.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) args = cli.parser.parse_args(['--batch-size', '500']) assert args.batch_size == 500 class TestCLIHandlerCommandRegistration: """Test command registration functionality.""" def test_register_builtin_commands(self): """Test built-in commands are registered.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) # Parse known commands to verify they exist args = cli.parser.parse_args(['version']) assert args.command == 'version' args = cli.parser.parse_args(['tools']) assert args.command == 'tools' args = cli.parser.parse_args(['tool']) assert args.command == 'tool' args = cli.parser.parse_args(['scan', '/path']) assert args.command == 'scan' args = cli.parser.parse_args(['similarity']) assert args.command == 'similarity' args = cli.parser.parse_args(['plan']) assert args.command == 'plan' def test_register_tool_commands_from_registry(self): """Test registering tool commands from registry.""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "TestTool" mock_tool.register_commands = Mock() mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry cli = CLIHandler.__new__(CLIHandler) cli.loader = mock_loader cli.parser = argparse.ArgumentParser() cli._register_commands() mock_tool.register_commands.assert_called_once() def test_register_tool_commands_handles_exception(self, caplog): """Test tool command registration handles exceptions.""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "FailingTool" mock_tool.register_commands = Mock(side_effect=Exception("Registration failed")) mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry cli = CLIHandler.__new__(CLIHandler) cli.loader = mock_loader cli.parser = argparse.ArgumentParser() with caplog.at_level(logging.WARNING): cli._register_commands() # Should not raise, just log warning mock_tool.register_commands.assert_called_once() def test_register_tool_commands_tool_without_register_commands(self): """Test tool without register_commands method.""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "NoRegisterTool" # Remove register_commands attribute del mock_tool.register_commands mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry cli = CLIHandler.__new__(CLIHandler) cli.loader = mock_loader cli.parser = argparse.ArgumentParser() # Should not raise cli._register_commands() class TestCLIHandlerRun: """Test CLIHandler run method.""" def test_run_with_no_args_shows_help(self): """Test run with no arguments shows help.""" mock_loader = Mock() mock_loader.tool_registry = None mock_loader.config = Mock() mock_loader.config.config = {} cli = CLIHandler(mock_loader) with patch.object(cli.parser, 'parse_args') as mock_parse: mock_parsed = Mock() mock_parsed.debug = False del mock_parsed.func mock_parse.return_value = mock_parsed with patch.object(cli.parser, 'print_help') as mock_help: result = cli.run([]) mock_help.assert_called_once() assert result == 0 def test_run_with_version_command(self, capsys): """Test run with version command.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['version']) captured = capsys.readouterr() assert "NoDupeLabs CLI v1.0.0" in captured.out assert result == 0 def test_run_with_tools_list_command(self, capsys): """Test run with tools --list command.""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "TestTool" mock_tool.version = "1.0.0" mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['tools', '--list']) captured = capsys.readouterr() assert "TestTool" in captured.out assert result == 0 def test_run_with_scan_command(self, capsys, tmp_path): """Test run with scan command.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) # Create a test directory test_dir = tmp_path / "test_scan" test_dir.mkdir() (test_dir / "file1.txt").write_text("content1") (test_dir / "file2.txt").write_text("content2") result = cli.run(['scan', str(test_dir)]) captured = capsys.readouterr() assert "Scanning:" in captured.out assert result == 0 def test_run_with_scan_nonexistent_path(self, capsys): """Test run with scan command on nonexistent path.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['scan', '/nonexistent/path']) # Just check that it returns error code 1 assert result == 1 def test_run_with_scan_file_not_directory(self, capsys, tmp_path): """Test run with scan command on file instead of directory.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) test_file = tmp_path / "test.txt" test_file.write_text("content") result = cli.run(['scan', str(test_file)]) # Just check that it returns error code 1 assert result == 1 def test_run_with_similarity_command(self, capsys): """Test run with similarity command.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['similarity', '/path']) captured = capsys.readouterr() assert "Similarity analysis" in captured.out assert result == 0 def test_run_with_plan_command(self, capsys): """Test run with plan command.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['plan', '/path']) captured = capsys.readouterr() assert "Creating deduplication plan" in captured.out assert result == 0 def test_run_with_debug_flag(self, capsys): """Test run with debug flag.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) with patch.object(cli, '_setup_debug_logging') as mock_setup: result = cli.run(['--debug', 'version']) mock_setup.assert_called_once() assert result == 0 def test_run_with_exception(self, capsys): """Test run handles exceptions in commands.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) # Create args that will raise exception mock_parsed = Mock() mock_parsed.func = Mock(side_effect=Exception("Command failed")) mock_parsed.debug = False with patch.object(cli.parser, 'parse_args', return_value=mock_parsed): result = cli.run([]) assert result == 1 def test_run_with_debug_and_exception(self, capsys): """Test run with debug flag and exception shows traceback.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) mock_parsed = Mock() mock_parsed.func = Mock(side_effect=Exception("Command failed")) mock_parsed.debug = True with patch.object(cli.parser, 'parse_args', return_value=mock_parsed): with patch('traceback.print_exc') as mock_traceback: result = cli.run([]) mock_traceback.assert_called_once() assert result == 1 class TestCLIHandlerCommands: """Test individual CLI handler commands.""" def test_cmd_version_basic(self, capsys): """Test version command basic output.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli._cmd_version(Mock()) captured = capsys.readouterr() assert "NoDupeLabs CLI v1.0.0" in captured.out assert result == 0 def test_cmd_version_with_config(self, capsys): """Test version command with config.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = { 'drive_type': 'SSD', 'cpu_cores': 8, 'ram_gb': 16 } mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli._cmd_version(Mock()) captured = capsys.readouterr() assert "SSD" in captured.out assert "8" in captured.out assert "16" in captured.out assert result == 0 def test_cmd_version_no_config_attribute(self, capsys): """Test version command when config has no config attribute.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock(spec=[]) mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli._cmd_version(Mock()) captured = capsys.readouterr() assert "NoDupeLabs CLI v1.0.0" in captured.out assert result == 0 def test_cmd_tools_no_registry(self, capsys): """Test tools command when registry is not active.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) result = cli._cmd_tool(Mock()) # Just check that it returns error code 1 assert result == 1 def test_cmd_tools_list(self, capsys): """Test tools list command.""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "TestTool" mock_tool.version = "1.0.0" mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.list = True result = cli._cmd_tool(mock_args) captured = capsys.readouterr() assert "TestTool" in captured.out assert result == 0 def test_cmd_tools_list_accessible_tool(self, capsys): """Test tools list with accessible tool.""" from nodupe.core.tool_system.base import AccessibleTool mock_loader = Mock() class MockAccessibleTool(AccessibleTool): """Mock accessible tool for testing.""" name = "AccessibleTool" version = "1.0.0" dependencies = [] def initialize(self, container): """Initialize the mock tool.""" pass def shutdown(self): """Shutdown the mock tool.""" pass def get_capabilities(self): """Get tool capabilities.""" return {} @property def api_methods(self): """Return API methods.""" return {} def run_standalone(self, args): """Run tool as standalone.""" return 0 def describe_usage(self): """Describe tool usage.""" return "Usage" mock_registry = Mock() mock_registry.get_tools.return_value = [MockAccessibleTool()] mock_loader.tool_registry = mock_registry cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.list = True result = cli._cmd_tool(mock_args) captured = capsys.readouterr() assert "AccessibleTool" in captured.out assert result == 0 def test_cmd_tools_list_non_accessible_tool(self, capsys): """Test tools list with non-accessible tool.""" from nodupe.core.tool_system.base import Tool mock_loader = Mock() class MockNonAccessibleTool(Tool): """Mock non-accessible tool for testing.""" name = "NonAccessibleTool" version = "1.0.0" dependencies = [] def initialize(self, container): """Initialize the mock tool.""" pass def shutdown(self): """Shutdown the mock tool.""" pass def get_capabilities(self): """Get tool capabilities.""" return {} @property def api_methods(self): """Return API methods.""" return {} def run_standalone(self, args): """Run tool as standalone.""" return 0 def describe_usage(self): """Describe tool usage.""" return "Usage" mock_registry = Mock() mock_registry.get_tools.return_value = [MockNonAccessibleTool()] mock_loader.tool_registry = mock_registry cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.list = True result = cli._cmd_tool(mock_args) captured = capsys.readouterr() assert "NonAccessibleTool" in captured.out assert result == 0 def test_cmd_tools_no_list_flag(self, capsys): """Test tools command without list flag.""" mock_loader = Mock() mock_registry = Mock() mock_registry.get_tools.return_value = [] mock_loader.tool_registry = mock_registry cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.list = False result = cli._cmd_tool(mock_args) assert result == 0 def test_cmd_scan_basic(self, capsys, tmp_path): """Test scan command basic functionality.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) test_dir = tmp_path / "scan_test" test_dir.mkdir() mock_args = Mock() mock_args.path = str(test_dir) result = cli._cmd_scan(mock_args) captured = capsys.readouterr() assert "Scanning:" in captured.out assert result == 0 def test_cmd_similarity_basic(self, capsys): """Test similarity command basic functionality.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.path = "/test/path" result = cli._cmd_similarity(mock_args) captured = capsys.readouterr() assert "Similarity analysis" in captured.out assert result == 0 def test_cmd_plan_basic(self, capsys): """Test plan command basic functionality.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.path = "/test/path" result = cli._cmd_plan(mock_args) captured = capsys.readouterr() assert "Creating deduplication plan" in captured.out assert result == 0 class TestCLIHandlerUtilities: """Test CLIHandler utility methods.""" def test_setup_debug_logging(self): """Test debug logging setup.""" mock_loader = Mock() mock_loader.tool_registry = None cli = CLIHandler(mock_loader) with patch('logging.getLogger') as mock_get_logger: mock_logger = Mock() mock_get_logger.return_value = mock_logger cli._setup_debug_logging() mock_logger.setLevel.assert_called_once() def test_apply_overrides_all(self): """Test applying all performance overrides.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config_obj = Mock() mock_config_obj.config = {} mock_loader.config = mock_config_obj cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.cores = 8 mock_args.max_workers = 16 mock_args.batch_size = 500 cli._apply_overrides(mock_args) assert mock_config_obj.config['cpu_cores'] == 8 assert mock_config_obj.config['max_workers'] == 16 assert mock_config_obj.config['batch_size'] == 500 def test_apply_overrides_partial(self): """Test applying partial performance overrides.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config_obj = Mock() mock_config_obj.config = {} mock_loader.config = mock_config_obj cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.cores = 4 mock_args.max_workers = None mock_args.batch_size = None cli._apply_overrides(mock_args) assert mock_config_obj.config['cpu_cores'] == 4 assert 'max_workers' not in mock_config_obj.config assert 'batch_size' not in mock_config_obj.config def test_apply_overrides_no_config(self): """Test apply overrides with no config.""" mock_loader = Mock() mock_loader.tool_registry = None mock_loader.config = None cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.cores = 8 # Should not raise cli._apply_overrides(mock_args) def test_apply_overrides_config_no_config_attr(self): """Test apply overrides when config has no config attribute.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock(spec=[]) mock_loader.config = mock_config cli = CLIHandler(mock_loader) mock_args = Mock() mock_args.cores = 8 # Should not raise cli._apply_overrides(mock_args) class TestMainFunction: """Test main entry point function.""" def test_main_success(self): """Test main function success case.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap, \ patch('nodupe.core.main.CLIHandler') as mock_cli_class: mock_loader = Mock() mock_bootstrap.return_value = mock_loader mock_cli = Mock() mock_cli.run.return_value = 0 mock_cli_class.return_value = mock_cli result = main(['version']) mock_bootstrap.assert_called_once() mock_cli_class.assert_called_once_with(mock_loader) mock_cli.run.assert_called_once_with(['version']) mock_loader.shutdown.assert_called_once() assert result == 0 def test_main_keyboard_interrupt(self, capsys): """Test main function with keyboard interrupt.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap, \ patch('nodupe.core.main.CLIHandler') as mock_cli_class: mock_loader = Mock() mock_bootstrap.return_value = mock_loader mock_cli = Mock() mock_cli.run.side_effect = KeyboardInterrupt() mock_cli_class.return_value = mock_cli result = main([]) assert result == 130 mock_loader.shutdown.assert_called_once() def test_main_exception_during_run(self, capsys): """Test main function with exception during run.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap, \ patch('nodupe.core.main.CLIHandler') as mock_cli_class: mock_loader = Mock() mock_bootstrap.return_value = mock_loader mock_cli = Mock() mock_cli.run.side_effect = Exception("Run failed") mock_cli_class.return_value = mock_cli result = main([]) assert result == 1 mock_loader.shutdown.assert_called_once() def test_main_bootstrap_exception(self, capsys): """Test main function with bootstrap exception.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap: mock_bootstrap.side_effect = Exception("Bootstrap failed") result = main([]) assert result == 1 def test_main_shutdown_exception(self, capsys): """Test main function with shutdown exception.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap, \ patch('nodupe.core.main.CLIHandler') as mock_cli_class: mock_loader = Mock() mock_bootstrap.return_value = mock_loader mock_loader.shutdown.side_effect = Exception("Shutdown failed") mock_cli = Mock() mock_cli.run.return_value = 0 mock_cli_class.return_value = mock_cli result = main([]) assert result == 0 mock_loader.shutdown.assert_called_once() def test_main_with_none_args(self): """Test main function with None args.""" with patch('nodupe.core.main.bootstrap') as mock_bootstrap, \ patch('nodupe.core.main.CLIHandler') as mock_cli_class: mock_loader = Mock() mock_bootstrap.return_value = mock_loader mock_cli = Mock() mock_cli.run.return_value = 0 mock_cli_class.return_value = mock_cli result = main(None) assert result == 0 class TestCLIHandlerEdgeCases: """Test edge cases for CLIHandler.""" def test_tool_alias_command(self, capsys): """Test tool alias command (alias for tools).""" mock_loader = Mock() mock_tool = Mock() mock_tool.name = "TestTool" mock_tool.version = "1.0.0" mock_registry = Mock() mock_registry.get_tools.return_value = [mock_tool] mock_loader.tool_registry = mock_registry mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli.run(['tool', '--list']) captured = capsys.readouterr() assert "TestTool" in captured.out assert result == 0 def test_run_injects_container_into_args(self): """Test that run injects container into args.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) mock_parsed = Mock() mock_parsed.func = Mock(return_value=0) mock_parsed.debug = False with patch.object(cli.parser, 'parse_args', return_value=mock_parsed): cli.run([]) assert mock_parsed.container == mock_loader.container def test_cmd_version_accessibility_compliance(self, capsys): """Test version command reports accessibility compliance.""" mock_loader = Mock() mock_loader.tool_registry = None mock_config = Mock() mock_config.config = {} mock_loader.config = mock_config cli = CLIHandler(mock_loader) result = cli._cmd_version(Mock()) captured = capsys.readouterr() assert "ISO Accessibility Compliant" in captured.out assert result == 0