"""Step definitions for plugin_extension_points.feature. Tests all 31 spec-defined extension points: registration, lookup by name, lookup by category, protocol type correctness, entry point discovery, context subsystem wiring, and idempotent registration. Based on issue #939. """ from __future__ import annotations from typing import Any from behave import given, then, when from behave.runner import Context from cleveragents.infrastructure.plugins.extension_catalog import ( TOTAL_EXTENSION_POINTS, get_extension_point_definitions, get_extension_points_by_category, register_all_extension_points, ) from cleveragents.infrastructure.plugins.extension_protocols import ( ALL_EXTENSION_PROTOCOLS, A2AExtensionMethodExtension, A2ATransportExtension, ConfigSourceExtension, ConfigValidatorExtension, ContextPipelineComponentExtension, ContextStorageBackendExtension, ContextStrategyExtension, EventFilterExtension, EventHandlerExtension, OutputFormatExtension, OutputMaterializerExtension, OutputRendererExtension, ResourceResolverExtension, ResourceTypeHandlerExtension, SafetyGuardrailExtension, ScopeChainResolverExtension, SkillProviderExtension, SkillTemplateExtension, ToolMiddlewareExtension, ToolProviderExtension, ValidationRuleProviderExtension, ValidationRunnerExtension, ) from cleveragents.infrastructure.plugins.manager import PluginManager from cleveragents.infrastructure.plugins.types import ExtensionPoint # --------------------------------------------------------------------------- # Fresh PluginManager # --------------------------------------------------------------------------- @given("a fresh PluginManager for extension point registration") def step_fresh_pm(context: Context) -> None: context.ep_manager = PluginManager() @given("a PluginManager with all extension points registered") def step_pm_with_all_eps(context: Context) -> None: context.ep_manager = PluginManager() register_all_extension_points(context.ep_manager) # --------------------------------------------------------------------------- # Register all extension points # --------------------------------------------------------------------------- @when("I register all spec-defined extension points") def step_register_all(context: Context) -> None: context.ep_registered_count = register_all_extension_points( context.ep_manager, ) @when("I register all spec-defined extension points again") def step_register_all_again(context: Context) -> None: register_all_extension_points(context.ep_manager) @then("exactly {count:d} extension points should be registered") def step_exactly_n_registered(context: Context, count: int) -> None: eps = context.ep_manager.list_extension_points() assert len(eps) == count, f"Expected {count}, got {len(eps)}" # --------------------------------------------------------------------------- # Extension point definitions # --------------------------------------------------------------------------- @when("I retrieve the extension point definitions") def step_retrieve_defs(context: Context) -> None: context.ep_definitions = get_extension_point_definitions() @then("I should get exactly {count:d} extension point definitions") def step_defs_count(context: Context, count: int) -> None: assert len(context.ep_definitions) == count, ( f"Expected {count}, got {len(context.ep_definitions)}" ) @then("each definition should have a non-empty name") def step_each_def_has_name(context: Context) -> None: for ep in context.ep_definitions: assert ep.name, f"Extension point has empty name: {ep}" @then("each definition should have a protocol_type") def step_each_def_has_protocol(context: Context) -> None: for ep in context.ep_definitions: assert ep.protocol_type is not None, ( f"Extension point {ep.name} has no protocol_type" ) @then("each definition should have a non-empty description") def step_each_def_has_description(context: Context) -> None: for ep in context.ep_definitions: assert ep.description, f"Extension point {ep.name} has empty description" # --------------------------------------------------------------------------- # Lookup by name # --------------------------------------------------------------------------- @when('I look up extension point "{name}"') def step_lookup_by_name(context: Context, name: str) -> None: eps = context.ep_manager.list_extension_points() matches = [ep for ep in eps if ep.name == name] context.ep_lookup_result: ExtensionPoint | None = matches[0] if matches else None @then('the looked-up extension point name should be "{name}"') def step_lookup_name_is(context: Context, name: str) -> None: assert context.ep_lookup_result is not None, f"Extension point '{name}' not found" assert context.ep_lookup_result.name == name @then("the looked-up extension point should be None") def step_lookup_is_none(context: Context) -> None: assert context.ep_lookup_result is None # --------------------------------------------------------------------------- # Lookup by category # --------------------------------------------------------------------------- @when("I retrieve extension points grouped by category") def step_grouped_by_category(context: Context) -> None: context.ep_categories = get_extension_points_by_category() @then('the "{category}" category should have {count:d} extension points') def step_category_count(context: Context, category: str, count: int) -> None: actual = len(context.ep_categories.get(category, [])) assert actual == count, f"Category '{category}': expected {count}, got {actual}" # --------------------------------------------------------------------------- # Protocol type correctness # --------------------------------------------------------------------------- @then('the protocol type name should be "{proto_name}"') def step_protocol_type_name(context: Context, proto_name: str) -> None: assert context.ep_lookup_result is not None actual = context.ep_lookup_result.protocol_type.__name__ assert actual == proto_name, f"Expected '{proto_name}', got '{actual}'" # --------------------------------------------------------------------------- # Entry point discovery # --------------------------------------------------------------------------- @when('I discover extension point plugins from group "{group}"') def step_discover_ep_group(context: Context, group: str) -> None: context.ep_discovered = context.ep_manager.discover(group=group) @then("the entry point discovered list should be empty") def step_ep_discovered_empty(context: Context) -> None: assert len(context.ep_discovered) == 0 # --------------------------------------------------------------------------- # TOTAL_EXTENSION_POINTS constant # --------------------------------------------------------------------------- @then("the TOTAL_EXTENSION_POINTS constant should equal {value:d}") def step_total_constant(context: Context, value: int) -> None: assert value == TOTAL_EXTENSION_POINTS, ( f"Expected {value}, got {TOTAL_EXTENSION_POINTS}" ) # --------------------------------------------------------------------------- # Context extension point wiring in ACMSPipeline # --------------------------------------------------------------------------- @given("an ACMSPipeline wired with the plugin manager") def step_pipeline_with_pm(context: Context) -> None: from cleveragents.application.services.acms_service import ACMSPipeline context.ep_pipeline = ACMSPipeline( plugin_manager=context.ep_manager, ) @given("an ACMSPipeline without a plugin manager") def step_pipeline_without_pm(context: Context) -> None: from cleveragents.application.services.acms_service import ACMSPipeline context.ep_pipeline = ACMSPipeline() @then("the pipeline should report {count:d} context extension points") def step_pipeline_context_eps(context: Context, count: int) -> None: actual = len(context.ep_pipeline.context_extension_points) assert actual == count, f"Expected {count} context extension points, got {actual}" # --------------------------------------------------------------------------- # Runtime-checkable protocols # --------------------------------------------------------------------------- @when("I retrieve all extension protocol types") def step_retrieve_all_protocols(context: Context) -> None: context.ep_protocol_map = ALL_EXTENSION_PROTOCOLS @then("every protocol should be runtime-checkable") def step_all_protocols_runtime_checkable(context: Context) -> None: for name, proto in context.ep_protocol_map.items(): # runtime_checkable protocols have _is_runtime_protocol = True is_rc: Any = getattr(proto, "_is_runtime_protocol", False) assert is_rc, f"Protocol {name} ({proto.__name__}) is not runtime-checkable" # --------------------------------------------------------------------------- # registry_key matches category # --------------------------------------------------------------------------- @then("each definition registry_key should match its name category prefix") def step_registry_key_matches(context: Context) -> None: for ep in context.ep_definitions: expected_category = ep.name.split(".")[0] assert ep.registry_key == expected_category, ( f"Extension point '{ep.name}': registry_key '{ep.registry_key}' " f"does not match expected category '{expected_category}'" ) # --------------------------------------------------------------------------- # Concrete protocol implementations for coverage # --------------------------------------------------------------------------- class _StubContextStrategy: @property def name(self) -> str: return "stub" def can_handle(self, request: Any) -> float: return 1.0 def assemble(self, fragments: Any, budget: Any) -> list[Any]: return [] class _StubPipelineComponent: @property def component_name(self) -> str: return "stub_component" def process(self, fragments: Any, context: Any) -> list[Any]: return list(fragments) class _StubStorageBackend: def __init__(self) -> None: self._store: dict[str, Any] = {} @property def backend_type(self) -> str: return "memory" def store(self, key: str, data: Any) -> None: self._store[key] = data def retrieve(self, key: str) -> Any: return self._store.get(key) class _StubOutputRenderer: @property def format_name(self) -> str: return "text" def render(self, data: Any) -> str: return str(data) class _StubOutputMaterializer: @property def target_type(self) -> str: return "stdout" def materialize(self, content: str, destination: str) -> None: pass class _StubOutputFormat: @property def format_id(self) -> str: return "custom" def serialize(self, data: Any) -> str: return str(data) def deserialize(self, raw: str) -> Any: return raw class _StubValidationRunner: @property def runner_name(self) -> str: return "stub_runner" def validate(self, target: Any) -> list[Any]: return [] class _StubValidationRuleProvider: @property def provider_name(self) -> str: return "stub_provider" def get_rules(self) -> list[Any]: return [] class _StubToolProvider: @property def provider_name(self) -> str: return "stub_tools" def list_tools(self) -> list[str]: return ["tool1"] def invoke(self, tool_name: str, arguments: Any) -> Any: return None class _StubToolMiddleware: @property def middleware_name(self) -> str: return "stub_mw" def before_invoke(self, tool_name: str, arguments: Any) -> dict[str, Any]: return dict(arguments) def after_invoke(self, tool_name: str, result: Any) -> Any: return result class _StubSkillProvider: @property def provider_name(self) -> str: return "stub_skills" def list_skills(self) -> list[str]: return ["skill1"] def get_skill(self, skill_name: str) -> Any: return None class _StubSkillTemplate: @property def template_name(self) -> str: return "stub_template" def render(self, context: Any) -> str: return "rendered" class _StubResourceResolver: @property def resolver_name(self) -> str: return "stub_resolver" def resolve(self, uri: str) -> Any: return None class _StubResourceTypeHandler: @property def handled_type(self) -> str: return "file" def can_handle(self, resource_type: str) -> bool: return resource_type == "file" def load(self, uri: str) -> Any: return None class _StubA2ATransport: @property def transport_name(self) -> str: return "stub_transport" def send(self, destination: str, payload: Any) -> None: pass def receive(self) -> Any: return None class _StubA2AExtensionMethod: @property def method_name(self) -> str: return "stub_method" def execute(self, params: Any) -> Any: return None class _StubEventHandler: @property def handler_name(self) -> str: return "stub_handler" def handle(self, event: Any) -> None: pass class _StubEventFilter: @property def filter_name(self) -> str: return "stub_filter" def should_propagate(self, event: Any) -> bool: return True class _StubConfigSource: @property def source_name(self) -> str: return "stub_source" def load(self) -> dict[str, Any]: return {} class _StubConfigValidator: @property def validator_name(self) -> str: return "stub_validator" def validate(self, config: Any) -> list[str]: return [] class _StubScopeChainResolver: @property def resolver_name(self) -> str: return "stub_scope_resolver" def can_resolve(self, name: str) -> bool: return name.startswith("stub:") def resolve(self, name: str, context: Any) -> str | None: if self.can_resolve(name): return f"resolved_{name[5:]}" return None class _StubSafetyGuardrail: @property def guardrail_name(self) -> str: return "stub_guardrail" def check(self, action: Any) -> bool: return True def explain(self) -> str: return "Always allows" # --- Context Strategy --- @given("a concrete ContextStrategyExtension implementation") def step_concrete_ctx_strategy(context: Context) -> None: context.ep_impl = _StubContextStrategy() @then("it should be recognized as a ContextStrategyExtension instance") def step_isinstance_ctx_strategy(context: Context) -> None: assert isinstance(context.ep_impl, ContextStrategyExtension) @then("calling its protocol methods should succeed") def step_call_ctx_strategy_methods(context: Context) -> None: impl = context.ep_impl assert impl.name == "stub" assert impl.can_handle({}) == 1.0 assert impl.assemble([], None) == [] # --- Pipeline Component --- @given("a concrete ContextPipelineComponentExtension implementation") def step_concrete_pipeline(context: Context) -> None: context.ep_impl = _StubPipelineComponent() @then("it should be recognized as a ContextPipelineComponentExtension instance") def step_isinstance_pipeline(context: Context) -> None: assert isinstance(context.ep_impl, ContextPipelineComponentExtension) @then("calling its pipeline component methods should succeed") def step_call_pipeline_methods(context: Context) -> None: impl = context.ep_impl assert impl.component_name == "stub_component" assert impl.process(["a", "b"], {}) == ["a", "b"] # --- Storage Backend --- @given("a concrete ContextStorageBackendExtension implementation") def step_concrete_storage(context: Context) -> None: context.ep_impl = _StubStorageBackend() @then("it should be recognized as a ContextStorageBackendExtension instance") def step_isinstance_storage(context: Context) -> None: assert isinstance(context.ep_impl, ContextStorageBackendExtension) @then("calling its storage methods should succeed") def step_call_storage_methods(context: Context) -> None: impl = context.ep_impl assert impl.backend_type == "memory" impl.store("k", "v") assert impl.retrieve("k") == "v" # --- Output Renderer --- @given("a concrete OutputRendererExtension implementation") def step_concrete_renderer(context: Context) -> None: context.ep_impl = _StubOutputRenderer() @then("it should be recognized as an OutputRendererExtension instance") def step_isinstance_renderer(context: Context) -> None: assert isinstance(context.ep_impl, OutputRendererExtension) @then("calling its renderer methods should succeed") def step_call_renderer_methods(context: Context) -> None: impl = context.ep_impl assert impl.format_name == "text" assert impl.render(42) == "42" # --- Output Materializer --- @given("a concrete OutputMaterializerExtension implementation") def step_concrete_materializer(context: Context) -> None: context.ep_impl = _StubOutputMaterializer() @then("it should be recognized as an OutputMaterializerExtension instance") def step_isinstance_materializer(context: Context) -> None: assert isinstance(context.ep_impl, OutputMaterializerExtension) # --- Output Format --- @given("a concrete OutputFormatExtension implementation") def step_concrete_format(context: Context) -> None: context.ep_impl = _StubOutputFormat() @then("it should be recognized as an OutputFormatExtension instance") def step_isinstance_format(context: Context) -> None: assert isinstance(context.ep_impl, OutputFormatExtension) # --- Validation Runner --- @given("a concrete ValidationRunnerExtension implementation") def step_concrete_val_runner(context: Context) -> None: context.ep_impl = _StubValidationRunner() @then("it should be recognized as a ValidationRunnerExtension instance") def step_isinstance_val_runner(context: Context) -> None: assert isinstance(context.ep_impl, ValidationRunnerExtension) # --- Validation Rule Provider --- @given("a concrete ValidationRuleProviderExtension implementation") def step_concrete_val_rule(context: Context) -> None: context.ep_impl = _StubValidationRuleProvider() @then("it should be recognized as a ValidationRuleProviderExtension instance") def step_isinstance_val_rule(context: Context) -> None: assert isinstance(context.ep_impl, ValidationRuleProviderExtension) # --- Tool Provider --- @given("a concrete ToolProviderExtension implementation") def step_concrete_tool_provider(context: Context) -> None: context.ep_impl = _StubToolProvider() @then("it should be recognized as a ToolProviderExtension instance") def step_isinstance_tool_provider(context: Context) -> None: assert isinstance(context.ep_impl, ToolProviderExtension) # --- Tool Middleware --- @given("a concrete ToolMiddlewareExtension implementation") def step_concrete_tool_mw(context: Context) -> None: context.ep_impl = _StubToolMiddleware() @then("it should be recognized as a ToolMiddlewareExtension instance") def step_isinstance_tool_mw(context: Context) -> None: assert isinstance(context.ep_impl, ToolMiddlewareExtension) # --- Skill Provider --- @given("a concrete SkillProviderExtension implementation") def step_concrete_skill_provider(context: Context) -> None: context.ep_impl = _StubSkillProvider() @then("it should be recognized as a SkillProviderExtension instance") def step_isinstance_skill_provider(context: Context) -> None: assert isinstance(context.ep_impl, SkillProviderExtension) # --- Skill Template --- @given("a concrete SkillTemplateExtension implementation") def step_concrete_skill_template(context: Context) -> None: context.ep_impl = _StubSkillTemplate() @then("it should be recognized as a SkillTemplateExtension instance") def step_isinstance_skill_template(context: Context) -> None: assert isinstance(context.ep_impl, SkillTemplateExtension) # --- Resource Resolver --- @given("a concrete ResourceResolverExtension implementation") def step_concrete_res_resolver(context: Context) -> None: context.ep_impl = _StubResourceResolver() @then("it should be recognized as a ResourceResolverExtension instance") def step_isinstance_res_resolver(context: Context) -> None: assert isinstance(context.ep_impl, ResourceResolverExtension) # --- Resource Type Handler --- @given("a concrete ResourceTypeHandlerExtension implementation") def step_concrete_res_handler(context: Context) -> None: context.ep_impl = _StubResourceTypeHandler() @then("it should be recognized as a ResourceTypeHandlerExtension instance") def step_isinstance_res_handler(context: Context) -> None: assert isinstance(context.ep_impl, ResourceTypeHandlerExtension) # --- A2A Transport --- @given("a concrete A2ATransportExtension implementation") def step_concrete_a2a_transport(context: Context) -> None: context.ep_impl = _StubA2ATransport() @then("it should be recognized as an A2ATransportExtension instance") def step_isinstance_a2a_transport(context: Context) -> None: assert isinstance(context.ep_impl, A2ATransportExtension) # --- A2A Extension Method --- @given("a concrete A2AExtensionMethodExtension implementation") def step_concrete_a2a_method(context: Context) -> None: context.ep_impl = _StubA2AExtensionMethod() @then("it should be recognized as an A2AExtensionMethodExtension instance") def step_isinstance_a2a_method(context: Context) -> None: assert isinstance(context.ep_impl, A2AExtensionMethodExtension) # --- Event Handler --- @given("a concrete EventHandlerExtension implementation") def step_concrete_event_handler(context: Context) -> None: context.ep_impl = _StubEventHandler() @then("it should be recognized as an EventHandlerExtension instance") def step_isinstance_event_handler(context: Context) -> None: assert isinstance(context.ep_impl, EventHandlerExtension) # --- Event Filter --- @given("a concrete EventFilterExtension implementation") def step_concrete_event_filter(context: Context) -> None: context.ep_impl = _StubEventFilter() @then("it should be recognized as an EventFilterExtension instance") def step_isinstance_event_filter(context: Context) -> None: assert isinstance(context.ep_impl, EventFilterExtension) # --- Config Source --- @given("a concrete ConfigSourceExtension implementation") def step_concrete_config_source(context: Context) -> None: context.ep_impl = _StubConfigSource() @then("it should be recognized as a ConfigSourceExtension instance") def step_isinstance_config_source(context: Context) -> None: assert isinstance(context.ep_impl, ConfigSourceExtension) # --- Config Validator --- @given("a concrete ConfigValidatorExtension implementation") def step_concrete_config_validator(context: Context) -> None: context.ep_impl = _StubConfigValidator() @then("it should be recognized as a ConfigValidatorExtension instance") def step_isinstance_config_validator(context: Context) -> None: assert isinstance(context.ep_impl, ConfigValidatorExtension) # --- Safety Guardrail --- @given("a concrete SafetyGuardrailExtension implementation") def step_concrete_safety(context: Context) -> None: context.ep_impl = _StubSafetyGuardrail() @then("it should be recognized as a SafetyGuardrailExtension instance") def step_isinstance_safety(context: Context) -> None: assert isinstance(context.ep_impl, SafetyGuardrailExtension) @then("calling its guardrail methods should succeed") def step_call_safety_methods(context: Context) -> None: impl = context.ep_impl assert impl.guardrail_name == "stub_guardrail" assert impl.check("some_action") is True assert impl.explain() == "Always allows" # --- Scope Chain Resolver --- @given("a concrete ScopeChainResolverExtension implementation") def step_concrete_scope_resolver(context: Context) -> None: context.ep_impl = _StubScopeChainResolver() @then("it should be recognized as a ScopeChainResolverExtension instance") def step_isinstance_scope_resolver(context: Context) -> None: assert isinstance(context.ep_impl, ScopeChainResolverExtension) @then("calling its scope chain resolver methods should succeed") def step_call_scope_resolver_methods(context: Context) -> None: impl = context.ep_impl assert impl.resolver_name == "stub_scope_resolver" assert impl.can_resolve("stub:test") is True assert impl.can_resolve("other:test") is False assert impl.resolve("stub:test", {}) == "resolved_test" assert impl.resolve("other:test", {}) is None