"""Step definitions for CLI consistency and UX polish feature.""" from __future__ import annotations import json from io import StringIO from unittest.mock import patch from behave import given, then, when from behave.runner import Context # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def _call_cli_error( context: Context, message: str, hint: str | None = None, exit_code: int | None = None, ) -> None: """Invoke cli_error with a captured stderr console.""" from rich.console import Console from cleveragents.cli.errors import cli_error buf = StringIO() mock_console = Console(file=buf, stderr=True, no_color=True) kwargs: dict[str, object] = {} if hint is not None: kwargs["hint"] = hint if exit_code is not None: kwargs["exit_code"] = exit_code with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console): try: cli_error(message, **kwargs) # type: ignore[arg-type] except SystemExit as exc: context.exit_code = exc.code context.stderr_capture = buf def _call_cli_warning( context: Context, message: str, hint: str | None = None, ) -> None: """Invoke cli_warning with a captured stderr console.""" from rich.console import Console from cleveragents.cli.errors import cli_warning buf = StringIO() mock_console = Console(file=buf, stderr=True, no_color=True) with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console): if hint is not None: cli_warning(message, hint=hint) else: cli_warning(message) context.stderr_capture = buf def _call_cli_not_found( context: Context, resource_type: str, name: str, ) -> None: """Invoke cli_not_found with a captured stderr console.""" from rich.console import Console from cleveragents.cli.errors import cli_not_found buf = StringIO() mock_console = Console(file=buf, stderr=True, no_color=True) with patch("cleveragents.cli.errors._get_err_console", return_value=mock_console): try: cli_not_found(resource_type, name) except SystemExit as exc: context.exit_code = exc.code context.stderr_capture = buf # ------------------------------------------------------------------ # Constants module # ------------------------------------------------------------------ @given("the CLI constants module is imported") def step_import_constants(context: Context) -> None: from cleveragents.cli.constants import ( DEFAULT_FORMAT, EXIT_CONFLICT, EXIT_ERROR, EXIT_NOT_FOUND, EXIT_SUCCESS, EXIT_USAGE, FORMAT_HELP, FORMAT_JSON, FORMAT_TABLE, FORMAT_TEXT, VALID_FORMATS, ) context.const = { "EXIT_SUCCESS": EXIT_SUCCESS, "EXIT_ERROR": EXIT_ERROR, "EXIT_USAGE": EXIT_USAGE, "EXIT_NOT_FOUND": EXIT_NOT_FOUND, "EXIT_CONFLICT": EXIT_CONFLICT, "FORMAT_HELP": FORMAT_HELP, "DEFAULT_FORMAT": DEFAULT_FORMAT, "VALID_FORMATS": VALID_FORMATS, "FORMAT_TEXT": FORMAT_TEXT, "FORMAT_JSON": FORMAT_JSON, "FORMAT_TABLE": FORMAT_TABLE, } @then("EXIT_SUCCESS should equal {value:d}") def step_exit_success_equals(context: Context, value: int) -> None: assert context.const["EXIT_SUCCESS"] == value @then("EXIT_ERROR should equal {value:d}") def step_exit_error_equals(context: Context, value: int) -> None: assert context.const["EXIT_ERROR"] == value @then("EXIT_USAGE should equal {value:d}") def step_exit_usage_equals(context: Context, value: int) -> None: assert context.const["EXIT_USAGE"] == value @then("EXIT_NOT_FOUND should equal {value:d}") def step_exit_not_found_equals(context: Context, value: int) -> None: assert context.const["EXIT_NOT_FOUND"] == value @then("EXIT_CONFLICT should equal {value:d}") def step_exit_conflict_equals(context: Context, value: int) -> None: assert context.const["EXIT_CONFLICT"] == value @then('FORMAT_HELP should mention "{text}"') def step_format_help_mentions(context: Context, text: str) -> None: assert text in context.const["FORMAT_HELP"], ( f"Expected '{text}' in FORMAT_HELP: {context.const['FORMAT_HELP']}" ) @then('DEFAULT_FORMAT should equal "{value}"') def step_default_format_equals(context: Context, value: str) -> None: assert context.const["DEFAULT_FORMAT"] == value @then('VALID_FORMATS should contain "{value}"') def step_valid_formats_contains(context: Context, value: str) -> None: assert value in context.const["VALID_FORMATS"] @then('FORMAT_TEXT should equal "{value}"') def step_format_text_equals(context: Context, value: str) -> None: assert context.const["FORMAT_TEXT"] == value @then('FORMAT_JSON should equal "{value}"') def step_format_json_equals(context: Context, value: str) -> None: assert context.const["FORMAT_JSON"] == value @then('FORMAT_TABLE should equal "{value}"') def step_format_table_equals(context: Context, value: str) -> None: assert context.const["FORMAT_TABLE"] == value # ------------------------------------------------------------------ # Error formatting (parameterised steps matching feature scenarios) # ------------------------------------------------------------------ @given("the CLI errors module is imported") def step_import_errors(context: Context) -> None: context.stderr_capture = StringIO() context.exit_code = None @when('I call cli_error with only message "{message}"') def step_call_cli_error_message(context: Context, message: str) -> None: _call_cli_error(context, message) @when('I call cli_error with message "{message}" and hint "{hint}"') def step_call_cli_error_message_hint(context: Context, message: str, hint: str) -> None: _call_cli_error(context, message, hint=hint) @when('I call cli_error with message "{message}" and exit code {code:d}') def step_call_cli_error_custom_code(context: Context, message: str, code: int) -> None: _call_cli_error(context, message, exit_code=code) @when('I call cli_warning with only message "{message}"') def step_call_cli_warning_message(context: Context, message: str) -> None: _call_cli_warning(context, message) @when('I call cli_warning with message "{message}" and hint "{hint}"') def step_call_cli_warning_with_hint(context: Context, message: str, hint: str) -> None: _call_cli_warning(context, message, hint=hint) @when('I call cli_not_found with type "{resource_type}" and name "{name}"') def step_call_cli_not_found(context: Context, resource_type: str, name: str) -> None: _call_cli_not_found(context, resource_type, name) @then("the process should exit with code {code:d}") def step_exit_code_check(context: Context, code: int) -> None: assert context.exit_code == code, ( f"Expected exit code {code}, got {context.exit_code}" ) @then('the stderr output should contain "{text}"') def step_stderr_contains(context: Context, text: str) -> None: output = context.stderr_capture.getvalue() assert text in output, f"Expected '{text}' in stderr: {output!r}" # ------------------------------------------------------------------ # CLI runner tests # ------------------------------------------------------------------ # NOTE: The following steps are defined in cli_steps.py and # garbage_collection_cli_steps.py respectively: # - @given("a CLI test runner") # - @when('I run the CLI with "{args}"') # - @then("the CLI exit code should be {code:d}") # - @then('the CLI output should contain "{text}"') # - @then("the CLI output should be valid JSON") # # Behave loads all step files, so those shared definitions are # available here without re-declaration. We only need to register # the ``a CLI test runner`` and ``I run the CLI with`` steps because # cli_steps.py uses a different phrasing ("I run the *CleverAgents* # CLI with"). @given("a CLI consistency test runner") def step_cli_consistency_runner(context: Context) -> None: from typer.testing import CliRunner context.cli_runner = CliRunner() @when('I invoke the CLI with "{args}"') def step_invoke_cli(context: Context, args: str) -> None: from typer.testing import CliRunner from cleveragents.cli.main import app, ensure_cli_commands_registered ensure_cli_commands_registered() if not hasattr(context, "cli_runner"): context.cli_runner = CliRunner() runner = context.cli_runner arg_list = args.split() context.cli_result = runner.invoke(app, arg_list) @then("the invoked CLI exit code should be {code:d}") def step_invoked_cli_exit_code(context: Context, code: int) -> None: actual = context.cli_result.exit_code assert actual == code, ( f"Expected exit code {code}, got {actual}.\nstdout: {context.cli_result.output}" ) @then('the invoked CLI output should contain "{text}"') def step_invoked_cli_output_contains(context: Context, text: str) -> None: output = context.cli_result.output assert text in output, f"Expected '{text}' in output: {output!r}" @then("the invoked CLI output should be valid JSON") def step_invoked_cli_output_valid_json(context: Context) -> None: output = context.cli_result.output.strip() try: json.loads(output) except json.JSONDecodeError as exc: raise AssertionError( f"Output is not valid JSON: {exc}\nOutput: {output!r}" ) from exc