Files
cleveragents-core/features/steps/cli_consistency_steps.py

292 lines
9.4 KiB
Python

"""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