feat(architecture): Added skeleton for phase 1

This commit is contained in:
CleverAgents Dev
2025-11-05 15:10:47 -05:00
parent b8d6d1ed32
commit fb6f559780
41 changed files with 2225 additions and 421 deletions
+74 -148
View File
@@ -1,4 +1,4 @@
"""Step definitions for the CleverAgents CLI feature."""
"""Step definitions for the CleverAgents CLI feature - Fixed for Typer."""
from __future__ import annotations
@@ -11,10 +11,10 @@ from unittest.mock import MagicMock, patch
import click
from behave import then, when
from click.testing import CliRunner
from typer.testing import CliRunner
from cleveragents.__main__ import run
from cleveragents.cli import _convert_exit_code, cli, main
from cleveragents.cli import app, cli, convert_exit_code, main
from cleveragents.platform import ensure_cli_importable
@@ -26,6 +26,9 @@ def _run_cli(context: Any, args: Sequence[str]) -> None:
"exit_code": exit_code,
"output": buffer.getvalue(),
}
# Also set context.output for compatibility with shared steps
context.output = buffer.getvalue()
context.exit_code = exit_code
@when('I run the CleverAgents CLI with "--help"')
@@ -33,10 +36,7 @@ def step_run_help(context: Any) -> None:
_run_cli(context, ["--help"])
@then('the output should contain "CleverAgents"')
def step_output_help(context: Any) -> None:
assert context.result["exit_code"] == 0
assert "CleverAgents" in context.result["output"]
# Removed specific @then - using generic one from cli_coverage_steps.py instead
@when('I run the CleverAgents CLI with "--version"')
@@ -44,10 +44,7 @@ def step_run_version(context: Any) -> None:
_run_cli(context, ["--version"])
@then('the output should contain "1.0.0"')
def step_output_version(context: Any) -> None:
assert context.result["exit_code"] == 0
assert "1.0.0" in context.result["output"]
# Removed specific @then - using generic one from cli_coverage_steps.py instead
# New generic steps for other commands
@@ -55,33 +52,40 @@ def step_output_version(context: Any) -> None:
def step_run_cli_with_args(context, args):
"""Execute the CLI with specific arguments."""
runner = CliRunner()
result = runner.invoke(cli, args.split())
result = runner.invoke(app, args.split())
# Combine stdout and stderr for error checking
output = result.stdout
if result.stderr:
output += result.stderr
context.result = {
"exit_code": result.exit_code,
"output": result.output,
"output": output,
"runner_result": result,
}
# Also set context.output for compatibility with shared steps
context.output = output
context.exit_code = result.exit_code
@then('the output should contain "{text}"')
def step_output_should_contain(context, text):
"""Verify the CLI output contains expected text."""
assert text in context.result["output"], (
f"Expected '{text}' not found in output: {context.result['output']}"
)
# Removed duplicate @then - using generic one from cli_coverage_steps.py instead
@then("the command should fail")
def step_command_should_fail(context):
def step_command_fails(context):
"""Verify the command failed."""
assert context.result["exit_code"] != 0
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 error output contains text."""
output = context.result["output"].lower() if "output" in context.result else ""
assert text.lower() in output, f"Expected '{text}' not found in error output"
"""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
@@ -89,9 +93,9 @@ def step_error_should_contain(context, text):
def step_convert_exit_code(context, code):
"""Convert an exit code."""
if code == "None":
context.converted_code = _convert_exit_code(None)
context.converted_code = convert_exit_code(None)
else:
context.converted_code = _convert_exit_code(int(code))
context.converted_code = convert_exit_code(int(code))
@then("the result should be {expected}")
@@ -104,16 +108,15 @@ def step_result_should_be(context, expected):
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)
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."""
with patch("cleveragents.cli.cli.main") as mock_cli:
mock_cli.side_effect = SystemExit(0)
context.main_result = main(args.split())
# Directly call main function
context.main_result = main(args.split())
@then("main should process the version flag")
@@ -125,133 +128,56 @@ def step_main_processes_version(context):
@when("I call main with no arguments")
def step_call_main_no_args(context):
"""Call main with no arguments."""
with patch("cleveragents.cli.cli.main") as mock_cli:
mock_cli.side_effect = SystemExit(0)
context.main_result = main()
# 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."""
assert context.main_result == 0
# 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):
"""Simulate CLI raising SystemExit."""
with patch("cleveragents.cli.cli.main") as mock_cli:
mock_cli.side_effect = SystemExit(int(code))
context.main_result = main(["bad-command"])
"""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
from cleveragents.cli import main as cli_main_module
# 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 {code}")
def step_main_returns_code(context, code):
@then("main should return {expected}")
def step_main_returns_code(context, expected):
"""Verify main returns expected code."""
assert context.main_result == int(code)
# Module steps
@when("I execute the __main__ module")
def step_execute_main_module(context):
"""Execute the __main__ module."""
with patch("sys.argv", ["agents", "--help"]):
with patch("cleveragents.__main__.main") as mock_main:
mock_main.return_value = 0
with patch("sys.exit") as mock_exit:
run()
context.main_called = mock_main.called
mock_exit.assert_called_once_with(0)
@then("the CLI should start")
def step_cli_should_start(context):
"""Verify CLI started."""
assert context.main_called
@when("I import the __main__ module")
def step_import_main_module(context):
"""Import the __main__ module."""
from cleveragents import __main__
context.main_module = __main__
@then("it should have a run function")
def step_has_run_function(context):
"""Verify module has run function."""
assert hasattr(context.main_module, "run")
assert callable(context.main_module.run)
# Platform module steps
@when("I call ensure_cli_importable")
def step_call_ensure_cli_importable(context):
"""Call ensure_cli_importable."""
context.cli_module = ensure_cli_importable()
@then("it should return the CLI module")
def step_returns_cli_module(context):
"""Verify it returns CLI module."""
assert context.cli_module is not None
@then("the module should have a cli attribute")
def step_module_has_cli(context):
"""Verify module has cli attribute."""
assert hasattr(context.cli_module, "cli")
@then("the module should have a main attribute")
def step_module_has_main(context):
"""Verify module has main attribute."""
assert hasattr(context.cli_module, "main")
@when("the CLI module is already imported")
def step_cli_already_imported(context):
"""Setup already imported module."""
context.mock_module = MagicMock()
context.original_module = sys.modules.get("cleveragents.cli")
sys.modules["cleveragents.cli"] = context.mock_module
@then("it should return the cached module")
def step_returns_cached_module(context):
"""Verify cached module returned."""
result = ensure_cli_importable()
assert result is context.mock_module
# Restore original
if context.original_module:
sys.modules["cleveragents.cli"] = context.original_module
else:
sys.modules.pop("cleveragents.cli", None)
@when("the CLI module import fails")
def step_cli_import_fails(context):
"""Setup import failure."""
# Save original module
context.original_module = sys.modules.get("cleveragents.cli")
# Remove from sys.modules to force import
sys.modules.pop("cleveragents.cli", None)
context.import_error = ImportError("Test error")
@then("it should propagate the import error")
def step_propagates_import_error(context):
"""Verify import error propagated."""
# Ensure module is not in sys.modules
sys.modules.pop("cleveragents.cli", None)
with patch("cleveragents.platform.import_module") as mock_import:
mock_import.side_effect = ImportError("Test error")
try:
ensure_cli_importable()
assert False, "Should have raised ImportError"
except ImportError as e:
assert str(e) == "Test error"
# Restore original module
if context.original_module:
sys.modules["cleveragents.cli"] = context.original_module
assert context.main_result == int(expected), (
f"Expected {expected}, got {context.main_result}"
)