"""Step definitions for container_handler.feature. Tests for the container infrastructure resource handler module (cleveragents.resource.handlers.container). """ from __future__ import annotations from typing import Any from unittest.mock import MagicMock from behave import given, then, when # type: ignore[attr-defined] from cleveragents.domain.models.core.resource import ( PhysVirt, Resource, ResourceCapabilities, ) from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH from cleveragents.resource.handlers.protocol import ResourceHandler # ── Handler class registry ─────────────────────────────────── def _get_handler_class(class_name: str) -> type: """Import and return a handler class from the container module.""" from cleveragents.resource.handlers import container as _container_mod cls = getattr(_container_mod, class_name, None) if cls is None: raise AssertionError( f"Class '{class_name}' not found in " "cleveragents.resource.handlers.container" ) return cls def _make_container_resource( resource_type: str, location: str | None = None, ) -> Resource: """Create a minimal container resource for testing.""" return Resource( resource_id="01KJ5C5TPMP8GGX3QC83E2MAQS", resource_type_name=resource_type, classification=PhysVirt.PHYSICAL, location=location, properties={}, capabilities=ResourceCapabilities( readable=True, writable=False, sandboxable=False, checkpointable=False, ), ) # ── When steps ─────────────────────────────────────────────── @when("I import the container handler module") def step_import_container_module(context: Any) -> None: """Attempt to import the container handler module.""" try: import cleveragents.resource.handlers.container as _mod context.import_error = None # type: ignore[attr-defined] context.container_module = _mod # type: ignore[attr-defined] except Exception as exc: context.import_error = exc # type: ignore[attr-defined] @when('I import "{class_name}" from the container handler module') def step_import_class_from_container_module(context: Any, class_name: str) -> None: """Attempt to import a specific class from the container handler module.""" try: cls = _get_handler_class(class_name) context.imported_class = cls # type: ignore[attr-defined] context.import_error = None # type: ignore[attr-defined] except Exception as exc: context.import_error = exc # type: ignore[attr-defined] context.imported_class = None # type: ignore[attr-defined] @when('I resolve the handler ""') def step_resolve_handler_ref_template(context: Any, handler_ref: str) -> None: """Resolve a handler by reference string (template step).""" step_resolve_handler_ref(context, handler_ref) @when('I resolve the handler "{handler_ref}"') def step_resolve_handler_ref(context: Any, handler_ref: str) -> None: """Resolve a handler by reference string.""" from cleveragents.resource.handlers.resolver import ( HandlerResolutionError, clear_handler_cache, resolve_handler, ) clear_handler_cache() try: context.resolved_handler = resolve_handler(handler_ref) # type: ignore[attr-defined] context.resolution_error = None # type: ignore[attr-defined] except HandlerResolutionError as exc: context.resolution_error = exc # type: ignore[attr-defined] context.resolved_handler = None # type: ignore[attr-defined] @when('I resolve the handler for resource type "{resource_type}"') def step_resolve_handler_for_type(context: Any, resource_type: str) -> None: """Resolve a handler for a resource type via the registry.""" from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES from cleveragents.resource.handlers.resolver import ( HandlerResolutionError, clear_handler_cache, resolve_handler, ) clear_handler_cache() handler_ref = None for type_def in BUILTIN_TYPES: if type_def["name"] == resource_type: handler_ref = type_def.get("handler") break if handler_ref is None: context.resolution_error = AssertionError( # type: ignore[attr-defined] f"Resource type '{resource_type}' not found in BUILTIN_TYPES" ) context.resolved_handler = None # type: ignore[attr-defined] return try: context.resolved_handler = resolve_handler(handler_ref) # type: ignore[attr-defined] context.resolution_error = None # type: ignore[attr-defined] except HandlerResolutionError as exc: context.resolution_error = exc # type: ignore[attr-defined] context.resolved_handler = None # type: ignore[attr-defined] @when("I call resolve on the container handler") def step_call_resolve(context: Any) -> None: """Call resolve() on the handler and capture any exception.""" mock_sandbox = MagicMock() try: context.handler.resolve( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] plan_id="test-plan-01", slot_name="test-slot", sandbox_manager=mock_sandbox, access="read_only", ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call read on the container handler") def step_call_read(context: Any) -> None: """Call read() on the handler and capture any exception.""" try: context.handler.read( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when('I call write on the container handler with data b"test"') def step_call_write(context: Any) -> None: """Call write() on the handler and capture any exception.""" try: context.handler.write( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] path="", data=b"test", ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call delete on the container handler") def step_call_delete(context: Any) -> None: """Call delete() on the handler and capture any exception.""" try: context.handler.delete( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call list_children on the container handler") def step_call_list_children(context: Any) -> None: """Call list_children() on the handler and capture any exception.""" try: context.handler.list_children( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when('I call diff on the container handler with other_location "/other"') def step_call_diff(context: Any) -> None: """Call diff() on the handler and capture any exception.""" try: context.handler.diff( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] other_location="/other", ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call discover_children on the container handler") def step_call_discover_children(context: Any) -> None: """Call discover_children() on the handler and capture any exception.""" try: context.handler.discover_children( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call create_checkpoint on the container handler") def step_call_create_checkpoint(context: Any) -> None: """Call create_checkpoint() on the handler and capture any exception.""" mock_sandbox = MagicMock() try: context.handler.create_checkpoint( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] plan_id="test-plan-01", sandbox_manager=mock_sandbox, ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call rollback_to on the container handler") def step_call_rollback_to(context: Any) -> None: """Call rollback_to() on the handler and capture any exception.""" mock_sandbox = MagicMock() try: context.handler.rollback_to( # type: ignore[attr-defined] resource=context.container_resource, # type: ignore[attr-defined] plan_id="test-plan-01", checkpoint_id="ckpt-001", sandbox_manager=mock_sandbox, ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] @when("I call content_hash on the container handler") def step_call_content_hash(context: Any) -> None: """Call content_hash() on the handler and capture the result.""" try: context.content_hash_result = context.handler.content_hash( # type: ignore[attr-defined] context.container_resource # type: ignore[attr-defined] ) context.raised_exception = None # type: ignore[attr-defined] except Exception as exc: context.raised_exception = exc # type: ignore[attr-defined] context.content_hash_result = None # type: ignore[attr-defined] # ── Given steps ────────────────────────────────────────────── @given('a "{class_name}" instance') def step_handler_instance(context: Any, class_name: str) -> None: """Create a handler instance by class name.""" cls = _get_handler_class(class_name) context.handler = cls() # type: ignore[attr-defined] context.handler_class_name = class_name # type: ignore[attr-defined] @given('a container resource of type "{resource_type}" with location "{location}"') def step_container_resource_with_location( context: Any, resource_type: str, location: str ) -> None: """Create a container resource with a specific location.""" context.container_resource = _make_container_resource( # type: ignore[attr-defined] resource_type=resource_type, location=location, ) @given('a container resource of type "{resource_type}" with no location') def step_container_resource_no_location(context: Any, resource_type: str) -> None: """Create a container resource with no location.""" context.container_resource = _make_container_resource( # type: ignore[attr-defined] resource_type=resource_type, location=None, ) # ── Then steps ─────────────────────────────────────────────── @then("the container handler module import should succeed without errors") def step_import_succeeded(context: Any) -> None: """Assert that the import succeeded.""" assert context.import_error is None, ( # type: ignore[attr-defined] f"Import failed with: {context.import_error}" # type: ignore[attr-defined] ) @then("the class should be importable") def step_class_importable(context: Any) -> None: """Assert that the class was imported successfully.""" assert context.import_error is None, ( # type: ignore[attr-defined] f"Class import failed with: {context.import_error}" # type: ignore[attr-defined] ) assert context.imported_class is not None # type: ignore[attr-defined] @then("the instance should satisfy the ResourceHandler protocol") def step_satisfies_protocol(context: Any) -> None: """Assert that the handler instance satisfies ResourceHandler protocol.""" assert isinstance(context.handler, ResourceHandler), ( # type: ignore[attr-defined] f"{context.handler_class_name} does not satisfy ResourceHandler protocol" # type: ignore[attr-defined] ) @then("the handler should resolve without HandlerResolutionError") def step_handler_resolved(context: Any) -> None: """Assert that the handler resolved without error.""" assert context.resolution_error is None, ( # type: ignore[attr-defined] f"Handler resolution failed: {context.resolution_error}" # type: ignore[attr-defined] ) assert context.resolved_handler is not None # type: ignore[attr-defined] @then('the handler type_label should be "{expected_label}"') def step_handler_type_label(context: Any, expected_label: str) -> None: """Assert the handler's _type_label class variable.""" actual = getattr(context.handler, "_type_label", None) # type: ignore[attr-defined] assert actual == expected_label, ( f"Expected _type_label='{expected_label}', got '{actual}'" ) @then("a container handler NotImplementedError should be raised") def step_not_implemented_raised(context: Any) -> None: """Assert that a NotImplementedError was raised.""" exc = context.raised_exception # type: ignore[attr-defined] assert exc is not None, "Expected NotImplementedError but no exception was raised" assert isinstance(exc, NotImplementedError), ( f"Expected NotImplementedError but got {type(exc).__name__}: {exc}" ) @then("the content_hash result should be the EMPTY_CONTENT_HASH sentinel") def step_content_hash_is_empty(context: Any) -> None: """Assert that content_hash returned the EMPTY_CONTENT_HASH sentinel.""" result = context.content_hash_result # type: ignore[attr-defined] assert result == EMPTY_CONTENT_HASH, ( f"Expected EMPTY_CONTENT_HASH but got: {result!r}" ) @then("the content_hash result should be a non-empty hex string") def step_content_hash_is_hex(context: Any) -> None: """Assert that content_hash returned a non-empty hex string.""" result = context.content_hash_result # type: ignore[attr-defined] assert result is not None, "content_hash returned None" assert isinstance(result, str), f"Expected str, got {type(result)}" assert len(result) > 0, "content_hash returned empty string" assert result != EMPTY_CONTENT_HASH, ( "content_hash returned EMPTY_CONTENT_HASH but expected identity hash" ) # Verify it's a valid hex string try: int(result, 16) except ValueError as exc: raise AssertionError( f"content_hash result is not a valid hex string: {result!r}" ) from exc