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
528 lines
19 KiB
Python
528 lines
19 KiB
Python
"""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}"
|