Files
temp/features/steps/sandbox_manager_strategy_cast_steps.py
CI Bot 68a3cc40ca fix(sandbox): remove type: ignore in SandboxManager strategy assignment
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
2026-04-05 04:37:45 +00:00

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"
)