Feat: Adding Actors support to database

This commit is contained in:
2025-12-18 17:13:18 -05:00
parent 0010f8f5cf
commit 34a29d7cc8
41 changed files with 3707 additions and 172 deletions
+298
View File
@@ -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()