Files
cleveragents-core/features/steps/actor_cli_steps.py
T
freemo 62ded31c24
CI / lint (pull_request) Failing after 29s
CI / unit_tests (pull_request) Failing after 2m2s
CI / helm (pull_request) Successful in 24s
CI / quality (pull_request) Successful in 3m42s
CI / typecheck (pull_request) Successful in 4m2s
CI / security (pull_request) Successful in 4m5s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / build (pull_request) Successful in 3m16s
CI / e2e_tests (pull_request) Failing after 10m40s
CI / integration_tests (pull_request) Failing after 23m19s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
fix(cli): route 'agents actor add' through ActorRegistry.add() YAML-first path
Route the 'agents actor add' CLI command through ActorRegistry.add() instead
of the legacy registry.upsert_actor() path. This ensures the original YAML
text, schema_version, and compiled_metadata are preserved in the database.

Changes:
- src/cleveragents/cli/commands/actor.py: Add _load_config_text() helper that
  returns both raw text and parsed dict. Refactor add() to call registry.add()
  with the raw yaml_text and update=update_existing flag when a registry is
  available. The service fallback path (no registry) is unchanged.
- features/steps/actor_cli_steps.py: Update add command step definitions to
  mock registry.add() instead of registry.upsert_actor(). Update 'the actor
  add should pass the loaded config' assertion to verify registry.add() is
  called with a non-empty yaml_text string.
- features/steps/actor_cli_yaml_steps.py: Update add command steps to mock
  registry.add() instead of registry.upsert_actor().
- features/steps/actor_add_rich_output_steps.py: Update add command steps to
  mock registry.add() instead of registry.upsert_actor().
- robot/helper_actor_add_rich_output.py: Update helper to mock registry.add()
  instead of registry.upsert_actor().
- features/actor_add_yaml_first_path.feature: New Behave feature verifying
  the YAML-first persistence path is used by actor add.
- features/steps/actor_add_yaml_first_path_steps.py: Step definitions for
  the new YAML-first path feature.
- robot/actor_add_yaml_first_path.robot: New Robot integration tests verifying
  yaml_text is preserved and upsert_actor is not called.
- robot/helper_actor_add_yaml_first_path.py: Helper script for Robot tests.

Fixes #3426

ISSUES CLOSED: #3426
2026-04-05 21:19:40 +00:00

1205 lines
42 KiB
Python

