Files
freemo 051ee7c290
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 21s
CI / quality (pull_request) Successful in 31s
CI / typecheck (pull_request) Successful in 47s
CI / security (pull_request) Successful in 52s
CI / build (pull_request) Successful in 56s
CI / e2e_tests (pull_request) Successful in 5m1s
CI / integration_tests (pull_request) Successful in 5m30s
CI / unit_tests (pull_request) Successful in 5m42s
CI / docker (pull_request) Successful in 58s
CI / coverage (pull_request) Successful in 7m35s
CI / build (push) Successful in 21s
CI / docker (push) Has been skipped
CI / benchmark-regression (pull_request) Failing after 49m24s
CI / lint (push) Successful in 22s
CI / quality (push) Successful in 39s
CI / security (push) Successful in 48s
CI / typecheck (push) Successful in 1m26s
CI / benchmark-regression (push) Has been skipped
CI / e2e_tests (push) Successful in 5m53s
CI / coverage (push) Successful in 9m4s
CI / benchmark-publish (push) Successful in 19m10s
CI / integration_tests (push) Failing after 19m18s
CI / unit_tests (push) Failing after 19m20s
test(coverage): add Behave BDD tests to improve coverage across 52 source files
Added 52 new .feature files and corresponding _steps.py files targeting
previously uncovered code paths in the following areas:

- TUI layer: app, commands, persona (state/schema/registry), widgets,
  input (shell_exec, reference_parser)
- Application services: plan lifecycle/service/executor, session,
  project, repo indexing, correction, checkpoint, actor, llm_actors,
  strategy coordinator, resource file watcher, service retry wiring
- CLI commands: session, resource, repl, plan, db, automation_profile
- Domain models: retry_policy, resource_type, cost_budget,
  docker_compose_analyzer, detail_level, _sql_string_aware,
  _postgresql_helpers
- Core: circuit_breaker, retry_service_patterns
- Infrastructure: repositories, transaction_sandbox, strategy_registry,
  plugins/loader, container
- Config: settings
- Agents: plan_generation, context_analysis, auto_debug
- A2A: facade

All new tests follow the Behave/Gherkin BDD standard. Resolved step
definition collisions with unique prefixes. Fixed Alembic fileConfig
logger disabling issue (disable_existing_loggers=False).

ISSUES CLOSED: #1068
2026-03-20 21:22:10 +00:00

737 lines
24 KiB
Python

