"""Step definitions for component_resolver.feature. Tests the ComponentResolver's 3-level scope chain resolution: plan > project > global. """ from __future__ import annotations from typing import Any, Protocol, runtime_checkable from behave import given, then, when # type: ignore[import-untyped] from cleveragents.application.services.component_resolver import ( ComponentNotFoundError, ComponentRegistrationError, ComponentResolver, ScopeLevel, ) # --------------------------------------------------------------------------- # Test Protocol and implementations # --------------------------------------------------------------------------- @runtime_checkable class _TestProtocol(Protocol): """A simple test protocol for scope chain testing.""" def execute(self) -> str: ... class _GlobalDefault: """Global default implementation.""" def execute(self) -> str: return "global" class _ProjectOverride: """Project-level override implementation.""" def execute(self) -> str: return "project" class _PlanOverride: """Plan-level override implementation.""" def execute(self) -> str: return "plan" # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("a fresh ComponentResolver instance") def step_fresh_resolver(context: Any) -> None: context.resolver = ComponentResolver() context.test_protocol = _TestProtocol context.global_impl = _GlobalDefault() context.project_impl = _ProjectOverride() context.plan_impl = _PlanOverride() context.error = None context.resolution_result = None context.resolution_result_2 = None @given("a global default implementation for a test protocol") def step_register_global_default(context: Any) -> None: resolver: ComponentResolver = context.resolver resolver.register_global(context.test_protocol, context.global_impl) @given('a project-level override for project "{project_id}" for the test protocol') def step_register_project_override(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver resolver.register_project(project_id, context.test_protocol, context.project_impl) @given('a plan-level override for plan "{plan_id}" for the test protocol') def step_register_plan_override(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver resolver.register_plan(plan_id, context.test_protocol, context.plan_impl) @given("an extensions config mapping with a known module path") def step_extensions_config(context: Any) -> None: # Use a real module path that exists in the codebase context.extensions_config = { "strategy_selector": ( "cleveragents.application.services.acms_pipeline:ConfidenceWeightedSelector" ), } from cleveragents.application.services.acms_service import StrategySelector context.type_registry = { "strategy_selector": StrategySelector, } context.ext_component_type = StrategySelector @given("a plan metadata mapping with a known module path") def step_plan_metadata_config(context: Any) -> None: context.plan_metadata = { "strategy_selector": ( "cleveragents.application.services.acms_pipeline:ConfidenceWeightedSelector" ), } from cleveragents.application.services.acms_service import StrategySelector context.type_registry = { "strategy_selector": StrategySelector, } context.ext_component_type = StrategySelector # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when("I resolve the test protocol with no plan or project context") def step_resolve_no_context(context: Any) -> None: resolver: ComponentResolver = context.resolver context.resolution_result = resolver.resolve(context.test_protocol) @when("I attempt to resolve an unregistered protocol") def step_resolve_unregistered(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.resolve(context.test_protocol) except ComponentNotFoundError as exc: context.error = exc @when('I resolve the test protocol for project "{project_id}"') def step_resolve_for_project(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver context.resolution_result = resolver.resolve( context.test_protocol, project_id=project_id ) @when('I resolve the test protocol for plan "{plan_id}" and project "{project_id}"') def step_resolve_for_plan_and_project( context: Any, plan_id: str, project_id: str ) -> None: resolver: ComponentResolver = context.resolver context.resolution_result = resolver.resolve( context.test_protocol, plan_id=plan_id, project_id=project_id ) @when('I resolve the test protocol for plan "{plan_id}"') def step_resolve_for_plan(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver context.resolution_result = resolver.resolve(context.test_protocol, plan_id=plan_id) @when("I resolve the test protocol twice with no context") def step_resolve_twice(context: Any) -> None: resolver: ComponentResolver = context.resolver context.resolution_result = resolver.resolve(context.test_protocol) context.resolution_result_2 = resolver.resolve(context.test_protocol) @when('I remove the project implementation for "{project_id}" and the test protocol') def step_remove_project(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver context.removal_result = resolver.remove_project(project_id, context.test_protocol) @when('I remove the plan implementation for "{plan_id}" and the test protocol') def step_remove_plan(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver context.removal_result = resolver.remove_plan(plan_id, context.test_protocol) @when("I remove the global implementation for the test protocol") def step_remove_global(context: Any) -> None: resolver: ComponentResolver = context.resolver context.removal_result = resolver.remove_global(context.test_protocol) @when("I attempt to register a project override with empty project_id") def step_register_empty_project_id(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.register_project("", context.test_protocol, context.global_impl) except ComponentRegistrationError as exc: context.error = exc @when("I attempt to register a plan override with empty plan_id") def step_register_empty_plan_id(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.register_plan("", context.test_protocol, context.global_impl) except ComponentRegistrationError as exc: context.error = exc @when( 'I register a project-level override for project "{project_id}" for the test protocol' ) def step_when_register_project_override(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver resolver.register_project(project_id, context.test_protocol, context.project_impl) @when('I load project extensions for project "{project_id}"') def step_load_project_extensions(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver context.ext_project_id = project_id resolver.load_project_extensions( project_id, context.extensions_config, context.type_registry ) @when('I load plan extensions for plan "{plan_id}"') def step_load_plan_extensions(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver context.ext_plan_id = plan_id resolver.load_plan_extensions(plan_id, context.plan_metadata, context.type_registry) @when("I clear the resolver") def step_clear_resolver(context: Any) -> None: resolver: ComponentResolver = context.resolver resolver.clear() @when("I register an extension point for the test protocol") def step_register_extension_point(context: Any) -> None: resolver: ComponentResolver = context.resolver resolver.register_extension_point( context.test_protocol, description="A test extension point", category="test", ) @when("I attempt to register None as a global implementation") def step_register_none_global(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.register_global(context.test_protocol, None) # type: ignore[arg-type] except ComponentRegistrationError as exc: context.error = exc @when("I attempt to register None as a project implementation") def step_register_none_project(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.register_project("p", context.test_protocol, None) # type: ignore[arg-type] except ComponentRegistrationError as exc: context.error = exc @when("I attempt to register None as a plan implementation") def step_register_none_plan(context: Any) -> None: resolver: ComponentResolver = context.resolver try: resolver.register_plan("p", context.test_protocol, None) # type: ignore[arg-type] except ComponentRegistrationError as exc: context.error = exc @when('I attempt to import a component from "{module_path}"') def step_import_disallowed(context: Any, module_path: str) -> None: resolver: ComponentResolver = context.resolver try: context.imported_component = resolver._import_component(module_path) except (ComponentRegistrationError, ImportError, AttributeError) as exc: context.error = exc @when('I import a component from "{module_path}"') def step_import_component(context: Any, module_path: str) -> None: resolver: ComponentResolver = context.resolver context.imported_component = resolver._import_component(module_path) # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then("the resolved component should be the global default") def step_assert_global_default(context: Any) -> None: assert context.resolution_result is not None assert context.resolution_result.component is context.global_impl @then("the resolved component should be the project override") def step_assert_project_override(context: Any) -> None: assert context.resolution_result is not None assert context.resolution_result.component is context.project_impl @then("the resolved component should be the plan override") def step_assert_plan_override(context: Any) -> None: assert context.resolution_result is not None assert context.resolution_result.component is context.plan_impl @then('the resolution scope should be "{scope}"') def step_assert_scope(context: Any, scope: str) -> None: assert context.resolution_result is not None expected = ScopeLevel(scope) assert context.resolution_result.scope == expected, ( f"Expected scope {expected}, got {context.resolution_result.scope}" ) @then("a ComponentNotFoundError should be raised") def step_assert_not_found_error(context: Any) -> None: assert isinstance(context.error, ComponentNotFoundError), ( f"Expected ComponentNotFoundError, got {type(context.error)}" ) @then("a ComponentRegistrationError should be raised") def step_assert_registration_error(context: Any) -> None: assert isinstance(context.error, ComponentRegistrationError), ( f"Expected ComponentRegistrationError, got {type(context.error)}" ) @then('the imported component class should be "{class_name}"') def step_assert_imported_component_class(context: Any, class_name: str) -> None: assert type(context.imported_component).__name__ == class_name @then("has_global should return True for the test protocol") def step_assert_has_global_true(context: Any) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_global(context.test_protocol) is True @then("has_global should return False for the test protocol") def step_assert_has_global_false(context: Any) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_global(context.test_protocol) is False @then('has_project should return True for "{project_id}" and the test protocol') def step_assert_has_project_true(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_project(project_id, context.test_protocol) is True @then('has_project should return False for "{project_id}" and the test protocol') def step_assert_has_project_false(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_project(project_id, context.test_protocol) is False @then('has_plan should return True for "{plan_id}" and the test protocol') def step_assert_has_plan_true(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_plan(plan_id, context.test_protocol) is True @then('has_plan should return False for "{plan_id}" and the test protocol') def step_assert_has_plan_false(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_plan(plan_id, context.test_protocol) is False @then("the cache size should be {size:d}") def step_assert_cache_size(context: Any, size: int) -> None: resolver: ComponentResolver = context.resolver assert resolver.cache_size() == size, ( f"Expected cache size {size}, got {resolver.cache_size()}" ) @then("both resolutions should return the same result") def step_assert_same_result(context: Any) -> None: assert context.resolution_result is context.resolution_result_2 @then("the extension should be registered at the project scope") def step_assert_extension_at_project(context: Any) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_project(context.ext_project_id, context.ext_component_type) @then("the extension should be registered at the plan scope") def step_assert_extension_at_plan(context: Any) -> None: resolver: ComponentResolver = context.resolver assert resolver.has_plan(context.ext_plan_id, context.ext_component_type) @then("list_global_types should include the test protocol name") def step_assert_list_global(context: Any) -> None: resolver: ComponentResolver = context.resolver names = resolver.list_global_types() assert "_TestProtocol" in names, f"Expected _TestProtocol in {names}" @then('list_project_types for "{project_id}" should include the test protocol name') def step_assert_list_project(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver names = resolver.list_project_types(project_id) assert "_TestProtocol" in names, f"Expected _TestProtocol in {names}" @then('list_plan_types for "{plan_id}" should include the test protocol name') def step_assert_list_plan(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver names = resolver.list_plan_types(plan_id) assert "_TestProtocol" in names, f"Expected _TestProtocol in {names}" @then("list_extension_points should include the test protocol") def step_assert_list_extension_points(context: Any) -> None: resolver: ComponentResolver = context.resolver points = resolver.list_extension_points() assert len(points) == 1 assert points[0].component_type is context.test_protocol # --------------------------------------------------------------------------- # Edge-case steps for coverage improvement # --------------------------------------------------------------------------- @given("an extensions config with an unknown extension type name") def step_ext_config_unknown_type(context: Any) -> None: context.edge_extensions_config = {"nonexistent_extension": "some:Module"} context.edge_type_registry = {} @given("an extensions config with a bad module path") def step_ext_config_bad_path(context: Any) -> None: from cleveragents.application.services.acms_service import StrategySelector context.edge_extensions_config = { "strategy_selector": "cleveragents.nonexistent_module:Foo" } context.edge_type_registry = {"strategy_selector": StrategySelector} @given("a plan metadata with an unknown extension type name") def step_plan_meta_unknown_type(context: Any) -> None: context.edge_plan_metadata = {"nonexistent_extension": "some:Module"} context.edge_type_registry = {} @given("a plan metadata with a bad module path") def step_plan_meta_bad_path(context: Any) -> None: from cleveragents.application.services.acms_service import StrategySelector context.edge_plan_metadata = { "strategy_selector": "cleveragents.nonexistent_module:Foo" } context.edge_type_registry = {"strategy_selector": StrategySelector} @when('I load project extensions with unknown type for project "{project_id}"') def step_load_proj_ext_unknown(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver context.loaded = resolver.load_project_extensions( project_id, context.edge_extensions_config, context.edge_type_registry ) @when('I load project extensions with bad path for project "{project_id}"') def step_load_proj_ext_bad_path(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver context.loaded = resolver.load_project_extensions( project_id, context.edge_extensions_config, context.edge_type_registry ) @when('I load plan extensions with unknown type for plan "{plan_id}"') def step_load_plan_ext_unknown(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver context.loaded = resolver.load_plan_extensions( plan_id, context.edge_plan_metadata, context.edge_type_registry ) @when('I load plan extensions with bad path for plan "{plan_id}"') def step_load_plan_ext_bad_path(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver context.loaded = resolver.load_plan_extensions( plan_id, context.edge_plan_metadata, context.edge_type_registry ) @when("I explicitly invalidate the cache") def step_explicit_invalidate(context: Any) -> None: resolver: ComponentResolver = context.resolver resolver.invalidate_cache() @then("the loaded extensions list should be empty") def step_assert_loaded_empty(context: Any) -> None: assert context.loaded == [], f"Expected empty, got {context.loaded}" @then("the component resolver removal result should be False") def step_assert_removal_false(context: Any) -> None: assert context.removal_result is False, ( f"Expected removal_result to be False, got {context.removal_result!r}" ) @then('list_project_types for "{project_id}" should be empty') def step_assert_project_types_empty(context: Any, project_id: str) -> None: resolver: ComponentResolver = context.resolver names = resolver.list_project_types(project_id) assert names == [], f"Expected empty, got {names}" @then('list_plan_types for "{plan_id}" should be empty') def step_assert_plan_types_empty(context: Any, plan_id: str) -> None: resolver: ComponentResolver = context.resolver names = resolver.list_plan_types(plan_id) assert names == [], f"Expected empty, got {names}"