# pyright: reportRedeclaration=false
"""Step definitions for the Actor CLI feature."""
from __future__ import annotations
import json
import tempfile
from pathlib import Path
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 app as actor_app
from cleveragents.core.exceptions import (
BusinessRuleViolation,
NotFoundError,
ValidationError,
)
from cleveragents.domain.models.core.actor import Actor
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,
)
def _register_cleanup(context, path: Path) -> None:
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
@given("an actor CLI runner")
def step_impl(context):
context.runner = CliRunner()
@given("I have an actor JSON config file")
def step_impl(context):
context.actor_config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.5,
"max_tokens": 256,
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor JSON config file with options")
def step_impl(context):
context.actor_config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o",
"options": {"temperature": 0.4, "enabled": True},
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor list config file")
def step_impl(context):
context.actor_config_data = [1, 2, 3]
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor YAML config file returning empty data")
def step_impl(context):
context.actor_config_data = {
"name": "local/existing-actor",
"provider": "existing-provider",
"model": "existing-model",
}
with tempfile.NamedTemporaryFile(delete=False, suffix=".yaml") as handle:
handle.write(
b"name: local/existing-actor\n"
b"provider: existing-provider\nmodel: existing-model\n"
)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor YAML-only config file")
def step_impl(context):
context.actor_config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o-mini",
"yaml_only": True,
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as handle:
handle.write(
"name: local/test-actor\nprovider: openai\n"
"model: gpt-4o-mini\nyaml_only: true\n"
)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor graph config file")
def step_impl(context):
context.actor_config_data = {
"name": "service/graph-actor",
"provider": "graph-provider",
"model": "graph-model",
"graph": {"nodes": ["start"], "edges": []},
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an empty actor YAML config file")
def step_impl(context):
context.actor_config_data = {}
context.expected_error = "Config file must contain a 'name' field"
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
) as handle:
handle.write("")
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an unsafe actor JSON config file")
def step_impl(context):
context.actor_config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o-mini",
"unsafe": True,
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@when("I run actor add without config")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["add", "local/test-actor"])
context.mock_actor_registry = mock_actor_registry
context.expected_error = "Config file is required"
@when("I run actor add with that config")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
if isinstance(context.actor_config_data, dict):
mock_actor = _make_actor(config=context.actor_config_data)
# registry.upsert_actor() is the YAML-first path used by the add command
mock_actor_registry.upsert_actor.return_value = mock_actor
else:
mock_actor_registry.upsert_actor.side_effect = ValidationError(
"Config must be a JSON/YAML object"
)
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
)
context.mock_actor_registry = mock_actor_registry
if not isinstance(context.actor_config_data, dict):
context.expected_error = "Config must be a JSON/YAML object"
elif isinstance(
context.actor_config_data, dict
) and context.actor_config_data.get("unsafe"):
context.expected_error = "Actor config is marked unsafe"
else:
context.expected_error = None
@when("I run actor add with that config and options overrides")
def step_impl(context):
overrides = {"temperature": 0.9, "max_tokens": 128, "mode": "fast"}
option_args: list[str] = []
for key, value in overrides.items():
formatted = json.dumps(value) if not isinstance(value, str) else value
option_args.extend(["--option", f"{key}={formatted}"])
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
mock_actor = _make_actor(config=context.actor_config_data)
# registry.upsert_actor() is the YAML-first path used by the add command
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_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),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_error = None
@when("I run actor add with boolean option override")
def step_impl(context):
option_args = ["--option", "enabled=false"]
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
mock_actor = _make_actor(config=context.actor_config_data)
# registry.upsert_actor() is the YAML-first path used by the add command
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_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),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_error = None
@when("I run actor add with uppercase boolean option override")
def step_impl(context):
option_args = ["--option", "enabled=FALSE"]
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
mock_actor = _make_actor(config=context.actor_config_data)
# registry.upsert_actor() is the YAML-first path used by the add command
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_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),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_error = None
@when("I run actor add with malformed option")
def step_impl(context):
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_get_services.return_value = (MagicMock(), MagicMock())
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--option",
"invalid",
],
)
context.expected_error = "Options must be provided as key=value pairs"
@when("I run actor add with empty option key")
def step_impl(context):
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_get_services.return_value = (MagicMock(), MagicMock())
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--option",
"=true",
],
)
context.expected_error = "Option keys cannot be empty"
@when("I run actor add with that config and unsafe flag")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
if isinstance(context.actor_config_data, dict):
mock_actor = _make_actor(config=context.actor_config_data, unsafe=True)
# registry.upsert_actor() is the YAML-first path used by the add command
mock_actor_registry.upsert_actor.return_value = mock_actor
else:
mock_actor_registry.upsert_actor.side_effect = ValidationError(
"Config must be a JSON/YAML object"
)
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--unsafe",
],
)
context.mock_actor_registry = mock_actor_registry
if not isinstance(context.actor_config_data, dict):
context.expected_error = "Config must be a JSON/YAML object"
elif context.actor_config_data == {}:
context.expected_error = "provider is required"
else:
context.expected_error = None
@when("I run actor add via service with that config")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
actor_service = MagicMock()
graph_descriptor = context.actor_config_data.get("graph")
actor_service.upsert_actor.return_value = _make_actor(
name="service/graph-actor",
provider=context.actor_config_data.get("provider", "openai"),
model=context.actor_config_data.get("model", "gpt-4o"),
config=context.actor_config_data,
graph_descriptor=graph_descriptor,
)
mock_get_services.return_value = (actor_service, None)
actor_name = context.actor_config_data.get("name", "service/graph-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.graph_descriptor = graph_descriptor
@when("I run actor add with missing config path")
def step_impl(context):
missing_path = Path(tempfile.gettempdir()) / "missing-actor-config.json"
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_get_services.return_value = (MagicMock(), MagicMock())
context.result = context.runner.invoke(
actor_app,
[
"add",
"local/test-actor",
"--config",
str(missing_path),
],
)
context.expected_error = "Config file not found"
@when("I run actor add with business rule violation")
def step_impl(context):
config_data = {
"name": "local/test-actor",
"provider": "openai",
"model": "gpt-4o",
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(config_data, handle)
handle.flush()
config_path = Path(handle.name)
_register_cleanup(context, config_path)
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
# The add command now uses registry.upsert_actor() (YAML-first path)
mock_actor_registry.upsert_actor.side_effect = BusinessRuleViolation("invalid")
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
"local/test-actor",
"--config",
str(config_path),
],
)
context.expected_error = "Error:"
@when("I run actor add via service without unsafe flag")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.expected_error = "Actor config is marked unsafe"
@when("I run actor add via service with unsafe canonical blob")
def step_impl(context):
with (
patch(
"cleveragents.cli.commands.actor._canonicalize_actor_config",
return_value=(
MagicMock(
provider="openai",
model="gpt-4o",
graph_descriptor=None,
unsafe=True,
options=None,
),
{
"provider": "openai",
"model": "gpt-4o",
"unsafe": True,
},
False,
),
),
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
):
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.expected_error = "Actor config is marked unsafe"
@when("I run actor update with safe flag and yaml config")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
current_actor = _make_actor(
name="local/existing-actor",
provider="existing-provider",
model="existing-model",
config={"existing": True},
unsafe=True,
)
mock_actor_registry.get_actor.return_value = current_actor
updated_actor = _make_actor(
name=current_actor.name,
provider=current_actor.provider,
model=current_actor.model,
config=context.actor_config_data,
unsafe=False,
)
mock_actor_registry.upsert_actor.return_value = updated_actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--config",
str(context.actor_config_path),
"--safe",
],
)
context.mock_actor_registry = mock_actor_registry
context.current_actor = current_actor
@when("I run actor update with unsafe flag")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_registry = MagicMock()
mock_actor_service = MagicMock()
current_actor = _make_actor(
name="local/unsafe-actor",
provider="unsafe-provider",
model="unsafe-model",
config={"existing": True},
unsafe=False,
)
mock_actor_registry.get_actor.return_value = current_actor
updated_actor = _make_actor(
name=current_actor.name,
provider=current_actor.provider,
model=current_actor.model,
config=current_actor.config_blob,
unsafe=True,
)
mock_actor_registry.upsert_actor.return_value = updated_actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--unsafe",
],
)
context.mock_actor_registry = mock_actor_registry
context.current_actor = current_actor
@when("I run actor update with validation error")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
current_actor = _make_actor(name="local/invalid-update")
mock_actor_registry.get_actor.return_value = current_actor
mock_actor_registry.upsert_actor.side_effect = ValidationError("bad update")
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_error = "Error:"
@when("I run actor update for missing actor")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.get_actor.side_effect = NotFoundError(
resource_type="actor", resource_id="local/missing"
)
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
"missing-actor",
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_error = "Actor not found"
@when("I run actor update with conflicting flags")
def step_impl(context):
context.result = context.runner.invoke(
actor_app,
[
"update",
"local/conflict",
"--unsafe",
"--safe",
],
)
context.expected_error = "Choose only one of --unsafe or --safe"
@when("I run actor update requiring unsafe confirmation")
def step_impl(context):
with (
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
patch(
"cleveragents.cli.commands.actor._canonicalize_actor_config"
) as mock_canonicalize,
):
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
current_actor = _make_actor(name="local/needs-confirm")
mock_actor_registry.get_actor.return_value = current_actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
mock_resolved = MagicMock(unsafe=True)
mock_canonicalize.return_value = (
mock_resolved,
{"provider": current_actor.provider, "model": current_actor.model},
True,
)
context.result = context.runner.invoke(
actor_app, ["update", current_actor.name]
)
context.expected_error = "Actor config is marked unsafe"
@when("I run actor update via service without unsafe flag")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
actor_service = MagicMock()
current_actor = _make_actor(
name="local/service-update",
provider="service-provider",
model="service-model",
config={"provider": "service-provider", "model": "service-model"},
)
actor_service.get_actor.return_value = current_actor
mock_get_services.return_value = (actor_service, None)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.expected_error = "Actor config is marked unsafe"
@when("I run actor update with option overrides")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
current_actor = _make_actor(
name="local/option-update",
provider="registry-provider",
model="registry-model",
config={
"provider": "registry-provider",
"model": "registry-model",
"options": {"keep": "existing"},
},
)
mock_actor_registry.get_actor.return_value = current_actor
updated_actor = _make_actor(
name=current_actor.name,
provider=current_actor.provider,
model=current_actor.model,
config=current_actor.config_blob,
)
mock_actor_registry.upsert_actor.return_value = updated_actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--option",
"retries=2",
"--option",
"enabled=false",
],
)
context.current_actor = current_actor
context.mock_actor_registry = mock_actor_registry
context.expected_option_overrides = {"retries": 2, "enabled": False}
@when("I run actor update with uppercase boolean option override")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
current_actor = _make_actor(
name="local/option-upper",
provider="registry-provider",
model="registry-model",
config={
"provider": "registry-provider",
"model": "registry-model",
"options": {"keep": "existing"},
},
)
mock_actor_registry.get_actor.return_value = current_actor
updated_actor = _make_actor(
name=current_actor.name,
provider=current_actor.provider,
model=current_actor.model,
config=current_actor.config_blob,
)
mock_actor_registry.upsert_actor.return_value = updated_actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--option",
"enabled=TRUE",
],
)
context.current_actor = current_actor
context.mock_actor_registry = mock_actor_registry
context.expected_option_overrides = {"enabled": True}
@when("I run actor update via service with unsafe canonical blob")
def step_impl(context):
with (
patch(
"cleveragents.cli.commands.actor._canonicalize_actor_config",
return_value=(
MagicMock(
provider="service-provider",
model="service-model",
graph_descriptor=None,
unsafe=True,
options=None,
),
{
"provider": "service-provider",
"model": "service-model",
"unsafe": True,
},
False,
),
),
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
):
actor_service = MagicMock()
current_actor = _make_actor(
name="local/service-canonical",
provider="service-provider",
model="service-model",
config={"provider": "service-provider", "model": "service-model"},
)
actor_service.get_actor.return_value = current_actor
mock_get_services.return_value = (actor_service, None)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.expected_error = "Actor config is marked unsafe"
@when("I run actor update via service with safe config")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
actor_service = MagicMock()
current_actor = _make_actor(
name="local/service-safe",
provider="service-provider",
model="service-model",
config={"provider": "service-provider", "model": "service-model"},
)
actor_service.get_actor.return_value = current_actor
updated_actor = _make_actor(
name=current_actor.name,
provider=context.actor_config_data.get("provider", "openai"),
model=context.actor_config_data.get("model", "gpt-4o-mini"),
config=context.actor_config_data,
unsafe=False,
)
actor_service.upsert_actor.return_value = updated_actor
mock_get_services.return_value = (actor_service, None)
context.result = context.runner.invoke(
actor_app,
[
"update",
current_actor.name,
"--config",
str(context.actor_config_path),
],
)
context.actor_service = actor_service
context.current_actor = current_actor
@when("I run actor remove successfully")
def step_impl(context):
with (
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
):
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
mock_impact.return_value = (0, 0, 0)
context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
context.mock_actor_registry = mock_actor_registry
@when("I run actor remove and it fails validation")
def step_impl(context):
with (
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
):
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.remove_actor.side_effect = ValidationError("invalid")
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
mock_impact.return_value = (0, 0, 0)
context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
context.mock_actor_registry = mock_actor_registry
context.expected_error = "Error:"
@when("I run actor remove via service path")
def step_impl(context):
with (
patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
):
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
mock_impact.return_value = (0, 0, 0)
context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
context.actor_service = actor_service
@when("I run actor list with no actors")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.list_actors.return_value = []
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["list"])
@when("I run actor list with two actors")
def step_impl(context):
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_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.list_actors.return_value = actors
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["list"])
context.actors = actors
@when("I run actor show successfully")
def step_impl(context):
actor = _make_actor(name="local/show", provider="provider", model="model")
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.get_actor.return_value = actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["show", actor.name])
context.actor = actor
@when("I run actor show with validation error")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.get_actor.side_effect = ValidationError("bad")
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["show", "local/show"])
context.expected_error = "Error:"
@when("I run set-default actor successfully")
def step_impl(context):
actor = _make_actor(name="provider/model", provider="provider", model="model")
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.set_default_actor.return_value = actor
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["set-default", actor.name])
context.actor = actor
@when("I run set-default actor with violation")
def step_impl(context):
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_actor_registry.set_default_actor.side_effect = BusinessRuleViolation(
"nope"
)
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["set-default", "local/fail"])
context.expected_error = "Error:"
@then("the actor add should succeed with default config")
def step_impl(context):
assert context.result.exit_code == 0
context.mock_actor_registry.upsert_actor.assert_called_once()
@then("the actor add should pass the loaded config")
def step_impl(context):
assert context.result.exit_code == 0
# The add command uses registry.upsert_actor() with yaml_text threaded through
# to preserve the original YAML text, schema_version, and compiled_metadata.
context.mock_actor_registry.upsert_actor.assert_called_once()
call_kwargs = context.mock_actor_registry.upsert_actor.call_args.kwargs
yaml_text_arg = call_kwargs.get("yaml_text", "")
assert isinstance(yaml_text_arg, str) and yaml_text_arg.strip()
@then("the service actor add should include graph descriptor")
def step_impl(context):
assert context.result.exit_code == 0
context.actor_service.upsert_actor.assert_called_once()
call_kwargs = context.actor_service.upsert_actor.call_args.kwargs
assert call_kwargs.get("graph_descriptor") == context.graph_descriptor
assert (
call_kwargs.get("config_blob", {}).get("graph_descriptor")
== context.graph_descriptor
)
@then("the actor command should fail with bad parameter")
def step_impl(context):
import re
assert context.result.exit_code != 0
expected = getattr(context, "expected_error", None)
if expected:
# Strip ANSI escape codes and normalize whitespace to handle
# Rich formatting and text wrapping in narrow CI terminals
raw = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", context.result.output)
normalized = " ".join(raw.split())
assert expected in normalized, f"Expected '{expected}' in output: {normalized}"
@then("the actor update should set safe and merge config")
def step_impl(context):
assert context.result.exit_code == 0
expected_config = dict(context.actor_config_data)
expected_config.setdefault("unsafe", False)
context.mock_actor_registry.upsert_actor.assert_called_once_with(
name=context.current_actor.name,
provider=None,
model=None,
config_blob=expected_config,
graph_descriptor=context.current_actor.graph_descriptor,
unsafe=False,
set_default=False,
is_built_in=context.current_actor.is_built_in,
allow_unsafe=False,
)
@then("the actor update should set unsafe flag")
def step_impl(context):
assert context.result.exit_code == 0
expected_config = dict(context.current_actor.config_blob)
expected_config.setdefault("provider", context.current_actor.provider)
expected_config.setdefault("model", context.current_actor.model)
expected_config.setdefault("unsafe", True)
context.mock_actor_registry.upsert_actor.assert_called_once_with(
name=context.current_actor.name,
provider=context.current_actor.provider,
model=context.current_actor.model,
config_blob=expected_config,
graph_descriptor=context.current_actor.graph_descriptor,
unsafe=True,
set_default=False,
is_built_in=context.current_actor.is_built_in,
allow_unsafe=True,
)
@then("the actor update should include option overrides")
def step_impl(context):
assert context.result.exit_code == 0
context.mock_actor_registry.upsert_actor.assert_called_once()
call_kwargs = context.mock_actor_registry.upsert_actor.call_args.kwargs
assert call_kwargs.get("option_overrides") == context.expected_option_overrides
options_blob = call_kwargs.get("config_blob", {}).get("options", {})
for key, value in context.expected_option_overrides.items():
assert options_blob.get(key) == value
@then("the actor service update should succeed")
def step_impl(context):
assert context.result.exit_code == 0
context.actor_service.upsert_actor.assert_called_once()
call_kwargs = context.actor_service.upsert_actor.call_args.kwargs
assert call_kwargs.get("name") == context.current_actor.name
assert call_kwargs.get("unsafe") is False
assert call_kwargs.get("provider") == context.actor_config_data.get("provider")
assert call_kwargs.get("model") == context.actor_config_data.get("model")
config_blob = call_kwargs.get("config_blob", {})
assert config_blob.get("provider") == context.actor_config_data.get("provider")
assert config_blob.get("model") == context.actor_config_data.get("model")
@then("the actor command should abort for missing actor")
def step_impl(context):
assert context.result.exit_code != 0
assert context.expected_error in context.result.output
@then("the actor remove should succeed")
def step_impl(context):
assert context.result.exit_code == 0
context.mock_actor_registry.remove_actor.assert_called_once_with("local/removable")
@then("the actor service remove should be called")
def step_impl(context):
assert context.result.exit_code == 0
context.actor_service.remove_actor.assert_called_once_with("local/removable")
@then("the actor command should abort with error")
def step_impl(context):
assert context.result.exit_code != 0
expected = getattr(context, "expected_error", None)
if expected:
assert expected in context.result.output
@then("the actor service should not be called")
def step_impl(context):
assert context.actor_service.upsert_actor.call_count == 0
@then("the actor list should report empty state")
def step_impl(context):
assert context.result.exit_code == 0
assert "No actors configured." in context.result.output
@then("the actor list should render rows")
def step_impl(context):
assert context.result.exit_code == 0
for actor in context.actors:
assert actor.name in context.result.output
@then("the actor show should display details")
def step_impl(context):
assert context.result.exit_code == 0
assert context.actor.name in context.result.output
@then("the set default actor should display details")
def step_impl(context):
assert context.result.exit_code == 0
assert context.actor.name in context.result.output