62c11edb4a
CI / lint (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 55s
CI / helm (pull_request) Successful in 26s
CI / quality (pull_request) Successful in 1m18s
CI / security (pull_request) Successful in 1m33s
CI / push-validation (pull_request) Successful in 20s
CI / unit_tests (pull_request) Successful in 6m49s
CI / docker (pull_request) Successful in 1m50s
CI / coverage (pull_request) Successful in 12m25s
CI / integration_tests (pull_request) Successful in 28m3s
CI / status-check (pull_request) Successful in 4s
- Add create_sandbox() override to _ContainerBaseHandler raising NotImplementedError, matching CloudResourceHandler pattern (issue #836) - Add project_access() override to _ContainerBaseHandler raising NotImplementedError, consistent with all other stub methods - Rename @then("the import should succeed without errors") step to @then("the container handler module import should succeed without errors") to resolve AmbiguousStep collision with tdd_a2a_sdk_dependency_steps.py - Update container_handler.feature to use renamed step - Apply ruff format to container.py ISSUES CLOSED: #2907
390 lines
16 KiB
Python
390 lines
16 KiB
Python
"""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 "<handler_ref>"')
|
|
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
|