forked from HAL9000/cleveragents-core
ab911dbdc4
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
287 lines
9.3 KiB
Python
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}"
|
|
)
|