"""Step definitions for actor_service_coverage_boost.feature. These steps target specific uncovered lines in actor_service.py: - Lines 106-107: upsert_actor raises ValidationError for non-local prefix when is_built_in is False (inner guard after normalize). - Lines 150-157: remove_actor emits ENTITY_DELETED event via event bus. - Lines 161-162: remove_actor catches Exception from event bus emit and logs a warning. All step patterns are prefixed with ``actsvc`` to avoid AmbiguousStep collisions with other feature step files. """ from contextlib import contextmanager from datetime import datetime from types import SimpleNamespace from behave import given, then, when from cleveragents.application.services.actor_service import ActorService from cleveragents.core.exceptions import ValidationError from cleveragents.domain.models.core import Actor from cleveragents.infrastructure.events.types import EventType # --------------------------------------------------------------------------- # Helpers: lightweight fakes for UnitOfWork / repository / settings # --------------------------------------------------------------------------- def _make_actor(name, *, is_built_in=False, is_default=False): """Create a minimal Actor instance for test fixtures.""" return Actor( id=1, name=name, provider="test-provider", model="test-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=datetime.now(), updated_at=datetime.now(), ) class _ActsvcFakeActorRepository: """Minimal actor repository stub that returns pre-configured data.""" def __init__(self): self._actors = {} self.deleted = [] def add_actor(self, actor): self._actors[actor.name] = actor def get_by_name(self, name): return self._actors.get(name) def list_all(self): return list(self._actors.values()) def upsert(self, actor): self._actors[actor.name] = actor return actor def set_default(self, name): actor = self._actors[name] return actor def delete(self, name): if name in self._actors: self.deleted.append(name) del self._actors[name] def get_default(self): for a in self._actors.values(): if a.is_default: return a return None class _ActsvcFakeUnitOfWork: """Minimal UoW stub returning a fake transaction context.""" def __init__(self, actor_repo): self._actor_repo = actor_repo @contextmanager def transaction(self): ctx = SimpleNamespace(actors=self._actor_repo) yield ctx class _ActsvcFakeSettings: """Minimal settings stub — ActorService only stores it.""" pass # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the actsvc coverage module is imported") def step_actsvc_module_imported(context): """Ensure the module is importable.""" assert ActorService is not None # --------------------------------------------------------------------------- # Scenario: upsert_actor rejects non-local prefix (lines 106-107) # --------------------------------------------------------------------------- @given("I have an actsvc service with a mock unit of work") def step_actsvc_create_service(context): context.actsvc_repo = _ActsvcFakeActorRepository() context.actsvc_uow = _ActsvcFakeUnitOfWork(context.actsvc_repo) context.actsvc_service = ActorService( settings=_ActsvcFakeSettings(), unit_of_work=context.actsvc_uow, ) @given('the actsvc repo has no existing actor for "{name}"') def step_actsvc_no_existing_actor(context, name): """Ensure there is no actor with the given name in the repo.""" assert context.actsvc_repo.get_by_name(name) is None @when( 'I call actsvc upsert_actor with name "{name}" provider "{provider}"' ' model "{model}" and is_built_in False' ) def step_actsvc_call_upsert_non_local(context, name, provider, model): """Call upsert_actor with a non-local prefix and is_built_in=False. This hits lines 106-107: the inner guard that rejects non-local prefixes when is_built_in is False, after _normalize_name has already passed (because allow_built_in=True). """ context.actsvc_upsert_error = None try: context.actsvc_service.upsert_actor( name=name, provider=provider, model=model, is_built_in=False, ) except ValidationError as exc: context.actsvc_upsert_error = exc @then('an actsvc ValidationError should be raised with message "{expected_msg}"') def step_actsvc_verify_validation_error(context, expected_msg): assert context.actsvc_upsert_error is not None, ( "Expected ValidationError but none was raised" ) assert expected_msg in str(context.actsvc_upsert_error), ( f"Expected '{expected_msg}' in '{context.actsvc_upsert_error}'" ) # --------------------------------------------------------------------------- # Scenario: remove_actor emits ENTITY_DELETED event (lines 150-157) # --------------------------------------------------------------------------- class _ActsvcRecordingEventBus: """Event bus that records all emitted events.""" def __init__(self): self.events = [] def emit(self, event): self.events.append(event) def subscribe(self, event_type, handler): pass @given("I have an actsvc service with a working event bus") def step_actsvc_create_service_with_event_bus(context): context.actsvc_repo = _ActsvcFakeActorRepository() context.actsvc_uow = _ActsvcFakeUnitOfWork(context.actsvc_repo) context.actsvc_event_bus = _ActsvcRecordingEventBus() context.actsvc_service = ActorService( settings=_ActsvcFakeSettings(), unit_of_work=context.actsvc_uow, event_bus=context.actsvc_event_bus, ) @given('the actsvc repo has an existing local actor named "{name}"') def step_actsvc_add_existing_actor(context, name): actor = _make_actor(name, is_built_in=False, is_default=False) context.actsvc_repo.add_actor(actor) @when('I call actsvc remove_actor with name "{name}"') def step_actsvc_call_remove_actor(context, name): context.actsvc_remove_error = None try: context.actsvc_service.remove_actor(name) except Exception as exc: context.actsvc_remove_error = exc @then("the actsvc actor should be deleted successfully") def step_actsvc_verify_actor_deleted(context): assert context.actsvc_remove_error is None, ( f"Expected no error, got {context.actsvc_remove_error!r}" ) assert len(context.actsvc_repo.deleted) > 0, "No actors were deleted" @then('the actsvc event bus should have received an ENTITY_DELETED event for "{name}"') def step_actsvc_verify_event_bus_event(context, name): assert len(context.actsvc_event_bus.events) == 1, ( f"Expected 1 event, got {len(context.actsvc_event_bus.events)}" ) event = context.actsvc_event_bus.events[0] assert event.event_type == EventType.ENTITY_DELETED assert event.details["entity_type"] == "actor" assert event.details["entity_name"] == name # --------------------------------------------------------------------------- # Scenario: remove_actor logs warning on emit failure (lines 161-162) # --------------------------------------------------------------------------- class _ActsvcFailingEventBus: """Event bus whose emit always raises.""" def emit(self, event): raise RuntimeError("event bus connection lost") def subscribe(self, event_type, handler): pass @given("I have an actsvc service with a failing event bus") def step_actsvc_create_service_with_failing_event_bus(context): context.actsvc_repo = _ActsvcFakeActorRepository() context.actsvc_uow = _ActsvcFakeUnitOfWork(context.actsvc_repo) context.actsvc_service = ActorService( settings=_ActsvcFakeSettings(), unit_of_work=context.actsvc_uow, event_bus=_ActsvcFailingEventBus(), ) @then("the actsvc actor should be deleted despite the emit failure") def step_actsvc_verify_actor_deleted_despite_failure(context): assert context.actsvc_remove_error is None, ( f"Expected no error, got {context.actsvc_remove_error!r}" ) assert len(context.actsvc_repo.deleted) > 0, "No actors were deleted" @then("the actsvc warning about audit_emit_failed should have been logged") def step_actsvc_verify_audit_warning(context): """Verify lines 161-162 executed. The fact that remove_actor completed without raising despite a _ActsvcFailingEventBus confirms the ``except Exception`` block (lines 161-162) was reached — the RuntimeError was caught and swallowed by the warning logger. """ assert context.actsvc_remove_error is None assert len(context.actsvc_repo.deleted) > 0