Files
cleveragents-core/features/steps/repl_uncovered_paths_steps.py
freemo 31472b5413
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 21s
CI / helm (pull_request) Successful in 22s
CI / lint (pull_request) Successful in 3m20s
CI / quality (pull_request) Successful in 3m43s
CI / typecheck (pull_request) Successful in 3m58s
CI / security (pull_request) Successful in 4m8s
CI / integration_tests (pull_request) Successful in 9m33s
CI / unit_tests (pull_request) Successful in 10m12s
CI / docker (pull_request) Successful in 1m28s
CI / coverage (pull_request) Successful in 12m18s
CI / e2e_tests (pull_request) Successful in 19m51s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 15s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 3m18s
CI / quality (push) Successful in 3m41s
CI / typecheck (push) Successful in 3m57s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m10s
CI / unit_tests (push) Failing after 6m58s
CI / docker (push) Has been skipped
CI / integration_tests (push) Successful in 9m15s
CI / coverage (push) Successful in 12m26s
CI / e2e_tests (push) Successful in 23m11s
CI / status-check (push) Failing after 1s
CI / benchmark-publish (push) Successful in 28m31s
CI / benchmark-regression (pull_request) Successful in 54m53s
test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

334 lines
12 KiB
Python

"""Behave step implementations for repl_uncovered_paths.feature.
Covers uncovered lines in ``src/cleveragents/cli/commands/repl.py``:
610-611 persona import file-not-found
619-621 persona import validation failure
625-626 unknown persona sub-command
651-652 session switch missing name
792-794 @ reference suggestions in run_repl
805 empty argv after shlex.split in run_repl
"""
from __future__ import annotations
import os
import shutil
import tempfile
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
# ===================================================================
# Helpers
# ===================================================================
class _FakeInputRplcov3:
"""Feed predefined lines then raise EOFError to exit the REPL."""
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 _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
# ===================================================================
# Background
# ===================================================================
@given("rplcov3 the repl module is imported")
def step_rplcov3_import(context: Context) -> None:
import cleveragents.cli.commands.repl as mod
context.rplcov3_mod = mod
# ===================================================================
# Shared — temporary persona registry
# ===================================================================
@given("rplcov3 a temporary persona registry")
def step_rplcov3_temp_registry(context: Context) -> None:
from cleveragents.cli.persona import PersonaRegistry
config_dir = Path(tempfile.mkdtemp())
context.rplcov3_config_dir = config_dir
context.add_cleanup(lambda: shutil.rmtree(str(config_dir), ignore_errors=True))
patcher = patch.dict(os.environ, {"CLEVERAGENTS_CONFIG_DIR": str(config_dir)})
patcher.start()
context.add_cleanup(patcher.stop)
registry = PersonaRegistry()
registry.ensure_default_persona()
context.rplcov3_registry = registry
# Prepare default sessions dict for slash-command tests
from cleveragents.cli.commands.repl import _ReplSessionState
active = registry.get_active_persona_name() or "default"
context.rplcov3_sessions = {
"default": _ReplSessionState(name="default", active_persona=active),
}
context.rplcov3_current_session = "default"
# ===================================================================
# Slash command runner helper
# ===================================================================
def _run_slash_command(context: Context, slash_text: str) -> None:
"""Execute _handle_slash_command and capture output."""
console, buf = _make_console_and_buf()
patcher = patch("cleveragents.cli.commands.repl._console", console)
patcher.start()
try:
exit_code, session = context.rplcov3_mod._handle_slash_command(
slash_text,
registry=context.rplcov3_registry,
sessions=context.rplcov3_sessions,
current_session=context.rplcov3_current_session,
)
finally:
patcher.stop()
context.rplcov3_exit_code = exit_code
context.rplcov3_session = session
context.rplcov3_output = buf.getvalue()
# ===================================================================
# Scenario: persona import file-not-found (lines 610-611)
# ===================================================================
@when('rplcov3 I handle slash command "{cmd}"')
def step_rplcov3_handle_slash(context: Context, cmd: str) -> None:
_run_slash_command(context, cmd)
@then("rplcov3 the exit code is {code:d}")
def step_rplcov3_exit_code(context: Context, code: int) -> None:
assert context.rplcov3_exit_code == code, (
f"Expected exit code {code}, got {context.rplcov3_exit_code}. "
f"Output: {context.rplcov3_output}"
)
@then('rplcov3 the output contains "{fragment}"')
def step_rplcov3_output_contains(context: Context, fragment: str) -> None:
assert fragment in context.rplcov3_output, (
f"Expected '{fragment}' in output. Got: {context.rplcov3_output}"
)
# ===================================================================
# Scenario: persona import validation failure (lines 619-621)
# ===================================================================
@given("rplcov3 an importable YAML file with invalid persona data")
def step_rplcov3_invalid_yaml_file(context: Context) -> None:
"""Create a valid YAML file whose contents fail PersonaConfig validation."""
tmp = tempfile.NamedTemporaryFile( # noqa: SIM115
suffix=".yaml",
delete=False,
mode="w",
encoding="utf-8",
)
# Missing required 'actor' field and name with path separator -> validation error
tmp.write("name: test-bad\nactor: noslash\n")
tmp.flush()
tmp.close()
context.rplcov3_invalid_yaml_path = tmp.name
context.add_cleanup(lambda: os.unlink(tmp.name))
@when("rplcov3 I handle slash command for import with invalid persona file")
def step_rplcov3_import_invalid_persona(context: Context) -> None:
"""Run persona import pointing at the invalid YAML file via a relative path."""
abs_path = Path(context.rplcov3_invalid_yaml_path)
# resolve_import_path requires a relative path; mock it to return the absolute path
console, buf = _make_console_and_buf()
patcher_console = patch("cleveragents.cli.commands.repl._console", console)
patcher_resolve = patch.object(
context.rplcov3_registry,
"resolve_import_path",
return_value=abs_path,
)
patcher_console.start()
patcher_resolve.start()
try:
exit_code, session = context.rplcov3_mod._handle_slash_command(
f"persona import {abs_path.name}",
registry=context.rplcov3_registry,
sessions=context.rplcov3_sessions,
current_session=context.rplcov3_current_session,
)
finally:
patcher_console.stop()
patcher_resolve.stop()
context.rplcov3_exit_code = exit_code
context.rplcov3_session = session
context.rplcov3_output = buf.getvalue()
# ===================================================================
# Scenario: @ reference suggestions in run_repl (lines 792-794)
# ===================================================================
@given("rplcov3 a reference catalog with known entries")
def step_rplcov3_reference_catalog(context: Context) -> None:
"""Prepare a catalog that has entries so suggestions are produced."""
context.rplcov3_catalog = {
"file": ["README.md", "setup.py", "main.py"],
"actor": ["local/helper"],
"tool": [],
"skill": [],
"project": [],
"plan": [],
}
@when('rplcov3 I run the REPL with input "@zzz_nomatch_token" and mocked catalog')
def step_rplcov3_repl_at_suggestions(context: Context) -> None:
"""Feed the REPL a line with an @ token that won't expand but will trigger suggestions."""
from cleveragents.cli.commands.repl import run_repl
console, buf = _make_console_and_buf()
# The line contains @ so _build_reference_catalog is called; mock it
# _expand_references will NOT change line (no fuzzy match for zzz_nomatch_token
# against our catalog entries), so we fall into the elif branch (line 791).
# _reference_suggestions should then return hints.
catalog = context.rplcov3_catalog
# Make _expand_references return the line unchanged (it will, because no match)
# Make _reference_suggestions return some hints
fake_input = _FakeInputRplcov3(["@file:rea"])
mock_dispatch = MagicMock(return_value=0)
patches = [
patch("cleveragents.cli.commands.repl._console", console),
patch("builtins.input", fake_input),
patch("cleveragents.cli.commands.repl.dispatch_command", mock_dispatch),
patch(
"cleveragents.cli.commands.repl._build_reference_catalog",
return_value=catalog,
),
# Mock _expand_references to return the input unchanged so we hit the elif branch
patch(
"cleveragents.cli.commands.repl._expand_references",
side_effect=lambda line, **kw: line,
),
# Mock _reference_suggestions to return hints
patch(
"cleveragents.cli.commands.repl._reference_suggestions",
return_value=["@file:README.md", "@file:setup.py"],
),
]
for p in patches:
p.start()
try:
code = run_repl(no_history=True)
finally:
for p in patches:
p.stop()
context.rplcov3_repl_exit_code = code
context.rplcov3_repl_output = buf.getvalue()
@then("rplcov3 the repl exit code is {code:d}")
def step_rplcov3_repl_exit_code(context: Context, code: int) -> None:
assert context.rplcov3_repl_exit_code == code, (
f"Expected REPL exit code {code}, got {context.rplcov3_repl_exit_code}. "
f"Output: {context.rplcov3_repl_output}"
)
@then('rplcov3 the repl output contains "{fragment}"')
def step_rplcov3_repl_output_contains(context: Context, fragment: str) -> None:
assert fragment in context.rplcov3_repl_output, (
f"Expected '{fragment}' in REPL output. Got: {context.rplcov3_repl_output}"
)
# ===================================================================
# Scenario: empty argv after shlex.split in run_repl (line 805)
# ===================================================================
@when("rplcov3 I run the REPL with input that tokenises to empty")
def step_rplcov3_repl_empty_argv(context: Context) -> None:
"""Feed the REPL a line that shlex.split produces an empty list from.
A line of whitespace is stripped to '' by line.strip() and caught
by the `if not line: continue` guard. To reach shlex.split we need
a non-empty string that tokenises to nothing. We can achieve this
by passing a line consisting of only a comment character, but shlex
will not produce empty there either.
The simplest way: mock shlex.split to return [] for a specific line,
then verify dispatch_command was never called.
"""
from cleveragents.cli.commands.repl import run_repl
console, buf = _make_console_and_buf()
# Feed a real non-empty line so it passes through all guards
fake_input = _FakeInputRplcov3(["someline"])
mock_dispatch = MagicMock(return_value=0)
original_shlex_split = __import__("shlex").split
def fake_shlex_split(text):
# Return empty list for our test line to hit line 805
if text == "someline":
return []
return original_shlex_split(text)
patches = [
patch("cleveragents.cli.commands.repl._console", console),
patch("builtins.input", fake_input),
patch("cleveragents.cli.commands.repl.dispatch_command", mock_dispatch),
patch("shlex.split", side_effect=fake_shlex_split),
]
for p in patches:
p.start()
try:
code = run_repl(no_history=True)
finally:
for p in patches:
p.stop()
context.rplcov3_repl_exit_code = code
context.rplcov3_repl_output = buf.getvalue()
context.rplcov3_mock_dispatch = mock_dispatch
@then("rplcov3 dispatch_command was not called")
def step_rplcov3_dispatch_not_called(context: Context) -> None:
assert not context.rplcov3_mock_dispatch.called, (
"dispatch_command should not have been called for empty argv"
)