Files
cleveragents-core/features/steps/plugin_extension_points_steps.py
T
freemo 09be821a38
CI / build (push) Successful in 16s
CI / security (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
fix(cli): make version command show actual git commit (#1520)
Implements issue #1520. The version command now displays the actual git commit SHA to help identify which version of the code is running.

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
2026-04-02 21:30:05 +00:00

797 lines
23 KiB
Python

"""Step definitions for plugin_extension_points.feature.
Tests all 30 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,
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 _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"