"""Helper utilities for devcontainer handler Robot smoke tests. Each command prints a single ``-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]} ", 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()