Files
temp/features/steps/sandbox_strategy_registry_coverage_steps.py
freemo 051ee7c290 test(coverage): add Behave BDD tests to improve coverage across 52 source files
Added 52 new .feature files and corresponding _steps.py files targeting
previously uncovered code paths in the following areas:

- TUI layer: app, commands, persona (state/schema/registry), widgets,
  input (shell_exec, reference_parser)
- Application services: plan lifecycle/service/executor, session,
  project, repo indexing, correction, checkpoint, actor, llm_actors,
  strategy coordinator, resource file watcher, service retry wiring
- CLI commands: session, resource, repl, plan, db, automation_profile
- Domain models: retry_policy, resource_type, cost_budget,
  docker_compose_analyzer, detail_level, _sql_string_aware,
  _postgresql_helpers
- Core: circuit_breaker, retry_service_patterns
- Infrastructure: repositories, transaction_sandbox, strategy_registry,
  plugins/loader, container
- Config: settings
- Agents: plan_generation, context_analysis, auto_debug
- A2A: facade

All new tests follow the Behave/Gherkin BDD standard. Resolved step
definition collisions with unique prefixes. Fixed Alembic fileConfig
logger disabling issue (disable_existing_loggers=False).

ISSUES CLOSED: #1068
2026-03-20 21:22:10 +00:00

316 lines
11 KiB
Python

