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
Remove rich formatting from output
1217 lines
43 KiB
Python
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
|