"""Behave steps covering actor registry behaviors.""" from __future__ import annotations import ast from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.actor.registry import ActorRegistry from cleveragents.config.settings import ProviderDefaults from cleveragents.core.exceptions import ValidationError from cleveragents.domain.models.core.actor import Actor from cleveragents.providers.registry import ( ProviderCapabilities, ProviderInfo, ProviderType, ) class _StubSettings: def __init__(self, defaults: ProviderDefaults) -> None: self._defaults = defaults def resolve_provider_defaults(self) -> ProviderDefaults: return self._defaults class _StubProviderRegistry: def __init__(self, providers: list[ProviderInfo]) -> None: self._providers = providers self.calls = 0 def get_configured_providers(self) -> list[ProviderInfo]: self.calls += 1 return list(self._providers) class _StubActorService: def __init__(self) -> None: self.actors: dict[str, Actor] = {} self.default_actor_name: str | None = None self.upsert_payloads: list[dict[str, Any]] = [] def upsert_actor( self, *, name: str, provider: str, model: str, config_blob: dict[str, Any], graph_descriptor: dict[str, Any] | None, unsafe: bool, set_default: bool, is_built_in: bool, ) -> Actor: actor = Actor( id=None, name=name, provider=provider, model=model, config_blob=config_blob, config_hash=Actor.compute_hash(config_blob), graph_descriptor=graph_descriptor, unsafe=unsafe, is_built_in=is_built_in, is_default=False, ) self.actors[name] = actor if set_default: self.set_default_actor(name) self.upsert_payloads.append( { "name": name, "provider": provider, "model": model, "config_blob": dict(config_blob), "graph_descriptor": graph_descriptor, "unsafe": unsafe, "set_default": set_default, "is_built_in": is_built_in, } ) return actor def get_default_actor(self) -> Actor | None: if self.default_actor_name and self.default_actor_name in self.actors: return self.actors[self.default_actor_name] return None def set_default_actor(self, name: str) -> Actor: actor = self.actors.get(name) if actor is None: raise ValueError("Actor does not exist") self.default_actor_name = name for value in self.actors.values(): value.is_default = value.name == name return actor def get_actor(self, name: str) -> Actor | None: return self.actors.get(name) def list_actors(self) -> list[Actor]: return list(self.actors.values()) def remove_actor(self, name: str) -> None: self.actors.pop(name, None) if self.default_actor_name == name: self.default_actor_name = None def _default_defaults() -> ProviderDefaults: return ProviderDefaults( provider=None, provider_source="test", model=None, model_source="test" ) def _build_registry( context: Context, providers: list[ProviderInfo], defaults: ProviderDefaults | None = None, ) -> None: context.actor_service = _StubActorService() context.provider_registry = _StubProviderRegistry(providers) context.settings = _StubSettings(defaults or _default_defaults()) context.registry = ActorRegistry( actor_service=context.actor_service, provider_registry=context.provider_registry, settings=context.settings, ) @given('provider defaults are provider "{provider}" model "{model}"') def step_provider_defaults(context: Context, provider: str, model: str) -> None: normalized_provider = provider or None normalized_model = model or None context.default_values = ProviderDefaults( provider=normalized_provider, provider_source="scenario", model=normalized_model, model_source="scenario", ) @given("an actor registry with configured providers:") def step_registry_with_providers(context: Context) -> None: providers: list[ProviderInfo] = [] for row in context.table: provider_type = ProviderType(row["type"]) providers.append( ProviderInfo( provider_type=provider_type, name=row["name"], api_key_env_var="ENV", default_model=row["model"], capabilities=ProviderCapabilities(), is_configured=True, ) ) defaults = getattr(context, "default_values", _default_defaults()) _build_registry(context, providers, defaults) @given("an actor registry with no configured providers") def step_registry_with_no_providers(context: Context) -> None: defaults = getattr(context, "default_values", _default_defaults()) _build_registry(context, [], defaults) @when("I ensure built-in actors are generated") def step_ensure_built_ins(context: Context) -> None: context.generated_actors = context.registry.ensure_built_in_actors() @then("no actors should be created") def step_no_actors_created(context: Context) -> None: assert not context.actor_service.actors assert context.generated_actors == [] @then("no default actor should be set") def step_no_default_set(context: Context) -> None: assert context.actor_service.get_default_actor() is None @then("the registry should list actors {expected}") def step_registry_should_list(context: Context, expected: str) -> None: expected_list = ast.literal_eval(expected) actual_names = [actor.name for actor in context.registry.list_actors()] assert actual_names == expected_list, actual_names @then('the default actor should be "{expected}"') def step_default_actor(context: Context, expected: str) -> None: actor = context.actor_service.get_default_actor() assert actor is not None, "No default actor was set" assert actor.name == expected @when('I remove actor "{name}"') def step_remove_actor(context: Context, name: str) -> None: context.registry.remove_actor(name) @then("the stored actors should be {expected}") def step_stored_actors(context: Context, expected: str) -> None: expected_list = ast.literal_eval(expected) assert sorted(context.actor_service.actors.keys()) == sorted(expected_list) @when('I attempt to upsert actor "{name}" with unsafe config') def step_upsert_unsafe_actor(context: Context, name: str) -> None: try: context.registry.upsert_actor( name=name, config_blob={"provider": "local", "model": "unsafe", "unsafe": True}, ) context.error = None except Exception as exc: context.error = exc @then("a registry validation error should be raised for unsafe actor") def step_expect_validation_error(context: Context) -> None: assert isinstance(context.error, ValidationError), type(context.error) @when( 'I upsert actor "{name}" with provider "{provider}" model "{model}" graph descriptor {graph_descriptor} and options {options_blob}' ) def step_upsert_actor_with_graph( context: Context, name: str, provider: str, model: str, graph_descriptor: str, options_blob: str, ) -> None: graph = ast.literal_eval(graph_descriptor) options = ast.literal_eval(options_blob) context.saved_actor = context.registry.upsert_actor( name=name, provider=provider, model=model, graph_descriptor=graph, config_blob=options, ) @when('I upsert actor "{name}" with raw config {config_blob}') def step_upsert_actor_raw_config(context: Context, name: str, config_blob: str) -> None: blob = ast.literal_eval(config_blob) context.saved_actor = context.registry.upsert_actor(name=name, config_blob=blob) @when('I retrieve actor "{name}" via the registry') def step_get_actor_via_registry(context: Context, name: str) -> None: context.retrieved_actor = context.registry.get_actor(name) @when('I set the default actor to "{name}" via the registry') def step_set_default_actor_via_registry(context: Context, name: str) -> None: context.registry.set_default_actor(name) @then( 'the stored actor config should include provider "{provider}" model "{model}" graph descriptor {graph_descriptor} unsafe false and option "{option_key}" "{option_value}"' ) def step_assert_canonical_blob( context: Context, provider: str, model: str, graph_descriptor: str, option_key: str, option_value: str, ) -> None: graph = ast.literal_eval(graph_descriptor) assert context.actor_service.upsert_payloads, "No actor was upserted" blob = context.actor_service.upsert_payloads[-1]["config_blob"] assert blob["provider"] == provider assert blob["model"] == model assert blob["graph_descriptor"] == graph assert blob.get("unsafe") is False assert blob.get(option_key) == option_value @then( 'the saved actor config should keep graph descriptor {graph_descriptor} and option "{option_key}" {option_value}' ) def step_assert_preserved_graph_and_option( context: Context, graph_descriptor: str, option_key: str, option_value: str, ) -> None: graph = ast.literal_eval(graph_descriptor) parsed_value = ast.literal_eval(option_value) assert context.actor_service.upsert_payloads, "No actor was upserted" blob = context.actor_service.upsert_payloads[-1]["config_blob"] assert blob["graph_descriptor"] == graph assert blob.get("options", {}).get(option_key) == parsed_value assert blob.get("unsafe") is False @then('the registry default actor via wrapper should be "{expected}"') def step_assert_wrapper_default_actor(context: Context, expected: str) -> None: actor = context.registry.get_default_actor() assert actor is not None, "No default actor set via registry" assert actor.name == expected @then( 'the built-in actor payload should include graph descriptor and capabilities for "{actor_name}"' ) def step_assert_built_in_payload(context: Context, actor_name: str) -> None: assert context.actor_service.upsert_payloads, "No actors were persisted" payload = None for item in context.actor_service.upsert_payloads: if item.get("name") == actor_name: payload = item break assert payload is not None, f"Actor {actor_name} not found in payloads" blob = payload["config_blob"] graph = blob.get("graph_descriptor") assert graph is not None, "graph_descriptor missing on built-in actor" assert graph.get("provider") assert graph.get("model") assert graph.get("source") == "provider-registry" capabilities = blob.get("capabilities") assert capabilities is not None, "capabilities missing on built-in actor config"