"""Step definitions for sandbox_strategy_registry_coverage.feature.
These steps target specific uncovered lines in strategy_registry.py:
- Lines 65-68: CustomStrategyConfig.__init__ assignment block
- Line 167: register_from_config delegation
- Lines 196-200: register_all_from_config skip on missing module/class
- Lines 205-209: register_all_from_config error handling on load failure
"""
import logging
from unittest.mock import MagicMock
from behave import given, then, when
from cleveragents.infrastructure.plugins.exceptions import (
PluginLoadError,
)
from cleveragents.infrastructure.sandbox.strategy_registry import (
CustomStrategyConfig,
SandboxStrategyRegistry,
)
# ---------------------------------------------------------------------------
# Helper: a class that satisfies _validate_protocol (has all 9 methods)
# ---------------------------------------------------------------------------
class _CompliantStrategy:
"""Dummy class with all 9 required SandboxStrategyProtocol methods."""
def create(self): ...
def read(self): ...
def write(self): ...
def diff(self): ...
def commit(self): ...
def rollback(self): ...
def checkpoint(self): ...
def restore_checkpoint(self): ...
def cleanup(self): ...
class _NonCompliantStrategy:
"""Dummy class missing required methods."""
def create(self): ...
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the sandbox strategy registry module is imported")
def step_module_imported(context):
"""Verify the module is importable."""
assert CustomStrategyConfig is not None
assert SandboxStrategyRegistry is not None
# ---------------------------------------------------------------------------
# CustomStrategyConfig success (lines 65-68)
# ---------------------------------------------------------------------------
@when(
'I create a CustomStrategyConfig with name "{name}" module "{module}" and class "{cls}"'
)
def step_create_config(context, name, module, cls):
"""Create a CustomStrategyConfig with the given values."""
context.strategy_config = CustomStrategyConfig(
name=name,
module=module,
class_name=cls,
)
@then('the config should have name "{name}" module "{module}" and class_name "{cls}"')
def step_verify_config_fields(context, name, module, cls):
"""Verify the config fields were assigned correctly."""
cfg = context.strategy_config
assert cfg.name == name, f"Expected name={name!r}, got {cfg.name!r}"
assert cfg.module == module, f"Expected module={module!r}, got {cfg.module!r}"
assert cfg.class_name == cls, f"Expected class_name={cls!r}, got {cfg.class_name!r}"
@then("the config options should default to an empty dict")
def step_verify_default_options(context):
"""Verify options defaults to empty dict when not provided."""
assert context.strategy_config.options == {}, (
f"Expected empty dict, got {context.strategy_config.options!r}"
)
@when(
'I create a CustomStrategyConfig with name "{name}" module "{module}" class "{cls}" and options'
)
def step_create_config_with_options(context, name, module, cls):
"""Create a CustomStrategyConfig with explicit options."""
context.strategy_config = CustomStrategyConfig(
name=name,
module=module,
class_name=cls,
options={"timeout": 30, "retries": 5},
)
@then("the config options should contain the provided values")
def step_verify_explicit_options(context):
"""Verify options stores the provided dict."""
opts = context.strategy_config.options
assert opts == {"timeout": 30, "retries": 5}, f"Unexpected options: {opts!r}"
# ---------------------------------------------------------------------------
# Registry with mock loader helpers
# ---------------------------------------------------------------------------
@given("a registry with a mock loader that returns a compliant class")
def step_registry_with_mock_loader(context):
"""Create a SandboxStrategyRegistry with a mocked PluginLoader."""
mock_loader = MagicMock()
mock_loader.load_class.return_value = _CompliantStrategy
context.registry = SandboxStrategyRegistry(loader=mock_loader)
context.mock_loader = mock_loader
@given('a CustomStrategyConfig named "{name}" with module "{module}" and class "{cls}"')
def step_create_named_config(context, name, module, cls):
"""Create and store a CustomStrategyConfig for later use."""
context.strategy_config = CustomStrategyConfig(
name=name,
module=module,
class_name=cls,
)
# ---------------------------------------------------------------------------
# register_from_config (line 167)
# ---------------------------------------------------------------------------
@when("I call register_from_config with that config")
def step_call_register_from_config(context):
"""Call register_from_config using the stored config."""
context.returned_cls = context.registry.register_from_config(
context.strategy_config,
)
@then('the strategy "{name}" should be registered in the registry')
def step_verify_strategy_registered(context, name):
"""Verify the strategy was successfully registered."""
assert context.registry.has(name), (
f"Strategy {name!r} not found; registered: {context.registry.list_strategies()}"
)
assert context.returned_cls is _CompliantStrategy
# ---------------------------------------------------------------------------
# register_all_from_config: skip missing module (lines 196-200)
# ---------------------------------------------------------------------------
@when("I call register_all_from_config with an entry that has an empty module")
def step_register_all_missing_module(context):
"""Call register_all_from_config with a config missing the module key."""
context.log_handler = _CapturingLogHandler()
logger = logging.getLogger("cleveragents.infrastructure.sandbox.strategy_registry")
# Re-enable the logger in case Alembic's fileConfig() disabled it
logger.disabled = False
logger.addHandler(context.log_handler)
logger.setLevel(logging.DEBUG)
def cleanup():
logger.removeHandler(context.log_handler)
context.add_cleanup(cleanup)
configs = {
"bad_no_module": {
"module": "",
"class": "SomeClass",
},
}
context.registered_names = context.registry.register_all_from_config(configs)
context.skipped_entry_name = "bad_no_module"
@then("the returned list should not contain the skipped entry name")
def step_verify_skipped_not_in_list(context):
"""Verify the skipped entry is not in the returned list."""
assert context.skipped_entry_name not in context.registered_names, (
f"Expected {context.skipped_entry_name!r} to be absent, "
f"got {context.registered_names}"
)
@then("a warning should be logged about the skipped entry")
def step_verify_warning_logged(context):
"""Verify a warning was logged for the skipped entry."""
warnings = [r for r in context.log_handler.records if r.levelno == logging.WARNING]
assert any("Skipping custom strategy" in r.getMessage() for r in warnings), (
f"Expected a 'Skipping custom strategy' warning, "
f"got: {[r.getMessage() for r in warnings]}"
)
@when("I call register_all_from_config with an entry that has an empty class")
def step_register_all_missing_class(context):
"""Call register_all_from_config with a config missing the class key."""
configs = {
"bad_no_class": {
"module": "some.module",
"class": "",
},
}
context.registered_names = context.registry.register_all_from_config(configs)
context.skipped_entry_name = "bad_no_class"
# ---------------------------------------------------------------------------
# register_all_from_config: PluginLoadError (lines 205-209)
# ---------------------------------------------------------------------------
@given("a registry whose loader raises PluginLoadError")
def step_registry_loader_raises_load_error(context):
"""Create a registry whose loader always raises PluginLoadError."""
mock_loader = MagicMock()
mock_loader.load_class.side_effect = PluginLoadError("boom")
context.registry = SandboxStrategyRegistry(loader=mock_loader)
# Attach a log handler to capture error logs
context.log_handler = _CapturingLogHandler()
logger = logging.getLogger("cleveragents.infrastructure.sandbox.strategy_registry")
# Re-enable the logger in case Alembic's fileConfig() disabled it
logger.disabled = False
logger.addHandler(context.log_handler)
logger.setLevel(logging.DEBUG)
def cleanup():
logger.removeHandler(context.log_handler)
context.add_cleanup(cleanup)
@when("I call register_all_from_config with an entry that will fail to load")
def step_register_all_fail_load(context):
"""Call register_all_from_config with a valid-looking config that will fail."""
configs = {
"failing_strategy": {
"module": "nonexistent.module",
"class": "NoSuchClass",
},
}
context.registered_names = context.registry.register_all_from_config(configs)
@then("the returned list should be empty")
def step_verify_empty_result(context):
"""Verify no strategies were registered."""
assert context.registered_names == [], (
f"Expected empty list, got {context.registered_names!r}"
)
@then("an error should be logged about the failed registration")
def step_verify_error_logged(context):
"""Verify an error was logged for the failed registration."""
errors = [r for r in context.log_handler.records if r.levelno == logging.ERROR]
assert any(
"Failed to register custom strategy" in r.getMessage() for r in errors
), f"Expected a 'Failed to register' error, got: {[r.getMessage() for r in errors]}"
# ---------------------------------------------------------------------------
# register_all_from_config: ProtocolMismatchError (lines 205-209)
# ---------------------------------------------------------------------------
@given("a registry whose loader returns a non-compliant class")
def step_registry_loader_returns_bad_class(context):
"""Create a registry whose loader returns a class that fails protocol check."""
mock_loader = MagicMock()
mock_loader.load_class.return_value = _NonCompliantStrategy
context.registry = SandboxStrategyRegistry(loader=mock_loader)
@when("I call register_all_from_config with an entry that will fail protocol check")
def step_register_all_fail_protocol(context):
"""Call register_all_from_config; the protocol validation will fail."""
configs = {
"bad_protocol": {
"module": "some.module",
"class": "BadClass",
},
}
context.registered_names = context.registry.register_all_from_config(configs)
# ---------------------------------------------------------------------------
# Internal helper: capturing log handler
# ---------------------------------------------------------------------------
class _CapturingLogHandler(logging.Handler):
"""A logging handler that captures LogRecords for assertion."""
def __init__(self):
super().__init__()
self.records: list[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
self.records.append(record)