forked from HAL9000/cleveragents-core
a808c395f9
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
259 lines
9.0 KiB
Python
259 lines
9.0 KiB
Python
"""Step definitions for provider_registry_coverage_boost.feature.
|
|
|
|
These steps target specific uncovered lines in
|
|
src/cleveragents/providers/registry.py identified from build/coverage.xml.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import types
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when # type: ignore[import-untyped]
|
|
|
|
from cleveragents.providers.registry import (
|
|
ProviderRegistry,
|
|
ProviderType,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_boost_settings(
|
|
openai: str | None = None,
|
|
anthropic: str | None = None,
|
|
google: str | None = None,
|
|
gemini: str | None = None,
|
|
azure: str | None = None,
|
|
openrouter: str | None = None,
|
|
cohere: str | None = None,
|
|
groq: str | None = None,
|
|
together: str | None = None,
|
|
default_provider: str | None = None,
|
|
default_model: str | None = None,
|
|
azure_endpoint: str | None = None,
|
|
azure_api_version: str | None = None,
|
|
azure_deployment: str | None = None,
|
|
openrouter_org: str | None = None,
|
|
) -> MagicMock:
|
|
"""Return a fake Settings-like object populated with API keys."""
|
|
settings = MagicMock()
|
|
settings.openai_api_key = openai
|
|
settings.anthropic_api_key = anthropic
|
|
settings.google_api_key = google
|
|
settings.gemini_api_key = gemini
|
|
settings.azure_api_key = azure
|
|
settings.openrouter_api_key = openrouter
|
|
settings.cohere_api_key = cohere
|
|
settings.groq_api_key = groq
|
|
settings.together_api_key = together
|
|
settings.default_provider = default_provider
|
|
settings.default_model = default_model
|
|
if azure and not azure_endpoint:
|
|
azure_endpoint = "https://example.openai.azure.com"
|
|
settings.azure_openai_endpoint = azure_endpoint
|
|
settings.azure_openai_api_version = azure_api_version
|
|
settings.azure_openai_deployment = azure_deployment
|
|
settings.openrouter_organization = openrouter_org
|
|
return settings
|
|
|
|
|
|
def _stub_azure_langchain(context: Any) -> None:
|
|
"""Stub the langchain_openai module so AzureChatOpenAI is a recorder."""
|
|
original = sys.modules.get("langchain_openai")
|
|
calls: list[dict[str, Any]] = []
|
|
|
|
class FakeAzureChatOpenAI:
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
calls.append(kwargs)
|
|
|
|
# Also provide ChatOpenAI so other imports from the module don't break.
|
|
class FakeChatOpenAI:
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
pass
|
|
|
|
stub = types.ModuleType("langchain_openai")
|
|
stub.AzureChatOpenAI = FakeAzureChatOpenAI
|
|
stub.ChatOpenAI = FakeChatOpenAI
|
|
sys.modules["langchain_openai"] = stub
|
|
|
|
context._boost_azure_calls = calls
|
|
context._boost_azure_cls = FakeAzureChatOpenAI
|
|
|
|
if not hasattr(context, "_boost_cleanup"):
|
|
context._boost_cleanup = []
|
|
|
|
def _restore() -> None:
|
|
if original is None:
|
|
sys.modules.pop("langchain_openai", None)
|
|
else:
|
|
sys.modules["langchain_openai"] = original
|
|
|
|
context._boost_cleanup.append(_restore)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
'a registry where settings default_provider is "{default_prov}" but only anthropic key is set'
|
|
)
|
|
def step_boost_registry_settings_default_unconfigured(
|
|
context: Any, default_prov: str
|
|
) -> None:
|
|
"""Create a registry whose settings.default_provider points to a valid but
|
|
unconfigured provider, while another provider IS configured."""
|
|
context.boost_registry = ProviderRegistry(
|
|
settings=_make_boost_settings(
|
|
anthropic="sk-ant-test",
|
|
default_provider=default_prov,
|
|
)
|
|
)
|
|
|
|
|
|
@given("CLEVERAGENTS_DEFAULT_PROVIDER env var is cleared")
|
|
def step_boost_clear_default_provider_env(context: Any) -> None:
|
|
context._boost_orig_provider_env = os.environ.get("CLEVERAGENTS_DEFAULT_PROVIDER")
|
|
os.environ.pop("CLEVERAGENTS_DEFAULT_PROVIDER", None)
|
|
|
|
|
|
@given('a registry with azure API key and deployment setting "{deployment}"')
|
|
def step_boost_azure_registry_with_deployment(context: Any, deployment: str) -> None:
|
|
context.boost_registry = ProviderRegistry(
|
|
settings=_make_boost_settings(
|
|
azure="sk-azure-test",
|
|
azure_deployment=deployment,
|
|
)
|
|
)
|
|
|
|
|
|
@given("a registry with azure API key and no deployment setting")
|
|
def step_boost_azure_registry_no_deployment(context: Any) -> None:
|
|
context.boost_registry = ProviderRegistry(
|
|
settings=_make_boost_settings(
|
|
azure="sk-azure-test",
|
|
azure_deployment=None,
|
|
)
|
|
)
|
|
|
|
|
|
@given("the Azure LangChain client is stubbed for boost")
|
|
def step_boost_stub_azure(context: Any) -> None:
|
|
_stub_azure_langchain(context)
|
|
|
|
|
|
@given("a registry with google API key configured for boost")
|
|
def step_boost_google_registry(context: Any) -> None:
|
|
context.boost_registry = ProviderRegistry(
|
|
settings=_make_boost_settings(google="sk-google-test")
|
|
)
|
|
|
|
|
|
@given("a registry with no API keys for boost")
|
|
def step_boost_no_keys_registry(context: Any) -> None:
|
|
context.boost_registry = ProviderRegistry(settings=_make_boost_settings())
|
|
|
|
|
|
@given("a registry with openrouter API key configured for boost")
|
|
def step_boost_openrouter_registry(context: Any) -> None:
|
|
context.boost_registry = ProviderRegistry(
|
|
settings=_make_boost_settings(openrouter="sk-or-test")
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I request the default provider type from the boost registry")
|
|
def step_boost_get_default_provider_type(context: Any) -> None:
|
|
context.boost_result = context.boost_registry.get_default_provider_type()
|
|
|
|
|
|
@when("I call _create_provider_llm for AZURE with model_id None via boost")
|
|
def step_boost_create_azure_llm_no_model(context: Any) -> None:
|
|
context.boost_result = context.boost_registry._create_provider_llm(
|
|
ProviderType.AZURE, None
|
|
)
|
|
|
|
|
|
@when("I create an AI provider for google with empty string model_id via boost")
|
|
def step_boost_google_ai_provider_empty_model(context: Any) -> None:
|
|
# Ensure env vars don't interfere
|
|
orig_model = os.environ.pop("CLEVERAGENTS_DEFAULT_MODEL", None)
|
|
context._boost_orig_model_env = orig_model
|
|
context.boost_registry._settings.default_model = None
|
|
context.boost_ai_provider = context.boost_registry.create_ai_provider(
|
|
provider_type="google", model_id=""
|
|
)
|
|
|
|
|
|
@when("I try to create an AI provider for openrouter via boost")
|
|
def step_boost_try_openrouter_ai_provider(context: Any) -> None:
|
|
try:
|
|
context.boost_registry.create_ai_provider(provider_type="openrouter")
|
|
context.boost_error = None
|
|
except ValueError as exc:
|
|
context.boost_error = exc
|
|
|
|
|
|
@when("I create an AI provider for openrouter with empty string model_id via boost")
|
|
def step_boost_openrouter_ai_provider_empty_model(context: Any) -> None:
|
|
orig_model = os.environ.pop("CLEVERAGENTS_DEFAULT_MODEL", None)
|
|
context._boost_orig_model_env = orig_model
|
|
context.boost_registry._settings.default_model = None
|
|
context.boost_ai_provider = context.boost_registry.create_ai_provider(
|
|
provider_type="openrouter", model_id=""
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the boost result should be ProviderType ANTHROPIC")
|
|
def step_boost_result_is_anthropic(context: Any) -> None:
|
|
assert context.boost_result == ProviderType.ANTHROPIC, (
|
|
f"Expected ProviderType.ANTHROPIC, got {context.boost_result!r}"
|
|
)
|
|
|
|
|
|
@then('the stubbed Azure client deployment_name should be "{expected}"')
|
|
def step_boost_azure_deployment_name(context: Any, expected: str) -> None:
|
|
calls = context._boost_azure_calls
|
|
assert calls, "Expected AzureChatOpenAI to be instantiated"
|
|
actual = calls[-1].get("deployment_name")
|
|
assert actual == expected, f"Expected deployment_name={expected!r}, got {actual!r}"
|
|
|
|
|
|
@then('the boost AI provider model_id should be "{expected}"')
|
|
def step_boost_ai_provider_model(context: Any, expected: str) -> None:
|
|
assert context.boost_ai_provider.model_id == expected, (
|
|
f"Expected model_id={expected!r}, got {context.boost_ai_provider.model_id!r}"
|
|
)
|
|
|
|
|
|
@then("a boost ValueError should be raised")
|
|
def step_boost_value_error(context: Any) -> None:
|
|
assert context.boost_error is not None, "Expected a ValueError to be raised"
|
|
assert isinstance(context.boost_error, ValueError), (
|
|
f"Expected ValueError, got {type(context.boost_error).__name__}"
|
|
)
|
|
|
|
|
|
@then('the boost error message should contain "{fragment}"')
|
|
def step_boost_error_contains(context: Any, fragment: str) -> None:
|
|
message = str(context.boost_error).lower()
|
|
assert fragment.lower() in message, (
|
|
f"Expected error to contain {fragment!r}, got: {message!r}"
|
|
)
|