Files
cleveragents-core/features/mocks/fake_provider.py
Brent E. Edwards 73d5552467 fix(actor): handle empty actor list without validation error
ActorRegistry._actor_name() built names via f"{provider}/{model}", which
produced names with multiple slashes when providers included models
containing "/" (e.g. OpenRouter's "anthropic/claude-sonnet-4-20250514").
The resulting name violated the spec pattern ^[a-z0-9_-]+/[a-z0-9_-]+$
and triggered a ValidationError during actor upsert.

Now sanitises both provider and model components by replacing "/" with "-"
and lowercasing, so multi-slash provider models no longer break actor
listing.

Includes 6 Behave BDD regression scenarios (covering zero-provider,
multi-slash, consecutive-slash, leading-slash, and name-validation
cases), Robot Framework integration smoke tests, and ASV benchmarks.

ISSUES CLOSED: #592
2026-03-10 23:11:22 +00:00

89 lines
2.7 KiB
Python

"""Fake provider stubs for BDD and benchmark tests.
These lightweight stand-ins avoid importing the real ``ProviderInfo`` (which
pulls in heavy dependencies) while satisfying ``ActorRegistry``'s duck-typed
contract for provider discovery.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from unittest.mock import MagicMock
from cleveragents.actor.registry import ActorRegistry
from cleveragents.domain.models.core.actor import Actor
from cleveragents.providers.registry import ProviderCapabilities
@dataclass
class FakeProviderInfo:
"""Minimal stand-in for ``ProviderInfo``."""
name: str
default_model: str
provider_type: Any = None
capabilities: Any = None
api_key_env_var: str = ""
def __post_init__(self) -> None:
if self.provider_type is None:
self.provider_type = MagicMock(value=self.name)
if self.capabilities is None:
self.capabilities = ProviderCapabilities()
@dataclass
class FakeProviderRegistry:
"""Stand-in for ``ProviderRegistry``."""
providers: list[FakeProviderInfo] = field(default_factory=list)
def get_configured_providers(self) -> list[FakeProviderInfo]:
return self.providers
def make_registry(
providers: list[FakeProviderInfo] | None = None,
) -> tuple[MagicMock, ActorRegistry]:
"""Build a real ``ActorRegistry`` with mocked service/settings.
The ``ActorService`` is mocked so there is no database dependency, but
the ``ActorRegistry`` code (including ``_actor_name()``) runs for real.
Actors successfully created by ``upsert_actor`` are captured so that
``list_actors`` returns them — ensuring Scenario 3 ("valid JSON") can
detect when the bug is actually fixed.
"""
provider_reg = FakeProviderRegistry(providers=providers or [])
captured_actors: list[Actor] = []
def _capturing_upsert(**kwargs: Any) -> Actor:
actor = Actor(
name=kwargs.get("name", "mock/actor"),
provider=kwargs.get("provider", "mock"),
model=kwargs.get("model", "actor"),
config_blob={},
config_hash=Actor.compute_hash({}),
)
captured_actors.append(actor)
return actor
mock_service = MagicMock()
mock_service.list_actors.side_effect = lambda: list(captured_actors)
mock_service.get_default_actor.return_value = None
mock_service.upsert_actor.side_effect = _capturing_upsert
mock_settings = MagicMock()
mock_settings.resolve_provider_defaults.return_value = MagicMock(
provider=None, model=None
)
registry = ActorRegistry(
actor_service=mock_service,
provider_registry=provider_reg,
settings=mock_settings,
)
return mock_service, registry