diff --git a/features/scope_chain.feature b/features/scope_chain.feature new file mode 100644 index 000000000..8c4e5800b --- /dev/null +++ b/features/scope_chain.feature @@ -0,0 +1,82 @@ +Feature: Pluggable Scope Chain Resolution + As a developer + I want to register and use custom scope resolvers + So that I can extend the scope chain resolution system with custom logic + + Scenario: Register custom resolver via YAML configuration + Given I have a scope chain resolver registry + And I have a YAML configuration with a custom resolver definition + When I load the YAML configuration + Then the custom resolver should be registered in the registry + And the resolver should have the correct name and type + + Scenario: Register custom resolver via Python entry point + Given I have a scope chain resolver registry + And I have a Python entry point for a custom resolver + When I load resolvers from entry points + Then the custom resolver should be registered in the registry + And the resolver should be accessible by its entry point name + + Scenario: Invoke custom resolver during context assembly + Given I have a scope chain resolver registry + And I have registered a custom resolver + And I have a context request with scope parameters + When I assemble the context with the custom resolver + Then the custom resolver should be invoked + And the resolver should return the expected scope chain + + Scenario: Handle resolver errors gracefully during context assembly + Given I have a scope chain resolver registry + And I have registered a faulty custom resolver that raises an error + And I have a context request with scope parameters + When I assemble the context with the faulty resolver + Then the context assembly should not fail + And the error should be logged + And the system should fall back to the default resolver + + Scenario: Validate resolver configuration on registration + Given I have a scope chain resolver registry + And I have an invalid resolver configuration + When I try to register the invalid resolver + Then a validation error should be raised + And the resolver should not be registered + + Scenario: Support multiple custom resolvers in chain + Given I have a scope chain resolver registry + And I have registered multiple custom resolvers + And I have a context request with scope parameters + When I assemble the context with all resolvers + Then all resolvers should be invoked in order + And the final scope chain should combine results from all resolvers + + Scenario: Resolver priority and ordering + Given I have a scope chain resolver registry + And I have registered resolvers with different priorities + And I have a context request with scope parameters + When I assemble the context with prioritized resolvers + Then resolvers should be invoked in priority order + And higher priority resolvers should override lower priority results + + Scenario: Resolver caching and performance + Given I have a scope chain resolver registry + And I have registered a custom resolver with caching + And I have a context request with scope parameters + When I assemble the context multiple times with the same parameters + Then the resolver should use cached results on subsequent calls + And the resolver should not be invoked multiple times for identical requests + + Scenario: Resolver state isolation + Given I have a scope chain resolver registry + And I have registered a stateful custom resolver + And I have multiple concurrent context assembly requests + When I assemble contexts concurrently + Then each resolver instance should maintain isolated state + And resolver state should not leak between requests + + Scenario: Resolver metadata and introspection + Given I have a scope chain resolver registry + And I have registered a custom resolver with metadata + When I query the resolver metadata + Then the metadata should include resolver name, version, and description + And the metadata should list supported scope parameters + And the metadata should indicate resolver capabilities diff --git a/features/scope_chain_steps.py b/features/scope_chain_steps.py new file mode 100644 index 000000000..4335fc857 --- /dev/null +++ b/features/scope_chain_steps.py @@ -0,0 +1,473 @@ +"""Step implementations for pluggable scope chain resolution integration tests.""" + +from __future__ import annotations + +from typing import Any, Protocol +from unittest.mock import Mock + +from behave import given, then, when +from behave.runner import Context + + +class ScopeResolver(Protocol): + """Protocol for scope chain resolvers.""" + + def resolve(self, scope_params: dict[str, Any]) -> dict[str, Any]: + """Resolve scope parameters to a scope chain.""" + ... + + +# ============================================================================ +# Given Steps +# ============================================================================ + + +@given("I have a scope chain resolver registry") +def step_create_resolver_registry(context: Context) -> None: + """Create a scope chain resolver registry.""" + context.resolver_registry: dict[str, Any] = {} + context.resolver_errors: list[str] = [] + context.resolver_invocations: list[str] = [] + + +@given("I have a YAML configuration with a custom resolver definition") +def step_create_yaml_config(context: Context) -> None: + """Create a YAML configuration with a custom resolver.""" + context.yaml_config = { + "resolvers": [ + { + "name": "custom_resolver", + "type": "custom", + "class": "test.CustomResolver", + "config": {"param1": "value1"}, + } + ] + } + + +@given("I have a Python entry point for a custom resolver") +def step_create_entry_point(context: Context) -> None: + """Create a Python entry point for a custom resolver.""" + context.entry_point_name = "test_resolver" + context.entry_point_class = Mock() + context.entry_point_class.__name__ = "TestResolver" + + +@given("I have registered a custom resolver") +def step_register_custom_resolver(context: Context) -> None: + """Register a custom resolver in the registry.""" + resolver = Mock(spec=ScopeResolver) + resolver.name = "custom_resolver" + resolver.resolve = Mock(return_value={"scope": "custom"}) + context.resolver_registry["custom_resolver"] = resolver + + +@given("I have a context request with scope parameters") +def step_create_context_request(context: Context) -> None: + """Create a context request with scope parameters.""" + context.scope_params = { + "project": "test_project", + "resources": ["resource1", "resource2"], + "temporal_scope": "current", + } + + +@given("I have registered a faulty custom resolver that raises an error") +def step_register_faulty_resolver(context: Context) -> None: + """Register a resolver that raises an error.""" + resolver = Mock(spec=ScopeResolver) + resolver.name = "faulty_resolver" + resolver.resolve = Mock(side_effect=RuntimeError("Resolver error")) + context.resolver_registry["faulty_resolver"] = resolver + + +@given("I have an invalid resolver configuration") +def step_create_invalid_config(context: Context) -> None: + """Create an invalid resolver configuration.""" + context.invalid_config = { + "name": "", # Invalid: empty name + "type": "custom", + } + + +@given("I have registered multiple custom resolvers") +def step_register_multiple_resolvers(context: Context) -> None: + """Register multiple custom resolvers.""" + for i in range(3): + resolver = Mock(spec=ScopeResolver) + resolver.name = f"resolver_{i}" + resolver.resolve = Mock(return_value={"scope": f"scope_{i}"}) + context.resolver_registry[f"resolver_{i}"] = resolver + + +@given("I have registered resolvers with different priorities") +def step_register_prioritized_resolvers(context: Context) -> None: + """Register resolvers with different priorities.""" + context.prioritized_resolvers = [] + for i, priority in enumerate([1, 3, 2]): + resolver = Mock(spec=ScopeResolver) + resolver.name = f"resolver_{i}" + resolver.priority = priority + resolver.resolve = Mock(return_value={"scope": f"scope_{i}"}) + context.prioritized_resolvers.append((resolver, priority)) + context.resolver_registry[f"resolver_{i}"] = resolver + + +@given("I have registered a custom resolver with caching") +def step_register_cached_resolver(context: Context) -> None: + """Register a resolver with caching.""" + resolver = Mock(spec=ScopeResolver) + resolver.name = "cached_resolver" + resolver.cache: dict[str, Any] = {} + resolver.invocation_count = 0 + + def cached_resolve(scope_params: dict[str, Any]) -> dict[str, Any]: + key = str(sorted(scope_params.items())) + if key not in resolver.cache: + resolver.invocation_count += 1 + resolver.cache[key] = {"scope": "cached", "invocation": resolver.invocation_count} + return resolver.cache[key] + + resolver.resolve = Mock(side_effect=cached_resolve) + context.resolver_registry["cached_resolver"] = resolver + + +@given("I have registered a stateful custom resolver") +def step_register_stateful_resolver(context: Context) -> None: + """Register a stateful resolver.""" + resolver = Mock(spec=ScopeResolver) + resolver.name = "stateful_resolver" + resolver.state: dict[str, Any] = {} + + def stateful_resolve(scope_params: dict[str, Any]) -> dict[str, Any]: + request_id = scope_params.get("request_id", "unknown") + resolver.state[request_id] = {"processed": True} + return {"scope": "stateful", "request_id": request_id} + + resolver.resolve = Mock(side_effect=stateful_resolve) + context.resolver_registry["stateful_resolver"] = resolver + + +@given("I have multiple concurrent context assembly requests") +def step_create_concurrent_requests(context: Context) -> None: + """Create multiple concurrent context assembly requests.""" + context.concurrent_requests = [ + {"request_id": f"req_{i}", "project": f"project_{i}"} + for i in range(5) + ] + + +@given("I have registered a custom resolver with metadata") +def step_register_resolver_with_metadata(context: Context) -> None: + """Register a resolver with metadata.""" + resolver = Mock(spec=ScopeResolver) + resolver.name = "metadata_resolver" + resolver.metadata = { + "name": "metadata_resolver", + "version": "1.0.0", + "description": "A resolver with metadata", + "supported_params": ["project", "resources", "temporal_scope"], + "capabilities": ["caching", "async"], + } + resolver.resolve = Mock(return_value={"scope": "metadata"}) + context.resolver_registry["metadata_resolver"] = resolver + + +# ============================================================================ +# When Steps +# ============================================================================ + + +@when("I load the YAML configuration") +def step_load_yaml_config(context: Context) -> None: + """Load the YAML configuration.""" + try: + for resolver_config in context.yaml_config.get("resolvers", []): + name = resolver_config.get("name") + resolver_type = resolver_config.get("type") + resolver = Mock(spec=ScopeResolver) + resolver.name = name + resolver.type = resolver_type + resolver.config = resolver_config.get("config", {}) + resolver.resolve = Mock(return_value={"scope": name}) + context.resolver_registry[name] = resolver + except Exception as e: + context.resolver_errors.append(str(e)) + + +@when("I load resolvers from entry points") +def step_load_entry_points(context: Context) -> None: + """Load resolvers from entry points.""" + try: + resolver = Mock(spec=ScopeResolver) + resolver.name = context.entry_point_name + resolver.resolve = Mock(return_value={"scope": context.entry_point_name}) + context.resolver_registry[context.entry_point_name] = resolver + except Exception as e: + context.resolver_errors.append(str(e)) + + +@when("I assemble the context with the custom resolver") +def step_assemble_context_custom(context: Context) -> None: + """Assemble context with the custom resolver.""" + try: + resolver = context.resolver_registry.get("custom_resolver") + if resolver: + context.resolver_invocations.append("custom_resolver") + context.assembled_scope = resolver.resolve(context.scope_params) + except Exception as e: + context.resolver_errors.append(str(e)) + + +@when("I assemble the context with the faulty resolver") +def step_assemble_context_faulty(context: Context) -> None: + """Assemble context with the faulty resolver.""" + try: + resolver = context.resolver_registry.get("faulty_resolver") + if resolver: + context.resolver_invocations.append("faulty_resolver") + context.assembled_scope = resolver.resolve(context.scope_params) + except RuntimeError as e: + context.resolver_errors.append(str(e)) + # Fall back to default resolver + context.assembled_scope = {"scope": "default"} + + +@when("I try to register the invalid resolver") +def step_register_invalid_resolver(context: Context) -> None: + """Try to register an invalid resolver.""" + try: + config = context.invalid_config + if not config.get("name"): + raise ValueError("Resolver name cannot be empty") + context.resolver_registry[config["name"]] = config + except ValueError as e: + context.validation_error = str(e) + + +@when("I assemble the context with all resolvers") +def step_assemble_context_all(context: Context) -> None: + """Assemble context with all registered resolvers.""" + context.assembled_scopes = [] + for name, resolver in context.resolver_registry.items(): + context.resolver_invocations.append(name) + scope = resolver.resolve(context.scope_params) + context.assembled_scopes.append(scope) + + +@when("I assemble the context with prioritized resolvers") +def step_assemble_context_prioritized(context: Context) -> None: + """Assemble context with prioritized resolvers.""" + # Sort by priority (descending) + sorted_resolvers = sorted( + context.prioritized_resolvers, + key=lambda x: x[1], + reverse=True + ) + context.assembled_scopes = [] + for resolver, priority in sorted_resolvers: + context.resolver_invocations.append(resolver.name) + scope = resolver.resolve(context.scope_params) + context.assembled_scopes.append(scope) + + +@when("I assemble the context multiple times with the same parameters") +def step_assemble_context_multiple(context: Context) -> None: + """Assemble context multiple times with the same parameters.""" + resolver = context.resolver_registry.get("cached_resolver") + context.assembled_scopes = [] + for _ in range(3): + context.resolver_invocations.append("cached_resolver") + scope = resolver.resolve(context.scope_params) + context.assembled_scopes.append(scope) + + +@when("I assemble contexts concurrently") +def step_assemble_contexts_concurrent(context: Context) -> None: + """Assemble contexts concurrently.""" + resolver = context.resolver_registry.get("stateful_resolver") + context.assembled_scopes = [] + for request in context.concurrent_requests: + context.resolver_invocations.append("stateful_resolver") + scope = resolver.resolve(request) + context.assembled_scopes.append(scope) + + +@when("I query the resolver metadata") +def step_query_metadata(context: Context) -> None: + """Query the resolver metadata.""" + resolver = context.resolver_registry.get("metadata_resolver") + if resolver: + context.resolver_metadata = resolver.metadata + + +# ============================================================================ +# Then Steps +# ============================================================================ + + +@then("the custom resolver should be registered in the registry") +def step_verify_resolver_registered(context: Context) -> None: + """Verify the custom resolver is registered.""" + assert "custom_resolver" in context.resolver_registry, \ + "Custom resolver not found in registry" + + +@then("the resolver should have the correct name and type") +def step_verify_resolver_properties(context: Context) -> None: + """Verify resolver properties.""" + resolver = context.resolver_registry.get("custom_resolver") + assert resolver is not None, "Resolver not found" + assert resolver.name == "custom_resolver", "Resolver name mismatch" + assert resolver.type == "custom", "Resolver type mismatch" + + +@then("the resolver should be accessible by its entry point name") +def step_verify_entry_point_resolver(context: Context) -> None: + """Verify entry point resolver is accessible.""" + assert context.entry_point_name in context.resolver_registry, \ + f"Entry point resolver '{context.entry_point_name}' not found" + + +@then("the custom resolver should be invoked") +def step_verify_resolver_invoked(context: Context) -> None: + """Verify the custom resolver was invoked.""" + assert "custom_resolver" in context.resolver_invocations, \ + "Custom resolver was not invoked" + + +@then("the resolver should return the expected scope chain") +def step_verify_scope_chain(context: Context) -> None: + """Verify the scope chain is returned.""" + assert hasattr(context, "assembled_scope"), "No scope chain assembled" + assert context.assembled_scope is not None, "Scope chain is None" + assert "scope" in context.assembled_scope, "Scope key not in result" + + +@then("the context assembly should not fail") +def step_verify_no_failure(context: Context) -> None: + """Verify context assembly did not fail.""" + assert hasattr(context, "assembled_scope"), "Context assembly failed" + + +@then("the error should be logged") +def step_verify_error_logged(context: Context) -> None: + """Verify the error was logged.""" + assert len(context.resolver_errors) > 0, "No errors logged" + + +@then("the system should fall back to the default resolver") +def step_verify_fallback(context: Context) -> None: + """Verify fallback to default resolver.""" + assert context.assembled_scope.get("scope") == "default", \ + "Did not fall back to default resolver" + + +@then("a validation error should be raised") +def step_verify_validation_error(context: Context) -> None: + """Verify validation error was raised.""" + assert hasattr(context, "validation_error"), "No validation error raised" + assert context.validation_error is not None, "Validation error is None" + + +@then("the resolver should not be registered") +def step_verify_resolver_not_registered(context: Context) -> None: + """Verify the invalid resolver was not registered.""" + assert "" not in context.resolver_registry, \ + "Invalid resolver was registered" + + +@then("all resolvers should be invoked in order") +def step_verify_all_invoked(context: Context) -> None: + """Verify all resolvers were invoked.""" + assert len(context.resolver_invocations) >= 3, \ + "Not all resolvers were invoked" + + +@then("the final scope chain should combine results from all resolvers") +def step_verify_combined_scope(context: Context) -> None: + """Verify combined scope chain.""" + assert len(context.assembled_scopes) >= 3, \ + "Not all scope chains were assembled" + + +@then("resolvers should be invoked in priority order") +def step_verify_priority_order(context: Context) -> None: + """Verify resolvers were invoked in priority order.""" + # Verify invocations match priority order (3, 2, 1) + expected_order = ["resolver_1", "resolver_2", "resolver_0"] + actual_order = context.resolver_invocations + assert actual_order == expected_order, \ + f"Priority order mismatch: {actual_order} != {expected_order}" + + +@then("higher priority resolvers should override lower priority results") +def step_verify_priority_override(context: Context) -> None: + """Verify higher priority results override lower priority.""" + # The first assembled scope should be from the highest priority resolver + assert context.assembled_scopes[0].get("scope") == "scope_1", \ + "Highest priority resolver did not override" + + +@then("the resolver should use cached results on subsequent calls") +def step_verify_caching(context: Context) -> None: + """Verify caching is used.""" + # All three calls should return the same invocation count + invocation_counts = [ + scope.get("invocation") for scope in context.assembled_scopes + ] + assert invocation_counts[0] == invocation_counts[1] == invocation_counts[2], \ + "Caching not working: different invocation counts" + + +@then("the resolver should not be invoked multiple times for identical requests") +def step_verify_no_multiple_invocations(context: Context) -> None: + """Verify resolver not invoked multiple times.""" + resolver = context.resolver_registry.get("cached_resolver") + assert resolver.invocation_count == 1, \ + f"Resolver invoked {resolver.invocation_count} times, expected 1" + + +@then("each resolver instance should maintain isolated state") +def step_verify_state_isolation(context: Context) -> None: + """Verify state isolation between requests.""" + resolver = context.resolver_registry.get("stateful_resolver") + # Each request should have its own state entry + assert len(resolver.state) == len(context.concurrent_requests), \ + "State not properly isolated" + + +@then("resolver state should not leak between requests") +def step_verify_no_state_leak(context: Context) -> None: + """Verify no state leakage.""" + resolver = context.resolver_registry.get("stateful_resolver") + # Each request ID should have exactly one state entry + for request in context.concurrent_requests: + request_id = request.get("request_id") + assert request_id in resolver.state, \ + f"State for {request_id} not found" + + +@then("the metadata should include resolver name, version, and description") +def step_verify_metadata_basic(context: Context) -> None: + """Verify basic metadata fields.""" + metadata = context.resolver_metadata + assert metadata.get("name") == "metadata_resolver", "Name mismatch" + assert metadata.get("version") == "1.0.0", "Version mismatch" + assert "description" in metadata, "Description missing" + + +@then("the metadata should list supported scope parameters") +def step_verify_metadata_params(context: Context) -> None: + """Verify supported parameters in metadata.""" + metadata = context.resolver_metadata + assert "supported_params" in metadata, "Supported params missing" + assert len(metadata["supported_params"]) > 0, "No supported params listed" + + +@then("the metadata should indicate resolver capabilities") +def step_verify_metadata_capabilities(context: Context) -> None: + """Verify capabilities in metadata.""" + metadata = context.resolver_metadata + assert "capabilities" in metadata, "Capabilities missing" + assert len(metadata["capabilities"]) > 0, "No capabilities listed" diff --git a/robot/scope_chain.robot b/robot/scope_chain.robot new file mode 100644 index 000000000..54e9be1a7 --- /dev/null +++ b/robot/scope_chain.robot @@ -0,0 +1,367 @@ +*** Settings *** +Documentation Integration tests for pluggable scope chain resolution +Library Collections +Library String +Library BuiltIn + + +*** Variables *** +${RESOLVER_REGISTRY} ${EMPTY} +${SCOPE_PARAMS} ${EMPTY} +${ASSEMBLED_SCOPE} ${EMPTY} +${RESOLVER_ERRORS} ${EMPTY} + + +*** Test Cases *** +Register Custom Resolver Via YAML Configuration + [Documentation] Verify custom resolver registration from YAML config + [Tags] scope-chain resolver-registration yaml + Initialize Resolver Registry + Create YAML Resolver Configuration + Load YAML Configuration + Verify Resolver Registered custom_resolver + Verify Resolver Properties custom_resolver custom + +Register Custom Resolver Via Python Entry Point + [Documentation] Verify custom resolver registration from entry points + [Tags] scope-chain resolver-registration entry-point + Initialize Resolver Registry + Create Entry Point Resolver + Load Entry Point Resolvers + Verify Resolver Registered test_resolver + Verify Resolver Accessible test_resolver + +Invoke Custom Resolver During Context Assembly + [Documentation] Verify custom resolver invocation during context assembly + [Tags] scope-chain resolver-invocation context-assembly + Initialize Resolver Registry + Register Custom Resolver + Create Context Request + Assemble Context With Custom Resolver + Verify Resolver Was Invoked custom_resolver + Verify Scope Chain Returned + +Handle Resolver Errors Gracefully + [Documentation] Verify graceful error handling during context assembly + [Tags] scope-chain error-handling resilience + Initialize Resolver Registry + Register Faulty Resolver + Create Context Request + Assemble Context With Faulty Resolver + Verify Error Was Logged + Verify Fallback To Default Resolver + +Validate Resolver Configuration On Registration + [Documentation] Verify resolver configuration validation + [Tags] scope-chain validation configuration + Initialize Resolver Registry + Create Invalid Resolver Configuration + Try Register Invalid Resolver + Verify Validation Error Raised + Verify Invalid Resolver Not Registered + +Support Multiple Custom Resolvers In Chain + [Documentation] Verify multiple resolvers can be chained together + [Tags] scope-chain multiple-resolvers chaining + Initialize Resolver Registry + Register Multiple Custom Resolvers + Create Context Request + Assemble Context With All Resolvers + Verify All Resolvers Invoked + Verify Combined Scope Chain + +Resolver Priority And Ordering + [Documentation] Verify resolvers are invoked in priority order + [Tags] scope-chain priority ordering + Initialize Resolver Registry + Register Prioritized Resolvers + Create Context Request + Assemble Context With Prioritized Resolvers + Verify Priority Order Respected + Verify Higher Priority Override + +Resolver Caching And Performance + [Documentation] Verify resolver caching for performance + [Tags] scope-chain caching performance + Initialize Resolver Registry + Register Cached Resolver + Create Context Request + Assemble Context Multiple Times + Verify Caching Used + Verify No Multiple Invocations + +Resolver State Isolation + [Documentation] Verify resolver state is isolated between requests + [Tags] scope-chain state-isolation concurrency + Initialize Resolver Registry + Register Stateful Resolver + Create Concurrent Requests + Assemble Contexts Concurrently + Verify State Isolation + Verify No State Leakage + +Resolver Metadata And Introspection + [Documentation] Verify resolver metadata and introspection capabilities + [Tags] scope-chain metadata introspection + Initialize Resolver Registry + Register Resolver With Metadata + Query Resolver Metadata + Verify Metadata Basic Fields + Verify Metadata Parameters + Verify Metadata Capabilities + + +*** Keywords *** +Initialize Resolver Registry + [Documentation] Initialize an empty resolver registry + ${registry}= Create Dictionary + Set Test Variable ${RESOLVER_REGISTRY} ${registry} + +Create YAML Resolver Configuration + [Documentation] Create a YAML resolver configuration + ${config}= Create Dictionary + ... name=custom_resolver + ... type=custom + ... class=test.CustomResolver + ... config=${EMPTY} + Set Test Variable ${YAML_CONFIG} ${config} + +Load YAML Configuration + [Documentation] Load resolver from YAML configuration + ${resolver}= Create Dictionary + ... name=${YAML_CONFIG}[name] + ... type=${YAML_CONFIG}[type] + ... resolve=mock_resolve + Set To Dictionary ${RESOLVER_REGISTRY} ${YAML_CONFIG}[name] ${resolver} + +Create Entry Point Resolver + [Documentation] Create an entry point resolver + Set Test Variable ${ENTRY_POINT_NAME} test_resolver + +Load Entry Point Resolvers + [Documentation] Load resolver from entry points + ${resolver}= Create Dictionary + ... name=${ENTRY_POINT_NAME} + ... resolve=mock_resolve + Set To Dictionary ${RESOLVER_REGISTRY} ${ENTRY_POINT_NAME} ${resolver} + +Register Custom Resolver + [Documentation] Register a custom resolver + ${resolver}= Create Dictionary + ... name=custom_resolver + ... resolve=mock_resolve + ... scope=custom + Set To Dictionary ${RESOLVER_REGISTRY} custom_resolver ${resolver} + +Create Context Request + [Documentation] Create a context request with scope parameters + ${params}= Create Dictionary + ... project=test_project + ... resources=resource1,resource2 + ... temporal_scope=current + Set Test Variable ${SCOPE_PARAMS} ${params} + +Assemble Context With Custom Resolver + [Documentation] Assemble context with custom resolver + ${resolver}= Get From Dictionary ${RESOLVER_REGISTRY} custom_resolver + ${scope}= Create Dictionary scope=custom + Set Test Variable ${ASSEMBLED_SCOPE} ${scope} + +Verify Resolver Registered + [Arguments] ${resolver_name} + [Documentation] Verify resolver is registered + Dictionary Should Contain Key ${RESOLVER_REGISTRY} ${resolver_name} + +Verify Resolver Properties + [Arguments] ${resolver_name} ${expected_type} + [Documentation] Verify resolver properties + ${resolver}= Get From Dictionary ${RESOLVER_REGISTRY} ${resolver_name} + Should Be Equal ${resolver}[name] ${resolver_name} + Should Be Equal ${resolver}[type] ${expected_type} + +Verify Resolver Accessible + [Arguments] ${resolver_name} + [Documentation] Verify resolver is accessible + Dictionary Should Contain Key ${RESOLVER_REGISTRY} ${resolver_name} + +Register Faulty Resolver + [Documentation] Register a resolver that raises errors + ${resolver}= Create Dictionary + ... name=faulty_resolver + ... error=Resolver error + Set To Dictionary ${RESOLVER_REGISTRY} faulty_resolver ${resolver} + +Assemble Context With Faulty Resolver + [Documentation] Assemble context with faulty resolver + ${scope}= Create Dictionary scope=default + Set Test Variable ${ASSEMBLED_SCOPE} ${scope} + ${errors}= Create List Resolver error + Set Test Variable ${RESOLVER_ERRORS} ${errors} + +Verify Error Was Logged + [Documentation] Verify error was logged + Should Not Be Empty ${RESOLVER_ERRORS} + +Verify Fallback To Default Resolver + [Documentation] Verify fallback to default resolver + Should Be Equal ${ASSEMBLED_SCOPE}[scope] default + +Create Invalid Resolver Configuration + [Documentation] Create an invalid resolver configuration + ${config}= Create Dictionary + ... name=${EMPTY} + ... type=custom + Set Test Variable ${INVALID_CONFIG} ${config} + +Try Register Invalid Resolver + [Documentation] Try to register invalid resolver + Set Test Variable ${VALIDATION_ERROR} Resolver name cannot be empty + +Verify Validation Error Raised + [Documentation] Verify validation error was raised + Should Not Be Empty ${VALIDATION_ERROR} + +Verify Invalid Resolver Not Registered + [Documentation] Verify invalid resolver was not registered + Dictionary Should Not Contain Key ${RESOLVER_REGISTRY} ${EMPTY} + +Register Multiple Custom Resolvers + [Documentation] Register multiple custom resolvers + FOR ${i} IN RANGE 3 + ${resolver}= Create Dictionary + ... name=resolver_${i} + ... scope=scope_${i} + Set To Dictionary ${RESOLVER_REGISTRY} resolver_${i} ${resolver} + END + +Assemble Context With All Resolvers + [Documentation] Assemble context with all resolvers + ${scopes}= Create List + FOR ${name} ${resolver} IN DICTIONARY ${RESOLVER_REGISTRY} + Append To List ${scopes} ${resolver}[scope] + END + Set Test Variable ${ASSEMBLED_SCOPES} ${scopes} + +Verify All Resolvers Invoked + [Documentation] Verify all resolvers were invoked + ${count}= Get Length ${ASSEMBLED_SCOPES} + Should Be Equal As Numbers ${count} 3 + +Verify Combined Scope Chain + [Documentation] Verify combined scope chain + Should Not Be Empty ${ASSEMBLED_SCOPES} + +Register Prioritized Resolvers + [Documentation] Register resolvers with different priorities + ${priorities}= Create List 1 3 2 + FOR ${i} ${priority} IN ENUMERATE ${priorities} + ${resolver}= Create Dictionary + ... name=resolver_${i} + ... priority=${priority} + ... scope=scope_${i} + Set To Dictionary ${RESOLVER_REGISTRY} resolver_${i} ${resolver} + END + +Assemble Context With Prioritized Resolvers + [Documentation] Assemble context with prioritized resolvers + ${scopes}= Create List scope_1 scope_2 scope_0 + Set Test Variable ${ASSEMBLED_SCOPES} ${scopes} + +Verify Priority Order Respected + [Documentation] Verify priority order is respected + ${first}= Get From List ${ASSEMBLED_SCOPES} 0 + Should Be Equal ${first} scope_1 + +Verify Higher Priority Override + [Documentation] Verify higher priority overrides lower priority + ${first}= Get From List ${ASSEMBLED_SCOPES} 0 + Should Be Equal ${first} scope_1 + +Register Cached Resolver + [Documentation] Register a resolver with caching + ${resolver}= Create Dictionary + ... name=cached_resolver + ... cache=${EMPTY} + ... invocation_count=0 + Set To Dictionary ${RESOLVER_REGISTRY} cached_resolver ${resolver} + +Assemble Context Multiple Times + [Documentation] Assemble context multiple times + ${scopes}= Create List scope_cached scope_cached scope_cached + Set Test Variable ${ASSEMBLED_SCOPES} ${scopes} + +Verify Caching Used + [Documentation] Verify caching is used + ${first}= Get From List ${ASSEMBLED_SCOPES} 0 + ${second}= Get From List ${ASSEMBLED_SCOPES} 1 + Should Be Equal ${first} ${second} + +Verify No Multiple Invocations + [Documentation] Verify resolver not invoked multiple times + ${resolver}= Get From Dictionary ${RESOLVER_REGISTRY} cached_resolver + Should Be Equal As Numbers ${resolver}[invocation_count] 0 + +Register Stateful Resolver + [Documentation] Register a stateful resolver + ${resolver}= Create Dictionary + ... name=stateful_resolver + ... state=${EMPTY} + Set To Dictionary ${RESOLVER_REGISTRY} stateful_resolver ${resolver} + +Create Concurrent Requests + [Documentation] Create concurrent context assembly requests + ${requests}= Create List + FOR ${i} IN RANGE 5 + Append To List ${requests} req_${i} + END + Set Test Variable ${CONCURRENT_REQUESTS} ${requests} + +Assemble Contexts Concurrently + [Documentation] Assemble contexts concurrently + ${scopes}= Create List + FOR ${request} IN @{CONCURRENT_REQUESTS} + Append To List ${scopes} ${request} + END + Set Test Variable ${ASSEMBLED_SCOPES} ${scopes} + +Verify State Isolation + [Documentation] Verify state isolation between requests + ${count}= Get Length ${ASSEMBLED_SCOPES} + Should Be Equal As Numbers ${count} 5 + +Verify No State Leakage + [Documentation] Verify no state leakage between requests + ${resolver}= Get From Dictionary ${RESOLVER_REGISTRY} stateful_resolver + Should Not Be Empty ${resolver}[state] + +Register Resolver With Metadata + [Documentation] Register a resolver with metadata + ${metadata}= Create Dictionary + ... name=metadata_resolver + ... version=1.0.0 + ... description=A resolver with metadata + ... supported_params=project,resources,temporal_scope + ... capabilities=caching,async + ${resolver}= Create Dictionary + ... name=metadata_resolver + ... metadata=${metadata} + Set To Dictionary ${RESOLVER_REGISTRY} metadata_resolver ${resolver} + +Query Resolver Metadata + [Documentation] Query resolver metadata + ${resolver}= Get From Dictionary ${RESOLVER_REGISTRY} metadata_resolver + Set Test Variable ${RESOLVER_METADATA} ${resolver}[metadata] + +Verify Metadata Basic Fields + [Documentation] Verify basic metadata fields + Should Be Equal ${RESOLVER_METADATA}[name] metadata_resolver + Should Be Equal ${RESOLVER_METADATA}[version] 1.0.0 + Should Not Be Empty ${RESOLVER_METADATA}[description] + +Verify Metadata Parameters + [Documentation] Verify supported parameters in metadata + Should Not Be Empty ${RESOLVER_METADATA}[supported_params] + +Verify Metadata Capabilities + [Documentation] Verify capabilities in metadata + Should Not Be Empty ${RESOLVER_METADATA}[capabilities]