Feat: Adding Actors support to database
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
"""Behave steps targeting actor service coverage gaps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.application.services.actor_service import ActorService
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.core.exceptions import (
|
||||
BusinessRuleViolation,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
|
||||
def _add_cleanup(context: Context, handler: Callable[[], None]) -> None:
|
||||
if not hasattr(context, "_cleanup_handlers"):
|
||||
context._cleanup_handlers = []
|
||||
context._cleanup_handlers.append(handler)
|
||||
|
||||
|
||||
class _StubActorRepository:
|
||||
def __init__(self) -> None:
|
||||
self._actors: dict[str, Actor] = {}
|
||||
self._default_name: str | None = None
|
||||
|
||||
def list_all(self) -> list[Actor]:
|
||||
return [self._actors[name] for name in sorted(self._actors.keys())]
|
||||
|
||||
def get_by_name(self, name: str) -> Actor | None:
|
||||
return self._actors.get(name)
|
||||
|
||||
def upsert(self, actor: Actor) -> Actor:
|
||||
self._actors[actor.name] = actor
|
||||
if actor.is_default:
|
||||
self.set_default(actor.name)
|
||||
return actor
|
||||
|
||||
def delete(self, name: str) -> None:
|
||||
actor = self._actors.get(name)
|
||||
if actor is None:
|
||||
raise ValueError("Actor does not exist")
|
||||
if actor.is_built_in or actor.is_default:
|
||||
raise ValueError("Cannot delete built-in or default actor")
|
||||
del self._actors[name]
|
||||
|
||||
def set_default(self, name: str) -> Actor:
|
||||
if name not in self._actors:
|
||||
raise ValueError("Actor does not exist")
|
||||
self._default_name = name
|
||||
for actor in self._actors.values():
|
||||
actor.is_default = actor.name == name
|
||||
return self._actors[name]
|
||||
|
||||
def get_default(self) -> Actor | None:
|
||||
if self._default_name and self._default_name in self._actors:
|
||||
return self._actors[self._default_name]
|
||||
for actor in self._actors.values():
|
||||
if actor.is_default:
|
||||
self._default_name = actor.name
|
||||
return actor
|
||||
return None
|
||||
|
||||
def clear(self) -> None:
|
||||
self._actors.clear()
|
||||
self._default_name = None
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._actors)
|
||||
|
||||
|
||||
class _StubTransaction:
|
||||
def __init__(self, repo: _StubActorRepository) -> None:
|
||||
self.actors = repo
|
||||
|
||||
def __enter__(self) -> _StubTransaction:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _StubUnitOfWork:
|
||||
def __init__(self, repo: _StubActorRepository) -> None:
|
||||
self._repo = repo
|
||||
|
||||
def transaction(self) -> _StubTransaction:
|
||||
return _StubTransaction(self._repo)
|
||||
|
||||
|
||||
def _make_actor(
|
||||
name: str, *, is_default: bool = False, is_built_in: bool = False
|
||||
) -> Actor:
|
||||
now = datetime.now()
|
||||
return Actor(
|
||||
id=None,
|
||||
name=name,
|
||||
provider="provider",
|
||||
model="model",
|
||||
config_blob={},
|
||||
config_hash=Actor.compute_hash({}),
|
||||
graph_descriptor=None,
|
||||
unsafe=False,
|
||||
is_built_in=is_built_in,
|
||||
is_default=is_default,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
|
||||
@given("an actor service with stubbed dependencies")
|
||||
def step_actor_service_stubbed(context: Context) -> None:
|
||||
repo = _StubActorRepository()
|
||||
context.actor_repo = repo
|
||||
context.unit_of_work = _StubUnitOfWork(repo)
|
||||
context.actor_service = ActorService(Settings(), context.unit_of_work)
|
||||
|
||||
original_env = os.environ.get("CLEVERAGENTS_TESTING_USE_MOCK_AI")
|
||||
|
||||
def cleanup_env() -> None:
|
||||
if original_env is None:
|
||||
os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)
|
||||
else:
|
||||
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = original_env
|
||||
|
||||
_add_cleanup(context, cleanup_env)
|
||||
|
||||
|
||||
@when('I normalize actor name "{name}"')
|
||||
def step_normalize_name(context: Context, name: str) -> None:
|
||||
context.normalized_name = context.actor_service._normalize_name(name)
|
||||
|
||||
|
||||
@then('the normalized actor name should be "{expected}"')
|
||||
def step_normalized_actor_name_should_be(context: Context, expected: str) -> None:
|
||||
assert context.normalized_name == expected, context.normalized_name
|
||||
|
||||
|
||||
@when('I try to normalize actor name "{name}" without allowing built-ins')
|
||||
def step_try_normalize_invalid(context: Context, name: str) -> None:
|
||||
try:
|
||||
context.actor_service._normalize_name(name, allow_built_in=False)
|
||||
context.error = None
|
||||
except Exception as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@when('I try to normalize actor name "" without allowing built-ins')
|
||||
def step_try_normalize_invalid_empty(context: Context) -> None:
|
||||
step_try_normalize_invalid(context, "")
|
||||
|
||||
|
||||
@then('a ValidationError should be raised containing "{message}"')
|
||||
def step_validation_error_message(context: Context, message: str) -> None:
|
||||
assert isinstance(context.error, ValidationError), type(context.error)
|
||||
assert message in str(context.error)
|
||||
|
||||
|
||||
@given('existing actors named "{first}" and "{second}"')
|
||||
def step_existing_actors(context: Context, first: str, second: str) -> None:
|
||||
for name in (first, second):
|
||||
context.actor_repo.upsert(_make_actor(name))
|
||||
|
||||
|
||||
@when("I list all actors")
|
||||
def step_list_actors(context: Context) -> None:
|
||||
context.listed_actors = context.actor_service.list_actors()
|
||||
|
||||
|
||||
@then("I should receive actors {actor_names}")
|
||||
def step_should_receive_actors(context: Context, actor_names: str) -> None:
|
||||
expected = ast.literal_eval(actor_names)
|
||||
assert [actor.name for actor in context.listed_actors] == expected
|
||||
|
||||
|
||||
@when('I attempt to fetch actor "{name}"')
|
||||
def step_attempt_fetch_actor(context: Context, name: str) -> None:
|
||||
try:
|
||||
context.fetched_actor = context.actor_service.get_actor(name)
|
||||
context.error = None
|
||||
except Exception as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then("a NotFoundError should be raised for the actor")
|
||||
def step_not_found_error(context: Context) -> None:
|
||||
assert isinstance(context.error, NotFoundError), type(context.error)
|
||||
|
||||
|
||||
@given('a built-in actor named "{name}" already exists')
|
||||
def step_builtin_actor_exists(context: Context, name: str) -> None:
|
||||
context.actor_repo.upsert(_make_actor(name, is_built_in=True))
|
||||
|
||||
|
||||
@when('I attempt to upsert "{name}" as custom')
|
||||
def step_attempt_upsert_custom(context: Context, name: str) -> None:
|
||||
try:
|
||||
context.actor_service.upsert_actor(name=name, provider="p", model="m")
|
||||
context.error = None
|
||||
except Exception as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then("a BusinessRuleViolation should be raised")
|
||||
def step_business_rule_violation(context: Context) -> None:
|
||||
assert isinstance(context.error, BusinessRuleViolation), type(context.error)
|
||||
|
||||
|
||||
@given('an actor named "{name}" exists')
|
||||
def step_actor_exists(context: Context, name: str) -> None:
|
||||
context.actor_repo.upsert(_make_actor(name))
|
||||
|
||||
|
||||
@when('I upsert "{name}" with set_default')
|
||||
def step_upsert_with_set_default(context: Context, name: str) -> None:
|
||||
context.saved_actor = context.actor_service.upsert_actor(
|
||||
name=name, provider="p", model="m", set_default=True
|
||||
)
|
||||
|
||||
|
||||
@then('the stored actor "{name}" should be default')
|
||||
def step_stored_actor_default(context: Context, name: str) -> None:
|
||||
actor = context.actor_repo.get_by_name(name)
|
||||
assert actor is not None, "Actor not found"
|
||||
assert actor.is_default, "Actor was not marked as default"
|
||||
assert context.actor_repo.get_default() is actor
|
||||
|
||||
|
||||
@when('I try to remove actor "{name}"')
|
||||
def step_try_remove_actor(context: Context, name: str) -> None:
|
||||
try:
|
||||
context.actor_service.remove_actor(name)
|
||||
context.error = None
|
||||
except Exception as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@when('I set default actor "{name}"')
|
||||
def step_set_default_actor(context: Context, name: str) -> None:
|
||||
try:
|
||||
context.result_actor = context.actor_service.set_default_actor(name)
|
||||
context.error = None
|
||||
except Exception as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@then('the returned actor name should be "{name}"')
|
||||
def step_returned_actor_name(context: Context, name: str) -> None:
|
||||
assert context.result_actor is not None, "No actor returned"
|
||||
assert context.result_actor.name == name
|
||||
|
||||
|
||||
@then("no additional actors should be created")
|
||||
def step_no_additional_actors(context: Context) -> None:
|
||||
assert context.actor_repo.count() == context.initial_actor_count
|
||||
|
||||
|
||||
@given('an actor named "{name}" exists as default')
|
||||
def step_actor_exists_as_default(context: Context, name: str) -> None:
|
||||
actor = _make_actor(name, is_default=True)
|
||||
context.actor_repo.upsert(actor)
|
||||
context.actor_repo.set_default(name)
|
||||
|
||||
|
||||
@given('a mock actor named "{name}" exists without default flag')
|
||||
def step_mock_actor_exists_without_default(context: Context, name: str) -> None:
|
||||
actor = _make_actor(name, is_built_in=True, is_default=False)
|
||||
context.actor_repo.upsert(actor)
|
||||
|
||||
|
||||
@given("any existing actors are cleared")
|
||||
def step_clear_existing_actors(context: Context) -> None:
|
||||
context.actor_repo.clear()
|
||||
|
||||
|
||||
@when('I ensure the default mock actor with env value "{value}"')
|
||||
def step_ensure_default_mock_actor(context: Context, value: str) -> None:
|
||||
context.initial_actor_count = context.actor_repo.count()
|
||||
os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = value
|
||||
context.result_actor = context.actor_service.ensure_default_mock_actor()
|
||||
|
||||
|
||||
@then("the result should be None")
|
||||
def step_result_none(context: Context) -> None:
|
||||
assert context.result_actor is None
|
||||
|
||||
|
||||
def after_scenario(context: Context, scenario) -> None:
|
||||
if hasattr(context, "_cleanup_handlers"):
|
||||
for handler in context._cleanup_handlers:
|
||||
handler()
|
||||
Reference in New Issue
Block a user