Files
temp/features/steps/server_cli_coverage_boost_steps.py
T
Luis Mendes ab911dbdc4 fix(cli): write machine-readable formats directly to stdout bypassing Rich line-wrapping
The format_output() function returned a string that callers passed to
Rich console.print(), which wraps long lines at terminal width.  This
injected literal newline characters into JSON string values (e.g. in
definition_of_done fields), producing invalid JSON that downstream
parsers could not decode (JSONDecodeError: Invalid control character).

For machine-readable formats (json, yaml, plain), format_output() now
writes the rendered output directly to sys.stdout and returns an empty
string.  This preserves the exact serialization from json.dumps/
yaml.dump without Rich text processing artifacts.

Refs: #746
2026-03-17 09:53:57 +00:00

287 lines
9.3 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
from io import StringIO
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from rich.console import Console
from cleveragents.application.services.config_service import (
ConfigLevel,
ResolvedValue,
)
# ---------------------------------------------------------------------------
# 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}"
)