Files
freemo 4f2aa4189c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3m20s
CI / typecheck (pull_request) Successful in 3m56s
CI / build (pull_request) Successful in 37s
CI / security (pull_request) Successful in 4m2s
CI / quality (pull_request) Successful in 4m8s
CI / unit_tests (pull_request) Failing after 4m13s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 7m10s
CI / e2e_tests (pull_request) Successful in 9m34s
CI / coverage (pull_request) Successful in 11m1s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-regression (pull_request) Has been cancelled
fix(test): guard result.stderr access against ValueError
Click/Typer CliRunner.Result.stderr is a property that raises
ValueError when stderr was not separately captured (mix_stderr=True
is the default).  Wrap all result.stderr accesses in try/except
to handle this gracefully.
2026-03-22 05:01:06 +00:00

220 lines
7.1 KiB
Python

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