Files
cleveragents-core/robot/helper_devcontainer_handler.py
T
freemo 8ea00f5185
CI / unit_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / security (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / build (push) Has been cancelled
CI / push-validation (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / helm (push) Has been cancelled
fix: restore CI quality tests to passing state (#4175)
Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
2026-04-08 11:02:14 +00:00

305 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.
F22/F25 fix: handler uses ``none`` because SandboxFactory has not yet
implemented ``snapshot``. The container itself provides isolation.
Known limitation — will switch to ``snapshot`` once implemented.
"""
handler = DevcontainerHandler()
assert handler._default_strategy.value == "none"
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()