forked from cleveragents/cleveragents-core
525 lines
18 KiB
Python
525 lines
18 KiB
Python
"""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 = {"temperature": 0.5, "max_tokens": 256}
|
|
handle = tempfile.NamedTemporaryFile(
|
|
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
|
)
|
|
json.dump(context.actor_config_data, handle)
|
|
handle.flush()
|
|
handle.close()
|
|
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]
|
|
handle = tempfile.NamedTemporaryFile(
|
|
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
|
)
|
|
json.dump(context.actor_config_data, handle)
|
|
handle.flush()
|
|
handle.close()
|
|
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 = {}
|
|
handle = tempfile.NamedTemporaryFile(delete=False, suffix=".yaml")
|
|
handle.write(b"{}\n")
|
|
handle.flush()
|
|
handle.close()
|
|
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 = {"yaml_only": True}
|
|
handle = tempfile.NamedTemporaryFile(
|
|
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
|
|
)
|
|
handle.write("yaml_only: true\n")
|
|
handle.flush()
|
|
handle.close()
|
|
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 = {}
|
|
handle = tempfile.NamedTemporaryFile(
|
|
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
|
|
)
|
|
handle.write("")
|
|
handle.flush()
|
|
handle.close()
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = mock_container.return_value.actor_service.return_value
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"add",
|
|
"test-actor",
|
|
"--provider",
|
|
"default-provider",
|
|
"--model",
|
|
"default-model",
|
|
],
|
|
)
|
|
context.mock_actor_service = mock_actor_service
|
|
|
|
|
|
@when("I run actor add with that config")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
if isinstance(context.actor_config_data, dict):
|
|
mock_actor = _make_actor(config=context.actor_config_data)
|
|
mock_actor_service.upsert_actor.return_value = mock_actor
|
|
else:
|
|
mock_actor_service.upsert_actor.side_effect = ValidationError(
|
|
"Config must be a JSON/YAML object"
|
|
)
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"add",
|
|
"test-actor",
|
|
"--provider",
|
|
"default-provider",
|
|
"--model",
|
|
"default-model",
|
|
"--config",
|
|
str(context.actor_config_path),
|
|
],
|
|
)
|
|
context.mock_actor_service = mock_actor_service
|
|
context.expected_config_blob = (
|
|
context.actor_config_data
|
|
if isinstance(context.actor_config_data, dict)
|
|
else None
|
|
)
|
|
context.expected_error = "Config must be a JSON/YAML object"
|
|
|
|
|
|
@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.application.container.get_container") as mock_container:
|
|
mock_container.return_value.actor_service.return_value = MagicMock()
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"add",
|
|
"test-actor",
|
|
"--provider",
|
|
"default-provider",
|
|
"--model",
|
|
"default-model",
|
|
"--config",
|
|
str(missing_path),
|
|
],
|
|
)
|
|
context.expected_error = "Config file not found"
|
|
|
|
|
|
@when("I run actor add with business rule violation")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.upsert_actor.side_effect = BusinessRuleViolation("invalid")
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"add",
|
|
"test-actor",
|
|
"--provider",
|
|
"default-provider",
|
|
"--model",
|
|
"default-model",
|
|
],
|
|
)
|
|
context.expected_error = "Error:"
|
|
|
|
|
|
@when("I run actor update with safe flag and yaml config")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
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_service.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_service.upsert_actor.return_value = updated_actor
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"update",
|
|
current_actor.name,
|
|
"--config",
|
|
str(context.actor_config_path),
|
|
"--safe",
|
|
],
|
|
)
|
|
context.mock_actor_service = mock_actor_service
|
|
context.current_actor = current_actor
|
|
|
|
|
|
@when("I run actor update with unsafe flag")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
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_service.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_service.upsert_actor.return_value = updated_actor
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"update",
|
|
current_actor.name,
|
|
"--unsafe",
|
|
],
|
|
)
|
|
context.mock_actor_service = mock_actor_service
|
|
context.current_actor = current_actor
|
|
|
|
|
|
@when("I run actor update with validation error")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
current_actor = _make_actor(name="local/invalid-update")
|
|
mock_actor_service.get_actor.return_value = current_actor
|
|
mock_actor_service.upsert_actor.side_effect = ValidationError("bad update")
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"update",
|
|
current_actor.name,
|
|
],
|
|
)
|
|
context.expected_error = "Error:"
|
|
|
|
|
|
@when("I run actor update for missing actor")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.get_actor.side_effect = NotFoundError(
|
|
resource_type="actor", resource_id="local/missing"
|
|
)
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(
|
|
actor_app,
|
|
[
|
|
"update",
|
|
"missing-actor",
|
|
"--provider",
|
|
"default-provider",
|
|
],
|
|
)
|
|
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 remove successfully")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
|
|
context.mock_actor_service = mock_actor_service
|
|
|
|
|
|
@when("I run actor remove and it fails validation")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.remove_actor.side_effect = ValidationError("invalid")
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
|
|
context.expected_error = "Error:"
|
|
|
|
|
|
@when("I run actor list with no actors")
|
|
def step_impl(context):
|
|
with patch("cleveragents.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.list_actors.return_value = []
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.list_actors.return_value = actors
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.get_actor.return_value = actor
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.get_actor.side_effect = ValidationError("bad")
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.set_default_actor.return_value = actor
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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.application.container.get_container") as mock_container:
|
|
mock_actor_service = MagicMock()
|
|
mock_actor_service.set_default_actor.side_effect = BusinessRuleViolation("nope")
|
|
mock_container.return_value.actor_service.return_value = mock_actor_service
|
|
|
|
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_service.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_service.upsert_actor.assert_called_once()
|
|
call_kwargs = context.mock_actor_service.upsert_actor.call_args.kwargs
|
|
assert call_kwargs.get("config_blob") == context.expected_config_blob
|
|
|
|
|
|
@then("the actor command should fail with bad parameter")
|
|
def step_impl(context):
|
|
assert context.result.exit_code != 0
|
|
if hasattr(context, "expected_error"):
|
|
assert context.expected_error in context.result.output
|
|
|
|
|
|
@then("the actor update should set safe and merge config")
|
|
def step_impl(context):
|
|
assert context.result.exit_code == 0
|
|
context.mock_actor_service.upsert_actor.assert_called_once_with(
|
|
name=context.current_actor.name,
|
|
provider=context.current_actor.provider,
|
|
model=context.current_actor.model,
|
|
config_blob=context.actor_config_data,
|
|
graph_descriptor=context.current_actor.graph_descriptor,
|
|
unsafe=False,
|
|
set_default=False,
|
|
is_built_in=context.current_actor.is_built_in,
|
|
)
|
|
|
|
|
|
@then("the actor update should set unsafe flag")
|
|
def step_impl(context):
|
|
assert context.result.exit_code == 0
|
|
context.mock_actor_service.upsert_actor.assert_called_once_with(
|
|
name=context.current_actor.name,
|
|
provider=context.current_actor.provider,
|
|
model=context.current_actor.model,
|
|
config_blob=context.current_actor.config_blob,
|
|
graph_descriptor=context.current_actor.graph_descriptor,
|
|
unsafe=True,
|
|
set_default=False,
|
|
is_built_in=context.current_actor.is_built_in,
|
|
)
|
|
|
|
|
|
@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_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
|
|
if hasattr(context, "expected_error"):
|
|
assert context.expected_error in context.result.output
|
|
|
|
|
|
@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
|