Files
placeholder/features/steps/actor_cli_coverage_boost_steps.py
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

314 lines
10 KiB
Python

# pyright: reportRedeclaration=false
"""Step definitions for actor_cli_coverage_boost.feature.
Targets uncovered lines in src/cleveragents/cli/commands/actor.py:
- Lines 180-185: _get_services()
- Line 311: _actor_spec_dict() with graph_descriptor
- Line 665: set_default() service fallback
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from typer.testing import CliRunner
from cleveragents.cli.commands.actor import (
_actor_spec_dict,
_get_services,
_print_actor,
)
from cleveragents.cli.commands.actor import (
app as actor_app,
)
from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.actor import Actor
# ── helpers ──────────────────────────────────────────────────────────────
def _make_actor(
*,
name: str = "local/test-actor",
provider: str = "test-provider",
model: str = "test-model",
config: dict[str, Any] | None = None,
graph_descriptor: dict[str, Any] | None = None,
unsafe: bool = False,
is_default: bool = False,
is_built_in: bool = False,
) -> Actor:
blob = config or {}
return Actor(
id=1,
name=name,
provider=provider,
model=model,
config_blob=blob,
config_hash=Actor.compute_hash(blob),
graph_descriptor=graph_descriptor,
unsafe=unsafe,
is_built_in=is_built_in,
is_default=is_default,
)
# ── _get_services (lines 180-185) ───────────────────────────────────────
@given("a mock container with actor_registry")
def step_container_with_registry(context):
context.mock_container = MagicMock()
context.mock_container.actor_service.return_value = MagicMock(
name="actor_service_instance"
)
context.mock_container.actor_registry.return_value = MagicMock(
name="actor_registry_instance"
)
# Ensure hasattr(container, 'actor_registry') is True
context.mock_container.actor_registry.__name__ = "actor_registry"
@given("a mock container without actor_registry")
def step_container_without_registry(context):
context.mock_container = MagicMock(spec=["actor_service"])
context.mock_container.actor_service.return_value = MagicMock(
name="actor_service_instance"
)
@when("I call _get_services")
def step_call_get_services(context):
with patch(
"cleveragents.cli.commands.actor.get_container",
return_value=context.mock_container,
):
context.svc_result = _get_services()
@then("I should receive both actor_service and actor_registry")
def step_both_returned(context):
actor_service, actor_registry = context.svc_result
assert actor_service is not None, "actor_service should not be None"
assert actor_registry is not None, "actor_registry should not be None"
@then("I should receive actor_service and None for registry")
def step_service_only(context):
actor_service, actor_registry = context.svc_result
assert actor_service is not None, "actor_service should not be None"
assert actor_registry is None, "actor_registry should be None when not on container"
# ── _actor_spec_dict with/without graph_descriptor (line 311) ────────────
@given("an actor with a graph_descriptor")
def step_actor_with_graph(context):
context.test_actor = _make_actor(
name="local/graph-actor",
provider="graph-provider",
model="graph-model",
graph_descriptor={"nodes": ["start", "end"], "edges": [["start", "end"]]},
)
@given("an actor without a graph_descriptor")
def step_actor_without_graph(context):
context.test_actor = _make_actor(
name="local/plain-actor",
provider="plain-provider",
model="plain-model",
graph_descriptor=None,
)
@when("I call _actor_spec_dict on that actor")
def step_call_actor_spec_dict(context):
context.spec_dict = _actor_spec_dict(context.test_actor)
@then("the result dict should contain the graph_descriptor key")
def step_dict_has_graph(context):
assert "graph_descriptor" in context.spec_dict, (
"Expected 'graph_descriptor' key in spec dict"
)
assert context.spec_dict["graph_descriptor"] == context.test_actor.graph_descriptor
@then("the result dict should not contain the graph_descriptor key")
def step_dict_no_graph(context):
assert "graph_descriptor" not in context.spec_dict, (
"Did not expect 'graph_descriptor' key in spec dict when graph_descriptor is None"
)
# ── set-default via service fallback (line 665) ──────────────────────────
@given("an actor CLI test runner")
def step_cli_test_runner(context):
context.runner = CliRunner()
@when("I run set-default via the service path")
def step_set_default_service(context):
actor = _make_actor(
name="local/service-default",
provider="svc-provider",
model="svc-model",
is_default=True,
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_gs:
service = MagicMock()
service.set_default_actor.return_value = actor
mock_gs.return_value = (service, None) # registry is None
context.result = context.runner.invoke(
actor_app, ["set-default", "local/service-default"]
)
context.service_mock = service
context.test_actor = actor
@then("the service set_default_actor should be called")
def step_service_set_default_called(context):
context.service_mock.set_default_actor.assert_called_once_with(
"local/service-default"
)
@then("the set-default output should contain the actor name")
def step_set_default_output_has_name(context):
assert context.result.exit_code == 0
assert context.test_actor.name in context.result.output
@when("I run set-default via service for a missing actor")
def step_set_default_service_not_found(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_gs:
service = MagicMock()
service.set_default_actor.side_effect = NotFoundError(
resource_type="actor", resource_id="local/missing"
)
mock_gs.return_value = (service, None) # registry is None
context.result = context.runner.invoke(
actor_app, ["set-default", "local/missing"]
)
@then("the set-default command should abort with an error")
def step_set_default_aborted(context):
assert context.result.exit_code != 0
assert "Error:" in context.result.output
# ── _print_actor non-rich format with graph_descriptor ────────────────────
@when("I print that actor in json format")
def step_print_actor_json(context):
import contextlib
from io import StringIO
from rich.console import Console
buf = StringIO()
test_console = Console(file=buf, force_terminal=False)
with (
patch("cleveragents.cli.commands.actor.console", test_console),
contextlib.redirect_stdout(buf),
):
_print_actor(context.test_actor, title="Test", fmt="json")
context.print_output = buf.getvalue()
@then("the json output should contain the graph_descriptor")
def step_json_has_graph(context):
assert "graph_descriptor" in context.print_output
assert "nodes" in context.print_output
# ── list_actors via service fallback ──────────────────────────────────────
@when("I run actor list via service with actors")
def step_list_service(context):
actors = [
_make_actor(name="local/svc-one", provider="p1", model="m1"),
_make_actor(name="local/svc-two", provider="p2", model="m2"),
]
with patch("cleveragents.cli.commands.actor._get_services") as mock_gs:
service = MagicMock()
service.list_actors.return_value = actors
mock_gs.return_value = (service, None)
context.result = context.runner.invoke(actor_app, ["list"])
context.test_actors = actors
@then("the actor list output should contain actor names")
def step_list_has_names(context):
assert context.result.exit_code == 0
# Rich table may truncate long names with "…", so check for the
# unique suffix portion of each name which is always visible.
for a in context.test_actors:
# e.g. "local/svc-one" → check "svc-one" or at least a prefix
short = a.name.split("/")[-1]
assert short[:5] in context.result.output, (
f"Expected '{short}' fragment in output: {context.result.output!r}"
)
@when("I run actor list via service in json format")
def step_list_service_json(context):
actors = [
_make_actor(name="local/json-one", provider="jp1", model="jm1"),
]
with patch("cleveragents.cli.commands.actor._get_services") as mock_gs:
service = MagicMock()
service.list_actors.return_value = actors
mock_gs.return_value = (service, None)
context.result = context.runner.invoke(actor_app, ["list", "--format", "json"])
context.test_actors = actors
@then("the actor list json output should be valid")
def step_list_json_valid(context):
assert context.result.exit_code == 0
# The output should be parseable JSON (may have Rich markup stripped)
raw = context.result.output.strip()
# The output may include ANSI codes from Rich; just verify it contains our actor
assert "local/json-one" in raw
# ── show actor via service fallback ───────────────────────────────────────
@when("I run actor show via service path")
def step_show_service(context):
actor = _make_actor(
name="local/show-svc",
provider="show-provider",
model="show-model",
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_gs:
service = MagicMock()
service.get_actor.return_value = actor
mock_gs.return_value = (service, None)
context.result = context.runner.invoke(actor_app, ["show", "local/show-svc"])
context.test_actor = actor
@then("the show output should contain the actor name")
def step_show_has_name(context):
assert context.result.exit_code == 0
assert context.test_actor.name in context.result.output