"""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. Ensures the default database path is writable by creating the ``.cleveragents`` directory and bootstrapping the schema so the ``list`` sub-command can query the ``ns_projects`` table even when no projects exist. """ import os from sqlalchemy import create_engine from cleveragents.infrastructure.database.models import Base # Ensure the fallback database directory exists inside the temp CWD db_dir = Path.cwd() / ".cleveragents" db_dir.mkdir(parents=True, exist_ok=True) db_path = db_dir / "db.sqlite" # Point the container at this database so it doesn't fail on open db_url = f"sqlite:///{db_path.absolute()}" os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url # Bootstrap the schema so the table exists for the query engine = create_engine(db_url, echo=False) Base.metadata.create_all(engine) engine.dispose() runner = CliRunner() 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 if hasattr(context.result, "stderr") and context.result.stderr: combined_output += context.result.stderr 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}" )