Files
cleveragents-core/robot/helper_devcontainer_lifecycle.py
HAL9000 fa215f86c6 fix(resource): move inline imports to top and remove redundant import
Addresses remaining CI review feedback from HAL9001 on PR #8304:

1. Moved all cleveragents imports from inside function bodies to module-level
   in features/steps/container_clone_into_steps.py (5 functions fixed)
2. Moved BUILTIN_TYPES import from inside step_look_up_in_builtin_types()
   to module level in features/steps/devcontainer_sandbox_strategy_steps.py
3. Removed redundant inline import of EMPTY_CONTENT_HASH and BaseResourceHandler
   inside diff() method of devcontainer.py (already available at module level)
4. Fixed stale docstring referencing old 'detected' terminology in
   robot/helper_devcontainer_lifecycle.py cmd_transition_valid()

All files pass ruff format and ruff check.
2026-06-02 20:37:35 -04:00

276 lines
9.3 KiB
Python

"""Helper utilities for devcontainer lifecycle Robot smoke tests.
Each command prints a single ``<command>-ok`` token on success so the
calling Robot test can assert on ``stdout``.
"""
from __future__ import annotations
import json
import sys
from dataclasses import dataclass, field
from cleveragents.application.services.cleanup_service import CleanupService
from cleveragents.domain.models.core.container_lifecycle import (
ContainerLifecycleState,
ContainerLifecycleTracker,
transition_state,
)
from cleveragents.resource.handlers.devcontainer import (
_parse_devcontainer_up_output,
activate_container,
clear_lifecycle_registry,
get_lifecycle_tracker,
rebuild_container,
set_lifecycle_tracker,
stop_all_active_containers,
stop_container,
)
# ---------------------------------------------------------------------------
# F29 fix: removed conditional import with ``# type: ignore`` suppressions.
# The inline mock classes are always used — they are self-contained and
# avoid the PYTHONPATH dependency on ``features/``, which is not always
# available in standalone Robot runs.
# ---------------------------------------------------------------------------
@dataclass
class _MockResult:
"""Minimal subprocess.CompletedProcess stand-in."""
returncode: int = 0
stdout: str = ""
stderr: str = ""
args: list[str] = field(default_factory=list)
class _MockRunner:
"""Callable mock for subprocess.run used by lifecycle functions."""
def __init__(self) -> None:
self.calls: list[tuple[list[str], dict[str, object]]] = []
self.up_result: _MockResult = _MockResult()
self.exec_result: _MockResult = _MockResult()
self.stop_result: _MockResult = _MockResult()
def __call__(self, args: list[str], **kwargs: object) -> _MockResult:
self.calls.append((list(args), dict(kwargs)))
if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "up":
return self.up_result
if len(args) >= 2 and args[0] == "devcontainer" and args[1] == "exec":
return self.exec_result
if len(args) >= 2 and args[0] == "docker" and args[1] == "stop":
return self.stop_result
return _MockResult(returncode=0)
def set_up_result(
self,
*,
container_id: str = "aabbccddee0011223344",
workspace: str = "/workspaces/project",
returncode: int = 0,
) -> None:
output = json.dumps(
{
"outcome": "success" if returncode == 0 else "error",
"containerId": container_id,
"remoteWorkspaceFolder": workspace,
}
)
self.up_result = _MockResult(returncode=returncode, stdout=output)
@property
def up_calls(self) -> list[tuple[list[str], dict[str, object]]]:
return [
(a, k)
for a, k in self.calls
if len(a) >= 2 and a[0] == "devcontainer" and a[1] == "up"
]
@property
def stop_calls(self) -> list[tuple[list[str], dict[str, object]]]:
return [
(a, k)
for a, k in self.calls
if len(a) >= 2 and a[0] == "docker" and a[1] == "stop"
]
# ---------------------------------------------------------------------------
# Test commands
# ---------------------------------------------------------------------------
def cmd_enum_values() -> None:
"""Verify all six lifecycle states exist."""
expected = {"discovered", "building", "running", "stopping", "stopped", "failed"}
actual = {s.value for s in ContainerLifecycleState}
assert actual == expected, f"Expected {expected}, got {actual}"
print("enum-values-ok")
def cmd_transition_valid() -> None:
"""Verify discovered->building transition."""
clear_lifecycle_registry()
tracker = ContainerLifecycleTracker(resource_id="01ROBOTTEST0000000000001")
tracker = transition_state(tracker, ContainerLifecycleState.BUILDING, reason="test")
assert tracker.current_state == ContainerLifecycleState.BUILDING
assert len(tracker.transitions) == 1
print("transition-valid-ok")
def cmd_transition_invalid() -> None:
"""Verify invalid transitions raise ValueError."""
clear_lifecycle_registry()
tracker = ContainerLifecycleTracker(resource_id="01ROBOTTEST0000000000002")
try:
transition_state(tracker, ContainerLifecycleState.RUNNING, reason="bad")
raise AssertionError("Should have raised ValueError")
except ValueError:
pass
print("transition-invalid-ok")
def cmd_lazy_activation() -> None:
"""Verify lazy activation with mock runner."""
clear_lifecycle_registry()
runner = _MockRunner()
runner.set_up_result(container_id="aabbccddee0022334455", workspace="/ws")
result = activate_container(
"01ROBOTTEST0000000000010",
"/workspace",
run_command=runner,
)
assert result.current_state == ContainerLifecycleState.RUNNING
assert result.container_id == "aabbccddee0022334455"
assert len(runner.up_calls) == 1
print("lazy-activation-ok")
def cmd_stop_container() -> None:
"""Verify stop transitions active to stopped."""
clear_lifecycle_registry()
runner = _MockRunner()
tracker = ContainerLifecycleTracker(
resource_id="01ROBOTTEST0000000000020",
current_state=ContainerLifecycleState.RUNNING,
container_id="ctr-stop",
)
set_lifecycle_tracker(tracker)
stop_container("01ROBOTTEST0000000000020", run_command=runner)
final = get_lifecycle_tracker("01ROBOTTEST0000000000020")
assert final.current_state == ContainerLifecycleState.STOPPED
assert len(runner.stop_calls) == 1
print("stop-container-ok")
def cmd_rebuild_container() -> None:
"""Verify rebuild transitions stopped to running."""
clear_lifecycle_registry()
runner = _MockRunner()
runner.set_up_result(container_id="aabbccddee0033445566", workspace="/ws")
tracker = ContainerLifecycleTracker(
resource_id="01ROBOTTEST0000000000030",
current_state=ContainerLifecycleState.STOPPED,
)
set_lifecycle_tracker(tracker)
# R16 fix: call rebuild_container instead of activate_container
rebuild_container(
"01ROBOTTEST0000000000030",
"/workspace",
run_command=runner,
)
final = get_lifecycle_tracker("01ROBOTTEST0000000000030")
assert final.current_state == ContainerLifecycleState.RUNNING
print("rebuild-container-ok")
def cmd_session_cleanup() -> None:
"""Verify session cleanup stops all active containers."""
clear_lifecycle_registry()
runner = _MockRunner()
for rid in ("01ROBOTTEST0000000000040", "01ROBOTTEST0000000000041"):
tracker = ContainerLifecycleTracker(
resource_id=rid,
current_state=ContainerLifecycleState.RUNNING,
container_id=f"ctr-{rid[-3:]}",
)
set_lifecycle_tracker(tracker)
stopped = stop_all_active_containers(run_command=runner, session_id="ses-001")
assert len(stopped) == 2
for rid in ("01ROBOTTEST0000000000040", "01ROBOTTEST0000000000041"):
t = get_lifecycle_tracker(rid)
assert t.current_state == ContainerLifecycleState.STOPPED
print("session-cleanup-ok")
def cmd_json_parsing() -> None:
"""Verify devcontainer up JSON output parsing."""
stdout = json.dumps(
{
"outcome": "success",
"containerId": "aabbccddee0044556677",
"remoteWorkspaceFolder": "/parsed/ws",
}
)
ctr_id, ws = _parse_devcontainer_up_output(stdout)
assert ctr_id == "aabbccddee0044556677"
assert ws == "/parsed/ws"
# Invalid JSON
ctr_id2, ws2 = _parse_devcontainer_up_output("not json {{")
assert ctr_id2 is None
assert ws2 is None
print("json-parsing-ok")
def cmd_registry_persist() -> None:
"""Verify tracker persists in registry."""
clear_lifecycle_registry()
tracker = ContainerLifecycleTracker(resource_id="01ROBOTTEST0000000000060")
set_lifecycle_tracker(tracker)
tracker2 = transition_state(tracker, ContainerLifecycleState.BUILDING, reason="t")
set_lifecycle_tracker(tracker2)
retrieved = get_lifecycle_tracker("01ROBOTTEST0000000000060")
assert retrieved.current_state == ContainerLifecycleState.BUILDING
print("registry-persist-ok")
def cmd_cleanup_method() -> None:
"""Verify CleanupService.stop_active_devcontainers exists."""
assert hasattr(CleanupService, "stop_active_devcontainers")
assert callable(CleanupService.stop_active_devcontainers)
print("cleanup-method-ok")
_COMMANDS = {
"enum-values": cmd_enum_values,
"transition-valid": cmd_transition_valid,
"transition-invalid": cmd_transition_invalid,
"lazy-activation": cmd_lazy_activation,
"stop-container": cmd_stop_container,
"rebuild-container": cmd_rebuild_container,
"session-cleanup": cmd_session_cleanup,
"json-parsing": cmd_json_parsing,
"registry-persist": cmd_registry_persist,
"cleanup-method": cmd_cleanup_method,
}
def main() -> None:
"""Dispatch subcommand from argv."""
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1]
func = _COMMANDS.get(cmd)
if func is None:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
func()
if __name__ == "__main__":
main()