@extensibility @component_resolver Feature: Pluggable Component Resolution with Scope Chain As a CleverAgents developer I want a ComponentResolver that resolves pluggable components through a 3-level scope chain (plan > project > global) So that extension points can be consistently overridden at different scopes # --------------------------------------------------------------------------- # Global default resolution # --------------------------------------------------------------------------- @scope_chain @global Scenario: Global default resolution when no overrides exist Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol with no plan or project context Then the resolved component should be the global default And the resolution scope should be "global" @scope_chain @global Scenario: Global default registered at startup is available Given a fresh ComponentResolver instance And a global default implementation for a test protocol Then has_global should return True for the test protocol @scope_chain @global Scenario: Resolving unregistered component raises error Given a fresh ComponentResolver instance When I attempt to resolve an unregistered protocol Then a ComponentNotFoundError should be raised # --------------------------------------------------------------------------- # Project-level override # --------------------------------------------------------------------------- @scope_chain @project Scenario: Project override takes precedence over global Given a fresh ComponentResolver instance And a global default implementation for a test protocol And a project-level override for project "my-project" for the test protocol When I resolve the test protocol for project "my-project" Then the resolved component should be the project override And the resolution scope should be "project" @scope_chain @project Scenario: Project without override falls through to global Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol for project "other-project" Then the resolved component should be the global default And the resolution scope should be "global" @scope_chain @project Scenario: Project override registered via register_project Given a fresh ComponentResolver instance And a project-level override for project "my-project" for the test protocol Then has_project should return True for "my-project" and the test protocol @scope_chain @project Scenario: Empty project_id raises error Given a fresh ComponentResolver instance When I attempt to register a project override with empty project_id Then a ComponentRegistrationError should be raised # --------------------------------------------------------------------------- # Plan-level override (highest priority) # --------------------------------------------------------------------------- @scope_chain @plan Scenario: Plan override takes highest precedence Given a fresh ComponentResolver instance And a global default implementation for a test protocol And a project-level override for project "my-project" for the test protocol And a plan-level override for plan "plan-001" for the test protocol When I resolve the test protocol for plan "plan-001" and project "my-project" Then the resolved component should be the plan override And the resolution scope should be "plan" @scope_chain @plan Scenario: Plan without override falls through to project then global Given a fresh ComponentResolver instance And a global default implementation for a test protocol And a project-level override for project "my-project" for the test protocol When I resolve the test protocol for plan "plan-002" and project "my-project" Then the resolved component should be the project override And the resolution scope should be "project" @scope_chain @plan Scenario: Empty plan_id raises error Given a fresh ComponentResolver instance When I attempt to register a plan override with empty plan_id Then a ComponentRegistrationError should be raised # --------------------------------------------------------------------------- # Fallthrough on missing scope # --------------------------------------------------------------------------- @scope_chain @fallthrough Scenario: Missing plan and project falls through to global Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol for plan "unknown-plan" and project "unknown-project" Then the resolved component should be the global default And the resolution scope should be "global" @scope_chain @fallthrough Scenario: Only plan registered without global still resolves from plan Given a fresh ComponentResolver instance And a plan-level override for plan "plan-only" for the test protocol When I resolve the test protocol for plan "plan-only" Then the resolved component should be the plan override And the resolution scope should be "plan" # --------------------------------------------------------------------------- # Caching # --------------------------------------------------------------------------- @scope_chain @caching Scenario: Resolution is cached per scope Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol twice with no context Then the cache size should be 1 And both resolutions should return the same result @scope_chain @caching Scenario: Cache is invalidated on new registration Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol with no plan or project context And I register a project-level override for project "p1" for the test protocol Then the cache size should be 0 # --------------------------------------------------------------------------- # Config.toml extension loading # --------------------------------------------------------------------------- @scope_chain @config Scenario: Load project extensions from config mapping Given a fresh ComponentResolver instance And an extensions config mapping with a known module path When I load project extensions for project "ext-project" Then the extension should be registered at the project scope # --------------------------------------------------------------------------- # Plan metadata extension loading # --------------------------------------------------------------------------- @scope_chain @config Scenario: Load plan extensions from metadata mapping Given a fresh ComponentResolver instance And a plan metadata mapping with a known module path When I load plan extensions for plan "ext-plan" Then the extension should be registered at the plan scope # --------------------------------------------------------------------------- # Removal # --------------------------------------------------------------------------- @scope_chain @removal Scenario: Remove global implementation Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I remove the global implementation for the test protocol Then has_global should return False for the test protocol @scope_chain @removal Scenario: Remove project implementation Given a fresh ComponentResolver instance And a project-level override for project "rm-project" for the test protocol When I remove the project implementation for "rm-project" and the test protocol Then has_project should return False for "rm-project" and the test protocol @scope_chain @removal Scenario: Remove plan implementation Given a fresh ComponentResolver instance And a plan-level override for plan "rm-plan" for the test protocol When I remove the plan implementation for "rm-plan" and the test protocol Then has_plan should return False for "rm-plan" and the test protocol # --------------------------------------------------------------------------- # Introspection # --------------------------------------------------------------------------- @scope_chain @introspection Scenario: List global types Given a fresh ComponentResolver instance And a global default implementation for a test protocol Then list_global_types should include the test protocol name @scope_chain @introspection Scenario: List project types Given a fresh ComponentResolver instance And a project-level override for project "intro-project" for the test protocol Then list_project_types for "intro-project" should include the test protocol name @scope_chain @introspection Scenario: List plan types Given a fresh ComponentResolver instance And a plan-level override for plan "intro-plan" for the test protocol Then list_plan_types for "intro-plan" should include the test protocol name # --------------------------------------------------------------------------- # Clear # --------------------------------------------------------------------------- @scope_chain @clear Scenario: Clear removes all registrations Given a fresh ComponentResolver instance And a global default implementation for a test protocol And a project-level override for project "c-project" for the test protocol And a plan-level override for plan "c-plan" for the test protocol When I clear the resolver Then has_global should return False for the test protocol And has_project should return False for "c-project" and the test protocol And has_plan should return False for "c-plan" and the test protocol # --------------------------------------------------------------------------- # Extension point catalog # --------------------------------------------------------------------------- @scope_chain @catalog Scenario: Register and list extension points Given a fresh ComponentResolver instance When I register an extension point for the test protocol Then list_extension_points should include the test protocol # --------------------------------------------------------------------------- # None implementation rejected # --------------------------------------------------------------------------- @scope_chain @validation Scenario: Registering None as global raises error Given a fresh ComponentResolver instance When I attempt to register None as a global implementation Then a ComponentRegistrationError should be raised @scope_chain @validation Scenario: Registering None as project implementation raises error Given a fresh ComponentResolver instance When I attempt to register None as a project implementation Then a ComponentRegistrationError should be raised @scope_chain @validation Scenario: Registering None as plan implementation raises error Given a fresh ComponentResolver instance When I attempt to register None as a plan implementation Then a ComponentRegistrationError should be raised # --------------------------------------------------------------------------- # Security: Module prefix restriction # --------------------------------------------------------------------------- @scope_chain @security Scenario: Import from disallowed module prefix raises error Given a fresh ComponentResolver instance When I attempt to import a component from "evil_module:Hacker" Then a ComponentRegistrationError should be raised @scope_chain @security Scenario: Import with invalid format raises error Given a fresh ComponentResolver instance When I attempt to import a component from "no_colon_here" Then a ComponentRegistrationError should be raised @scope_chain @security Scenario: Import builtin component path resolves through application services Given a fresh ComponentResolver instance When I import a component from "builtin:DepthReductionCompressor" Then the imported component class should be "DepthReductionCompressor" @scope_chain @security Scenario: Import builtin service outside the allowlist is rejected Given a fresh ComponentResolver instance When I attempt to import a component from "builtin:ConfigService" Then a ComponentRegistrationError should be raised # --------------------------------------------------------------------------- # Edge cases: unknown extension names and failed imports # --------------------------------------------------------------------------- @scope_chain @config @edge Scenario: Load project extensions with unknown type name skips gracefully Given a fresh ComponentResolver instance And an extensions config with an unknown extension type name When I load project extensions with unknown type for project "edge-proj" Then the loaded extensions list should be empty @scope_chain @config @edge Scenario: Load project extensions with bad module path logs warning Given a fresh ComponentResolver instance And an extensions config with a bad module path When I load project extensions with bad path for project "bad-proj" Then the loaded extensions list should be empty @scope_chain @config @edge Scenario: Load plan extensions with unknown type name skips gracefully Given a fresh ComponentResolver instance And a plan metadata with an unknown extension type name When I load plan extensions with unknown type for plan "edge-plan" Then the loaded extensions list should be empty @scope_chain @config @edge Scenario: Load plan extensions with bad module path logs warning Given a fresh ComponentResolver instance And a plan metadata with a bad module path When I load plan extensions with bad path for plan "bad-plan" Then the loaded extensions list should be empty # --------------------------------------------------------------------------- # Edge cases: remove from non-existent scope # --------------------------------------------------------------------------- @scope_chain @removal @edge Scenario: Remove project from non-existent project returns False Given a fresh ComponentResolver instance When I remove the project implementation for "nonexistent" and the test protocol Then the component resolver removal result should be False @scope_chain @removal @edge Scenario: Remove plan from non-existent plan returns False Given a fresh ComponentResolver instance When I remove the plan implementation for "nonexistent" and the test protocol Then the component resolver removal result should be False # --------------------------------------------------------------------------- # Edge cases: list types for non-existent scopes # --------------------------------------------------------------------------- @scope_chain @introspection @edge Scenario: List project types for non-existent project returns empty Given a fresh ComponentResolver instance Then list_project_types for "nonexistent" should be empty @scope_chain @introspection @edge Scenario: List plan types for non-existent plan returns empty Given a fresh ComponentResolver instance Then list_plan_types for "nonexistent" should be empty # --------------------------------------------------------------------------- # Edge case: _ScopeRegistry.remove returns False for missing type # --------------------------------------------------------------------------- @scope_chain @removal @edge Scenario: Remove non-existent type from global returns False Given a fresh ComponentResolver instance When I remove the global implementation for the test protocol Then the component resolver removal result should be False # --------------------------------------------------------------------------- # Edge case: invalidate_cache explicitly # --------------------------------------------------------------------------- @scope_chain @caching @edge Scenario: Explicit invalidate_cache clears all cached entries Given a fresh ComponentResolver instance And a global default implementation for a test protocol When I resolve the test protocol with no plan or project context And I explicitly invalidate the cache Then the cache size should be 0