# pyright: reportRedeclaration=false """Step definitions for the Actor CLI YAML-first alignment feature.""" from __future__ import annotations import json from typing import Any from unittest.mock import MagicMock, patch from behave import then, when from cleveragents.cli.commands.actor import app as actor_app from cleveragents.domain.models.core.actor import Actor _ENVELOPE_KEYS = {"command", "status", "exit_code", "data", "timing", "messages"} def _unwrap_envelope(parsed: Any) -> Any: """Return the ``data`` field if *parsed* is a spec envelope, else *parsed* as-is.""" if isinstance(parsed, dict) and _ENVELOPE_KEYS.issubset(parsed.keys()): return parsed["data"] return parsed def _make_actor( *, name: str = "local/test-actor", provider: str = "default-provider", model: str = "default-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, ) # ------------------------------------------------------------------ # List with format # ------------------------------------------------------------------ @when("I run actor list with two actors using format json") def step_list_json(context: Any) -> None: actors = [ _make_actor(name="local/first", provider="p1", model="m1"), _make_actor(name="local/second", provider="p2", model="m2", unsafe=True), ] with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_svc.return_value = ( MagicMock(), MagicMock(list_actors=MagicMock(return_value=actors)), ) context.result = context.runner.invoke(actor_app, ["list", "--format", "json"]) context.actors = actors @when("I run actor list with two actors using format yaml") def step_list_yaml(context: Any) -> None: actors = [ _make_actor(name="local/first", provider="p1", model="m1"), _make_actor(name="local/second", provider="p2", model="m2"), ] with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_svc.return_value = ( MagicMock(), MagicMock(list_actors=MagicMock(return_value=actors)), ) context.result = context.runner.invoke(actor_app, ["list", "--format", "yaml"]) context.actors = actors @when("I run actor list with two actors using format plain") def step_list_plain(context: Any) -> None: actors = [ _make_actor(name="local/first", provider="p1", model="m1"), _make_actor(name="local/second", provider="p2", model="m2"), ] with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_svc.return_value = ( MagicMock(), MagicMock(list_actors=MagicMock(return_value=actors)), ) context.result = context.runner.invoke(actor_app, ["list", "--format", "plain"]) context.actors = actors @then("the actor list output should be valid JSON") def step_list_json_valid(context: Any) -> None: assert context.result.exit_code == 0 parsed = json.loads(context.result.output.strip()) data = _unwrap_envelope(parsed) assert isinstance(data, list) assert len(data) == 2 assert data[0]["name"] == "local/first" assert data[1]["name"] == "local/second" @then("the actor list output should contain YAML keys") def step_list_yaml_valid(context: Any) -> None: assert context.result.exit_code == 0 output = context.result.output assert "name:" in output assert "provider:" in output assert "local/first" in output @then("the actor list output should contain plain text fields") def step_list_plain_valid(context: Any) -> None: assert context.result.exit_code == 0 output = context.result.output assert "name:" in output assert "provider:" in output assert "local/first" in output # ------------------------------------------------------------------ # Show with format # ------------------------------------------------------------------ @when("I run actor show with format json") def step_show_json(context: Any) -> None: actor = _make_actor(name="local/show-json", provider="openai", model="gpt-4") with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: registry = MagicMock() registry.get_actor.return_value = actor mock_svc.return_value = (MagicMock(), registry) context.result = context.runner.invoke( actor_app, ["show", actor.name, "--format", "json"] ) context.actor = actor @when("I run actor show with format yaml") def step_show_yaml(context: Any) -> None: actor = _make_actor(name="local/show-yaml", provider="openai", model="gpt-4") with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: registry = MagicMock() registry.get_actor.return_value = actor mock_svc.return_value = (MagicMock(), registry) context.result = context.runner.invoke( actor_app, ["show", actor.name, "--format", "yaml"] ) context.actor = actor @when("I run actor show with format plain") def step_show_plain(context: Any) -> None: actor = _make_actor(name="local/show-plain", provider="openai", model="gpt-4") with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: registry = MagicMock() registry.get_actor.return_value = actor mock_svc.return_value = (MagicMock(), registry) context.result = context.runner.invoke( actor_app, ["show", actor.name, "--format", "plain"] ) context.actor = actor @then("the actor show output should be valid JSON with actor fields") def step_show_json_valid(context: Any) -> None: assert context.result.exit_code == 0 parsed = json.loads(context.result.output.strip()) data = _unwrap_envelope(parsed) assert isinstance(data, dict) assert data["name"] == "local/show-json" assert data["provider"] == "openai" assert data["model"] == "gpt-4" assert "config_hash" in data assert "updated_at" in data @then("the actor show output should contain YAML actor keys") def step_show_yaml_valid(context: Any) -> None: assert context.result.exit_code == 0 output = context.result.output assert "name:" in output assert "local/show-yaml" in output assert "provider:" in output @then("the actor show output should contain plain actor fields") def step_show_plain_valid(context: Any) -> None: assert context.result.exit_code == 0 output = context.result.output assert "name:" in output assert "local/show-plain" in output assert "provider:" in output # ------------------------------------------------------------------ # Add with --update flag # ------------------------------------------------------------------ @when("I run actor add with update flag") def step_add_update(context: Any) -> None: with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_registry = MagicMock() mock_service = MagicMock() mock_actor = _make_actor(config=context.actor_config_data) # registry.add() is the YAML-first path used by the add command mock_registry.add.return_value = mock_actor mock_svc.return_value = (mock_service, mock_registry) actor_name = context.actor_config_data.get("name", "local/test-actor") context.result = context.runner.invoke( actor_app, [ "add", actor_name, "--config", str(context.actor_config_path), "--update", ], ) context.mock_actor_registry = mock_registry context.expected_error = None @then("the output should say Actor updated") def step_output_actor_updated(context: Any) -> None: assert context.result.exit_code == 0 assert "Actor updated" in context.result.output # ------------------------------------------------------------------ # Add with --format # ------------------------------------------------------------------ @when("I run actor add with format json") def step_add_format_json(context: Any) -> None: with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_registry = MagicMock() mock_service = MagicMock() mock_actor = _make_actor(config=context.actor_config_data) # registry.add() is the YAML-first path used by the add command mock_registry.add.return_value = mock_actor mock_svc.return_value = (mock_service, mock_registry) actor_name = context.actor_config_data.get("name", "local/test-actor") context.result = context.runner.invoke( actor_app, [ "add", actor_name, "--config", str(context.actor_config_path), "--format", "json", ], ) @when("I run actor add with format yaml") def step_add_format_yaml(context: Any) -> None: with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_registry = MagicMock() mock_service = MagicMock() mock_actor = _make_actor(config=context.actor_config_data) # registry.add() is the YAML-first path used by the add command mock_registry.add.return_value = mock_actor mock_svc.return_value = (mock_service, mock_registry) actor_name = context.actor_config_data.get("name", "local/test-actor") context.result = context.runner.invoke( actor_app, [ "add", actor_name, "--config", str(context.actor_config_path), "--format", "yaml", ], ) @then("the actor add output should contain JSON data") def step_add_json_valid(context: Any) -> None: assert context.result.exit_code == 0 parsed = json.loads(context.result.output.strip()) data = _unwrap_envelope(parsed) assert isinstance(data, dict) assert "name" in data @then("the actor add output should contain YAML data") def step_add_yaml_valid(context: Any) -> None: assert context.result.exit_code == 0 output = context.result.output assert "name:" in output assert "provider:" in output # ------------------------------------------------------------------ # Remove by namespaced name # ------------------------------------------------------------------ @when("I run actor remove with namespaced name") def step_remove_namespaced(context: Any) -> None: with ( patch("cleveragents.cli.commands.actor._get_services") as mock_svc, patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact, ): mock_registry = MagicMock() mock_svc.return_value = (MagicMock(), mock_registry) mock_impact.return_value = (0, 0, 0) context.result = context.runner.invoke( actor_app, ["remove", "local/my-custom-actor"] ) context.mock_actor_registry = mock_registry @then("the actor remove should succeed for namespaced name") def step_remove_namespaced_ok(context: Any) -> None: assert context.result.exit_code == 0 context.mock_actor_registry.remove_actor.assert_called_once_with( "local/my-custom-actor" ) assert ( "Actor removed" in context.result.output or "Removed actor" in context.result.output ) # ------------------------------------------------------------------ # Update with --format # ------------------------------------------------------------------ @when("I run actor update with format json") def step_update_format_json(context: Any) -> None: with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: mock_registry = MagicMock() mock_service = MagicMock() current = _make_actor( name="local/update-json", provider="openai", model="gpt-4", config={"provider": "openai", "model": "gpt-4"}, ) mock_registry.get_actor.return_value = current updated = _make_actor( name=current.name, provider=current.provider, model=current.model, config=current.config_blob, ) mock_registry.upsert_actor.return_value = updated mock_svc.return_value = (mock_service, mock_registry) context.result = context.runner.invoke( actor_app, ["update", current.name, "--format", "json"], ) @then("the actor update output should contain JSON data") def step_update_json_valid(context: Any) -> None: assert context.result.exit_code == 0 parsed = json.loads(context.result.output.strip()) data = _unwrap_envelope(parsed) assert isinstance(data, dict) assert data["name"] == "local/update-json"