# 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