diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a2af440..920ff22a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -344,6 +344,14 @@ ensuring data is stored with proper parameter values. - **BDD Feature File Tag Coverage** (#9124): Added required `@a2a`, `@session`, and `@cli` Gherkin tags to all A2A, session, and CLI feature files (30 files) to enable tag-based test filtering via `behave --tags=a2a,session,cli`. This restores the ability to selectively run test categories and enables CI to execute targeted test suites without running the full suite. +- **`ProviderRegistry.FALLBACK_ORDER` missing `ProviderType.GEMINI`** (#10906): Added + `ProviderType.GEMINI` to the fallback provider order list so that when only a + Gemini API key is configured, the registry correctly selects it as the default + provider instead of falling through to no provider. The enum value, capabilities, + models, and the `_create_provider_instance()` factory already supported Gemini — this + fix closes the gap in the fallback chain. Includes BDD regression scenarios in + `features/fallback_gemini_provider.feature`. + - **Cross-actor subgraph cycle detection reads actor_ref field** (#1431): Fixed `_detect_subgraph_cycles()`, `_map_node()`, and the `compile_actor()` main loop in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 648ddbba8..8521aa707 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -117,3 +117,4 @@ Below are some specific details of individual PR contributions. * HAL 9000 has contributed the data-integrity fix for ProjectRepository (#8179): removed unconditional ``session.rollback()`` calls from exception handlers in ``ProjectRepository.create()`` and ``NamespacedProjectRepository.create/update/delete``, delegating transaction rollback to the Unit of Work outer-layer handler where it belongs. * Jeffrey Phillips Freeman has contributed the `--format`/`-f` flag to `agents session tell` (issue #10466): adds JSON envelope output for machine-readable workflows alongside existing Rich console output, with Behave BDD test coverage verifying all four non-rich format paths (JSON, YAML, plain, table) and the short `-f` flag alias. * HAL 9000 has contributed the Semgrep guard for broad exception suppression (PR #9185 / issue #9103): added two new Semgrep rules (`python-no-suppressed-exception` and `python-no-suppress-exception`) to automate enforcement of error propagation guidelines, integrated Semgrep into `nox -s lint` in audit mode with migration plan for ~337 existing violations, and comprehensive BDD test coverage across all rule patterns and escape hatch scenarios. +* HAL 9000 has contributed the `ProviderRegistry.FALLBACK_ORDER` fix (#10906): added the missing `ProviderType.GEMINI` to the fallback provider order list so that when only a Gemini API key is configured, the registry correctly selects it as the default provider. Includes BDD regression scenarios in `features/fallback_gemini_provider.feature`. diff --git a/features/fallback_gemini_provider.feature b/features/fallback_gemini_provider.feature new file mode 100644 index 000000000..665f828ad --- /dev/null +++ b/features/fallback_gemini_provider.feature @@ -0,0 +1,40 @@ +Feature: Fallback order includes ProviderType.GEMINI + As a developer + I want ProviderType.GEMINI to be included in ProviderRegistry.FALLBACK_ORDER + So that Gemini becomes a valid fallback when no higher-priority provider is configured + + @unit @providers @registry @fallback + Scenario: GEMINI appears in FALLBACK_ORDER list + Given I have the ProviderRegistry class + When I check the FALLBACK_ORDER contents + Then GEMINI should be present in the fallback order + And OPENAI should still be first in the order + And ANTHROPIC should still be second in the order + + @unit @providers @registry @fallback + Scenario: Gemini-only provider gets selected as default via fallback order + Given a registry with only gemini API key set to "sk-gemini-test" + And CLEVERAGENTS_DEFAULT_PROVIDER is not set + When I request the default provider type + Then the gemini fallback default should be ProviderType "GEMINI" + + @unit @providers @registry @fallback + Scenario: Gemini fallback selected after OPENAI, ANTHROPIC, GOOGLE are unconfigured + Given a registry with only gemini API key set to "sk-gemini-test" + And CLEVERAGENTS_DEFAULT_PROVIDER is not set + When I iterate through FALLBACK_ORDER and find the first configured provider + Then GEMINI should be the first configured provider found + + @unit @providers @registry @fallback + Scenario: Gemini in fallback order does not affect explicit env override + Given a registry with only gemini API key set to "sk-gemini-test" + And CLEVERAGENTS_DEFAULT_PROVIDER is set to "gemini" + When I request the default provider type + Then the gemini fallback default should be ProviderType "GEMINI" + + @unit @providers @registry @fallback + Scenario: Gemini with all other providers unconfigured returns None only when GEMINI also has no key + Given a gemini-registry with no API keys configured + And CLEVERAGENTS_ALLOW_MOCK_PROVIDER is not set + When I request the default provider type from a clean registry + Then the clean gemini registry default should be None diff --git a/features/steps/fallback_gemini_provider_steps.py b/features/steps/fallback_gemini_provider_steps.py new file mode 100644 index 000000000..4d7332976 --- /dev/null +++ b/features/steps/fallback_gemini_provider_steps.py @@ -0,0 +1,149 @@ +"""Step definitions for fallback_gemini_provider.feature. + +Verifies that ProviderType.GEMINI is present in +ProviderRegistry.FALLBACK_ORDER and selected as the default provider when +only the Gemini API key is configured. +""" + +from __future__ import annotations + +from typing import Any + +from behave import given, then, when # type: ignore[import-untyped] + +from cleveragents.providers.registry import ( + ProviderRegistry, + ProviderType, +) + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- + + +def _make_gemini_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, +) -> object: + """Return a minimal Settings-like mock for BDD steps.""" + from unittest.mock import MagicMock + + 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 = None + return settings + + +# --------------------------------------------------------------------------- +# Given +# --------------------------------------------------------------------------- + + +@given('a registry with only gemini API key set to "{key}"') +def step_gemini_only_registry(context: Any, key: str) -> None: + settings = _make_gemini_settings(gemini=key) + context.gemini_registry = ProviderRegistry(settings=settings) + + +@given("a gemini-registry with no API keys configured") +def step_no_keys_gemini_registry(context: Any) -> None: + context.gemini_registry = ProviderRegistry(settings=_make_gemini_settings()) + + +# --------------------------------------------------------------------------- +# When +# --------------------------------------------------------------------------- + + +@when("I check the FALLBACK_ORDER contents") +def step_check_fallback_order(context: Any) -> None: + """Read the FALLBACK_ORDER class variable.""" + context.fallback_order = list(ProviderRegistry.FALLBACK_ORDER) + + +@when("I request the default provider type") +def step_request_default_provider_type(context: Any) -> None: + result = context.gemini_registry.get_default_provider_type() + context.gemini_default = result + + +@when("I iterate through FALLBACK_ORDER and find the first configured provider") +def step_iterate_fallback_order(context: Any) -> None: + """Manually walk FALLBACK_ORDER to verify GEMINI is checked.""" + found = None + for pt in ProviderRegistry.FALLBACK_ORDER: + if context.gemini_registry.is_provider_configured(pt): + found = pt + break + context.fallback_iteration_result = found + + +@when("I request the default provider type from a clean registry") +def step_request_default_from_clean(context: Any) -> None: + result = context.gemini_registry.get_default_provider_type() + context.clean_default = result + + +# --------------------------------------------------------------------------- +# Then +# --------------------------------------------------------------------------- + + +@then("GEMINI should be present in the fallback order") +def step_gemini_in_fallback_order(context: Any) -> None: + assert ProviderType.GEMINI in context.fallback_order, ( + f"Expected GEMINI in FALLBACK_ORDER={context.fallback_order}" + ) + + +@then("OPENAI should still be first in the order") +def step_openai_first(context: Any) -> None: + assert context.fallback_order[0] == ProviderType.OPENAI, ( + f"Expected OPENAI first, got {context.fallback_order[0]}" + ) + + +@then("ANTHROPIC should still be second in the order") +def step_anthropic_second(context: Any) -> None: + assert context.fallback_order[1] == ProviderType.ANTHROPIC, ( + f"Expected ANTHROPIC second, got {context.fallback_order[1]}" + ) + + +@then('the gemini fallback default should be ProviderType "GEMINI"') +def step_result_is_gemini(context: Any) -> None: + assert context.gemini_default == ProviderType.GEMINI, ( + f"Expected GEMINI, got {context.gemini_default!r}" + ) + + +@then("GEMINI should be the first configured provider found") +def step_gemini_first_configured(context: Any) -> None: + assert context.fallback_iteration_result == ProviderType.GEMINI, ( + f"Expected GEMINI as first found, got {context.fallback_iteration_result!r}" + ) + + +@then("the clean gemini registry default should be None") +def step_result_is_none(context: Any) -> None: + assert context.clean_default is None, ( + f"Expected None, got {context.clean_default!r}" + )