forked from HAL9000/cleveragents-core
436 lines
14 KiB
Python
436 lines
14 KiB
Python
"""Step definitions for server CLI coverage boost scenarios.
|
|
|
|
Targets uncovered lines in src/cleveragents/cli/commands/server.py:
|
|
- Lines 58-59: resolve_server_mode exception branches
|
|
- Lines 132-142: server_connect rich format Panel rendering
|
|
- Lines 157-204: server_status command (entire function)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import re
|
|
from io import StringIO
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import typer
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from rich.console import Console
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.application.services.config_service import (
|
|
ConfigLevel,
|
|
ResolvedValue,
|
|
)
|
|
|
|
_ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_resolved(key: str, value: Any) -> ResolvedValue:
|
|
"""Build a ResolvedValue with DEFAULT source."""
|
|
return ResolvedValue(key=key, value=value, source=ConfigLevel.DEFAULT)
|
|
|
|
|
|
def _build_mock_svc(
|
|
resolve_map: dict[str, Any] | None = None,
|
|
raise_map: dict[str, type[Exception]] | None = None,
|
|
) -> MagicMock:
|
|
"""Build a mock ConfigService whose resolve() returns controlled values.
|
|
|
|
Parameters
|
|
----------
|
|
resolve_map:
|
|
``{key: value}`` — the mock returns a ``ResolvedValue`` with this value.
|
|
raise_map:
|
|
``{key: ExceptionClass}`` — the mock raises the given exception.
|
|
"""
|
|
resolve_map = resolve_map or {}
|
|
raise_map = raise_map or {}
|
|
|
|
svc = MagicMock()
|
|
|
|
def _resolve(key: str, **_kw: Any) -> ResolvedValue:
|
|
if key in raise_map:
|
|
raise raise_map[key](f"mock error for {key}")
|
|
val = resolve_map.get(key)
|
|
return _make_resolved(key, val)
|
|
|
|
svc.resolve.side_effect = _resolve
|
|
svc.read_config.return_value = {}
|
|
svc.write_config.return_value = None
|
|
return svc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given — mock config services
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock config service that raises ValueError on resolve")
|
|
def step_mock_svc_valueerror(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(raise_map={"server.url": ValueError})
|
|
|
|
|
|
@given("a mock config service that raises KeyError on resolve")
|
|
def step_mock_svc_keyerror(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(raise_map={"server.url": KeyError})
|
|
|
|
|
|
@given("a mock config service that accepts writes")
|
|
def step_mock_svc_writes(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc()
|
|
|
|
|
|
@given("a mock config service that resolves with no server config")
|
|
def step_mock_svc_no_config(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(
|
|
resolve_map={
|
|
"server.url": None,
|
|
"server.namespace": None,
|
|
"server.tls-verify": None,
|
|
},
|
|
)
|
|
|
|
|
|
@given("a mock config service that resolves with full server config")
|
|
def step_mock_svc_full_config(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(
|
|
resolve_map={
|
|
"server.url": "https://configured.example.com",
|
|
"server.namespace": "production",
|
|
"server.tls-verify": False,
|
|
},
|
|
)
|
|
|
|
|
|
@given("a mock config service where namespace resolve raises ValueError")
|
|
def step_mock_svc_ns_error(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(
|
|
resolve_map={
|
|
"server.url": "https://ns-error.example.com",
|
|
"server.tls-verify": True,
|
|
},
|
|
raise_map={"server.namespace": ValueError},
|
|
)
|
|
|
|
|
|
@given("a mock config service where tls-verify resolve raises KeyError")
|
|
def step_mock_svc_tls_error(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(
|
|
resolve_map={
|
|
"server.url": "https://tls-error.example.com",
|
|
"server.namespace": "default",
|
|
},
|
|
raise_map={"server.tls-verify": KeyError},
|
|
)
|
|
|
|
|
|
@given("a mock config service where all resolves raise exceptions")
|
|
def step_mock_svc_all_errors(context: Context) -> None:
|
|
context.mock_svc = _build_mock_svc(
|
|
raise_map={
|
|
"server.url": ValueError,
|
|
"server.namespace": KeyError,
|
|
"server.tls-verify": ValueError,
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — resolve_server_mode
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call resolve_server_mode with the mock")
|
|
def step_call_resolve_mode(context: Context) -> None:
|
|
from cleveragents.cli.commands.server import resolve_server_mode
|
|
|
|
with patch(
|
|
"cleveragents.cli.commands.server._get_config_service",
|
|
return_value=context.mock_svc,
|
|
):
|
|
context.server_mode_result = resolve_server_mode()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — server_connect with rich format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I call server_connect with url "{url}" and rich format')
|
|
def step_call_connect_rich(context: Context, url: str) -> None:
|
|
from cleveragents.cli.commands.server import server_connect
|
|
|
|
buf = StringIO()
|
|
capture_console = Console(file=buf, width=200, no_color=True)
|
|
with (
|
|
patch(
|
|
"cleveragents.cli.commands.server._get_config_service",
|
|
return_value=context.mock_svc,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.server.console",
|
|
capture_console,
|
|
),
|
|
):
|
|
server_connect(server_url=url, namespace="default", tls_verify=True, fmt="rich")
|
|
|
|
context.captured_console_output = buf.getvalue()
|
|
|
|
|
|
@when(
|
|
'I call server_connect with url "{url}" namespace "{ns}" tls_verify false and rich format'
|
|
)
|
|
def step_call_connect_rich_ns_tls(context: Context, url: str, ns: str) -> None:
|
|
from cleveragents.cli.commands.server import server_connect
|
|
|
|
buf = StringIO()
|
|
capture_console = Console(file=buf, width=200, no_color=True)
|
|
with (
|
|
patch(
|
|
"cleveragents.cli.commands.server._get_config_service",
|
|
return_value=context.mock_svc,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.server.console",
|
|
capture_console,
|
|
),
|
|
):
|
|
server_connect(server_url=url, namespace=ns, tls_verify=False, fmt="rich")
|
|
|
|
context.captured_console_output = buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When — server_status
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I call server_status with format "{fmt}"')
|
|
def step_call_status(context: Context, fmt: str) -> None:
|
|
from cleveragents.cli.commands.server import server_status
|
|
|
|
echo_buf = StringIO()
|
|
console_buf = StringIO()
|
|
capture_console = Console(file=console_buf, width=200, no_color=True)
|
|
|
|
def _capture_echo(msg: str = "", **_kw: Any) -> None:
|
|
echo_buf.write(str(msg))
|
|
echo_buf.write("\n")
|
|
|
|
# We need to also patch resolve_server_mode since it calls _get_config_service
|
|
# internally. We compute the expected mode from the mock_svc.
|
|
mock_svc = context.mock_svc
|
|
|
|
# Determine what resolve_server_mode would return given this mock
|
|
try:
|
|
rv = mock_svc.resolve("server.url")
|
|
if rv.value is not None and str(rv.value).strip():
|
|
expected_mode = "stubbed"
|
|
else:
|
|
expected_mode = "disabled"
|
|
except (ValueError, KeyError):
|
|
expected_mode = "disabled"
|
|
|
|
stdout_buf = StringIO()
|
|
with (
|
|
patch(
|
|
"cleveragents.cli.commands.server._get_config_service",
|
|
return_value=mock_svc,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.server.resolve_server_mode",
|
|
return_value=expected_mode,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.server.typer.echo",
|
|
side_effect=_capture_echo,
|
|
),
|
|
patch(
|
|
"cleveragents.cli.commands.server.console",
|
|
capture_console,
|
|
),
|
|
contextlib.redirect_stdout(stdout_buf),
|
|
):
|
|
server_status(fmt=fmt)
|
|
|
|
echoed = echo_buf.getvalue().strip()
|
|
context.echoed_output = echo_buf.getvalue() if echoed else stdout_buf.getvalue()
|
|
context.captured_console_output = console_buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then — assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the server mode should be "{expected}"')
|
|
def step_assert_mode(context: Context, expected: str) -> None:
|
|
assert context.server_mode_result == expected, (
|
|
f"Expected mode '{expected}', got '{context.server_mode_result}'"
|
|
)
|
|
|
|
|
|
@then('the captured console output should contain "{text}"')
|
|
def step_assert_console_contains(context: Context, text: str) -> None:
|
|
output = getattr(context, "captured_console_output", "")
|
|
assert text in output, f"Expected '{text}' in console output:\n{output!r}"
|
|
|
|
|
|
@then('the echoed output should contain "{text}"')
|
|
def step_assert_echo_contains(context: Context, text: str) -> None:
|
|
output = getattr(context, "echoed_output", "")
|
|
assert text.lower() in output.lower(), (
|
|
f"Expected '{text}' in echoed output:\n{output!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# server_serve — uvicorn.run mapping assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("uvicorn run is patched for server serve assertions")
|
|
def step_patch_uvicorn_run(context: Context) -> None:
|
|
uvicorn_run_patcher = patch(
|
|
"cleveragents.cli.commands.server.uvicorn_run",
|
|
)
|
|
context.uvicorn_run_patcher = uvicorn_run_patcher
|
|
context.uvicorn_run_mock = uvicorn_run_patcher.start()
|
|
context.add_cleanup(uvicorn_run_patcher.stop)
|
|
|
|
|
|
@given("uvicorn run is unavailable for server serve assertions")
|
|
def step_patch_uvicorn_unavailable(context: Context) -> None:
|
|
uvicorn_run_patcher = patch(
|
|
"cleveragents.cli.commands.server.uvicorn_run",
|
|
None,
|
|
)
|
|
context.uvicorn_run_patcher = uvicorn_run_patcher
|
|
uvicorn_run_patcher.start()
|
|
context.add_cleanup(uvicorn_run_patcher.stop)
|
|
|
|
|
|
@when(
|
|
'I call server_serve with app "{app_target}" host "{host}" port {port:d} workers {workers:d} and log level "{log_level}"'
|
|
)
|
|
def step_call_server_serve_explicit(
|
|
context: Context,
|
|
app_target: str,
|
|
host: str,
|
|
port: int,
|
|
workers: int,
|
|
log_level: str,
|
|
) -> None:
|
|
from cleveragents.cli.commands import server as server_commands
|
|
|
|
server_commands.server_serve(
|
|
app_target=app_target,
|
|
host=host,
|
|
port=port,
|
|
workers=workers,
|
|
log_level=log_level,
|
|
)
|
|
|
|
|
|
@when("I call server_serve with default arguments")
|
|
def step_call_server_serve_defaults(context: Context) -> None:
|
|
from cleveragents.cli.commands import server as server_commands
|
|
|
|
server_commands.server_serve()
|
|
|
|
|
|
@when("I call server_serve with default arguments and capture failure")
|
|
def step_call_server_serve_defaults_capture_failure(context: Context) -> None:
|
|
from cleveragents.cli.commands import server as server_commands
|
|
|
|
buf = StringIO()
|
|
capture_console = Console(file=buf, width=200, no_color=True)
|
|
|
|
context.server_serve_exit = None
|
|
with patch("cleveragents.cli.commands.server.console", capture_console):
|
|
try:
|
|
server_commands.server_serve()
|
|
except typer.Exit as exc:
|
|
context.server_serve_exit = exc
|
|
|
|
context.captured_console_output = buf.getvalue()
|
|
|
|
|
|
@when('I invoke server_serve through the CLI with log level "{log_level}"')
|
|
def step_invoke_server_serve_cli(context: Context, log_level: str) -> None:
|
|
from cleveragents.cli.commands import server as server_commands
|
|
|
|
runner = CliRunner()
|
|
context.cli_result = runner.invoke(
|
|
server_commands.app,
|
|
["serve", "--log-level", log_level],
|
|
)
|
|
|
|
|
|
@when('I invoke server_serve through the CLI with invalid log level "{log_level}"')
|
|
def step_invoke_server_serve_cli_invalid(context: Context, log_level: str) -> None:
|
|
from cleveragents.cli.commands import server as server_commands
|
|
|
|
runner = CliRunner()
|
|
context.cli_result = runner.invoke(
|
|
server_commands.app,
|
|
["serve", "--log-level", log_level],
|
|
)
|
|
|
|
|
|
@then(
|
|
'uvicorn run should be called with app "{app_target}" host "{host}" port {port:d} workers {workers:d} and log level "{log_level}"'
|
|
)
|
|
def step_assert_server_serve_mapping(
|
|
context: Context,
|
|
app_target: str,
|
|
host: str,
|
|
port: int,
|
|
workers: int,
|
|
log_level: str,
|
|
) -> None:
|
|
run_mock = context.uvicorn_run_mock
|
|
run_mock.assert_called_once_with(
|
|
app_target,
|
|
host=host,
|
|
port=port,
|
|
workers=workers,
|
|
log_level=log_level,
|
|
)
|
|
|
|
|
|
@then("server_serve should exit with code {code:d}")
|
|
def step_assert_server_serve_exit_code(context: Context, code: int) -> None:
|
|
exit_exc = getattr(context, "server_serve_exit", None)
|
|
assert isinstance(exit_exc, typer.Exit), (
|
|
f"Expected typer.Exit, got {type(exit_exc).__name__}: {exit_exc!r}"
|
|
)
|
|
exit_code = exit_exc.exit_code
|
|
assert exit_code is not None, "Expected typer.Exit to carry an exit code"
|
|
assert exit_code == code, f"Expected exit code {code}, got {exit_code}"
|
|
|
|
|
|
@then("the CLI invocation should fail with exit code {code:d}")
|
|
def step_assert_cli_exit_code(context: Context, code: int) -> None:
|
|
result = context.cli_result
|
|
assert result.exit_code == code, (
|
|
f"Expected CLI exit code {code}, got {result.exit_code}. "
|
|
f"Output:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then('the server CLI invocation output should contain "{text}"')
|
|
def step_assert_cli_output_contains(context: Context, text: str) -> None:
|
|
result = context.cli_result
|
|
normalized_output = _ANSI_ESCAPE_RE.sub("", result.output)
|
|
assert text in normalized_output, (
|
|
f"Expected '{text}' in CLI output, got:\n{result.output!r}"
|
|
)
|