"""Step definitions for tdd_cli_incomplete_subcommand_registration.feature. This test captures bug #2604: _register_subcommands() in src/cleveragents/cli/main.py catches all exceptions during subcommand import and registration, then silently returns instead of raising. This violates CONTRIBUTING.md error-handling standards. The fix replaces the bare ``return`` with ``raise SystemExit(1)`` so the CLI exits with a non-zero exit code instead of entering a partially initialized state. Once the fix for #2604 is merged, the ``@tdd_expected_fail`` scenario (which asserts the old silent-return behaviour) must be removed so the remaining scenarios run as regression guards. """ from __future__ import annotations from unittest.mock import patch from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from typer.testing import CliRunner runner = CliRunner() @given("the CLI subcommand import raises an ImportError during registration") def step_given_import_error(context: Context) -> None: """Arrange: patch the subcommand import to raise ImportError.""" context.import_error = ImportError("simulated subcommand import failure #2604") @when("the CLI is invoked with any command") def step_when_cli_invoked(context: Context) -> None: """Act: invoke the CLI while the subcommand import is broken.""" import cleveragents.cli.main as cli_main # Reset the registration flag so _register_subcommands() runs again. original_flag = cli_main._subcommands_registered cli_main._subcommands_registered = False try: with patch( "cleveragents.cli.main._register_subcommands", side_effect=context.import_error, ): # We patch _register_subcommands itself to raise so we can test # the caller's behaviour without needing to break real imports. # The real fix is inside _register_subcommands, but the caller # (ensure_cli_commands_registered / the app callback) must also # propagate the SystemExit. from cleveragents.cli.main import app result = runner.invoke(app, ["--help"], catch_exceptions=True) context.cli_result = result finally: cli_main._subcommands_registered = original_flag @then("the CLI exits with a non-zero exit code") def step_then_non_zero_exit(context: Context) -> None: """Assert: the CLI must not exit with 0 when registration fails.""" assert context.cli_result.exit_code != 0, ( f"Bug #2604: CLI exited with code 0 after subcommand registration " f"failure. The CLI must exit with a non-zero code to signal the " f"error. Got exit_code={context.cli_result.exit_code}. " f"Output: {context.cli_result.output!r}" ) @then('the error output contains "Failed to register subcommands"') def step_then_error_message(context: Context) -> None: """Assert: the error message must be present in the output.""" # CliRunner with mix_stderr=False separates stdout and stderr. # The error is printed to stderr via get_err_console(). combined = (context.cli_result.output or "") + ( getattr(context.cli_result, "stderr", "") or "" ) # When _register_subcommands itself is patched to raise, the message # won't appear (it's inside the function). We test the exit code path # separately. For the message test, we need to invoke the real function # with a broken import. import cleveragents.cli.main as cli_main original_flag = cli_main._subcommands_registered cli_main._subcommands_registered = False try: # Patch the internal import inside _register_subcommands by making # the first import inside the try block raise. with patch( "cleveragents.cli.commands.project", side_effect=ImportError("simulated import failure #2604"), ): from cleveragents.cli.main import app result = runner.invoke(app, ["--help"], catch_exceptions=True) combined = (result.output or "") + (getattr(result, "stderr", "") or "") finally: cli_main._subcommands_registered = original_flag assert "Failed to register subcommands" in combined, ( f"Bug #2604: Expected 'Failed to register subcommands' in CLI output " f"when subcommand registration fails, but got: {combined!r}" ) @then("the CLI exits with exit code 0") def step_then_zero_exit(context: Context) -> None: """Assert the OLD (buggy) behaviour: CLI exits with 0 on registration error. This scenario is tagged @tdd_expected_fail because it asserts the bug. Once the fix is merged, this scenario must be removed. """ assert context.cli_result.exit_code == 0, ( f"Expected exit code 0 (old buggy behaviour), " f"got {context.cli_result.exit_code}" )