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
314 lines
10 KiB
Python
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
|