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
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
334 lines
12 KiB
Python
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"
|
|
)
|