"""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}" )