forked from HAL9000/cleveragents-core
68a3cc40ca
Implemented removal of the type: ignore in SandboxManager strategy assignment by introducing a proper typing.cast usage and added tests to cover the new path.
- What was implemented
- Added from typing import cast to src/cleveragents/infrastructure/sandbox/manager.py.
- Replaced strategy = boundary_resource.sandbox_strategy # type: ignore[assignment] with strategy = cast("SandboxStrategyStr", boundary_resource.sandbox_strategy) at line 618 in get_or_create_sandbox_for_resource.
- The cast is semantically correct: SandboxStrategy (StrEnum) values are exactly the same string set as SandboxStrategyStr (Literal), so the cast is a truthful type assertion with zero runtime overhead.
- Added features/sandbox_manager_strategy_cast.feature with 2 Behave scenarios exercising the cast conversion path.
- Added features/steps/sandbox_manager_strategy_cast_steps.py with step definitions.
- Key design decisions
- Used cast() from typing rather than SandboxStrategyStr(...) constructor call, because SandboxStrategyStr is a Literal type alias (not a callable), so calling it as a constructor would fail at runtime.
- Used string form "SandboxStrategyStr" in the cast call because the file uses from __future__ import annotations.
- Mocked the boundary cache in tests to avoid DAG traversal, following the pattern established in sandbox_manager_coverage_r3_steps.py.
- Affected modules/components
- src/cleveragents/infrastructure/sandbox/manager.py
- features/sandbox_manager_strategy_cast.feature
- features/steps/sandbox_manager_strategy_cast_steps.py
- Testing considerations
- The new Behave scenarios exercise the cast path in isolation, reducing DAG traversal concerns and aligning with existing testing patterns.
ISSUES CLOSED: #2828
166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
"""Step definitions for sandbox manager strategy cast coverage.
|
|
|
|
Exercises the cast("SandboxStrategyStr", boundary_resource.sandbox_strategy)
|
|
line in get_or_create_sandbox_for_resource, which is reached whenever the
|
|
boundary resource has a non-None sandbox_strategy.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.domain.models.core.resource import (
|
|
PhysVirt,
|
|
Resource,
|
|
ResourceCapabilities,
|
|
SandboxStrategy,
|
|
)
|
|
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
|
|
from cleveragents.infrastructure.sandbox.manager import SandboxManager
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Valid ULID strings for test resources
|
|
_ULID_CHILD = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
_ULID_BOUNDARY = "01BRZ3NDEKTSV4RRFFQ69G5FAV"
|
|
|
|
_STRATEGY_MAP: dict[str, SandboxStrategy] = {
|
|
"NONE": SandboxStrategy.NONE,
|
|
"GIT_WORKTREE": SandboxStrategy.GIT_WORKTREE,
|
|
"COPY_ON_WRITE": SandboxStrategy.COPY_ON_WRITE,
|
|
"TRANSACTION_ROLLBACK": SandboxStrategy.TRANSACTION_ROLLBACK,
|
|
"SNAPSHOT": SandboxStrategy.SNAPSHOT,
|
|
"OVERLAY": SandboxStrategy.OVERLAY,
|
|
}
|
|
|
|
|
|
def _make_resource(
|
|
resource_id: str = _ULID_CHILD,
|
|
location: str | None = "/tmp/repo",
|
|
sandbox_strategy: SandboxStrategy | None = SandboxStrategy.GIT_WORKTREE,
|
|
sandboxable: bool = True,
|
|
parents: list[str] | None = None,
|
|
) -> Resource:
|
|
"""Create a Resource domain object for testing."""
|
|
return Resource(
|
|
resource_id=resource_id,
|
|
resource_type_name="git-checkout",
|
|
classification=PhysVirt.PHYSICAL,
|
|
location=location,
|
|
sandbox_strategy=sandbox_strategy,
|
|
capabilities=ResourceCapabilities(sandboxable=sandboxable),
|
|
parents=parents or [],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("smcast a sandbox factory instance")
|
|
def step_smcast_given_factory(context: Any) -> None:
|
|
context.smcast_factory = SandboxFactory()
|
|
|
|
|
|
@given("smcast a sandbox manager with the factory")
|
|
def step_smcast_given_manager(context: Any) -> None:
|
|
context.smcast_manager = SandboxManager(
|
|
factory=context.smcast_factory, cleanup_on_exit=False
|
|
)
|
|
context.smcast_error = None
|
|
context.smcast_sandbox = None
|
|
context.smcast_boundary_resource = None
|
|
context.smcast_child_resource = None
|
|
context.smcast_registry = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
'smcast a boundary resource with strategy "{strategy}" and location "{location}"'
|
|
)
|
|
def step_smcast_given_boundary_resource(
|
|
context: Any, strategy: str, location: str
|
|
) -> None:
|
|
"""Set up a boundary resource with the given strategy and location."""
|
|
sandbox_strategy = _STRATEGY_MAP[strategy]
|
|
|
|
boundary_resource = _make_resource(
|
|
resource_id=_ULID_BOUNDARY,
|
|
location=location,
|
|
sandbox_strategy=sandbox_strategy,
|
|
sandboxable=True,
|
|
)
|
|
|
|
child_resource = _make_resource(
|
|
resource_id=_ULID_CHILD,
|
|
location="/tmp/smcast-child",
|
|
sandbox_strategy=None,
|
|
sandboxable=False,
|
|
parents=[_ULID_BOUNDARY],
|
|
)
|
|
|
|
context.smcast_boundary_resource = boundary_resource
|
|
context.smcast_child_resource = child_resource
|
|
context.smcast_registry = {
|
|
_ULID_BOUNDARY: boundary_resource,
|
|
_ULID_CHILD: child_resource,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('smcast I call get_or_create_sandbox_for_resource for plan "{plan_id}"')
|
|
def step_smcast_when_call_for_resource(context: Any, plan_id: str) -> None:
|
|
"""Call get_or_create_sandbox_for_resource with the prepared boundary resource.
|
|
|
|
The boundary cache is patched to return the boundary resource directly,
|
|
bypassing DAG traversal.
|
|
"""
|
|
context.smcast_error = None
|
|
context.smcast_sandbox = None
|
|
|
|
patcher = patch.object(
|
|
context.smcast_manager._boundary_cache,
|
|
"get_boundary",
|
|
return_value=context.smcast_boundary_resource,
|
|
)
|
|
patcher.start()
|
|
context.add_cleanup(patcher.stop)
|
|
|
|
try:
|
|
context.smcast_sandbox = (
|
|
context.smcast_manager.get_or_create_sandbox_for_resource(
|
|
plan_id=plan_id,
|
|
resource=context.smcast_child_resource,
|
|
resource_registry=context.smcast_registry,
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
context.smcast_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("smcast a sandbox is returned without error")
|
|
def step_smcast_then_sandbox_returned(context: Any) -> None:
|
|
assert context.smcast_error is None, f"Unexpected error: {context.smcast_error}"
|
|
assert context.smcast_sandbox is not None, (
|
|
"Expected a sandbox to be returned, but got None"
|
|
)
|