Files
cleveragents-core/features/steps/provider_fixes_steps.py
T
freemo a074b4846f
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 19s
CI / quality (pull_request) Successful in 28s
CI / security (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 56s
CI / integration_tests (pull_request) Successful in 4m51s
CI / unit_tests (pull_request) Successful in 19m29s
CI / docker (pull_request) Successful in 39s
CI / benchmark-regression (pull_request) Successful in 26m10s
CI / coverage (pull_request) Successful in 47m42s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 17s
CI / build (push) Successful in 23s
CI / typecheck (push) Successful in 30s
CI / security (push) Successful in 30s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m52s
CI / unit_tests (push) Successful in 10m8s
CI / docker (push) Successful in 1m19s
CI / benchmark-publish (push) Successful in 11m54s
CI / coverage (push) Failing after 39m51s
fix(provider): remove FakeListLLM defaults
Remove FakeListLLM as a silent fallback in agent graph constructors
(plan_generation.py, context_analysis.py, auto_debug.py). All three now
raise ValueError when llm=None, making missing-provider errors explicit.

Add Settings.mock_providers flag and validate_provider_availability()
method. Update container.get_ai_provider() to check Settings.mock_providers
first, with env-var fallback for backward compatibility.

Add resolve_provider_by_name() helper to the provider registry and export
it from cleveragents.providers. Add structlog trace logging to
ProviderRegistry.get_default_provider_type() to record selection reasoning.

Update all existing behave step files, robot tests, and benchmarks that
relied on the implicit FakeListLLM default to pass an explicit LLM
instance instead.

Add new BDD tests (features/provider_fixes.feature with 17 scenarios),
Robot Framework integration tests (robot/provider_detection_smoke.robot),
and ASV benchmarks (benchmarks/provider_selection_bench.py).

ISSUES CLOSED: #323
2026-02-27 09:47:10 -05:00

393 lines
13 KiB
Python

"""Step definitions for provider_fixes.feature."""
from __future__ import annotations
import logging
import os
from typing import Any
from unittest.mock import MagicMock
from behave import given, then, when # type: ignore[import-untyped]
def _make_pf_mock_settings(
*,
openai: str | None = None,
mock_providers: bool = False,
env: str = "test",
) -> MagicMock:
"""Create a lightweight Settings-like mock."""
settings = MagicMock()
settings.openai_api_key = openai
settings.anthropic_api_key = None
settings.google_api_key = None
settings.gemini_api_key = None
settings.azure_api_key = None
settings.openrouter_api_key = None
settings.cohere_api_key = None
settings.groq_api_key = None
settings.together_api_key = None
settings.default_provider = None
settings.default_model = None
settings.azure_openai_endpoint = None
settings.azure_openai_api_version = None
settings.azure_openai_deployment = None
settings.openrouter_organization = None
settings.mock_providers = mock_providers
settings.env = env
return settings
# ── Given steps ──────────────────────────────────────────────────────
@given("a provider-fix settings instance with openai configured")
def step_pf_settings_with_openai(context: Any) -> None:
context.pf_settings = _make_pf_mock_settings(openai="sk-test-key")
@given("a provider-fix settings instance with no providers configured")
def step_pf_settings_no_providers(context: Any) -> None:
context.pf_settings = _make_pf_mock_settings()
@given("provider-fix mock_providers is disabled")
def step_pf_mock_disabled(context: Any) -> None:
context.pf_settings.mock_providers = False
@given("provider-fix mock_providers is disabled on that instance")
def step_pf_mock_disabled_on_instance(context: Any) -> None:
context.pf_settings.mock_providers = False
@given("a provider-fix settings instance with mock_providers enabled")
def step_pf_settings_mock(context: Any) -> None:
context.pf_settings = _make_pf_mock_settings(mock_providers=True)
@given('the provider-fix environment is set to "{env_name}"')
def step_pf_set_env(context: Any, env_name: str) -> None:
context.pf_settings.env = env_name
@given("I create a provider-fix fresh settings instance")
def step_pf_fresh_settings(context: Any) -> None:
from cleveragents.config.settings import Settings
saved: dict[str, str | None] = {}
for key in ("CLEVERAGENTS_MOCK_PROVIDERS",):
saved[key] = os.environ.pop(key, None)
try:
Settings._instance = None # type: ignore[attr-defined]
context.pf_fresh_settings = Settings()
finally:
for key, val in saved.items():
if val is not None:
os.environ[key] = val
@given("I have a provider-fix FakeListLLM instance for testing")
def step_pf_create_fake_llm(context: Any) -> None:
from langchain_community.llms import FakeListLLM
context.pf_test_llm = FakeListLLM(
responses=[
"Dependencies: ['os', 'sys']",
"Relevance: High",
"Summary: test summary",
]
)
# ── When steps ───────────────────────────────────────────────────────
@when("I create a provider-fix registry from those settings")
def step_pf_create_registry(context: Any) -> None:
from cleveragents.providers.registry import ProviderRegistry
context.pf_registry = ProviderRegistry(settings=context.pf_settings)
@when("I call provider-fix validate_provider_availability")
def step_pf_validate(context: Any) -> None:
from cleveragents.config.settings import Settings
# Clear all provider env vars so Settings sees no providers
provider_env_keys = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"GOOGLE_API_KEY",
"GOOGLE_GENAI_API_KEY",
"AZURE_OPENAI_API_KEY",
"AZURE_API_KEY",
"OPENROUTER_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_GEMINI_API_KEY",
"HF_TOKEN",
"HUGGINGFACEHUB_API_TOKEN",
"HUGGING_FACE_HUB_TOKEN",
"COHERE_API_KEY",
"PERPLEXITY_API_KEY",
"GROQ_API_KEY",
"TOGETHER_API_KEY",
]
saved_env: dict[str, str | None] = {}
if not context.pf_settings.openai_api_key:
for key in provider_env_keys:
saved_env[key] = os.environ.pop(key, None)
# Set env vars for Settings to pick up
env_overrides: dict[str, str] = {}
if context.pf_settings.mock_providers:
env_overrides["CLEVERAGENTS_MOCK_PROVIDERS"] = "true"
if hasattr(context.pf_settings, "env"):
env_overrides["CLEVERAGENTS_ENV"] = context.pf_settings.env
for k, v in env_overrides.items():
os.environ[k] = v
try:
Settings._instance = None # type: ignore[attr-defined]
real = Settings()
if context.pf_settings.openai_api_key:
real.openai_api_key = context.pf_settings.openai_api_key
context.pf_error = None
context.pf_warning_logged = False
class _Catcher(logging.Handler):
def __init__(self) -> None:
super().__init__()
self.warnings: list[str] = []
def emit(self, record: logging.LogRecord) -> None:
if record.levelno >= logging.WARNING:
self.warnings.append(record.getMessage())
handler = _Catcher()
log = logging.getLogger("cleveragents.config.settings")
log.addHandler(handler)
try:
real.validate_provider_availability()
except ValueError as exc:
context.pf_error = exc
finally:
log.removeHandler(handler)
context.pf_warning_logged = len(handler.warnings) > 0
finally:
for key, val in saved_env.items():
if val is not None:
os.environ[key] = val
else:
os.environ.pop(key, None)
for k in env_overrides:
os.environ.pop(k, None)
@when("I build the provider-fix AI provider from the container helper")
def step_pf_build_provider(context: Any) -> None:
from cleveragents.application.container import get_ai_provider
from cleveragents.providers.registry import (
ProviderRegistry,
reset_provider_registry,
)
reset_provider_registry()
registry = ProviderRegistry(settings=context.pf_settings)
context.pf_provider_result = get_ai_provider(
settings=context.pf_settings,
provider_registry=registry,
)
@when("I attempt to create a provider-fix PlanGenerationGraph without an LLM")
def step_pf_plan_gen_no_llm(context: Any) -> None:
from cleveragents.agents.graphs.plan_generation import (
PlanGenerationGraph,
)
context.pf_error = None
try:
PlanGenerationGraph(llm=None)
except ValueError as exc:
context.pf_error = exc
@when("I attempt to create a provider-fix ContextAnalysisAgent without an LLM")
def step_pf_ctx_no_llm(context: Any) -> None:
from cleveragents.agents.graphs.context_analysis import (
ContextAnalysisAgent,
)
context.pf_error = None
try:
ContextAnalysisAgent(llm=None)
except ValueError as exc:
context.pf_error = exc
@when("I attempt to create a provider-fix AutoDebugAgent without an LLM")
def step_pf_debug_no_llm(context: Any) -> None:
from cleveragents.agents.graphs.auto_debug import AutoDebugAgent
context.pf_error = None
try:
AutoDebugAgent(llm=None)
except ValueError as exc:
context.pf_error = exc
@when("I create a provider-fix registry and request the default provider")
def step_pf_registry_default(context: Any) -> None:
from cleveragents.providers.registry import ProviderRegistry
context.pf_registry = ProviderRegistry(settings=context.pf_settings)
context.pf_default = context.pf_registry.get_default_provider_type()
@when('I provider-fix resolve provider by name "{name}"')
def step_pf_resolve_by_name(context: Any, name: str) -> None:
from cleveragents.providers.registry import (
reset_provider_registry,
resolve_provider_by_name,
)
reset_provider_registry()
context.pf_error = None
context.pf_resolved = None
try:
context.pf_resolved = resolve_provider_by_name(
name, settings=context.pf_settings
)
except ValueError as exc:
context.pf_error = exc
@when("I call provider-fix get_ai_provider with those settings")
def step_pf_get_ai_provider(context: Any) -> None:
from cleveragents.application.container import get_ai_provider
from cleveragents.providers.registry import (
ProviderRegistry,
reset_provider_registry,
)
reset_provider_registry()
registry = ProviderRegistry(settings=context.pf_settings)
context.pf_provider_result = get_ai_provider(
settings=context.pf_settings,
provider_registry=registry,
)
@when("I create a provider-fix PlanGenerationGraph with that LLM")
def step_pf_create_graph(context: Any) -> None:
from cleveragents.agents.graphs.plan_generation import (
PlanGenerationGraph,
)
context.pf_error = None
try:
context.pf_graph = PlanGenerationGraph(llm=context.pf_test_llm)
except Exception as exc:
context.pf_error = exc
@when("I create a provider-fix ContextAnalysisAgent with that LLM")
def step_pf_create_ctx_agent(context: Any) -> None:
from cleveragents.agents.graphs.context_analysis import (
ContextAnalysisAgent,
)
context.pf_error = None
try:
context.pf_agent = ContextAnalysisAgent(llm=context.pf_test_llm)
except Exception as exc:
context.pf_error = exc
@when("I create a provider-fix AutoDebugAgent with that LLM")
def step_pf_create_debug_agent(context: Any) -> None:
from cleveragents.agents.graphs.auto_debug import AutoDebugAgent
context.pf_error = None
try:
context.pf_agent = AutoDebugAgent(llm=context.pf_test_llm)
except Exception as exc:
context.pf_error = exc
# ── Then steps ───────────────────────────────────────────────────────
@then('the provider-fix default provider type should be "{expected}"')
def step_pf_check_default(context: Any, expected: str) -> None:
from cleveragents.providers.registry import ProviderType
default = context.pf_registry.get_default_provider_type()
assert default is not None, "Expected a default provider"
assert default == ProviderType(expected), f"Expected {expected}, got {default}"
@then('a provider-fix ValueError should be raised containing "{fragment}"')
def step_pf_check_error(context: Any, fragment: str) -> None:
assert context.pf_error is not None, (
f"Expected ValueError with '{fragment}' but no error"
)
assert fragment in str(context.pf_error), (
f"Expected '{fragment}' in: {context.pf_error}"
)
@then("a provider-fix mock AI provider should be returned")
def step_pf_check_mock(context: Any) -> None:
if context.pf_provider_result is not None:
name = getattr(context.pf_provider_result, "_name", "")
cls = type(context.pf_provider_result).__name__.lower()
assert "mock" in name.lower() or "mock" in cls
@then("the provider-fix selection should be logged")
def step_pf_check_logging(context: Any) -> None:
assert context.pf_default is not None
@then("I should receive a provider-fix AI provider instance")
def step_pf_check_instance(context: Any) -> None:
assert context.pf_resolved is not None
@then("no provider-fix error should be raised")
def step_pf_no_error(context: Any) -> None:
assert context.pf_error is None, f"Expected no error but got: {context.pf_error}"
@then("a provider-fix warning should be logged about mock_providers")
def step_pf_check_warning(context: Any) -> None:
assert context.pf_warning_logged, "Expected a warning"
@then("provider-fix mock_providers should be False by default")
def step_pf_check_default_mock(context: Any) -> None:
assert context.pf_fresh_settings.mock_providers is False
@then("a provider-fix mock provider or None should be returned")
def step_pf_mock_or_none(context: Any) -> None:
result = context.pf_provider_result
if result is not None:
cls = type(result).__name__.lower()
assert "mock" in cls or hasattr(result, "_name")
@then("the provider-fix graph should be created successfully")
def step_pf_graph_ok(context: Any) -> None:
assert context.pf_error is None, f"Expected no error but got: {context.pf_error}"
assert context.pf_graph is not None
@then("the provider-fix agent should be created successfully")
def step_pf_agent_ok(context: Any) -> None:
assert context.pf_error is None, f"Expected no error but got: {context.pf_error}"
assert context.pf_agent is not None