"""Behave step implementations for repl_coverage.feature.
Provides full unit-level coverage of every function and branch in
``src/cleveragents/cli/commands/repl.py``.
"""
from __future__ import annotations
import os
import tempfile
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, call, patch
from behave import given, then, when
from behave.runner import Context
# ===================================================================
# Helpers
# ===================================================================
class _FakeInputCov:
"""Feed a list of strings then raise EOFError."""
def __init__(self, lines: list[str]) -> None:
self._lines = list(lines)
self._idx = 0
def __call__(self, prompt: str = "") -> str:
if self._idx >= len(self._lines):
raise EOFError
line = self._lines[self._idx]
self._idx += 1
if line == "__KEYBOARD_INTERRUPT__":
raise KeyboardInterrupt
return line
def _make_console_and_buf():
"""Return a (Console, StringIO) pair for capturing Rich output."""
from rich.console import Console
buf = StringIO()
console = Console(file=buf, no_color=True, width=200)
return console, buf
# ===================================================================
# Constants
# ===================================================================
@given("replcov the repl module is imported")
def step_import_repl_module(context: Context) -> None:
import cleveragents.cli.commands.repl as mod
context.repl_mod = mod
@then("the constant _MULTILINE_CONTINUATION equals backslash")
def step_check_multiline_continuation(context: Context) -> None:
assert context.repl_mod._MULTILINE_CONTINUATION == "\\"
@then('the constant _LAST_COMMAND_TOKEN equals "!!"')
def step_check_last_command_token(context: Context) -> None:
assert context.repl_mod._LAST_COMMAND_TOKEN == "!!"
@then("the _REPL_COMMANDS list is non-empty")
def step_repl_commands_non_empty(context: Context) -> None:
assert len(context.repl_mod._REPL_COMMANDS) > 0
@then("the _BUILTIN_COMMANDS list contains help exit and quit")
def step_builtin_commands(context: Context) -> None:
builtins_list = context.repl_mod._BUILTIN_COMMANDS
for token in (":help", ":exit", ":quit"):
assert token in builtins_list, f"{token} not in _BUILTIN_COMMANDS"
# ===================================================================
# _get_prompt_context
# ===================================================================
@given("neither CLEVERAGENTS_PROJECT nor CLEVERAGENTS_PLAN is set")
def step_clear_prompt_env(context: Context) -> None:
context.saved_project = os.environ.pop("CLEVERAGENTS_PROJECT", None)
context.saved_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)
def _restore():
if context.saved_project is not None:
os.environ["CLEVERAGENTS_PROJECT"] = context.saved_project
if context.saved_plan is not None:
os.environ["CLEVERAGENTS_PLAN"] = context.saved_plan
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_restore)
@given('CLEVERAGENTS_PROJECT is set to "proj1" and CLEVERAGENTS_PLAN is unset')
def step_project_only_env(context: Context) -> None:
context.saved_project = os.environ.get("CLEVERAGENTS_PROJECT")
context.saved_plan = os.environ.get("CLEVERAGENTS_PLAN")
os.environ["CLEVERAGENTS_PROJECT"] = "proj1"
os.environ.pop("CLEVERAGENTS_PLAN", None)
def _restore():
if context.saved_project is not None:
os.environ["CLEVERAGENTS_PROJECT"] = context.saved_project
else:
os.environ.pop("CLEVERAGENTS_PROJECT", None)
if context.saved_plan is not None:
os.environ["CLEVERAGENTS_PLAN"] = context.saved_plan
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_restore)
@given('CLEVERAGENTS_PROJECT is set to "proj1" and CLEVERAGENTS_PLAN is set to "plan1"')
def step_project_and_plan_env(context: Context) -> None:
context.saved_project = os.environ.get("CLEVERAGENTS_PROJECT")
context.saved_plan = os.environ.get("CLEVERAGENTS_PLAN")
os.environ["CLEVERAGENTS_PROJECT"] = "proj1"
os.environ["CLEVERAGENTS_PLAN"] = "plan1"
def _restore():
if context.saved_project is not None:
os.environ["CLEVERAGENTS_PROJECT"] = context.saved_project
else:
os.environ.pop("CLEVERAGENTS_PROJECT", None)
if context.saved_plan is not None:
os.environ["CLEVERAGENTS_PLAN"] = context.saved_plan
else:
os.environ.pop("CLEVERAGENTS_PLAN", None)
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_restore)
@when("I call _get_prompt_context")
def step_call_get_prompt_context(context: Context) -> None:
from cleveragents.cli.commands.repl import _get_prompt_context
context.prompt_result = _get_prompt_context()
@then('the prompt result is "{expected}"')
def step_check_prompt_result(context: Context, expected: str) -> None:
assert context.prompt_result == expected, (
f"Expected {expected!r}, got {context.prompt_result!r}"
)
# ===================================================================
# _setup_history
# ===================================================================
@given("a temporary history file that exists")
def step_temp_history_exists(context: Context) -> None:
fd, path = tempfile.mkstemp(suffix="_hist")
os.close(fd)
context.history_path = Path(path)
def _cleanup():
context.history_path.unlink(missing_ok=True)
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_cleanup)
@given("a temporary history path that does not exist")
def step_temp_history_not_exists(context: Context) -> None:
tmpdir = tempfile.mkdtemp()
context.history_path = Path(tmpdir) / "subdir" / "nonexistent_history"
def _cleanup():
import shutil
shutil.rmtree(tmpdir, ignore_errors=True)
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_cleanup)
@when("I call _setup_history with that path")
def step_call_setup_history(context: Context) -> None:
from cleveragents.cli.commands.repl import _setup_history
with patch("cleveragents.cli.commands.repl.readline") as mock_rl:
_setup_history(context.history_path)
context.mock_readline = mock_rl
@then("readline.read_history_file is called with the path")
def step_rl_read_called(context: Context) -> None:
context.mock_readline.read_history_file.assert_called_once_with(
str(context.history_path)
)
@then("readline.read_history_file is not called")
def step_rl_read_not_called(context: Context) -> None:
context.mock_readline.read_history_file.assert_not_called()
@then("readline.set_history_length is called with 10000")
def step_rl_set_length(context: Context) -> None:
context.mock_readline.set_history_length.assert_called_once_with(10_000)
# ===================================================================
# _save_history
# ===================================================================
@given("a temporary history path for saving")
def step_temp_save_path(context: Context) -> None:
context.save_path = Path(tempfile.mkdtemp()) / "save_hist"
@when("I call _save_history with that path")
def step_call_save_history(context: Context) -> None:
from cleveragents.cli.commands.repl import _save_history
with patch("cleveragents.cli.commands.repl.readline") as mock_rl:
_save_history(context.save_path)
context.mock_readline = mock_rl
@then("readline.write_history_file is called with the path")
def step_rl_write_called(context: Context) -> None:
context.mock_readline.write_history_file.assert_called_once_with(
str(context.save_path)
)
@given("readline.write_history_file will raise OSError")
def step_rl_write_raises(context: Context) -> None:
context.oserror_flag = True
@when("I call _save_history with a dummy path")
def step_call_save_history_oserror(context: Context) -> None:
from cleveragents.cli.commands.repl import _save_history
with patch("cleveragents.cli.commands.repl.readline") as mock_rl:
mock_rl.write_history_file.side_effect = OSError("disk full")
context.exc = None
try:
_save_history(Path("/tmp/dummy_hist"))
except Exception as exc:
context.exc = exc
@then("no exception is raised")
def step_no_exception(context: Context) -> None:
assert context.exc is None, f"Unexpected exception: {context.exc}"
# ===================================================================
# _setup_completer
# ===================================================================
@when("I call _setup_completer")
def step_call_setup_completer(context: Context) -> None:
from cleveragents.cli.commands.repl import _setup_completer
with patch("cleveragents.cli.commands.repl.readline") as mock_rl:
_setup_completer()
context.mock_readline = mock_rl
# Grab the completer function that was passed to set_completer
context.completer_fn = mock_rl.set_completer.call_args[0][0]
@then("readline.set_completer is called with a callable")
def step_rl_set_completer(context: Context) -> None:
assert context.mock_readline.set_completer.called
assert callable(context.completer_fn)
@then("readline.set_completer_delims is called")
def step_rl_set_delims(context: Context) -> None:
context.mock_readline.set_completer_delims.assert_called_once()
@then("readline.parse_and_bind is called with tab complete")
def step_rl_parse_bind(context: Context) -> None:
context.mock_readline.parse_and_bind.assert_called_once_with("tab: complete")
@given("the completer is installed via _setup_completer")
def step_install_completer(context: Context) -> None:
from cleveragents.cli.commands.repl import _setup_completer
with patch("cleveragents.cli.commands.repl.readline") as mock_rl:
_setup_completer()
context.completer_fn = mock_rl.set_completer.call_args[0][0]
@when('I query the completer with text "{text}" and state {state:d}')
def step_query_completer(context: Context, text: str, state: int) -> None:
context.completer_result = context.completer_fn(text, state)
@when("I query the completer with empty text and state {state:d}")
def step_query_completer_empty(context: Context, state: int) -> None:
context.completer_result = context.completer_fn("", state)
@then('the completer returns "{expected}"')
def step_completer_returns(context: Context, expected: str) -> None:
assert context.completer_result == expected, (
f"Expected {expected!r}, got {context.completer_result!r}"
)
@then("the completer returns None")
def step_completer_returns_none(context: Context) -> None:
assert context.completer_result is None
@then("the completer returns a non-None value")
def step_completer_returns_non_none(context: Context) -> None:
assert context.completer_result is not None, "Expected non-None from completer"
# ===================================================================
# _read_multiline
# ===================================================================
@given('builtins.input will return "continued line"')
def step_input_returns_single(context: Context) -> None:
context.fake_continuation_lines = ["continued line"]
context.multiline_initial = "first\\"
@given("builtins.input will return chained continuations")
def step_input_returns_chained(context: Context) -> None:
context.fake_continuation_lines = ["second\\", "third"]
context.multiline_initial = "first\\"
@given("builtins.input will raise EOFError on continuation")
def step_input_raises_eof(context: Context) -> None:
context.fake_continuation_lines = "__EOF__"
context.multiline_initial = "only\\"
@given("builtins.input will raise KeyboardInterrupt on continuation")
def step_input_raises_kbi(context: Context) -> None:
context.fake_continuation_lines = "__KBI__"
context.multiline_initial = "only\\"
@when("I call _read_multiline with initial line ending in backslash")
def step_call_read_multiline(context: Context) -> None:
from cleveragents.cli.commands.repl import _read_multiline
if context.fake_continuation_lines == "__EOF__":
fake = MagicMock(side_effect=EOFError)
elif context.fake_continuation_lines == "__KBI__":
fake = MagicMock(side_effect=KeyboardInterrupt)
else:
fake = _FakeInputCov(context.fake_continuation_lines)
with patch("builtins.input", fake):
context.multiline_result = _read_multiline(context.multiline_initial)
@then('the multiline result is "{expected}"')
def step_check_multiline_result(context: Context, expected: str) -> None:
assert context.multiline_result == expected, (
f"Expected {expected!r}, got {context.multiline_result!r}"
)
# ===================================================================
# _print_repl_help
# ===================================================================
@when("I call _print_repl_help")
def step_call_print_repl_help(context: Context) -> None:
from cleveragents.cli.commands.repl import _print_repl_help
console, buf = _make_console_and_buf()
context.help_exc = None
try:
with patch("cleveragents.cli.commands.repl._console", console):
_print_repl_help()
except Exception as exc:
context.help_exc = exc
context.help_output = buf.getvalue()
@then("no exception is raised from _print_repl_help")
def step_no_help_exception(context: Context) -> None:
assert context.help_exc is None, f"Unexpected exception: {context.help_exc}"
# ===================================================================
# dispatch_command
# ===================================================================
@given("the root Typer app is mocked to succeed")
def step_mock_typer_success(context: Context) -> None:
context.dispatch_typer_side_effect = None
@given("the root Typer app raises SystemExit with code 42")
def step_mock_typer_sysexit_42(context: Context) -> None:
context.dispatch_typer_side_effect = SystemExit(42)
@given("the root Typer app raises SystemExit with None code")
def step_mock_typer_sysexit_none(context: Context) -> None:
context.dispatch_typer_side_effect = SystemExit(None)
@given("the root Typer app raises KeyboardInterrupt")
def step_mock_typer_kbi(context: Context) -> None:
context.dispatch_typer_side_effect = KeyboardInterrupt()
@given("the root Typer app raises RuntimeError boom")
def step_mock_typer_runtime_error(context: Context) -> None:
context.dispatch_typer_side_effect = RuntimeError("boom")
@when('I call dispatch_command with args "{arg}"')
def step_call_dispatch_command(context: Context, arg: str) -> None:
from cleveragents.cli.commands.repl import dispatch_command
console, buf = _make_console_and_buf()
mock_app = MagicMock()
if context.dispatch_typer_side_effect is not None:
mock_app.side_effect = context.dispatch_typer_side_effect
with (
patch("cleveragents.cli.commands.repl._console", console),
patch(
"cleveragents.cli.main._register_subcommands",
MagicMock(),
),
patch("cleveragents.cli.main.app", mock_app),
):
context.dispatch_rc = dispatch_command([arg])
context.dispatch_output = buf.getvalue()
@then("the dispatch return code is {code:d}")
def step_check_dispatch_rc(context: Context, code: int) -> None:
assert context.dispatch_rc == code, (
f"Expected dispatch rc={code}, got {context.dispatch_rc}"
)
@then('the dispatch error output contains "boom"')
def step_dispatch_output_boom(context: Context) -> None:
assert "boom" in context.dispatch_output, (
f"Expected 'boom' in output: {context.dispatch_output}"
)
# ===================================================================
# run_repl — shared helpers
# ===================================================================
def _run_repl_cov(
lines: list[str],
*,
no_history: bool = True,
mock_dispatch: bool = False,
mock_setup_hist: bool = False,
mock_save_hist: bool = False,
):
"""Run the REPL with mocked input/console and optionally mocked dispatch."""
from cleveragents.cli.commands.repl import run_repl
console, buf = _make_console_and_buf()
fake = _FakeInputCov(lines)
patches = [
patch("cleveragents.cli.commands.repl._console", console),
patch("builtins.input", fake),
]
mock_dispatch_obj = None
mock_setup_obj = None
mock_save_obj = None
if mock_dispatch:
m = MagicMock(return_value=0)
patches.append(patch("cleveragents.cli.commands.repl.dispatch_command", m))
mock_dispatch_obj = m
if mock_setup_hist:
m = MagicMock()
patches.append(patch("cleveragents.cli.commands.repl._setup_history", m))
mock_setup_obj = m
if mock_save_hist:
m = MagicMock()
patches.append(patch("cleveragents.cli.commands.repl._save_history", m))
mock_save_obj = m
for p in patches:
p.start()
try:
code = run_repl(no_history=no_history)
finally:
for p in patches:
p.stop()
return code, buf.getvalue(), mock_dispatch_obj, mock_setup_obj, mock_save_obj
# --- Given steps for REPL input sequences (using tables) ---
@given("the REPL input sequence is")
def step_repl_input_from_table(context: Context) -> None:
context.cov_repl_lines = [row["line"] for row in context.table]
@given("the REPL input sequence is empty")
def step_repl_input_empty(context: Context) -> None:
context.cov_repl_lines = []
@given('the REPL input sequence for multiline is "version \\" then "" then ":exit"')
def step_repl_input_multiline(context: Context) -> None:
context.cov_repl_lines = ["version \\", "", ":exit"]
@given("the REPL input sequence for interrupt then exit")
def step_repl_input_kbi(context: Context) -> None:
context.cov_repl_lines = ["__KEYBOARD_INTERRUPT__", ":exit"]
@given("the REPL input sequence has an unclosed quote then exit")
def step_repl_input_shlex_error(context: Context) -> None:
context.cov_repl_lines = ['"unclosed', ":exit"]
@given("dispatch_command is mocked for run_repl")
def step_mock_dispatch_for_repl(context: Context) -> None:
context.mock_dispatch_flag = True
# --- When steps ---
@when("I run the REPL loop")
def step_run_repl_loop(context: Context) -> None:
mock_dispatch = getattr(context, "mock_dispatch_flag", False)
code, output, mock_d, _, _ = _run_repl_cov(
context.cov_repl_lines,
no_history=True,
mock_dispatch=mock_dispatch,
)
context.cov_repl_exit_code = code
context.cov_repl_output = output
context.cov_mock_dispatch = mock_d
@when("I run the REPL loop with history enabled")
def step_run_repl_loop_history(context: Context) -> None:
code, output, _, mock_setup, mock_save = _run_repl_cov(
context.cov_repl_lines,
no_history=False,
mock_setup_hist=True,
mock_save_hist=True,
)
context.cov_repl_exit_code = code
context.cov_repl_output = output
context.cov_mock_setup_hist = mock_setup
context.cov_mock_save_hist = mock_save
@when("I run the REPL loop with no_history")
def step_run_repl_loop_no_history(context: Context) -> None:
code, output, _, mock_setup, mock_save = _run_repl_cov(
context.cov_repl_lines,
no_history=True,
mock_setup_hist=True,
mock_save_hist=True,
)
context.cov_repl_exit_code = code
context.cov_repl_output = output
context.cov_mock_setup_hist = mock_setup
context.cov_mock_save_hist = mock_save
# ===================================================================
# run_repl — Then steps
# ===================================================================
@then("the REPL loop exit code is {code:d}")
def step_check_repl_loop_ec(context: Context, code: int) -> None:
assert context.cov_repl_exit_code == code, (
f"Expected {code}, got {context.cov_repl_exit_code}"
)
@then('the REPL loop output contains "{text}"')
def step_repl_loop_output_contains(context: Context, text: str) -> None:
assert text in context.cov_repl_output, (
f"Expected {text!r} in output:\n{context.cov_repl_output}"
)
@then('dispatch_command was called at least 2 times with "{arg}"')
def step_dispatch_called_twice(context: Context, arg: str) -> None:
assert context.cov_mock_dispatch is not None
calls_matching = [
c for c in context.cov_mock_dispatch.call_args_list if c == call([arg])
]
assert len(calls_matching) >= 2, (
f"Expected dispatch(['{arg}']) >= 2 times, got {len(calls_matching)}: "
f"{context.cov_mock_dispatch.call_args_list}"
)
@then('dispatch_command was called with args "{arg}"')
def step_dispatch_called_with(context: Context, arg: str) -> None:
assert context.cov_mock_dispatch is not None
context.cov_mock_dispatch.assert_any_call([arg])
@then("_setup_history was called")
def step_setup_hist_called(context: Context) -> None:
assert context.cov_mock_setup_hist.called
@then("_setup_history was not called")
def step_setup_hist_not_called(context: Context) -> None:
assert not context.cov_mock_setup_hist.called
@then("_save_history was called")
def step_save_hist_called(context: Context) -> None:
assert context.cov_mock_save_hist.called
@then("_save_history was not called")
def step_save_hist_not_called(context: Context) -> None:
assert not context.cov_mock_save_hist.called
# ===================================================================
# repl_callback
# ===================================================================
@given("sys.stdout.isatty returns True")
def step_isatty_true(context: Context) -> None:
context.isatty_value = True
@given("sys.stdout.isatty returns False")
def step_isatty_false(context: Context) -> None:
context.isatty_value = False
@given("run_repl is mocked to return 0")
def step_mock_run_repl(context: Context) -> None:
context.mock_run_repl_rc = 0
@given("CLEVERAGENTS_FORCE_REPL is set in the environment")
def step_force_repl_set(context: Context) -> None:
os.environ["CLEVERAGENTS_FORCE_REPL"] = "1"
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(
lambda: os.environ.pop("CLEVERAGENTS_FORCE_REPL", None)
)
@given("CLEVERAGENTS_FORCE_REPL is not set")
def step_force_repl_unset(context: Context) -> None:
os.environ.pop("CLEVERAGENTS_FORCE_REPL", None)
@when("I call repl_callback")
def step_call_repl_callback(context: Context) -> None:
from cleveragents.cli.commands.repl import repl_callback
mock_isatty = MagicMock(return_value=context.isatty_value)
mock_run = MagicMock(return_value=getattr(context, "mock_run_repl_rc", 0))
context.cov_repl_callback_exc = None
context.cov_mock_run_repl = mock_run
with (
patch("cleveragents.cli.commands.repl.sys") as mock_sys,
patch("cleveragents.cli.commands.repl.run_repl", mock_run),
):
mock_sys.stdout.isatty = mock_isatty
try:
repl_callback(no_history=False, history_path=Path("/tmp/fake_hist"))
except SystemExit as exc:
context.cov_repl_callback_exc = exc
@then("run_repl was invoked")
def step_run_repl_invoked(context: Context) -> None:
assert context.cov_mock_run_repl.called, "run_repl was not called"
@then("run_repl was not invoked")
def step_run_repl_not_invoked(context: Context) -> None:
assert not context.cov_mock_run_repl.called, "run_repl should not have been called"
@then("repl_callback raises SystemExit with code 0")
def step_callback_sysexit_0(context: Context) -> None:
assert context.cov_repl_callback_exc is not None, "Expected SystemExit"
assert context.cov_repl_callback_exc.code == 0, (
f"Expected SystemExit(0), got SystemExit({context.cov_repl_callback_exc.code})"
)