Files
cleveragents-core/robot/helper_devcontainer_handler.py
T
HAL9000 2ea588f395 fix(resource): align DETECTED->DISCOVERED rename and remove build artifacts
- Remove committed build artifacts (test_reports/summary.txt,
  test_reports/test_results.json) and add test_reports/ to .gitignore
- Fix CLI resource.py: update lifecycle state condition and warning
  banner from 'detected (not built)' to 'discovered (not built)' and
  'Devcontainer detected' to 'Devcontainer discovered' to align with
  ContainerLifecycleState.DISCOVERED rename
- Fix BDD feature file: update resource_list_lifecycle_state.feature
  scenario title and assertion from 'Devcontainer detected' to
  'Devcontainer discovered'
- Fix robot/helper_devcontainer_lifecycle.py: update enum value check
  from 'detected' to 'discovered' in cmd_enum_values()
- Fix robot/helper_devcontainer_handler.py: update strategy check
  assertion from 'none' to 'snapshot' to match PR #8304 change
- Update CONTRIBUTORS.md with HAL 9000 feature contribution entry

Closes #7555 (via PR #8304)

Fixes: CI unit_tests and integration_tests failures from PR #8304
2026-06-02 20:37:35 -04:00

304 lines
11 KiB
Python

"""Helper utilities for devcontainer handler 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
import tempfile
from pathlib import Path
from typing import Any
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler
from cleveragents.resource.handlers.discovery import (
DevcontainerDiscoveryResult,
discover_devcontainers,
)
from cleveragents.resource.handlers.protocol import ResourceHandler
from cleveragents.resource.handlers.resolver import (
clear_handler_cache,
resolve_handler,
)
_VALID_DC_JSON: str = json.dumps(
{"name": "test-devcontainer", "image": "ubuntu:latest"}
)
# -- Built-in type definitions for DAG checks --------------------------------
_BUILTIN_TYPES: list[dict[str, Any]] = [
{
"name": "git-checkout",
"description": "Git checkout",
"resource_kind": "physical",
"sandbox_strategy": "git_worktree",
"built_in": True,
"child_types": [
"fs-directory",
"fs-file",
"devcontainer-instance",
"devcontainer-file",
],
},
{
"name": "devcontainer-instance",
"description": "Devcontainer instance",
"resource_kind": "physical",
"sandbox_strategy": "snapshot",
"built_in": True,
"parent_types": ["git-checkout", "fs-directory"],
"child_types": ["devcontainer-file"],
},
{
"name": "devcontainer-file",
"description": "Devcontainer config file",
"resource_kind": "physical",
"sandbox_strategy": "copy_on_write",
"built_in": True,
"parent_types": ["devcontainer-instance"],
"child_types": [],
},
]
def cmd_protocol_check() -> None:
"""Verify DevcontainerHandler satisfies ResourceHandler."""
handler = DevcontainerHandler()
assert isinstance(handler, ResourceHandler), "Protocol check failed"
print("protocol-check-ok")
def cmd_strategy_check() -> None:
"""Verify DevcontainerHandler default strategy.
PR #8304 fix: handler now uses ``snapshot`` strategy for safe plan
execution inside containers (ADR-043 devcontainer integration).
"""
handler = DevcontainerHandler()
assert handler._default_strategy.value == "snapshot"
print("strategy-check-ok")
def cmd_discovery_valid() -> None:
"""Discover devcontainer from valid config."""
with tempfile.TemporaryDirectory() as tmp:
dc_dir = Path(tmp) / ".devcontainer"
dc_dir.mkdir()
(dc_dir / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "git-checkout")
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
assert str(results[0].config_path).endswith("devcontainer.json")
print("discovery-valid-ok")
def cmd_discovery_invalid() -> None:
"""Skip invalid devcontainer.json."""
with tempfile.TemporaryDirectory() as tmp:
dc_dir = Path(tmp) / ".devcontainer"
dc_dir.mkdir()
(dc_dir / "devcontainer.json").write_text("not valid json {{", encoding="utf-8")
results = discover_devcontainers(tmp, "git-checkout")
assert len(results) == 0, f"Expected 0 results, got {len(results)}"
print("discovery-invalid-ok")
def cmd_discovery_root() -> None:
"""Discover root-level .devcontainer.json."""
with tempfile.TemporaryDirectory() as tmp:
(Path(tmp) / ".devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "fs-directory")
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
print("discovery-root-ok")
def cmd_discovery_nontrigger() -> None:
"""No discovery for non-trigger types."""
with tempfile.TemporaryDirectory() as tmp:
dc_dir = Path(tmp) / ".devcontainer"
dc_dir.mkdir()
(dc_dir / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "fs-file")
assert len(results) == 0, f"Expected 0 results, got {len(results)}"
print("discovery-nontrigger-ok")
def cmd_builtin_types() -> None:
"""Verify devcontainer types in BUILTIN_NAMES."""
names = ResourceTypeSpec.BUILTIN_NAMES
assert "devcontainer-instance" in names
assert "devcontainer-file" in names
assert "container-instance" in names
print("builtin-types-ok")
def cmd_dag_hierarchy() -> None:
"""Validate devcontainer DAG parent/child constraints."""
for type_def in _BUILTIN_TYPES:
name = type_def["name"]
if name == "devcontainer-instance":
assert "git-checkout" in type_def["parent_types"]
assert "fs-directory" in type_def["parent_types"]
assert "devcontainer-file" in type_def["child_types"]
elif name == "devcontainer-file":
assert "devcontainer-instance" in type_def["parent_types"]
assert len(type_def["child_types"]) == 0
elif name == "git-checkout":
assert "devcontainer-instance" in type_def["child_types"]
print("dag-hierarchy-ok")
def cmd_resolver_import() -> None:
"""Verify resolve_handler loads DevcontainerHandler."""
clear_handler_cache()
handler = resolve_handler(
"cleveragents.resource.handlers.devcontainer:DevcontainerHandler"
)
assert isinstance(handler, ResourceHandler)
assert isinstance(handler, DevcontainerHandler)
print("resolver-import-ok")
def cmd_result_validation() -> None:
"""Verify DevcontainerDiscoveryResult validates inputs."""
with tempfile.TemporaryDirectory() as tmp:
dc_file = Path(tmp) / "devcontainer.json"
dc_file.write_text(_VALID_DC_JSON, encoding="utf-8")
# Valid creation
result = DevcontainerDiscoveryResult(
config_path=dc_file,
config_data=json.loads(_VALID_DC_JSON),
parent_location=tmp,
)
assert result.config_path == dc_file
# Empty parent_location should raise
try:
DevcontainerDiscoveryResult(
config_path=dc_file,
config_data=json.loads(_VALID_DC_JSON),
parent_location="",
)
raise AssertionError("Should have raised ValueError")
except ValueError:
pass
print("result-validation-ok")
def cmd_discovery_named_single() -> None:
"""Discover a single named devcontainer configuration."""
with tempfile.TemporaryDirectory() as tmp:
named_dir = Path(tmp) / ".devcontainer" / "api"
named_dir.mkdir(parents=True)
(named_dir / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "git-checkout")
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
assert results[0].config_name == "api", (
f"Expected config_name='api', got {results[0].config_name!r}"
)
print("discovery-named-single-ok")
def cmd_discovery_named_multiple() -> None:
"""Discover multiple named devcontainer configurations."""
with tempfile.TemporaryDirectory() as tmp:
for name in ["api", "frontend"]:
d = Path(tmp) / ".devcontainer" / name
d.mkdir(parents=True)
(d / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "git-checkout")
assert len(results) == 2, f"Expected 2 results, got {len(results)}"
names = {r.config_name for r in results}
assert names == {"api", "frontend"}, (
f"Expected {{'api', 'frontend'}}, got {names!r}"
)
print("discovery-named-multiple-ok")
def cmd_discovery_named_mixed() -> None:
"""Discover mixed root and named devcontainer configurations."""
with tempfile.TemporaryDirectory() as tmp:
dc_dir = Path(tmp) / ".devcontainer"
dc_dir.mkdir()
(dc_dir / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
named_dir = dc_dir / "api"
named_dir.mkdir()
(named_dir / "devcontainer.json").write_text(_VALID_DC_JSON, encoding="utf-8")
results = discover_devcontainers(tmp, "git-checkout")
assert len(results) == 2, f"Expected 2 results, got {len(results)}"
root_results = [r for r in results if r.config_name is None]
named_results = [r for r in results if r.config_name == "api"]
assert len(root_results) == 1, "Expected 1 root result"
assert len(named_results) == 1, "Expected 1 named result"
print("discovery-named-mixed-ok")
def cmd_discovery_named_config_name_attr() -> None:
"""Verify config_name attribute on DevcontainerDiscoveryResult."""
with tempfile.TemporaryDirectory() as tmp:
# Named config has config_name set
named_dir = Path(tmp) / ".devcontainer" / "backend"
named_dir.mkdir(parents=True)
dc_file = named_dir / "devcontainer.json"
dc_file.write_text(_VALID_DC_JSON, encoding="utf-8")
result = DevcontainerDiscoveryResult(
config_path=dc_file,
config_data=json.loads(_VALID_DC_JSON),
parent_location=tmp,
config_name="backend",
)
assert result.config_name == "backend", (
f"Expected 'backend', got {result.config_name!r}"
)
# Root config has config_name=None
root_result = DevcontainerDiscoveryResult(
config_path=dc_file,
config_data=json.loads(_VALID_DC_JSON),
parent_location=tmp,
config_name=None,
)
assert root_result.config_name is None, (
f"Expected None, got {root_result.config_name!r}"
)
print("discovery-named-config-name-attr-ok")
_COMMANDS = {
"protocol-check": cmd_protocol_check,
"strategy-check": cmd_strategy_check,
"discovery-valid": cmd_discovery_valid,
"discovery-invalid": cmd_discovery_invalid,
"discovery-root": cmd_discovery_root,
"discovery-nontrigger": cmd_discovery_nontrigger,
"builtin-types": cmd_builtin_types,
"dag-hierarchy": cmd_dag_hierarchy,
"resolver-import": cmd_resolver_import,
"result-validation": cmd_result_validation,
"discovery-named-single": cmd_discovery_named_single,
"discovery-named-multiple": cmd_discovery_named_multiple,
"discovery-named-mixed": cmd_discovery_named_mixed,
"discovery-named-config-name-attr": cmd_discovery_named_config_name_attr,
}
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()