"""Step definitions for CLI init --yes flag tests (bug #522). These tests are TDD-style: they assert the CORRECT expected behaviour of ``agents init --yes`` and are expected to FAIL until the bug fix is applied. """ from __future__ import annotations import os import re import shutil import tempfile from pathlib import Path from unittest.mock import create_autospec, patch from behave import given, then, when from typer.testing import CliRunner from cleveragents.application.services.project_service import ProjectService from cleveragents.cli.main import app def _restore_cwd(context): """Restore the original working directory, env var, and clean up.""" os.chdir(context._init_original_cwd) if context._init_original_home is None: os.environ.pop("CLEVERAGENTS_HOME", None) else: os.environ["CLEVERAGENTS_HOME"] = context._init_original_home shutil.rmtree(context.temp_dir, ignore_errors=True) def _create_init_mocks(context): """Create and configure mocked container, service, and project. Returns ``(patcher, mock_service)`` where *patcher* is the started ``patch`` context manager. The service mock uses ``create_autospec(ProjectService)`` so that attribute access and method calls are validated against the real ``ProjectService`` interface. The project mock uses a typed ``_MockProject`` class instead of ``MagicMock`` because the spec-required output fields (``data_dir``, ``config_path``, ``database_status``, ``directories``) do not yet exist on the legacy ``Project`` model — they will be added when the #522 fix aligns the model with ``docs/specification.md:1381-1386``. Using ``create_autospec(Project)`` would reject those attribute assignments. """ mock_service = create_autospec(ProjectService, instance=True) class _MockProject: """Lightweight stand-in for the Project model with spec fields.""" name: str path: Path data_dir: Path config_path: Path database_status: str directories: list[str] mock_project = _MockProject() mock_project.name = Path(context.temp_dir).name mock_project.path = Path(context.temp_dir) # Pre-populate spec-required fields (specification.md:1381-1386) so # that when the #522 fix lands, output assertions fail for the right # reason (real bugs) rather than MagicMock stringification artefacts. mock_project.data_dir = Path(context.temp_dir) mock_project.config_path = Path(context.temp_dir) / "config.toml" mock_project.database_status = "initialized (schema v3)" mock_project.directories = ["logs", "cache", "sessions", "contexts"] mock_service.initialize_project.return_value = mock_project patcher = patch("cleveragents.application.container.get_container") mock_container = patcher.start() mock_container.return_value.project_service.return_value = mock_service return patcher, mock_service @given("I have a temporary project directory for init") def step_temp_project_directory(context): """Create a temporary directory and store it on *context*.""" context.temp_dir = tempfile.mkdtemp() # Use step-private attribute names (prefixed with _init_) to avoid # colliding with environment.py's context.original_cwd (str). # Match the framework's type (str via os.getcwd()) for consistency. context._init_original_cwd = os.getcwd() context._init_original_home = os.environ.get("CLEVERAGENTS_HOME") os.environ["CLEVERAGENTS_HOME"] = context.temp_dir os.chdir(context.temp_dir) context.add_cleanup(_restore_cwd, context) def _run_init_with_flag(context, flag: str) -> None: """Invoke ``agents init`` with the given flag via the Typer test runner.""" runner = CliRunner() patcher, mock_service = _create_init_mocks(context) try: result = runner.invoke(app, ["init", flag]) finally: patcher.stop() context.init_yes_result = { "exit_code": result.exit_code, "output": result.output, } context.init_yes_raw_result = result context.init_yes_mock_service = mock_service @when("I run agents init with the --yes flag") def step_run_init_yes(context): """Invoke ``agents init --yes`` via the Typer test runner.""" _run_init_with_flag(context, "--yes") @when("I run agents init with the -y flag") def step_run_init_short_yes(context): """Invoke ``agents init -y`` via the Typer test runner.""" _run_init_with_flag(context, "-y") @then("the init command should exit with code {code:d}") def step_init_exit_code(context, code): """Assert the init command exited with the expected code.""" actual = context.init_yes_result["exit_code"] assert actual == code, ( f"Expected exit code {code}, got {actual}. " f"Output: {context.init_yes_result['output']}" ) @then('the init output should contain "{text}"') def step_init_output_contains(context, text): """Assert that the init command output contains *text*.""" output = context.init_yes_result["output"] assert text in output, f"Expected '{text}' in output:\n{output}" @then("the project service initialize_project should have been called") def step_initialize_project_called(context): """Assert the mock project service's initialize_project was invoked.""" context.init_yes_mock_service.initialize_project.assert_called_once() @when("I run agents init without the --yes flag") def step_run_init_no_yes(context): """Invoke ``agents init`` without --yes (interactive mode).""" runner = CliRunner() patcher, _mock_service = _create_init_mocks(context) try: result = runner.invoke(app, ["init"]) finally: patcher.stop() context.init_yes_result = { "exit_code": result.exit_code, "output": result.output, } context.init_yes_raw_result = result @then("the init output should indicate interactive mode") def step_output_indicates_interactive(context): """Assert that without --yes the output does NOT contain the non-interactive marker, indicating the command ran in interactive mode. This is the negative complement to the --yes scenarios: when the fix lands, ``agents init`` (without --yes) should either present a prompt or omit the ``Initialized (non-interactive)`` marker. """ output = context.init_yes_result["output"] assert "Initialized (non-interactive)" not in output, ( "Expected interactive mode but output contains " f"'Initialized (non-interactive)':\n{output}" ) @then("no interactive prompt should have been presented") def step_no_interactive_prompt(context): """Assert that no interactive prompt was presented. Verifies that the command output does not contain common prompt-like tokens, which would indicate the ``--yes`` flag failed to suppress interactive prompts. """ output = context.init_yes_result["output"] prompt_tokens = ( "[Y/n]", "[y/N]", "Continue? ", "Proceed? ", "Confirm ", "(yes/no)", ) for token in prompt_tokens: assert token not in output, ( f"Unexpected prompt token '{token}' found in output:\n{output}" ) # Regex catches "Enter project name:", "Enter path:", "Enter value?" # without false-positiving on "Entered configuration" or "Enterprise". # Non-greedy .*? stops at the first : or ? to avoid over-matching # when the line contains colons in non-prompt contexts (e.g. "Config: /path"). enter_prompt = re.search(r"Enter\s+\S+.*?[:?]", output) assert enter_prompt is None, ( f"Unexpected Enter-style prompt found in output: " f"'{enter_prompt.group()}'\n{output}" )