Files
cleveragents-core/features/steps/actor_cli_steps.py
T
hurui200320 8dc55655e9
CI / push-validation (push) Successful in 41s
CI / helm (push) Successful in 42s
CI / benchmark-publish (push) Failing after 56s
CI / build (push) Successful in 1m6s
CI / lint (push) Successful in 1m13s
CI / quality (push) Successful in 1m34s
CI / typecheck (push) Successful in 2m12s
CI / security (push) Successful in 2m13s
CI / e2e_tests (push) Successful in 3m45s
CI / integration_tests (push) Successful in 3m57s
CI / unit_tests (push) Successful in 4m53s
CI / docker (push) Successful in 1m34s
CI / coverage (push) Successful in 11m27s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 32s
CI / push-validation (pull_request) Successful in 33s
CI / build (pull_request) Successful in 53s
CI / lint (pull_request) Successful in 59s
CI / quality (pull_request) Successful in 1m28s
CI / security (pull_request) Successful in 1m29s
CI / typecheck (pull_request) Successful in 1m32s
CI / e2e_tests (pull_request) Successful in 4m1s
CI / integration_tests (pull_request) Successful in 5m4s
CI / unit_tests (pull_request) Successful in 5m51s
CI / docker (pull_request) Successful in 1m27s
CI / coverage (pull_request) Successful in 11m51s
CI / status-check (pull_request) Successful in 3s
feat(actor): make built-in actors virtual, resolved on-demand from provider registry
Replace the DB-persistence approach for built-in actors with in-memory virtual
resolution. Built-in actors (e.g. openai/gpt-4o, anthropic/claude-sonnet) are
now resolved on-demand from ProviderRegistry at query time and merged with
persisted custom actors — no database writes occur for built-in actors.

Key changes:
- Add ActorRegistry._resolve_virtual_builtin_actors(): generates virtual Actor
  objects in-memory from configured providers (is_built_in=True, id=None)
- ActorRegistry.list()/list_actors(): merges virtual built-ins with custom DB
  actors; custom actors win on name collision; result sorted alphabetically
- ActorRegistry.get()/get_actor(): DB-first, virtual built-in fallback,
  NotFoundError if neither
- ActorRegistry.remove()/remove_actor(): rejects virtual built-in names with
  ValidationError
- ActorRegistry.set_default_actor(): stores only the actor name string via new
  actor_preferences singleton table; no actor row created for virtual built-ins
- ActorRegistry.get_default_actor(): reads preference name, resolves via
  DB→virtual chain, returns actor with is_default=True
- Remove ensure_built_in_actors() entirely — 20+ call sites cleaned up including
  plan.py
- Remove ActorRepository.upsert_built_in() — no longer needed
- Remove is_built_in from ActorModel DB column (kept on Actor domain model for
  virtual actors)
- New Alembic migration m10_001_virtual_builtin_actors: drops is_built_in column,
  adds actor_preferences singleton table
- Add ActorService.set_default_actor_name() and get_default_actor_name() for
  preference storage without requiring a DB actor row
- Update 15+ Behave step files and 5 feature files; add new
  features/virtual_builtin_actors.feature with 8 scenarios covering list, show,
  remove, set-default, get-default, no-DB-writes guarantees
- Rewrite tests/actor/test_registry_builtin_yaml.py: TestEnsureBuiltInActorsWithYaml
  → TestResolveVirtualBuiltinActors plus new TestListActors, TestGetActor,
  TestRemoveActor, TestDefaultActor test classes

Quality gates: lint ✓, typecheck ✓, unit_tests ✓ (15674 scenarios), coverage ✓
(97.10%), integration_tests ✓ (1997 tests)

ISSUES CLOSED: #10923
2026-04-30 10:56:21 +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.update_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.update_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.update_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.update_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.update_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.update_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,
allow_unsafe=False,
option_overrides=None,
)
@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.update_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,
allow_unsafe=True,
option_overrides=None,
)
@then("the actor update should include option overrides")
def step_impl(context):
assert context.result.exit_code == 0
context.mock_actor_registry.update_actor.assert_called_once()
call_kwargs = context.mock_actor_registry.update_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