f6d27de1cd
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 36s
CI / unit_tests (pull_request) Successful in 2m30s
CI / integration_tests (pull_request) Successful in 3m6s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 4m28s
CI / benchmark-regression (pull_request) Successful in 29m56s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 20s
CI / build (push) Successful in 21s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 34s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m38s
CI / integration_tests (push) Successful in 3m12s
CI / docker (push) Successful in 40s
CI / coverage (push) Successful in 4m27s
CI / benchmark-publish (push) Successful in 16m51s
Introduce ComponentResolver with a deterministic 3-level scope chain (plan > project > global) for resolving pluggable Protocol-based components. The resolver supports caching, thread-safety, config.toml extension loading, plan metadata loading, introspection APIs, and security-restricted dynamic imports. Includes 39 BDD scenarios, 9 Robot Framework integration tests, ASV benchmarks, and vulture whitelist updates. 100% code coverage on component_resolver.py; overall coverage at 97.07%. ISSUES CLOSED: #552
205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
"""Helper script for component_resolver.robot end-to-end tests.
|
|
|
|
Each subcommand is a self-contained check that prints a sentinel on success.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
# Ensure local source tree is importable
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.application.services.component_resolver import ( # noqa: E402
|
|
ComponentNotFoundError,
|
|
ComponentRegistrationError,
|
|
ComponentResolver,
|
|
ScopeLevel,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test protocol and implementations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@runtime_checkable
|
|
class _Greeter(Protocol):
|
|
def greet(self) -> str: ...
|
|
|
|
|
|
class _DefaultGreeter:
|
|
def greet(self) -> str:
|
|
return "hello-global"
|
|
|
|
|
|
class _ProjectGreeter:
|
|
def greet(self) -> str:
|
|
return "hello-project"
|
|
|
|
|
|
class _PlanGreeter:
|
|
def greet(self) -> str:
|
|
return "hello-plan"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def global_default_resolution() -> None:
|
|
"""Verify global default resolution."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
result = resolver.resolve(_Greeter)
|
|
if result.scope == ScopeLevel.GLOBAL and result.component.greet() == "hello-global":
|
|
print("component-resolver-global-ok")
|
|
else:
|
|
print(f"FAIL: {result.scope} / {result.component.greet()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def project_override() -> None:
|
|
"""Verify project override takes precedence over global."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
resolver.register_project("proj-1", _Greeter, _ProjectGreeter())
|
|
result = resolver.resolve(_Greeter, project_id="proj-1")
|
|
if (
|
|
result.scope == ScopeLevel.PROJECT
|
|
and result.component.greet() == "hello-project"
|
|
):
|
|
print("component-resolver-project-override-ok")
|
|
else:
|
|
print(f"FAIL: {result.scope} / {result.component.greet()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def plan_override() -> None:
|
|
"""Verify plan override takes highest precedence."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
resolver.register_project("proj-1", _Greeter, _ProjectGreeter())
|
|
resolver.register_plan("plan-1", _Greeter, _PlanGreeter())
|
|
result = resolver.resolve(_Greeter, plan_id="plan-1", project_id="proj-1")
|
|
if result.scope == ScopeLevel.PLAN and result.component.greet() == "hello-plan":
|
|
print("component-resolver-plan-override-ok")
|
|
else:
|
|
print(f"FAIL: {result.scope} / {result.component.greet()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def fallthrough_to_global() -> None:
|
|
"""Verify fallthrough when no plan or project override exists."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
result = resolver.resolve(_Greeter, plan_id="no-plan", project_id="no-project")
|
|
if result.scope == ScopeLevel.GLOBAL and result.component.greet() == "hello-global":
|
|
print("component-resolver-fallthrough-ok")
|
|
else:
|
|
print(f"FAIL: {result.scope} / {result.component.greet()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def missing_component_error() -> None:
|
|
"""Verify ComponentNotFoundError for unregistered components."""
|
|
resolver = ComponentResolver()
|
|
try:
|
|
resolver.resolve(_Greeter)
|
|
print("FAIL: expected ComponentNotFoundError", file=sys.stderr)
|
|
sys.exit(1)
|
|
except ComponentNotFoundError:
|
|
print("component-resolver-not-found-ok")
|
|
|
|
|
|
def caching_behavior() -> None:
|
|
"""Verify resolution caching."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
r1 = resolver.resolve(_Greeter)
|
|
r2 = resolver.resolve(_Greeter)
|
|
if r1 is r2 and resolver.cache_size() == 1:
|
|
print("component-resolver-caching-ok")
|
|
else:
|
|
print(f"FAIL: same={r1 is r2}, cache={resolver.cache_size()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def security_module_prefix() -> None:
|
|
"""Verify module prefix restriction."""
|
|
resolver = ComponentResolver()
|
|
try:
|
|
resolver._import_component("evil_module:Hacker")
|
|
print("FAIL: expected error", file=sys.stderr)
|
|
sys.exit(1)
|
|
except ComponentRegistrationError:
|
|
print("component-resolver-security-ok")
|
|
|
|
|
|
def introspection_apis() -> None:
|
|
"""Verify introspection methods."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
resolver.register_project("p1", _Greeter, _ProjectGreeter())
|
|
resolver.register_plan("pl1", _Greeter, _PlanGreeter())
|
|
g = resolver.list_global_types()
|
|
p = resolver.list_project_types("p1")
|
|
pl = resolver.list_plan_types("pl1")
|
|
if "_Greeter" in g and "_Greeter" in p and "_Greeter" in pl:
|
|
print("component-resolver-introspection-ok")
|
|
else:
|
|
print(f"FAIL: global={g}, project={p}, plan={pl}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def clear_all() -> None:
|
|
"""Verify clear removes all registrations."""
|
|
resolver = ComponentResolver()
|
|
resolver.register_global(_Greeter, _DefaultGreeter())
|
|
resolver.register_project("p1", _Greeter, _ProjectGreeter())
|
|
resolver.register_plan("pl1", _Greeter, _PlanGreeter())
|
|
resolver.clear()
|
|
ok = (
|
|
not resolver.has_global(_Greeter)
|
|
and not resolver.has_project("p1", _Greeter)
|
|
and not resolver.has_plan("pl1", _Greeter)
|
|
)
|
|
if ok:
|
|
print("component-resolver-clear-ok")
|
|
else:
|
|
print("FAIL: clear did not remove all", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main dispatcher
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS: dict[str, Any] = {
|
|
"global-default": global_default_resolution,
|
|
"project-override": project_override,
|
|
"plan-override": plan_override,
|
|
"fallthrough": fallthrough_to_global,
|
|
"not-found": missing_component_error,
|
|
"caching": caching_behavior,
|
|
"security": security_module_prefix,
|
|
"introspection": introspection_apis,
|
|
"clear": clear_all,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
|
|
print(f"Commands: {', '.join(sorted(_COMMANDS))}", file=sys.stderr)
|
|
sys.exit(1)
|
|
cmd = sys.argv[1]
|
|
fn = _COMMANDS.get(cmd)
|
|
if fn is None:
|
|
print(f"Unknown command: {cmd}", file=sys.stderr)
|
|
sys.exit(1)
|
|
fn()
|