Files
cleveragents-core/features/steps/actor_cli_steps.py
T
brent.edwards c9605c9381
CI / lint (pull_request) Successful in 17s
CI / security (pull_request) Successful in 17s
CI / typecheck (pull_request) Successful in 24s
CI / quality (pull_request) Successful in 15s
CI / build (pull_request) Successful in 13s
CI / behave (3.13) (pull_request) Successful in 3m43s
CI / docker (pull_request) Failing after 33s
CI / helm (pull_request) Failing after 6s
CI / coverage (pull_request) Successful in 4m20s
test(features/steps): remove ansi codes
Remove rich formatting from output
2026-02-12 05:44:57 +00:00

1217 lines
43 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 = {
"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 = {
"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 = {
"provider": "existing-provider",
"model": "existing-model",
}
with tempfile.NamedTemporaryFile(delete=False, suffix=".yaml") as handle:
handle.write(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 = {
"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("provider: openai\nmodel: 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 = {
"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 = "provider is required"
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 = {
"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", "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()
# Mock ensure_built_in_actors to return empty list
mock_actor_registry.ensure_built_in_actors.return_value = []
# Set up the actor service to handle the built-in actor calls
mock_actor_service.upsert_actor.return_value = _make_actor(name="openai/gpt-4o")
mock_actor_service.get_default_actor.return_value = _make_actor(
name="openai/gpt-4o", is_default=True
)
if isinstance(context.actor_config_data, dict):
mock_actor = _make_actor(config=context.actor_config_data)
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)
context.result = context.runner.invoke(
actor_app,
[
"add",
"test-actor",
"--config",
str(context.actor_config_path),
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_config_blob = (
dict(context.actor_config_data)
if isinstance(context.actor_config_data, dict)
else None
)
if isinstance(context.expected_config_blob, dict):
context.expected_config_blob.setdefault("unsafe", False)
context.expected_allow_unsafe = False
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"
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 ensure_built_in_actors to return empty list
mock_actor_registry.ensure_built_in_actors.return_value = []
# Set up the actor service to handle the built-in actor calls
mock_actor_service.upsert_actor.return_value = _make_actor(name="openai/gpt-4o")
mock_actor_service.get_default_actor.return_value = _make_actor(
name="openai/gpt-4o", is_default=True
)
mock_actor = _make_actor(config=context.actor_config_data)
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
"test-actor",
"--config",
str(context.actor_config_path),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_config_blob = dict(context.actor_config_data)
context.expected_config_blob.setdefault("unsafe", False)
merged_options = dict(context.expected_config_blob.get("options", {}))
merged_options.update(overrides)
context.expected_config_blob["options"] = merged_options
context.expected_allow_unsafe = False
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 ensure_built_in_actors to return empty list
mock_actor_registry.ensure_built_in_actors.return_value = []
# Set up the actor service to handle the built-in actor calls
mock_actor_service.upsert_actor.return_value = _make_actor(name="openai/gpt-4o")
mock_actor_service.get_default_actor.return_value = _make_actor(
name="openai/gpt-4o", is_default=True
)
mock_actor = _make_actor(config=context.actor_config_data)
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
"test-actor",
"--config",
str(context.actor_config_path),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_config_blob = dict(context.actor_config_data)
context.expected_config_blob.setdefault("unsafe", False)
merged_options = dict(context.actor_config_data.get("options", {}))
merged_options["enabled"] = False
context.expected_config_blob["options"] = merged_options
context.expected_allow_unsafe = False
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 ensure_built_in_actors to return empty list
mock_actor_registry.ensure_built_in_actors.return_value = []
# Set up the actor service to handle the built-in actor calls
mock_actor_service.upsert_actor.return_value = _make_actor(name="openai/gpt-4o")
mock_actor_service.get_default_actor.return_value = _make_actor(
name="openai/gpt-4o", is_default=True
)
mock_actor = _make_actor(config=context.actor_config_data)
mock_actor_registry.upsert_actor.return_value = mock_actor
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
"test-actor",
"--config",
str(context.actor_config_path),
*option_args,
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_config_blob = dict(context.actor_config_data)
context.expected_config_blob.setdefault("unsafe", False)
merged_options = dict(context.actor_config_data.get("options", {}))
merged_options["enabled"] = False
context.expected_config_blob["options"] = merged_options
context.expected_allow_unsafe = False
context.expected_error = None
@when("I run actor add with malformed option")
def step_impl(context):
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",
"test-actor",
"--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):
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",
"test-actor",
"--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()
# Mock ensure_built_in_actors to return empty list
mock_actor_registry.ensure_built_in_actors.return_value = []
# Set up the actor service to handle the built-in actor calls
mock_actor_service.upsert_actor.return_value = _make_actor(name="openai/gpt-4o")
mock_actor_service.get_default_actor.return_value = _make_actor(
name="openai/gpt-4o", is_default=True
)
if isinstance(context.actor_config_data, dict):
mock_actor = _make_actor(config=context.actor_config_data, unsafe=True)
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)
context.result = context.runner.invoke(
actor_app,
[
"add",
"test-actor",
"--config",
str(context.actor_config_path),
"--unsafe",
],
)
context.mock_actor_registry = mock_actor_registry
context.expected_config_blob = (
dict(context.actor_config_data)
if isinstance(context.actor_config_data, dict)
else None
)
if isinstance(context.expected_config_blob, dict):
context.expected_config_blob.setdefault("unsafe", True)
context.expected_allow_unsafe = True
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)
context.result = context.runner.invoke(
actor_app,
[
"add",
"service/graph-actor",
"--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",
"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 = {"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()
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",
"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)
context.result = context.runner.invoke(
actor_app,
[
"add",
"service/unsafe",
"--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)
context.result = context.runner.invoke(
actor_app,
[
"add",
"service/unsafe",
"--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:
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, ["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:
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)
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:
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
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
context.mock_actor_registry.upsert_actor.assert_called_once()
call_kwargs = context.mock_actor_registry.upsert_actor.call_args.kwargs
assert call_kwargs.get("config_blob") == context.expected_config_blob
expected_allow = getattr(context, "expected_allow_unsafe", False)
assert call_kwargs.get("allow_unsafe") == expected_allow
@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