"""Step definitions for retry_policy_model_coverage.feature. These steps target specific uncovered lines in retry_policy.py: - Lines 228-232: CircuitBreakerConfig._check_cooldown_le_recovery validator - Lines 268-269: ServiceRetryPolicy._reject_unicode_control_chars validator - Lines 556-560: apply_overrides unknown keys warning - Lines 566-572: apply_overrides invalid service name skip - Lines 579-581: apply_overrides non-dict sub-key warning - Lines 600-607: apply_overrides final ValidationError skip - Lines 546-552: apply_overrides non-dict override data """ from behave import given, then, when from pydantic import ValidationError from cleveragents.domain.models.core.retry_policy import ( CircuitBreakerConfig, RetryCategory, RetryPolicyConfig, ServiceRetryPolicy, ServiceRetryPolicyRegistry, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the retry policy model module is imported") def step_module_imported(context): """Ensure the retry_policy module is importable.""" assert CircuitBreakerConfig is not None assert ServiceRetryPolicy is not None assert ServiceRetryPolicyRegistry is not None # --------------------------------------------------------------------------- # CircuitBreakerConfig: cooldown > recovery (lines 228-232) # --------------------------------------------------------------------------- @when( "I create a CircuitBreakerConfig with cooldown {cooldown:d} and recovery timeout {recovery:d}" ) def step_create_cb_config(context, cooldown, recovery): """Attempt to create a CircuitBreakerConfig with given cooldown and recovery.""" context.cb_error = None context.cb_config = None try: context.cb_config = CircuitBreakerConfig( cooldown_seconds=float(cooldown), recovery_timeout=float(recovery), ) except (ValidationError, ValueError) as exc: context.cb_error = exc @then("a ValueError should be raised mentioning cooldown and recovery_timeout") def step_verify_cooldown_error(context): """Verify the validator rejects cooldown > recovery.""" assert context.cb_error is not None, "Expected an error but none was raised" error_text = str(context.cb_error) assert "cooldown_seconds" in error_text or "cooldown" in error_text, ( f"Error should mention cooldown: {error_text}" ) @then("the CircuitBreakerConfig should be created successfully") def step_verify_cb_success(context): """Verify the config was created without error.""" assert context.cb_error is None, f"Unexpected error: {context.cb_error}" assert context.cb_config is not None # --------------------------------------------------------------------------- # ServiceRetryPolicy: Unicode control chars (lines 268-269) # --------------------------------------------------------------------------- @when("I create a ServiceRetryPolicy with a zero-width-space in the name") def step_create_policy_zwsp(context): """Attempt to create a policy with a zero-width space (U+200B).""" context.unicode_error = None try: ServiceRetryPolicy(service_name="my\u200bservice") except (ValidationError, ValueError) as exc: context.unicode_error = exc @when("I create a ServiceRetryPolicy with an RTL override character in the name") def step_create_policy_rtl(context): """Attempt to create a policy with an RTL override (U+202E).""" context.unicode_error = None try: ServiceRetryPolicy(service_name="my\u202eservice") except (ValidationError, ValueError) as exc: context.unicode_error = exc @then("a ValidationError should be raised mentioning invisible Unicode") def step_verify_unicode_error(context): """Verify the validator rejects invisible Unicode characters.""" assert context.unicode_error is not None, ( "Expected a ValidationError for invisible Unicode chars" ) error_text = str(context.unicode_error) assert ( "invisible" in error_text.lower() or "unicode" in error_text.lower() or "control" in error_text.lower() ), f"Error should mention invisible/unicode/control: {error_text}" # --------------------------------------------------------------------------- # Fresh registry helper # --------------------------------------------------------------------------- @given("a fresh ServiceRetryPolicyRegistry") def step_fresh_registry(context): """Create a new registry for the scenario.""" context.registry = ServiceRetryPolicyRegistry() # --------------------------------------------------------------------------- # apply_overrides: unknown keys (lines 556-560) # --------------------------------------------------------------------------- @when('I apply overrides with unknown keys for service "plan_service"') def step_apply_unknown_keys(context): """Apply overrides containing unrecognised top-level keys.""" context.registry.apply_overrides( { "plan_service": { "bogus_key": True, "another_unknown": 42, } } ) @then("the unknown keys should be silently ignored") def step_verify_unknown_keys_ignored(context): """Verify the override did not crash despite unknown keys.""" # The registry should still function policy = context.registry.get("plan_service") assert policy is not None @then('the policy for "plan_service" should still be valid') def step_verify_plan_service_valid(context): """Verify the plan_service policy is intact.""" policy = context.registry.get("plan_service") assert policy.service_name == "plan_service" assert policy.retry is not None assert policy.circuit_breaker is not None # --------------------------------------------------------------------------- # apply_overrides: invalid service name — empty (lines 566-572) # --------------------------------------------------------------------------- @when("I apply overrides with an empty-string service name") def step_apply_empty_service_name(context): """Apply overrides with an empty string as the service name key.""" context.apply_error = None try: context.registry.apply_overrides({"": {"retry": {"max_attempts": 5}}}) except Exception as exc: context.apply_error = exc @when("I apply overrides with a whitespace-only service name") def step_apply_whitespace_service_name(context): """Apply overrides with a whitespace-only service name key. After stripping, the name is empty, triggering a validation error which is caught and skipped by the except block (lines 566-572). """ context.apply_error = None try: context.registry.apply_overrides({" ": {"retry": {"max_attempts": 5}}}) except Exception as exc: context.apply_error = exc @then("the override should be skipped without crashing") def step_verify_no_crash(context): """Verify no exception propagated from apply_overrides.""" assert context.apply_error is None, f"Expected no error, got: {context.apply_error}" # --------------------------------------------------------------------------- # apply_overrides: non-dict sub-key (lines 579-581) # --------------------------------------------------------------------------- @when('I apply overrides where "retry" value is a string instead of dict') def step_apply_non_dict_retry(context): """Apply overrides where the 'retry' value is not a dict.""" context.registry.apply_overrides({"plan_service": {"retry": "not_a_dict"}}) @then("the non-dict retry sub-key should be ignored") def step_verify_non_dict_retry_ignored(context): """Verify the non-dict retry value was ignored.""" policy = context.registry.get("plan_service") # The retry config should be unchanged from defaults assert policy.retry is not None assert isinstance(policy.retry, RetryPolicyConfig) @when('I apply overrides where "circuit_breaker" value is an integer') def step_apply_non_dict_cb(context): """Apply overrides where the 'circuit_breaker' value is an int.""" context.registry.apply_overrides({"plan_service": {"circuit_breaker": 999}}) @then("the non-dict circuit_breaker sub-key should be ignored") def step_verify_non_dict_cb_ignored(context): """Verify the non-dict circuit_breaker value was ignored.""" policy = context.registry.get("plan_service") assert policy.circuit_breaker is not None assert isinstance(policy.circuit_breaker, CircuitBreakerConfig) # --------------------------------------------------------------------------- # apply_overrides: final ValidationError from invalid merged policy # (lines 600-607) # --------------------------------------------------------------------------- @when("I apply overrides with an invalid retry max_attempts of -5") def step_apply_invalid_max_attempts(context): """Apply overrides that produce an invalid merged policy. max_attempts has ge=1 constraint so -5 will fail validation on the final ServiceRetryPolicy.model_validate() call (lines 595-607). """ context.original_policy = context.registry.get("plan_service") context.apply_error = None try: context.registry.apply_overrides( {"plan_service": {"retry": {"max_attempts": -5}}} ) except Exception as exc: context.apply_error = exc @then("the invalid merged override should be skipped") def step_verify_invalid_merged_skipped(context): """Verify the override did not crash and was silently skipped.""" assert context.apply_error is None, ( f"Expected no exception, got: {context.apply_error}" ) @then('the policy for "plan_service" should retain its original max_attempts') def step_verify_original_max_attempts(context): """Verify the policy was not corrupted by the invalid override.""" policy = context.registry.get("plan_service") assert policy.retry.max_attempts == context.original_policy.retry.max_attempts # --------------------------------------------------------------------------- # apply_overrides: non-dict override_data (lines 546-552) # --------------------------------------------------------------------------- @when("I apply overrides where the override data is a string") def step_apply_non_dict_override_data(context): """Apply overrides where the value for a service is not a dict.""" context.apply_error = None try: context.registry.apply_overrides({"plan_service": "this_is_not_a_dict"}) except Exception as exc: context.apply_error = exc @then("the non-dict override data should be ignored") def step_verify_non_dict_data_ignored(context): """Verify the non-dict data was ignored without crashing.""" assert context.apply_error is None, f"Expected no error, got: {context.apply_error}" # The policy should still exist and be valid policy = context.registry.get("plan_service") assert policy.service_name == "plan_service" # --------------------------------------------------------------------------- # Combined: unknown keys + valid description change # --------------------------------------------------------------------------- @when( 'I apply overrides with unknown keys and a valid description change for "plan_service"' ) def step_apply_mixed_override(context): """Apply overrides with both unknown keys and a valid description.""" context.registry.apply_overrides( { "plan_service": { "unknown_field": True, "description": "Updated description via override", } } ) @then('the description for "plan_service" should be updated') def step_verify_description_updated(context): """Verify the valid description field was applied.""" policy = context.registry.get("plan_service") assert policy.description == "Updated description via override" @then("the unknown keys should not appear in the policy") def step_verify_no_unknown_fields(context): """Verify unknown keys did not leak into the policy.""" policy = context.registry.get("plan_service") dumped = policy.model_dump() assert "unknown_field" not in dumped # --------------------------------------------------------------------------- # apply_overrides: retry_category scalar merge (lines 593-594) # --------------------------------------------------------------------------- @when('I apply overrides changing retry_category to "network" for "plan_service"') def step_apply_retry_category_override(context): """Apply overrides that change the retry_category scalar field.""" context.registry.apply_overrides({"plan_service": {"retry_category": "network"}}) @then('the retry_category for "plan_service" should be "network"') def step_verify_retry_category(context): """Verify the retry_category was updated.""" policy = context.registry.get("plan_service") assert policy.retry_category == RetryCategory.NETWORK # --------------------------------------------------------------------------- # RetryPolicyConfig: max_delay < base_delay (lines 159-163) # --------------------------------------------------------------------------- @when("I create a RetryPolicyConfig with base_delay {base:d} and max_delay {max_d:d}") def step_create_retry_config_bad_delays(context, base, max_d): """Attempt to create a RetryPolicyConfig with max_delay < base_delay.""" context.retry_config_error = None context.retry_config = None try: context.retry_config = RetryPolicyConfig( base_delay=float(base), max_delay=float(max_d), ) except (ValidationError, ValueError) as exc: context.retry_config_error = exc @then("a ValueError should be raised mentioning max_delay and base_delay") def step_verify_delay_error(context): """Verify the validator rejects max_delay < base_delay.""" assert context.retry_config_error is not None, ( "Expected an error but none was raised" ) error_text = str(context.retry_config_error) assert "max_delay" in error_text or "base_delay" in error_text, ( f"Error should mention max_delay/base_delay: {error_text}" ) # --------------------------------------------------------------------------- # Registry.get: auto-generate default for unknown service (lines 496-497) # --------------------------------------------------------------------------- @when('I get the policy for unknown service "{service_name}"') def step_get_unknown_service(context, service_name): """Request a policy for a service not in the default registry.""" context.auto_policy = context.registry.get(service_name) @then('a default policy should be returned for "{service_name}"') def step_verify_auto_policy(context, service_name): """Verify a policy was returned with the correct service_name.""" assert context.auto_policy is not None assert context.auto_policy.service_name == service_name @then("the auto-generated policy should have the database retry category") def step_verify_auto_category(context): """Verify the auto-generated policy defaults to DATABASE category.""" assert context.auto_policy.retry_category == RetryCategory.DATABASE # --------------------------------------------------------------------------- # Registry.register: store and retrieve a custom policy (lines 510-511) # --------------------------------------------------------------------------- @when('I register a custom policy for service "{svc}" with max_attempts {attempts:d}') def step_register_custom_policy(context, svc, attempts): """Create and register a custom policy.""" policy = ServiceRetryPolicy( service_name=svc, retry_category=RetryCategory.NETWORK, retry=RetryPolicyConfig(max_attempts=attempts), description=f"Custom policy for {svc}", ) context.registry.register(policy) @then('the registry should return a policy for "{svc}" with max_attempts {attempts:d}') def step_verify_registered_policy(context, svc, attempts): """Verify the registered policy can be retrieved with correct values.""" policy = context.registry.get(svc) assert policy.service_name == svc assert policy.retry.max_attempts == attempts # --------------------------------------------------------------------------- # Registry.all_policies: snapshot of all policies (lines 618-619) # --------------------------------------------------------------------------- @when("I request all policies from the registry") def step_request_all_policies(context): """Get a snapshot of all policies.""" context.all_policies = context.registry.all_policies() @then("the snapshot should contain all default service policies") def step_verify_all_policies_count(context): """Verify the snapshot contains the expected number of policies.""" assert len(context.all_policies) >= 11, ( f"Expected at least 11 default policies, got {len(context.all_policies)}" ) assert "plan_service" in context.all_policies assert "session_service" in context.all_policies @then("the snapshot should be a deep copy not sharing identity") def step_verify_deep_copy(context): """Verify the returned dict is a deep copy.""" snapshot1 = context.all_policies snapshot2 = context.registry.all_policies() # Same keys but different object identity assert set(snapshot1.keys()) == set(snapshot2.keys()) assert snapshot1["plan_service"] is not snapshot2["plan_service"] # --------------------------------------------------------------------------- # Registry.registered_services: sorted list (lines 628-629) # --------------------------------------------------------------------------- @when("I request the registered services list") def step_request_services_list(context): """Get the sorted list of registered service names.""" context.services_list = context.registry.registered_services() @then("rpmcov the list should be sorted alphabetically") def step_verify_sorted(context): """Verify the list is sorted.""" assert context.services_list == sorted(context.services_list) @then("the list should contain the default service names") def step_verify_default_names(context): """Verify expected default service names are present.""" assert "plan_service" in context.services_list assert "session_service" in context.services_list assert "context_service" in context.services_list