"""Behave step implementations for the REPL feature.""" from __future__ import annotations import shutil import tempfile from pathlib import Path from unittest.mock import patch from behave import given, then, when from behave.runner import Context from cleveragents.cli.commands.repl import ( _get_prompt_context, run_repl, ) from features.mocks.fake_repl_input import FakeReplInput # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _run_repl_with_input( lines: list[str], *, no_history: bool = False, history_path: Path | None = None, ) -> tuple[int, str]: """Run the REPL with simulated input and capture console output.""" from io import StringIO from rich.console import Console buf = StringIO() console = Console(file=buf, no_color=True, width=200) kw: dict[str, object] = {"no_history": no_history} if history_path is not None: kw["history_path"] = history_path fake = FakeReplInput(lines, interrupt_token="__KEYBOARD_INTERRUPT__") with ( patch("cleveragents.cli.commands.repl._console", console), patch("builtins.input", fake), ): code = run_repl(**kw) # type: ignore[arg-type] return code, buf.getvalue() # --------------------------------------------------------------------------- # Given # --------------------------------------------------------------------------- @given("the REPL is started") def step_repl_started(context: Context) -> None: context.repl_lines = [] context.repl_output = "" context.repl_exit_code = 0 context.repl_no_history = True # default to no history in tests context.repl_history_path = None @given("the REPL is started with --no-history") def step_repl_started_no_history(context: Context) -> None: context.repl_lines = [] context.repl_output = "" context.repl_exit_code = 0 context.repl_no_history = True context.repl_history_path = None @given("the REPL is started with a custom history path") def step_repl_started_custom_history(context: Context) -> None: context.repl_lines = [] context.repl_output = "" context.repl_exit_code = 0 context.repl_no_history = False tmpdir = Path(tempfile.mkdtemp()) context.repl_history_path = tmpdir / "test_history" context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True)) # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- @when('I send the input "{text}"') def step_send_input(context: Context, text: str) -> None: context.repl_lines.append(text) # Execute REPL with accumulated lines code, output = _run_repl_with_input( context.repl_lines, no_history=context.repl_no_history, history_path=context.repl_history_path, ) context.repl_exit_code = code context.repl_output = output @when("I send a multi-line version command") def step_send_multiline_version(context: Context) -> None: # Simulate typing ``version \`` followed by an empty continuation line context.repl_lines.append("version \\") context.repl_lines.append("") code, output = _run_repl_with_input( context.repl_lines, no_history=context.repl_no_history, history_path=context.repl_history_path, ) context.repl_exit_code = code context.repl_output = output @when("I send an empty input line") def step_send_empty_input(context: Context) -> None: context.repl_lines.append("") code, output = _run_repl_with_input( context.repl_lines, no_history=context.repl_no_history, history_path=context.repl_history_path, ) context.repl_exit_code = code context.repl_output = output @when("the REPL exits") def step_repl_exits(context: Context) -> None: # Re-run with the accumulated lines (EOFError ends the REPL) code, output = _run_repl_with_input( context.repl_lines, no_history=context.repl_no_history, history_path=context.repl_history_path, ) context.repl_exit_code = code context.repl_output = output @when("I query the REPL prompt context") def step_query_prompt(context: Context) -> None: context.repl_prompt = _get_prompt_context() @when("I simulate a KeyboardInterrupt during input") def step_simulate_ctrl_c(context: Context) -> None: # Send a keyboard interrupt marker, then :exit context.repl_lines = ["__KEYBOARD_INTERRUPT__", ":exit"] code, output = _run_repl_with_input( context.repl_lines, no_history=True, ) context.repl_exit_code = code context.repl_output = output @when("I simulate an EOFError during input") def step_simulate_ctrl_d(context: Context) -> None: # Empty lines list → immediate EOFError → clean exit context.repl_lines = [] code, output = _run_repl_with_input( context.repl_lines, no_history=True, ) context.repl_exit_code = code context.repl_output = output # --------------------------------------------------------------------------- # Then # --------------------------------------------------------------------------- @then('the REPL output should contain "{text}"') def step_output_contains(context: Context, text: str) -> None: assert text in context.repl_output, ( f"Expected '{text}' in output but got:\n{context.repl_output}" ) @then("the REPL exit code should be {code:d}") def step_exit_code(context: Context, code: int) -> None: assert context.repl_exit_code == code, ( f"Expected exit code {code} but got {context.repl_exit_code}" ) @then("the REPL should exit cleanly") def step_exit_cleanly(context: Context) -> None: # A clean exit means exit code 0 if not hasattr(context, "repl_exit_code") or context.repl_exit_code is None: # Run with empty input to trigger EOFError → clean exit code, output = _run_repl_with_input( context.repl_lines, no_history=True, ) context.repl_exit_code = code context.repl_output = output assert context.repl_exit_code == 0, ( f"Expected clean exit (code 0) but got {context.repl_exit_code}" ) @then("the REPL should continue running") def step_repl_continues(context: Context) -> None: # After Ctrl+C the REPL should still exit cleanly (via :exit or EOFError) assert context.repl_exit_code == 0, ( f"REPL did not continue after interrupt (exit code {context.repl_exit_code})" ) @then("the REPL should continue without error") def step_repl_continues_no_error(context: Context) -> None: assert context.repl_exit_code == 0 @then("no history file should be written") def step_no_history_file(context: Context) -> None: if context.repl_history_path is not None: assert not context.repl_history_path.exists(), ( f"History file should not exist but found: {context.repl_history_path}" ) @then("the history file should exist") def step_history_exists(context: Context) -> None: assert context.repl_history_path is not None assert context.repl_history_path.exists(), ( f"History file should exist at {context.repl_history_path}" ) @then('the prompt should contain "{text}"') def step_prompt_contains(context: Context, text: str) -> None: assert text in context.repl_prompt, ( f"Expected '{text}' in prompt '{context.repl_prompt}'" )