"""Step definitions for the CleverAgents CLI feature - Fixed for Typer.""" from __future__ import annotations import contextlib import io from collections.abc import Sequence from typing import Any from unittest.mock import patch import click from behave import then, when from typer.testing import CliRunner from cleveragents.cli.main import ( app, convert_exit_code, ensure_cli_commands_registered, main, ) def _run_cli(context: Any, args: Sequence[str]) -> None: buffer = io.StringIO() with contextlib.redirect_stdout(buffer): exit_code = main(list(args)) context.result = { "exit_code": exit_code, "output": buffer.getvalue(), } # Also set context.output for compatibility with shared steps context.output = buffer.getvalue() context.command_output = ( buffer.getvalue() ) # For cli_plan_context_commands_steps compatibility context.exit_code = exit_code @when('I run the CleverAgents CLI with "--help"') def step_run_help(context: Any) -> None: _run_cli(context, ["--help"]) # Removed specific @then - using generic one from cli_coverage_steps.py instead @when('I run the CleverAgents CLI with "--version"') def step_run_version(context: Any) -> None: _run_cli(context, ["--version"]) # Removed specific @then - using generic one from cli_coverage_steps.py instead # New generic steps for other commands @when('I run the CleverAgents CLI with "{args}"') def step_run_cli_with_args(context, args): """Execute the CLI with specific arguments.""" ensure_cli_commands_registered() runner = CliRunner() result = runner.invoke(app, args.split()) # Combine stdout and stderr for error checking output = result.stdout try: if result.stderr: output += result.stderr except (ValueError, AttributeError): pass context.result = { "exit_code": result.exit_code, "output": output, "runner_result": result, } # Also set context.output for compatibility with shared steps context.output = output context.command_output = output # For cli_plan_context_commands_steps compatibility context.exit_code = result.exit_code @then('the CLI output should contain "{text}"') def step_cli_output_contains(context, text): output = getattr(context, "output", "") or context.result.get("output", "") assert text in output, f"Expected '{text}' in output: {output}" @then('the CLI output should not contain "{text}"') def step_cli_output_not_contains(context, text): output = getattr(context, "output", "") or context.result.get("output", "") assert text not in output, f"Did not expect '{text}' in output: {output}" # Removed duplicate @then - using generic one from cli_coverage_steps.py instead @then("the CLI command should fail") def step_cli_command_fails(context): """Verify the CLI command failed.""" assert context.result["exit_code"] != 0, ( f"Expected non-zero exit code, got {context.result['exit_code']}" ) @then('the error should contain "{text}"') def step_error_should_contain(context, text): """Verify the error output contains expected text.""" # For Typer, errors may also be in stdout output = context.result.get("output", "") assert text.lower() in output.lower(), ( f"Expected '{text}' not found in output: {output}" ) # Exit code conversion steps @then("the exit code should be {expected:d}") def step_exit_code_should_be(context, expected): """Check exit code.""" # Check multiple possible attributes for backward compatibility if hasattr(context, "last_exit_code"): actual = context.last_exit_code elif hasattr(context, "exit_code"): actual = context.exit_code elif hasattr(context, "result") and isinstance(context.result, dict): actual = context.result.get("exit_code", 0) else: actual = 0 assert actual == expected, f"Expected exit code {expected}, got {actual}" @when("I convert exit code {code}") def step_convert_exit_code(context, code): """Convert an exit code.""" if code == "None": context.converted_code = convert_exit_code(None) else: context.converted_code = convert_exit_code(int(code)) @then("the result should be {expected}") def step_result_should_be(context, expected): """Verify the conversion result.""" assert context.converted_code == int(expected) @when("I convert a Click Exit exception with code {code}") def step_convert_click_exit(context, code): """Convert a Click Exit exception.""" exit_exc = click.exceptions.Exit(int(code)) context.converted_code = convert_exit_code(exit_exc) # Main function steps @when('I call main with arguments "{args}"') def step_call_main_with_args(context, args): """Call main function with arguments.""" # Directly call main function context.main_result = main(args.split()) @then("main should process the version flag") def step_main_processes_version(context): """Verify main processed the version flag.""" assert context.main_result == 0 @when("I call main with no arguments") def step_call_main_no_args(context): """Call main with no arguments.""" # Capture output to check if help is displayed buffer = io.StringIO() err_buffer = io.StringIO() with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(err_buffer): try: context.main_result = main([]) context.main_output = buffer.getvalue() context.main_stderr = err_buffer.getvalue() except SystemExit as e: context.main_result = e.code context.main_output = buffer.getvalue() context.main_stderr = err_buffer.getvalue() @then("main should display help") def step_main_displays_help(context): """Verify main displays help.""" # Check that help text was displayed output = context.main_output + context.main_stderr assert "Usage:" in output or "Commands:" in output or "help" in output.lower(), ( f"Expected help output, got: {output}" ) @when("I call main and CLI raises SystemExit {code}") def step_main_system_exit(context, code): """Call main with SystemExit.""" # We need to mock at a lower level since Typer doesn't use __call__ directly # Mock the version command to raise SystemExit # Use an invalid command that will trigger error handling # but mock the error handling to raise SystemExit with our code with patch("cleveragents.cli.main.err_console.print") as mock_print: # Make the function raise SystemExit when error is printed def raise_exit(*args, **kwargs): raise SystemExit(int(code)) mock_print.side_effect = raise_exit buffer = io.StringIO() with contextlib.redirect_stdout(buffer): # Use an invalid command to trigger the error path context.main_result = main(["invalid-test-command-xyz"]) @then("main should return {expected}") def step_main_returns_code(context, expected): """Verify main returns expected code.""" assert context.main_result == int(expected), ( f"Expected {expected}, got {context.main_result}" )