Files
cleveragents-core/features/steps/plugins_loader_coverage_steps.py
HAL9000 5b6224daa8
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m59s
CI / push-validation (pull_request) Successful in 36s
CI / build (pull_request) Successful in 1m5s
CI / helm (pull_request) Successful in 51s
CI / benchmark-regression (pull_request) Failing after 1m45s
CI / typecheck (pull_request) Successful in 2m23s
CI / quality (pull_request) Successful in 2m13s
CI / security (pull_request) Successful in 2m19s
CI / integration_tests (pull_request) Successful in 4m30s
CI / e2e_tests (pull_request) Successful in 4m42s
CI / unit_tests (pull_request) Successful in 5m54s
CI / docker (pull_request) Successful in 1m44s
CI / coverage (pull_request) Successful in 11m32s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Has started running
CI / helm (push) Successful in 38s
CI / build (push) Successful in 1m6s
CI / push-validation (push) Successful in 24s
CI / lint (push) Successful in 1m16s
CI / quality (push) Successful in 1m29s
CI / typecheck (push) Successful in 1m41s
CI / security (push) Successful in 1m55s
CI / integration_tests (push) Successful in 3m54s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 5m5s
CI / e2e_tests (push) Successful in 5m21s
CI / docker (push) Successful in 1m52s
CI / coverage (push) Successful in 15m41s
CI / status-check (push) Successful in 3s
fix(plugins): prevent arbitrary code execution in PluginLoader.validate_protocol()
- Add guard in TypeError fallback path: raise ProtocolMismatchError when
  issubclass raises TypeError and protocol has no inspectable members,
  preventing silent True return for unverifiable protocols
- Update test scenario descriptions to accurately reflect the new
  implementation (no instantiation occurs at any point)
- Update step definition comments to remove references to old
  instantiation-based code paths

ISSUES CLOSED: #7418
2026-05-08 09:32:29 +00:00

202 lines
7.3 KiB
Python

