"""Step definitions for project commands coverage tests.""" from __future__ import annotations import tempfile from pathlib import Path from unittest.mock import MagicMock, patch from behave import given, then, when from typer.testing import CliRunner from cleveragents.application.services.context_service import DEFAULT_IGNORE_PATTERNS from cleveragents.cli.commands import project from cleveragents.core.exceptions import ( CleverAgentsError, ConfigurationError, ValidationError, ) @given("I have a temporary working directory") def step_temp_working_directory(context): """Create a temporary working directory.""" context.temp_dir = tempfile.mkdtemp() context.original_cwd = Path.cwd() import os os.chdir(context.temp_dir) @given('I create a subdirectory "{dirname}"') def step_create_subdirectory(context, dirname): """Create a subdirectory in the temp directory.""" subdir = Path(context.temp_dir) / dirname subdir.mkdir(parents=True, exist_ok=True) context.subdirectory = subdir @given("I have already initialized a project") def step_already_initialized(context): """Simulate an already initialized project.""" project_dir = Path(context.temp_dir) / ".cleveragents" project_dir.mkdir(exist_ok=True) context.existing_project = True @given("I have an initialized project with stats") def step_initialized_with_stats(context): """Mock an initialized project with statistics.""" context.mock_project = MagicMock() context.mock_project.name = "test-project" context.mock_project.path = Path(context.temp_dir) context.mock_project.created_at = "2024-01-01 10:00:00" context.mock_stats = { "plans": 5, "context_files": 10, "changes": 25, "current_plan": "plan-001", } @given("there is no initialized project") def step_no_project(context): """Ensure no project exists.""" context.no_project = True @given("the project service will raise a ValidationError with details") def step_mock_validation_error(context): """Mock project service to raise ValidationError.""" context.mock_error = ValidationError( "Invalid project name", details={"name": "contains invalid characters", "path": "does not exist"}, ) @given("the project service will raise a ConfigurationError") def step_mock_configuration_error(context): """Mock project service to raise ConfigurationError.""" context.mock_error = ConfigurationError("Missing required configuration") @given("the project service will raise a CleverAgentsError") def step_mock_cleveragents_error(context): """Mock project service to raise CleverAgentsError.""" context.mock_error = CleverAgentsError("Generic project error") @given("the project service will raise an unexpected exception") def step_mock_unexpected_error(context): """Mock project service to raise unexpected exception.""" context.mock_error = Exception("Unexpected error occurred") @given("the project service will raise a CleverAgentsError on status") def step_mock_status_error(context): """Mock project service to raise error on status.""" context.mock_status_error = CleverAgentsError("Cannot get project status") @when("I run project init without any parameters") def step_run_project_init_default(context): """Run project init with default parameters.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_project = MagicMock() mock_project.name = Path.cwd().name mock_project.path = Path.cwd() mock_service.initialize_project.return_value = mock_project mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init"]) context.result = result context.mock_service = mock_service @when('I run project init with name "{name}"') def step_run_project_init_with_name(context, name): """Run project init with custom name.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_project = MagicMock() mock_project.name = name mock_project.path = Path.cwd() mock_service.initialize_project.return_value = mock_project mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init", name]) context.result = result context.project_name = name @when('I run project init with path "{path}"') def step_run_project_init_with_path(context, path): """Run project init with custom path.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_project = MagicMock() project_path = Path(context.temp_dir) / path mock_project.name = Path(path).name mock_project.path = project_path mock_service.initialize_project.return_value = mock_project mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init", "--path", str(project_path)]) context.result = result context.project_path = project_path @when("I execute project init with force option") def step_execute_project_init_force(context): """Run project init with force flag.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_project = MagicMock() mock_project.name = "forced-project" mock_project.path = Path.cwd() mock_service.initialize_project.return_value = mock_project mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init", "--force"]) context.result = result @when("I run project init and it fails with validation error") def step_run_project_init_validation_error(context): """Run project init that fails with ValidationError.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.initialize_project.side_effect = context.mock_error mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init"]) context.result = result @when("I run project init and it fails with configuration error") def step_run_project_init_config_error(context): """Run project init that fails with ConfigurationError.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.initialize_project.side_effect = context.mock_error mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init"]) context.result = result @when("I run project init and it fails with CleverAgentsError") def step_run_project_init_cleveragents_error(context): """Run project init that fails with CleverAgentsError.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.initialize_project.side_effect = context.mock_error mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init"]) context.result = result @when("I run project init and it fails unexpectedly") def step_run_project_init_unexpected_error(context): """Run project init that fails with unexpected error.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.initialize_project.side_effect = context.mock_error mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["init"]) context.result = result @when("I execute project status command") def step_execute_project_status(context): """Run project status command.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() if hasattr(context, "no_project") and context.no_project: mock_service.get_current_project.return_value = None elif hasattr(context, "mock_status_error"): mock_service.get_current_project.side_effect = context.mock_status_error else: mock_service.get_current_project.return_value = context.mock_project mock_service.get_project_stats.return_value = context.mock_stats mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["status"]) context.result = result @when("I execute project status and it fails") def step_execute_project_status_error(context): """Run project status that fails.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.side_effect = context.mock_status_error mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["status"]) context.result = result @when("I run project list command") def step_run_project_list(context): """Run project list command. Mocks the project repository to return an empty list so the test is fully isolated from database state left by prior scenarios (which caused flaky failures in sequential / coverage mode). """ from unittest.mock import MagicMock, patch mock_repo = MagicMock() mock_repo.list_projects.return_value = [] runner = CliRunner() with patch( "cleveragents.cli.commands.project._get_namespaced_project_repo", return_value=mock_repo, ): result = runner.invoke(project.app, ["list"]) context.result = result @when("I run project clean command") def step_run_project_clean(context): """Run project clean command.""" runner = CliRunner() result = runner.invoke(project.app, ["clean"]) context.result = result @when("I run project clean command with --yes flag") def step_run_project_clean_yes(context): """Run project clean with confirmation.""" runner = CliRunner() result = runner.invoke(project.app, ["clean", "--yes"]) context.result = result @then("the project should be initialized in current directory") def step_check_project_initialized(context): """Check project was initialized.""" assert context.result.exit_code == 0 assert "initialized successfully" in context.result.output @then("the success message should show project name and location") def step_check_success_message(context): """Check success message content.""" assert "Project" in context.result.output assert "Location:" in context.result.output @then("the .cleveragents directory should be created") def step_check_cleveragents_dir(context): """Check .cleveragents directory creation.""" # This is mocked, but we check the service was called correctly assert context.mock_service.initialize_project.called @then('the project should be initialized with name "{name}"') def step_check_project_name(context, name): """Check project initialized with correct name.""" assert context.result.exit_code == 0 assert name in context.result.output @then('the success panel should display "{name}"') def step_check_panel_name(context, name): """Check success panel displays name.""" assert name in context.result.output @then('the project should be initialized in "{dirname}" directory') def step_check_project_in_dir(context, dirname): """Check project initialized in correct directory.""" assert context.result.exit_code == 0 @then('the .cleveragents directory should exist in "{dirname}"') def step_check_cleveragents_in_dir(context, dirname): """Check .cleveragents exists in specified directory.""" # This is mocked, just verify the call happened assert context.result.exit_code == 0 @then("the project should be reinitialized successfully") def step_check_reinitialized(context): """Check project was reinitialized.""" assert context.result.exit_code == 0 assert "initialized successfully" in context.result.output @then("no error should be raised") def step_check_no_error(context): """Check no error was raised.""" assert context.result.exit_code == 0 @then("the error message should be displayed in red") def step_check_error_red(context): """Check error displayed in red.""" assert "Validation Error" in context.result.output @then("the validation error details should be shown") def step_check_validation_details(context): """Check validation error details shown.""" assert "name:" in context.result.output or "path:" in context.result.output @then("the command should abort with ValidationError") def step_check_abort_validation(context): """Check command aborted with validation error.""" assert context.result.exit_code != 0 @then("the configuration error message should be displayed") def step_check_config_error(context): """Check configuration error displayed.""" assert "Configuration Error" in context.result.output @then("the command should abort with ConfigurationError") def step_check_abort_config(context): """Check command aborted with config error.""" assert context.result.exit_code != 0 @then("the generic error message should be displayed") def step_check_generic_error(context): """Check generic error displayed.""" assert "Error:" in context.result.output @then("the command should abort with CleverAgentsError") def step_check_abort_cleveragents(context): """Check command aborted with CleverAgentsError.""" assert context.result.exit_code != 0 @then("the unexpected error message should be displayed") def step_check_unexpected_error(context): """Check unexpected error displayed.""" assert "Unexpected error" in context.result.output @then("the command should abort with generic exception") def step_check_abort_generic(context): """Check command aborted with generic exception.""" assert context.result.exit_code != 0 @then("the project information should be displayed") def step_check_project_info(context): """Check project info displayed.""" assert "Project Status" in context.result.output assert "test-project" in context.result.output @then("the statistics should include plans count") def step_check_plans_count(context): """Check plans count in statistics.""" assert "Plans: 5" in context.result.output @then("the statistics should include context files count") def step_check_context_files(context): """Check context files count.""" assert "Context Files: 10" in context.result.output @then("the statistics should include changes count") def step_check_changes_count(context): """Check changes count.""" assert "Total Changes: 25" in context.result.output @then("the current plan should be shown") def step_check_current_plan(context): """Check current plan shown.""" assert "Current Plan: plan-001" in context.result.output @then('an error should show "{message}"') def step_check_error_message(context, message): """Check specific error message.""" assert message in context.result.output @then('it should suggest running "{command}"') def step_check_suggestion(context, command): """Check command suggestion.""" assert command in context.result.output @then("the error message should be shown") def step_check_error_shown(context): """Check error message shown.""" assert "Error:" in context.result.output @then("the command should abort") def step_check_abort(context): """Check command aborted.""" assert context.result.exit_code != 0 @then('it should display "{message}"') def step_check_display_message(context, message): """Check specific message displayed.""" assert message in context.result.output @then("the command should abort regardless of confirmation") def step_check_abort_regardless(context): """Check command aborts even with confirmation.""" assert context.result.exit_code != 0 assert "not yet implemented" in context.result.output @given("I have a mocked project with file filters") def step_mock_project_filters(context): """Prepare a mocked project for file filter commands.""" context.mock_project = MagicMock() context.mock_project.name = "filters-project" context.mock_project.path = Path(getattr(context, "temp_dir", Path.cwd())) context.project_includes = ["src/*"] context.project_excludes = ["tests/*"] @when("I run project file-filter show") def step_run_file_filter_show(context): """Run the file-filter show command.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.return_value = context.mock_project mock_service.get_project_filters.return_value = ( getattr(context, "project_includes", []), getattr(context, "project_excludes", []), ) mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["file-filter", "show"]) context.result = result context.mock_service = mock_service @when("I run project file-filter show with no project") def step_run_file_filter_show_no_project(context): """Run the file-filter show command without an active project.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.return_value = None mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["file-filter", "show"]) context.result = result @when( 'I add file filters include "{include_glob}" exclude "{exclude_glob}" with defaults' ) def step_add_file_filters_with_defaults(context, include_glob, exclude_glob): """Run the file-filter add command with include, exclude, and defaults.""" runner = CliRunner() expected_excludes = [exclude_glob, *DEFAULT_IGNORE_PATTERNS] with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.return_value = context.mock_project mock_service.update_file_filters.return_value = context.mock_project mock_service.get_project_filters.return_value = ( [include_glob], expected_excludes, ) mock_container.return_value.project_service.return_value = mock_service result = runner.invoke( project.app, [ "file-filter", "add", "--include", include_glob, "--exclude", exclude_glob, "--defaults", ], ) context.result = result context.mock_service = mock_service context.expected_include = include_glob context.expected_excludes = expected_excludes @when("I clear project file filters without flags") def step_clear_file_filters(context): """Run the file-filter clear command without specifying flags.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.return_value = context.mock_project mock_service.update_file_filters.return_value = context.mock_project mock_service.get_project_filters.return_value = ([], []) mock_container.return_value.project_service.return_value = mock_service result = runner.invoke(project.app, ["file-filter", "clear"]) context.result = result context.mock_service = mock_service @when('I remove project file filters include "{include_glob}" exclude "{exclude_glob}"') def step_remove_file_filters(context, include_glob, exclude_glob): """Run the file-filter remove command with include and exclude globs.""" runner = CliRunner() with patch("cleveragents.application.container.get_container") as mock_container: mock_service = MagicMock() mock_service.get_current_project.return_value = context.mock_project mock_service.update_file_filters.return_value = context.mock_project mock_service.get_project_filters.return_value = ([], []) mock_container.return_value.project_service.return_value = mock_service result = runner.invoke( project.app, [ "file-filter", "remove", "--include", include_glob, "--exclude", exclude_glob, ], ) context.result = result context.mock_service = mock_service context.remove_include = include_glob context.remove_exclude = exclude_glob @then("it should display the current file filters") def step_check_file_filter_output(context): """Verify file-filter show output displays includes and excludes.""" assert context.result.exit_code == 0 assert "Project File Filters" in context.result.output for pattern in context.project_includes + context.project_excludes: assert pattern in context.result.output @then("it should warn about the missing project") def step_check_missing_project_warning(context): """Verify missing project warning and exit code.""" combined_output = context.result.output try: if context.result.stderr: combined_output += context.result.stderr except (ValueError, AttributeError): pass assert "No project found in current directory" in combined_output @then("it should update filters with defaults applied") def step_check_filters_updated_with_defaults(context): """Validate that defaults were included when adding filters.""" assert context.result.exit_code == 0 context.mock_service.update_file_filters.assert_called_once_with( context.mock_project, include_add=[context.expected_include], exclude_add=context.expected_excludes, ) assert "Updated Project File Filters" in context.result.output for pattern in context.expected_excludes: assert pattern in context.result.output @then("it should clear both include and exclude filters") def step_check_clear_filters(context): """Ensure clear command cleared both include and exclude filters.""" assert context.result.exit_code == 0 context.mock_service.update_file_filters.assert_called_once_with( context.mock_project, clear_include=True, clear_exclude=True, ) assert "Cleared Project File Filters" in context.result.output @then("it should remove the specified file filters") def step_check_remove_filters(context): """Ensure remove command removed provided filters.""" assert context.result.exit_code == 0 context.mock_service.update_file_filters.assert_called_once_with( context.mock_project, include_remove=[context.remove_include], exclude_remove=[context.remove_exclude], ) assert "Updated Project File Filters" in context.result.output @then("the project command should succeed") def step_project_command_should_succeed(context): """Assert the project command completed successfully.""" assert context.result.exit_code == 0 @then("the project command should exit with code {code:d}") def step_project_command_exit_code(context, code): """Assert the project command exited with the expected code.""" assert context.result.exit_code == code, ( f"Expected exit code {code}, got {context.result.exit_code}. Output: {context.result.output}" ) @then('the project list output should contain "{text}"') def step_project_list_output_contains(context, text): """Assert the project list output contains expected text.""" assert context.result.exit_code == 0, ( f"Expected exit code 0, got {context.result.exit_code}. Output: {context.result.output}" ) assert text in context.result.output, ( f"Expected '{text}' in output: {context.result.output}" )