Files
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

105 lines
3.2 KiB
Python

"""Benchmarks for CleverAgents REPL startup and helpers."""
from __future__ import annotations
import os
import sys
from io import StringIO
from pathlib import Path
from unittest.mock import patch
try:
from cleveragents.cli.commands.repl import (
_get_prompt_context,
_setup_completer,
dispatch_command,
run_repl,
)
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cleveragents.cli.commands.repl import (
_get_prompt_context,
_setup_completer,
dispatch_command,
run_repl,
)
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_repl_silent(lines: list[str]) -> int:
"""Run REPL with mocked console and input; return exit code."""
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),
):
return run_repl(no_history=True)
class TimeSuite:
"""Measure REPL startup and helper latency."""
def time_prompt_context_default(self) -> None:
"""Benchmark ``_get_prompt_context`` with no env vars."""
old_proj = os.environ.pop("CLEVERAGENTS_PROJECT", None)
old_plan = os.environ.pop("CLEVERAGENTS_PLAN", None)
try:
_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 time_prompt_context_with_vars(self) -> None:
"""Benchmark ``_get_prompt_context`` with project and plan set."""
old_proj = os.environ.get("CLEVERAGENTS_PROJECT")
old_plan = os.environ.get("CLEVERAGENTS_PLAN")
os.environ["CLEVERAGENTS_PROJECT"] = "bench-proj"
os.environ["CLEVERAGENTS_PLAN"] = "bench-plan"
try:
_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 time_setup_completer(self) -> None:
"""Benchmark installing the readline completer."""
_setup_completer()
def time_repl_immediate_eof(self) -> None:
"""Benchmark REPL startup → immediate EOF exit."""
_run_repl_silent([])
def time_repl_help_then_exit(self) -> None:
"""Benchmark REPL: ``:help`` then exit."""
_run_repl_silent([":help"])
def time_dispatch_version(self) -> None:
"""Benchmark ``dispatch_command(["--version"])``."""
dispatch_command(["--version"])