"""Step definitions for plugins_loader_coverage.feature.
These steps target specific uncovered lines in
cleveragents/infrastructure/plugins/loader.py:
- Lines 203-209: except block in load_from_entry_points when ep.load() fails
- validate_protocol: issubclass succeeds (class satisfies protocol structurally)
- validate_protocol: issubclass raises TypeError for unverifiable protocol
- validate_protocol: issubclass returns False (class missing required members)
"""
from typing import Protocol, runtime_checkable
from unittest.mock import MagicMock, patch
from behave import given, then, when
from cleveragents.infrastructure.plugins.exceptions import ProtocolMismatchError
from cleveragents.infrastructure.plugins.loader import PluginLoader
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the plugin loader module is imported")
def step_plugin_loader_imported(context):
"""Verify the PluginLoader class is importable."""
assert PluginLoader is not None
context.plugin_loader = PluginLoader()
# ---------------------------------------------------------------------------
# Entry-point discovery: failure path (lines 203-209)
# ---------------------------------------------------------------------------
@given("the entry points are mocked with one that raises on load")
def step_mock_failing_entry_point(context):
"""Create a mock entry point whose load() raises an exception."""
failing_ep = MagicMock()
failing_ep.name = "broken_plugin"
failing_ep.value = "cleveragents.domain.models.acms.nonexistent:BrokenClass"
failing_ep.load.side_effect = ImportError("Simulated import failure")
# Mock the entry_points() call to return a selectable object
mock_eps = MagicMock()
mock_eps.select.return_value = [failing_ep]
context.mock_entry_points = mock_eps
context.failing_ep = failing_ep
@when("I call load_from_entry_points")
def step_call_load_from_entry_points(context):
"""Call load_from_entry_points with the mocked entry points."""
with patch(
"cleveragents.infrastructure.plugins.loader.importlib.metadata.entry_points",
return_value=context.mock_entry_points,
):
context.discovered = context.plugin_loader.load_from_entry_points()
@then("the plugin result should be an empty descriptor list")
def step_verify_empty_descriptors(context):
"""The failing entry point should have been skipped."""
assert context.discovered == [], f"Expected empty list, got {context.discovered}"
@then("the failed entry point should have been logged as a warning")
def step_verify_warning_logged(context):
"""Verify the entry point was attempted (load was called)."""
context.failing_ep.load.assert_called_once()
# ---------------------------------------------------------------------------
# validate_protocol: issubclass succeeds for class satisfying protocol
# ---------------------------------------------------------------------------
@runtime_checkable
class _SampleProtocol(Protocol):
"""A simple runtime-checkable Protocol for testing issubclass check."""
def do_work(self) -> str: ...
@given("I have a class that requires constructor arguments")
def step_class_requires_args(context):
"""Create a class whose __init__ requires mandatory arguments."""
class RequiresArgs(_SampleProtocol):
def __init__(self, mandatory_arg):
self._arg = mandatory_arg
def do_work(self) -> str:
return "done"
context.non_instantiable_class = RequiresArgs
@given("I have a runtime checkable protocol the class satisfies via issubclass")
def step_protocol_satisfied_by_subclass(context):
"""Store the protocol that the class satisfies structurally."""
context.target_protocol = _SampleProtocol
@when("I call validate_protocol with the non-instantiable class and protocol")
def step_call_validate_protocol_subclass_fallback(context):
"""Call validate_protocol; issubclass succeeds and returns True."""
context.validate_result = PluginLoader.validate_protocol(
context.non_instantiable_class,
context.target_protocol,
)
@then("validate_protocol should return True")
def step_verify_validate_true(context):
"""The issubclass check should have returned True."""
assert context.validate_result is True
# ---------------------------------------------------------------------------
# validate_protocol: issubclass raises TypeError for unverifiable protocol
# ---------------------------------------------------------------------------
@given("I have a class that cannot be instantiated without arguments")
def step_class_cannot_instantiate(context):
"""Create a class whose __init__ requires mandatory arguments."""
class NeedsArgs:
def __init__(self, required):
self._r = required
context.non_instantiable_class = NeedsArgs
@given("I have a protocol-like object that causes issubclass to raise TypeError")
def step_protocol_causes_typeerror(context):
"""Create a protocol-like object that causes issubclass to raise TypeError.
We need an object that:
- Has a __name__ attribute (so the error message in validate_protocol works)
- Causes issubclass() to raise TypeError when used as second arg
A class with a metaclass that raises TypeError on __subclasscheck__
achieves this. Since this protocol declares no members, the structural
fallback cannot verify conformance and must raise ProtocolMismatchError.
"""
class TypeErrorMeta(type):
def __instancecheck__(cls, instance):
raise TypeError("Simulated TypeError from isinstance")
def __subclasscheck__(cls, instance):
raise TypeError("Simulated TypeError from issubclass")
class BadProtocol(metaclass=TypeErrorMeta):
pass
context.target_protocol = BadProtocol
@when("I call validate_protocol expecting a mismatch error")
def step_call_validate_protocol_expect_error(context):
"""Call validate_protocol and capture the expected error."""
try:
PluginLoader.validate_protocol(
context.non_instantiable_class,
context.target_protocol,
)
context.validate_error = None
except ProtocolMismatchError as exc:
context.validate_error = exc
except Exception as exc:
context.validate_error = exc
@then("a plugin-loader ProtocolMismatchError should be raised")
def step_verify_protocol_mismatch(context):
"""Verify that ProtocolMismatchError was raised."""
assert context.validate_error is not None, "Expected an error but none was raised"
assert isinstance(context.validate_error, ProtocolMismatchError), (
f"Expected ProtocolMismatchError, got {type(context.validate_error).__name__}: "
f"{context.validate_error}"
)
# ---------------------------------------------------------------------------
# validate_protocol: issubclass returns False (class missing required members)
# ---------------------------------------------------------------------------
@given("I have a runtime checkable protocol the class does not satisfy")
def step_protocol_not_satisfied(context):
"""Store a protocol the class does NOT satisfy."""
@runtime_checkable
class UnrelatedProtocol(Protocol):
def completely_different_method(self) -> int: ...
context.target_protocol = UnrelatedProtocol