Files
cleveragents-core/robot/helper_repl_smoke.py
T
brent.edwards f48eb5a05f
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 19s
CI / build (pull_request) Successful in 24s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 43s
CI / integration_tests (pull_request) Successful in 2m37s
CI / unit_tests (pull_request) Successful in 6m12s
CI / docker (pull_request) Successful in 1m1s
CI / benchmark-regression (pull_request) Successful in 15m33s
CI / coverage (pull_request) Successful in 21m54s
feat(cli): add interactive repl
Implement `agents repl` command providing an interactive read-eval-print
loop that dispatches to existing CLI commands without the `agents` prefix.

Features:
- Command dispatch via existing Typer app
- readline history with --no-history opt-out (default: ~/.cleveragents/history)
- Tab-completion for all CLI commands and built-in commands
- !! last-command repetition
- Prompt context from CLEVERAGENTS_PROJECT / CLEVERAGENTS_PLAN env vars
- Ctrl+C (continue) and Ctrl+D (exit) signal handling
- Multi-line input with \ continuation
- :help and :exit/:quit built-in commands

Test coverage:
- 15 Behave scenarios (features/repl.feature)
- 10 Robot Framework smoke tests (robot/repl_smoke.robot)
- ASV benchmark suite (benchmarks/repl_bench.py)
- All nox sessions pass: lint, typecheck, unit_tests, integration_tests
- Coverage: 97.2% (threshold: 97%)
2026-02-23 03:11:12 +00:00

144 lines
4.2 KiB
Python

"""Helper script for repl_smoke.robot smoke tests.
Each function is a self-contained check used as a Robot Framework keyword.
"""
from __future__ import annotations
import os
import sys
from io import StringIO
from pathlib import Path
from unittest.mock import patch
# Ensure local source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.cli.commands.repl import ( # noqa: E402
_get_prompt_context,
_setup_completer,
dispatch_command,
run_repl,
)
# ---------------------------------------------------------------------------
# Fake input helper
# ---------------------------------------------------------------------------
class _FakeInput:
"""Feed predetermined lines 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
return line
def _run(lines: list[str]) -> tuple[int, str]:
from rich.console import Console
buf = StringIO()
console = Console(file=buf, no_color=True, width=200)
fake = _FakeInput(lines)
with (
patch("cleveragents.cli.commands.repl._console", console),
patch("builtins.input", fake),
):
code = run_repl(no_history=True)
return code, buf.getvalue()
# ---------------------------------------------------------------------------
# Robot keywords
# ---------------------------------------------------------------------------
def get_default_prompt() -> str:
"""Return prompt context with no environment variables set."""
old_proj = os.environ.pop("CLEVERAGENTS_PROJECT", None)
old_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)
try:
return _get_prompt_context()
finally:
if old_proj is not None:
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
if old_plan is not None:
os.environ["CLEVERAGENTS_PLAN"] = old_plan
def get_prompt_with_project(project: str) -> str:
"""Return prompt context with CLEVERAGENTS_PROJECT set."""
old = os.environ.get("CLEVERAGENTS_PROJECT")
os.environ["CLEVERAGENTS_PROJECT"] = project
try:
return _get_prompt_context()
finally:
if old is None:
os.environ.pop("CLEVERAGENTS_PROJECT", None)
else:
os.environ["CLEVERAGENTS_PROJECT"] = old
def get_prompt_with_project_and_plan(project: str, plan: str) -> str:
"""Return prompt context with both env vars set."""
old_proj = os.environ.get("CLEVERAGENTS_PROJECT")
old_plan = os.environ.get("CLEVERAGENTS_PLAN")
os.environ["CLEVERAGENTS_PROJECT"] = project
os.environ["CLEVERAGENTS_PLAN"] = plan
try:
return _get_prompt_context()
finally:
if old_proj is None:
os.environ.pop("CLEVERAGENTS_PROJECT", None)
else:
os.environ["CLEVERAGENTS_PROJECT"] = old_proj
if old_plan is None:
os.environ.pop("CLEVERAGENTS_PLAN", None)
else:
os.environ["CLEVERAGENTS_PLAN"] = old_plan
def dispatch_argv(*args: str) -> int:
"""Call ``dispatch_command`` with the given argv tokens."""
return dispatch_command(list(args))
def run_repl_with_eof() -> int:
"""Run the REPL with no input (immediate EOF)."""
code, _ = _run([])
return code
def run_repl_with_help() -> str:
"""Run the REPL, send ``:help`` then exit, return output."""
_, output = _run([":help"])
return output
def run_repl_with_bang_bang() -> str:
"""Run the REPL, send ``!!`` with no prior command, return output."""
_, output = _run(["!!"])
return output
def verify_completer() -> bool:
"""Verify that the tab completer is installed and recognises commands."""
import readline
_setup_completer()
# The completer should match ':help' when given ':'
completer_func = readline.get_completer()
if completer_func is None:
return False
match = completer_func(":", 0)
return match is not None and match.startswith(":")