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
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
737 lines
24 KiB
Python
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})"
|
|
)
|