fix(resource-registry): use full inheritance chain in contextual binding type compatibility
CI / lint (pull_request) Successful in 27s
CI / typecheck (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 32s
CI / security (pull_request) Successful in 59s
CI / build (pull_request) Successful in 29s
CI / helm (pull_request) Successful in 23s
CI / unit_tests (pull_request) Successful in 6m58s
CI / e2e_tests (pull_request) Successful in 19m39s
CI / integration_tests (pull_request) Successful in 22m49s
CI / coverage (pull_request) Successful in 10m43s
CI / docker (pull_request) Successful in 1m23s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m57s
CI / lint (pull_request) Successful in 27s
CI / typecheck (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 32s
CI / security (pull_request) Successful in 59s
CI / build (pull_request) Successful in 29s
CI / helm (pull_request) Successful in 23s
CI / unit_tests (pull_request) Successful in 6m58s
CI / e2e_tests (pull_request) Successful in 19m39s
CI / integration_tests (pull_request) Successful in 22m49s
CI / coverage (pull_request) Successful in 10m43s
CI / docker (pull_request) Successful in 1m23s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m57s
Replace the flat `parent_types` membership check in `BindingResolutionService._is_type_compatible` with a call to `self._registry.is_subtype_of()`, which uses `resolve_inheritance_chain()` from `cleveragents.resource.inheritance` to walk the full ADR-042 inheritance chain. Previously, a tool requiring type `container-instance` would fail to bind a `devcontainer-instance` resource because the check only looked at the immediate `parent_types` list rather than traversing the full chain. The fix ensures multi-level inheritance (e.g. `local/special-dev-container` → `devcontainer-instance` → `container-instance`) is correctly resolved for contextual, static, and parameter bindings. Changes: - `binding_resolution_service.py`: `_is_type_compatible` now delegates to `registry.is_subtype_of()`; removed unused `ResourceTypeSpec` import. - `consolidated_binding_resolution.feature`: 4 new scenarios covering 2-level chain, 3-level chain, unrelated-type rejection, and static binding via inheritance chain. - `binding_resolution_steps.py`: mock registry exposes `is_subtype_of()` backed by the real inheritance engine; new step definitions for inheritance chain setup. ISSUES CLOSED: #2929
This commit is contained in:
@@ -261,12 +261,12 @@ Feature: Consolidated Binding Resolution
|
||||
Then a binding ValidationError should be raised mentioning "Ambiguous contextual binding"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _is_type_compatible -- show_type raises NotFoundError (lines 369-370)
|
||||
# _is_type_compatible -- is_subtype_of returns False for unknown type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@type_compat @not_found
|
||||
Scenario: Type compatibility returns False when show_type raises NotFoundError
|
||||
Scenario: Type compatibility returns False for unknown type
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a project "local/typed" with a linked resource:
|
||||
@@ -278,3 +278,80 @@ Feature: Consolidated Binding Resolution
|
||||
When the binding resolution is attempted
|
||||
Then a binding ValidationError should be raised mentioning "No resource of type"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-level inheritance -- contextual binding (ADR-042 fix)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@type_compat @inheritance @multi_level
|
||||
Scenario: Contextual binding resolves via two-level inheritance chain
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a project "local/typed" with a linked resource:
|
||||
| resource_id | type_name | alias |
|
||||
| 01HGZ6FE0AQDYTR4BX00000100 | devcontainer-instance | env |
|
||||
And the resource type "container-instance" inherits from nothing
|
||||
And the resource type "devcontainer-instance" inherits from "container-instance"
|
||||
And a tool "local/runner" with a contextual slot "env" of type "container-instance"
|
||||
When the bindings are resolved
|
||||
Then the binding for slot "env" should have resource_id "01HGZ6FE0AQDYTR4BX00000100"
|
||||
And the binding for slot "env" should have mode "contextual"
|
||||
And the binding for slot "env" should not be deferred
|
||||
|
||||
@type_compat @inheritance @multi_level
|
||||
Scenario: Contextual binding resolves via three-level inheritance chain
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a project "local/typed" with a linked resource:
|
||||
| resource_id | type_name | alias |
|
||||
| 01HGZ6FE0AQDYTR4BX00000101 | local/special-dev-container | env |
|
||||
And the resource type "container-instance" inherits from nothing
|
||||
And the resource type "devcontainer-instance" inherits from "container-instance"
|
||||
And the resource type "local/special-dev-container" inherits from "devcontainer-instance"
|
||||
And a tool "local/runner" with a contextual slot "env" of type "container-instance"
|
||||
When the bindings are resolved
|
||||
Then the binding for slot "env" should have resource_id "01HGZ6FE0AQDYTR4BX00000101"
|
||||
And the binding for slot "env" should have mode "contextual"
|
||||
And the binding for slot "env" should not be deferred
|
||||
|
||||
@type_compat @inheritance @unrelated_type
|
||||
Scenario: Contextual binding rejects unrelated type even with inheritance present
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a project "local/typed" with a linked resource:
|
||||
| resource_id | type_name | alias |
|
||||
| 01HGZ6FE0AQDYTR4BX00000102 | fs-directory | dir |
|
||||
And the resource type "container-instance" inherits from nothing
|
||||
And a tool "local/runner" with a contextual slot "env" of type "container-instance"
|
||||
And the slot "env" is required
|
||||
When the binding resolution is attempted
|
||||
Then a binding ValidationError should be raised mentioning "No resource of type"
|
||||
|
||||
@type_compat @inheritance @static_binding
|
||||
Scenario: Static binding resolves via two-level inheritance chain
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a resource "local/my-devcontainer" of type "devcontainer-instance" in the registry
|
||||
And the resource type "container-instance" inherits from nothing
|
||||
And the resource type "devcontainer-instance" inherits from "container-instance"
|
||||
And a tool "local/runner" with a static slot "env" bound to "local/my-devcontainer" of type "container-instance"
|
||||
And a project "local/proj" with no linked resources
|
||||
When the bindings are resolved
|
||||
Then the binding for slot "env" should have mode "static"
|
||||
And the binding for slot "env" should not be deferred
|
||||
|
||||
@type_compat @inheritance @parameter_binding
|
||||
Scenario: Parameter binding resolves via two-level inheritance chain
|
||||
Given a mock resource registry
|
||||
And a binding resolution service using the registry
|
||||
Given a named resource "01HGZ6FE0AQDYTR4BX00000103" called "local/my-devcontainer" of type "devcontainer-instance" in the registry
|
||||
And the resource type "container-instance" inherits from nothing
|
||||
And the resource type "devcontainer-instance" inherits from "container-instance"
|
||||
And a tool "local/runner" with a parameter slot "env" of type "container-instance"
|
||||
And a project "local/proj" with no linked resources
|
||||
When the bindings are resolved with invocation params:
|
||||
| slot | ref |
|
||||
| env | local/my-devcontainer |
|
||||
Then the binding for slot "env" should have mode "parameter"
|
||||
And the binding for slot "env" should not be deferred
|
||||
|
||||
@@ -75,6 +75,8 @@ def _make_type_spec(
|
||||
|
||||
@given("a mock resource registry")
|
||||
def step_mock_registry(context: Context) -> None:
|
||||
from cleveragents.resource.inheritance import is_subtype_of as _is_subtype_of
|
||||
|
||||
registry = MagicMock()
|
||||
registry._resources: dict[str, Resource] = {}
|
||||
registry._types: dict[str, ResourceTypeSpec] = {
|
||||
@@ -85,6 +87,13 @@ def step_mock_registry(context: Context) -> None:
|
||||
),
|
||||
"fs-file": _make_type_spec("fs-file", parent_types=[]),
|
||||
}
|
||||
# Inheritance map used by is_subtype_of: maps type name -> inherits value.
|
||||
# Populated by step helpers when multi-level inheritance is needed.
|
||||
registry._inherits: dict[str, str | None] = {
|
||||
"git-checkout": None,
|
||||
"fs-directory": None,
|
||||
"fs-file": None,
|
||||
}
|
||||
|
||||
def show_resource(name_or_id: str) -> Resource:
|
||||
res = registry._resources.get(name_or_id)
|
||||
@@ -104,8 +113,17 @@ def step_mock_registry(context: Context) -> None:
|
||||
)
|
||||
return spec
|
||||
|
||||
def is_subtype_of_mock(type_name: str, ancestor_name: str) -> bool:
|
||||
"""Walk the full inheritance chain using the _inherits map."""
|
||||
type_registry = {
|
||||
t: {"inherits": registry._inherits.get(t)}
|
||||
for t in set(list(registry._types.keys()) + list(registry._inherits.keys()))
|
||||
}
|
||||
return _is_subtype_of(type_name, ancestor_name, type_registry)
|
||||
|
||||
registry.show_resource = show_resource
|
||||
registry.show_type = show_type
|
||||
registry.is_subtype_of = is_subtype_of_mock
|
||||
context.mock_registry = registry
|
||||
|
||||
|
||||
@@ -223,6 +241,10 @@ def step_type_parent(context: Context, tname: str, parent: str) -> None:
|
||||
if parent not in parents:
|
||||
parents.append(parent)
|
||||
context.mock_registry._types[tname] = _make_type_spec(tname, parent_types=parents)
|
||||
# Also update the _inherits map so is_subtype_of() walks the chain correctly.
|
||||
# Use the first parent as the direct ancestor (single-inheritance model).
|
||||
if parents:
|
||||
context.mock_registry._inherits[tname] = parents[0]
|
||||
|
||||
|
||||
@given('the resource type "{tname}" has no parent_types')
|
||||
@@ -432,3 +454,29 @@ def _find_result(context: Context, slot_name: str) -> BindingResult:
|
||||
if result.slot_name == slot_name:
|
||||
return result
|
||||
raise AssertionError(f"No binding result for slot '{slot_name}'")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Given: inheritance chain setup for multi-level tests
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@given('the resource type "{tname}" inherits from nothing')
|
||||
def step_type_inherits_nothing(context: Context, tname: str) -> None:
|
||||
"""Register a root type with no parent in the inheritance chain."""
|
||||
if tname not in context.mock_registry._types:
|
||||
context.mock_registry._types[tname] = _make_type_spec(tname, parent_types=[])
|
||||
context.mock_registry._inherits[tname] = None
|
||||
|
||||
|
||||
@given('the resource type "{tname}" inherits from "{parent}"')
|
||||
def step_type_inherits_from(context: Context, tname: str, parent: str) -> None:
|
||||
"""Register a type that directly inherits from *parent*."""
|
||||
if tname not in context.mock_registry._types:
|
||||
context.mock_registry._types[tname] = _make_type_spec(
|
||||
tname, parent_types=[parent]
|
||||
)
|
||||
# Ensure parent is also in the inherits map (as a root if not already set).
|
||||
if parent not in context.mock_registry._inherits:
|
||||
context.mock_registry._inherits[parent] = None
|
||||
context.mock_registry._inherits[tname] = parent
|
||||
|
||||
@@ -43,7 +43,6 @@ from cleveragents.domain.models.core.project import (
|
||||
)
|
||||
from cleveragents.domain.models.core.resource import Resource
|
||||
from cleveragents.domain.models.core.resource_slot import BindingResult
|
||||
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
|
||||
from cleveragents.domain.models.core.tool import (
|
||||
BindingMode,
|
||||
ResourceSlot,
|
||||
@@ -60,7 +59,7 @@ class BindingResolutionService:
|
||||
look-ups) and the *project* that owns the plan being activated.
|
||||
|
||||
The registry is injected as a protocol-compatible object exposing
|
||||
``show_resource`` and ``show_type`` methods (matching
|
||||
``show_resource`` and ``is_subtype_of`` methods (matching
|
||||
:class:`~cleveragents.application.services.resource_registry_service.ResourceRegistryService`).
|
||||
"""
|
||||
|
||||
@@ -69,7 +68,7 @@ class BindingResolutionService:
|
||||
|
||||
Args:
|
||||
resource_registry: Object with ``show_resource(name_or_id)``
|
||||
and ``show_type(name)`` methods.
|
||||
and ``is_subtype_of(type_name, ancestor_name)`` methods.
|
||||
"""
|
||||
self._registry = resource_registry
|
||||
|
||||
@@ -355,21 +354,12 @@ class BindingResolutionService:
|
||||
) -> bool:
|
||||
"""Check if *actual_type* satisfies *required_type*.
|
||||
|
||||
Exact match always passes. If *actual_type* declares
|
||||
*required_type* in its ``parent_types`` hierarchy, the
|
||||
check also passes (sub-type relationship).
|
||||
Exact match always passes. Uses the full inheritance chain via
|
||||
is_subtype_of() on the registry so that multi-level
|
||||
inheritance (e.g. devcontainer-instance → container-instance)
|
||||
is correctly resolved (ADR-042 §Tool Binding Polymorphism).
|
||||
"""
|
||||
if actual_type == required_type:
|
||||
return True
|
||||
|
||||
try:
|
||||
spec: ResourceTypeSpec = self._registry.show_type(
|
||||
actual_type,
|
||||
)
|
||||
except NotFoundError:
|
||||
return False
|
||||
|
||||
return required_type in spec.parent_types
|
||||
return self._registry.is_subtype_of(actual_type, required_type)
|
||||
|
||||
def _check_type_compatibility(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user