137d040c4d
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 22s
CI / quality (pull_request) Successful in 56s
CI / lint (pull_request) Successful in 3m21s
CI / typecheck (pull_request) Successful in 4m0s
CI / security (pull_request) Successful in 4m19s
CI / unit_tests (pull_request) Successful in 9m32s
CI / docker (pull_request) Successful in 1m22s
CI / coverage (pull_request) Successful in 12m27s
CI / e2e_tests (pull_request) Successful in 20m3s
CI / integration_tests (pull_request) Successful in 21m20s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 54m56s
Add a production skeleton compressor that re-renders inherited fragments to overview depths via the UKO detail-level map chain, fits the result within the configured skeleton budget, and wires the pipeline default to the new compressor. Address prior review feedback by extracting the render visitors into a dedicated module, restoring projected metadata to native runtime types, constraining builtin component resolution with an allowlist, and keeping child-context inheritance compatible with CRP context fragments for the Robot integration path. Reproduced the Forgejo lint job in a clean python:3.13-slim container with the CI commands All checks passed! and 1740 files already formatted; both passed, so the earlier lint failure appears to have been transient runner behavior rather than a source-level defect. ISSUES CLOSED: #919
361 lines
16 KiB
Gherkin
361 lines
16 KiB
Gherkin
@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
|