"""Step definitions for settings_coverage_boost.feature. These steps target specific uncovered lines in settings.py: - Lines 561-563: _max_delay_ge_base_delay model validator raising ValueError - Lines 602, 604-608, 610-611: __repr__ method with sensitive-key masking """ from __future__ import annotations import os from behave import given, then, when from pydantic import ValidationError from cleveragents.config.settings import Settings from cleveragents.shared.redaction import REDACTED # Provider env var names that might leak into Settings from the test runner. _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", ) def _save_and_clear_provider_env(context): """Save current provider env vars and remove them so Settings sees None.""" saved = {} for key in _PROVIDER_ENV_KEYS: val = os.environ.pop(key, None) if val is not None: saved[key] = val context._saved_provider_env = saved def _restore_provider_env(context): """Restore previously saved provider env vars.""" saved = getattr(context, "_saved_provider_env", {}) for key, val in saved.items(): os.environ[key] = val # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the settings coverage module is imported") def step_settings_module_imported(context): """Ensure the Settings class is importable.""" assert Settings is not None # --------------------------------------------------------------------------- # Scenario: _max_delay_ge_base_delay rejects invalid delay configuration # (lines 561-563) # --------------------------------------------------------------------------- @when("I create Settings with retry_max_delay less than retry_base_delay") def step_create_settings_bad_delays(context): """Attempt to create Settings where retry_max_delay < retry_base_delay.""" context.delay_error = None # Use env vars because pydantic-settings reads these for Field values os.environ["CLEVERAGENTS_RETRY_BASE_DELAY"] = "10.0" os.environ["CLEVERAGENTS_RETRY_MAX_DELAY"] = "1.0" os.environ["CLEVERAGENTS_ENV"] = "test" os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true" try: Settings() except ValidationError as exc: context.delay_error = exc finally: os.environ.pop("CLEVERAGENTS_RETRY_BASE_DELAY", None) os.environ.pop("CLEVERAGENTS_RETRY_MAX_DELAY", None) os.environ.pop("CLEVERAGENTS_ENV", None) os.environ.pop("CLEVERAGENTS_MOCK_PROVIDERS", None) @then("a ValidationError should be raised about delay ordering") def step_check_delay_validation_error(context): """Verify that a ValidationError was raised.""" assert context.delay_error is not None, ( "Expected a ValidationError but none was raised" ) assert isinstance(context.delay_error, ValidationError) @then("the error message should mention both delay values") def step_check_delay_error_message(context): """Verify the error message contains information about both delays.""" msg = str(context.delay_error) assert "retry_max_delay" in msg, f"Expected 'retry_max_delay' in: {msg}" assert "retry_base_delay" in msg, f"Expected 'retry_base_delay' in: {msg}" # --------------------------------------------------------------------------- # Scenario: __repr__ masks sensitive field values when set # (lines 602, 604-608, 610-611) # --------------------------------------------------------------------------- @given("I have a Settings instance with openai_api_key set to a secret value") def step_create_settings_with_secret(context): """Create a Settings instance that has a sensitive key populated.""" _save_and_clear_provider_env(context) os.environ["OPENAI_API_KEY"] = "sk-test-secret-12345" os.environ["CLEVERAGENTS_ENV"] = "test" os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true" try: context.settings_repr_target = Settings() finally: os.environ.pop("OPENAI_API_KEY", None) os.environ.pop("CLEVERAGENTS_ENV", None) os.environ.pop("CLEVERAGENTS_MOCK_PROVIDERS", None) _restore_provider_env(context) @when("I call repr on the settings instance") def step_call_repr(context): """Invoke repr() on the stored Settings instance.""" context.settings_repr_output = repr(context.settings_repr_target) @then("the repr output should contain REDACTED for the openai_api_key field") def step_check_repr_redacted(context): """Verify the sensitive field is masked.""" expected = f"openai_api_key={REDACTED!r}" assert expected in context.settings_repr_output, ( f"Expected '{expected}' in repr output:\n{context.settings_repr_output}" ) @then('the repr output should start with "Settings(" and end with ")"') def step_check_repr_format(context): """Verify overall repr format.""" assert context.settings_repr_output.startswith("Settings("), ( f"Expected repr to start with 'Settings(': {context.settings_repr_output[:50]}" ) assert context.settings_repr_output.endswith(")"), ( f"Expected repr to end with ')': {context.settings_repr_output[-50:]}" ) # --------------------------------------------------------------------------- # Scenario: __repr__ shows non-sensitive fields normally # (lines 604, 605, 606, 610, 611) # --------------------------------------------------------------------------- @given("I have a Settings instance with default values") def step_create_settings_defaults(context): """Create a Settings instance with defaults (no secrets set).""" _save_and_clear_provider_env(context) os.environ["CLEVERAGENTS_ENV"] = "test" os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true" try: context.settings_repr_target = Settings() finally: os.environ.pop("CLEVERAGENTS_ENV", None) os.environ.pop("CLEVERAGENTS_MOCK_PROVIDERS", None) _restore_provider_env(context) @then("the repr output should contain the env field with its actual value") def step_check_repr_env_field(context): """Verify that non-sensitive fields are shown with their actual values.""" assert "env='test'" in context.settings_repr_output, ( f"Expected \"env='test'\" in repr output:\n{context.settings_repr_output}" ) @then("the repr output should not contain REDACTED for non-sensitive fields") def step_check_repr_no_unnecessary_redaction(context): """Verify that the 'env' field (non-sensitive) is not redacted.""" assert f"env={REDACTED!r}" not in context.settings_repr_output, ( "Did not expect 'env' to be redacted" ) # --------------------------------------------------------------------------- # Scenario: __repr__ shows None for sensitive keys that are None # (lines 606, 607 false-branch, 610) # --------------------------------------------------------------------------- @given("I have a Settings instance where all sensitive keys are None") def step_create_settings_no_secrets(context): """Create a Settings with no API keys set (all remain None).""" _save_and_clear_provider_env(context) os.environ["CLEVERAGENTS_ENV"] = "test" os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true" try: context.settings_repr_target = Settings() finally: os.environ.pop("CLEVERAGENTS_ENV", None) os.environ.pop("CLEVERAGENTS_MOCK_PROVIDERS", None) _restore_provider_env(context) @then("the repr output should show None for sensitive key fields without redaction") def step_check_repr_none_not_redacted(context): """When a sensitive key is None, it should show None rather than REDACTED.""" output = context.settings_repr_output # azure_api_key defaults to None and should NOT be redacted assert "azure_api_key=None" in output, ( f"Expected 'azure_api_key=None' in repr output:\n{output}" ) # Confirm REDACTED is NOT used for a None-valued sensitive key assert f"azure_api_key={REDACTED!r}" not in output, ( "Did not expect azure_api_key to be redacted when its value is None" )