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
Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me> Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
305 lines
11 KiB
Python
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()
|