fa215f86c6
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.
276 lines
9.3 KiB
Python
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()
|