Compare commits

...

1 Commits

Author SHA1 Message Date
brent.edwards bdc36ad2b9 Merge remote-tracking branch 'origin/master' into test/int-wf17-explicit-container
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 44s
CI / quality (pull_request) Successful in 3m47s
CI / lint (pull_request) Successful in 7m19s
CI / security (pull_request) Successful in 7m21s
CI / typecheck (pull_request) Successful in 8m33s
CI / integration_tests (pull_request) Successful in 12m48s
CI / unit_tests (pull_request) Successful in 13m13s
CI / docker (pull_request) Successful in 1m8s
CI / e2e_tests (pull_request) Successful in 17m6s
CI / coverage (pull_request) Successful in 11m36s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 49m13s
2026-03-26 20:03:11 +00:00
3 changed files with 576 additions and 0 deletions
+9
View File
@@ -2,6 +2,15 @@
## Unreleased
- Added Robot Framework integration test and Python helper for Specification
Workflow Example 17 (explicit container with directory mount). Covers
explicit `container-instance` resource configuration with dual mounts
(resource-ref rw + host-path ro), project execution environment override,
container lifecycle activation/stop, trusted profile execution, and session
ID propagation — all exercised with mocked LLM providers and mocked
container operations.
(`robot/wf17_explicit_container.robot`,
`robot/helper_wf17_explicit_container.py`) (#781)
- Added TDD bug-capture tests for bug #1076`use_action()` does not
propagate `automation_profile` to Plan. Three Behave BDD scenarios
(`@tdd_bug @tdd_bug_1076 @tdd_expected_fail`) verify the full precedence
+493
View File
@@ -0,0 +1,493 @@
"""Robot Framework helper for Workflow Example 17.
Explicit Container with Directory Mount.
Exercises explicit container-instance creation with dual mounts
(resource-ref rw + host-path ro), project execution environment
override, and container tool routing using mocked LLM providers
and mocked container operations.
Each subcommand prints a sentinel on success and exits 0/1.
Usage:
python robot/helper_wf17_explicit_container.py <command>
"""
from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import sys
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import NoReturn
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
_ROBOT = str(Path(__file__).resolve().parent)
if _ROBOT not in sys.path:
sys.path.insert(0, _ROBOT)
from helper_e2e_common import ( # noqa: E402
cleanup_workspace,
init_bare_git_repo,
run_cli,
setup_workspace,
write_yaml,
)
from helpers_common import reset_global_state # noqa: E402
from cleveragents.application.services.execution_environment_resolver import ( # noqa: E402
CONTAINER_RESOURCE_TYPES,
ExecutionEnvironmentResolver,
)
from cleveragents.domain.models.core.container_lifecycle import ( # noqa: E402
ContainerLifecycleState,
)
from cleveragents.domain.models.core.plan import ExecutionEnvironment # noqa: E402
from cleveragents.resource.handlers.devcontainer import ( # noqa: E402
activate_container,
clear_lifecycle_registry,
get_lifecycle_tracker,
stop_container,
)
_ACTION_YAML: str = """\
name: local/apply-schema-migration
description: Apply database schema migration in container
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: Schema migration applied and validated
"""
_ENV: dict[str, str] = {"COLUMNS": "500"}
# -- Mock runner (no real Docker) -------------------------------------------
@dataclass
class _MockResult:
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.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] == "docker" and args[1] == "stop":
return self.stop_result
return _MockResult()
def set_up_result(
self,
*,
cid: str = "ff17c0a1a1e2aa112233",
ws: str = "/workspaces/data",
) -> None:
self.up_result = _MockResult(
stdout=json.dumps(
{"outcome": "success", "containerId": cid, "remoteWorkspaceFolder": ws}
),
)
@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"
]
# -- Helpers ----------------------------------------------------------------
def _fail(msg: str) -> NoReturn:
print(f"FAIL: {msg}", file=sys.stderr)
raise SystemExit(1)
def _plan_id(output: str) -> str | None:
m = re.search(r"\b([0-9A-Z]{26})\b", output)
return m.group(1) if m else None
def _make_repo() -> str:
"""Create git repo with sample data directory for mount testing."""
repo = init_bare_git_repo()
data_dir = Path(repo) / "data"
data_dir.mkdir()
(data_dir / "schema.sql").write_text(
"CREATE TABLE wf17_test (id INTEGER PRIMARY KEY);\n",
encoding="utf-8",
)
subprocess.run(["git", "add", "."], cwd=repo, capture_output=True, check=True)
subprocess.run(
["git", "commit", "-m", "Add data directory"],
cwd=repo,
capture_output=True,
check=True,
)
return repo
def _cli(ws: str, *args: str) -> subprocess.CompletedProcess[str]:
"""Run CLI command, fail on non-zero exit."""
r = run_cli(*args, workspace=ws, env_extra=_ENV)
if r.returncode != 0:
_fail(f"{' '.join(args[:2])}: {r.stderr}")
return r
def _setup_full(ws: str, repo: str, yaml: str) -> str:
"""Register resources, project, action, plan use. Return plan_id."""
# 1. Register git-checkout resource (acts as resource-ref rw mount)
_cli(
ws,
"resource",
"add",
"git-checkout",
"local/data-repo",
"--path",
repo,
"--branch",
"main",
)
# 2. Register container-instance resource with image
_cli(
ws,
"resource",
"add",
"container-instance",
"local/wf17-container",
"--image",
"postgres:16",
"--format",
"plain",
)
# 3. Create project linking the git-checkout resource
_cli(
ws,
"project",
"create",
"local/wf17-project",
"--resource",
"local/data-repo",
)
# 4. Create action
_cli(ws, "action", "create", "--config", yaml)
# 5. Plan use with trusted profile
r = _cli(
ws,
"plan",
"use",
"local/apply-schema-migration",
"local/wf17-project",
"--automation-profile",
"trusted",
"--format",
"plain",
)
pid = _plan_id(r.stdout)
if not pid:
_fail(f"no plan_id:\n{r.stdout}")
return pid
# -- Test commands ----------------------------------------------------------
def wf17_container_instance_creation() -> None:
"""Step 1: Register container-instance with dual mounts."""
ws = setup_workspace(prefix="wf17_ctr_")
repo = _make_repo()
try:
# Register git-checkout as rw resource-ref mount
_cli(
ws,
"resource",
"add",
"git-checkout",
"local/data-repo",
"--path",
repo,
"--branch",
"main",
"--format",
"plain",
)
r_git = _cli(ws, "resource", "show", "local/data-repo", "--format", "plain")
if "git-checkout" not in r_git.stdout:
_fail(f"not git-checkout:\n{r_git.stdout}")
# Register container-instance with image (host-path ro mount)
_cli(
ws,
"resource",
"add",
"container-instance",
"local/wf17-container",
"--image",
"postgres:16",
"--format",
"plain",
)
r_ctr = _cli(
ws, "resource", "show", "local/wf17-container", "--format", "plain"
)
if "container-instance" not in r_ctr.stdout:
_fail(f"not container-instance:\n{r_ctr.stdout}")
# Verify container-instance is in CONTAINER_RESOURCE_TYPES
if "container-instance" not in CONTAINER_RESOURCE_TYPES:
_fail("container-instance not in CONTAINER_RESOURCE_TYPES")
# Verify both resources are listed
r_list = _cli(ws, "resource", "list", "--format", "plain")
if "local/data-repo" not in r_list.stdout:
_fail(f"data-repo missing:\n{r_list.stdout}")
if "local/wf17-container" not in r_list.stdout:
_fail(f"wf17-container missing:\n{r_list.stdout}")
print("wf17-container-ok")
finally:
shutil.rmtree(repo, ignore_errors=True)
cleanup_workspace(ws)
def wf17_project_execution_env() -> None:
"""Step 2: Create project, set execution environment override."""
ws = setup_workspace(prefix="wf17_env_")
repo = _make_repo()
try:
# Register resources
_cli(
ws,
"resource",
"add",
"git-checkout",
"local/data-repo",
"--path",
repo,
"--branch",
"main",
)
_cli(
ws,
"resource",
"add",
"container-instance",
"local/wf17-container",
"--image",
"postgres:16",
)
# Create project with resource link
_cli(
ws,
"project",
"create",
"local/wf17-project",
"--description",
"WF17 explicit container",
"--resource",
"local/data-repo",
"--format",
"plain",
)
r = _cli(ws, "project", "show", "local/wf17-project", "--format", "plain")
if "local/wf17-project" not in r.stdout:
_fail(f"project missing:\n{r.stdout}")
# Verify execution environment resolver priority chain
resolver = ExecutionEnvironmentResolver()
# Default resolves to host
default_env = resolver.resolve()
if default_env != ExecutionEnvironment.HOST:
_fail(f"expected HOST default: {default_env}")
# Project-level override to container
proj_env = resolver.resolve(project_env="container")
if proj_env != ExecutionEnvironment.CONTAINER:
_fail(f"expected CONTAINER override: {proj_env}")
# Plan-level overrides project-level
plan_env = resolver.resolve(plan_env="host", project_env="container")
if plan_env != ExecutionEnvironment.HOST:
_fail(f"expected HOST plan override: {plan_env}")
# Validate container availability with container-instance type
is_valid = resolver.validate_container_available(["container-instance"])
if not is_valid:
_fail("container-instance should be valid container resource")
print("wf17-env-ok")
finally:
shutil.rmtree(repo, ignore_errors=True)
cleanup_workspace(ws)
def wf17_plan_with_container() -> None:
"""Step 3: plan use with --automation-profile trusted."""
ws = setup_workspace(prefix="wf17_plan_")
repo = _make_repo()
yaml = write_yaml(_ACTION_YAML)
try:
pid = _setup_full(ws, repo, yaml)
r = _cli(ws, "plan", "status", pid, "--format", "plain")
if pid not in r.stdout:
_fail(f"plan_id missing:\n{r.stdout}")
print("wf17-plan-ok")
finally:
os.unlink(yaml)
shutil.rmtree(repo, ignore_errors=True)
cleanup_workspace(ws)
def wf17_execute_routed_to_container() -> None:
"""Step 4: plan execute + domain-level container routing verification."""
ws = setup_workspace(prefix="wf17_exec_")
repo = _make_repo()
yaml = write_yaml(_ACTION_YAML)
try:
pid = _setup_full(ws, repo, yaml)
# CLI plan execute — controlled rejection (no real LLM)
r = run_cli("plan", "execute", pid, workspace=ws, env_extra=_ENV)
out = r.stdout + r.stderr
if "INTERNAL" in out or "Traceback" in out:
_fail(f"plan execute crashed:\n{out}")
# Domain-level container activation with mock runner
clear_lifecycle_registry()
runner = _MockRunner()
runner.set_up_result(cid="ff17a0e1c0112233aabb", ws="/workspaces/data")
rid = "01WF17TEST0000000000010"
tracker = activate_container(rid, repo, run_command=runner, session_id=pid)
if tracker.current_state != ContainerLifecycleState.RUNNING:
_fail(f"expected RUNNING: {tracker.current_state}")
if tracker.container_id != "ff17a0e1c0112233aabb":
_fail(f"container_id: {tracker.container_id}")
if tracker.workspace_path != "/workspaces/data":
_fail(f"workspace_path: {tracker.workspace_path}")
if tracker.host_workspace_path != os.path.realpath(repo):
_fail(f"host_workspace_path: {tracker.host_workspace_path}")
if len(runner.up_calls) != 1:
_fail(f"up_calls: {len(runner.up_calls)}")
if "--workspace-folder" not in runner.up_calls[0][0]:
_fail(f"missing --workspace-folder: {runner.up_calls[0][0]}")
# Verify session ID propagation
retrieved = get_lifecycle_tracker(rid)
if retrieved.session_id != pid:
_fail(f"session_id: {retrieved.session_id}")
# Verify execution environment resolver routes to container
resolver = ExecutionEnvironmentResolver()
env = resolver.resolve_and_validate(
linked_resource_types=["container-instance", "git-checkout"],
project_name="local/wf17-project",
project_env="container",
)
if env != ExecutionEnvironment.CONTAINER:
_fail(f"expected CONTAINER resolved env: {env}")
print("wf17-exec-ok")
finally:
clear_lifecycle_registry()
os.unlink(yaml)
shutil.rmtree(repo, ignore_errors=True)
cleanup_workspace(ws)
def wf17_apply_from_container() -> None:
"""Step 5: plan apply + domain-level mount path verification."""
ws = setup_workspace(prefix="wf17_apply_")
repo = _make_repo()
yaml = write_yaml(_ACTION_YAML)
try:
pid = _setup_full(ws, repo, yaml)
# CLI lifecycle-apply — controlled rejection
r = run_cli("plan", "lifecycle-apply", pid, workspace=ws, env_extra=_ENV)
out = r.stdout + r.stderr
if "INTERNAL" in out or "Traceback" in out:
_fail(f"lifecycle-apply crashed:\n{out}")
# Domain-level dual-mount path tracking
clear_lifecycle_registry()
runner = _MockRunner()
runner.set_up_result(cid="ff17a00e00112233aabb", ws="/workspaces/data")
rid = "01WF17TEST0000000000020"
activate_container(rid, repo, run_command=runner, session_id=pid)
t = get_lifecycle_tracker(rid)
# Verify rw mount (host workspace path = repo)
if t.host_workspace_path != os.path.realpath(repo):
_fail(f"host_workspace_path: {t.host_workspace_path}")
# Verify container workspace path
if t.workspace_path != "/workspaces/data":
_fail(f"workspace_path: {t.workspace_path}")
# Stop container (simulates post-apply cleanup)
stop_container(rid, run_command=runner)
final = get_lifecycle_tracker(rid)
if final.current_state != ContainerLifecycleState.STOPPED:
_fail(f"expected STOPPED: {final.current_state}")
if len(runner.stop_calls) != 1:
_fail(f"stop_calls: {len(runner.stop_calls)}")
print("wf17-apply-ok")
finally:
clear_lifecycle_registry()
os.unlink(yaml)
shutil.rmtree(repo, ignore_errors=True)
cleanup_workspace(ws)
# -- Dispatcher -------------------------------------------------------------
_COMMANDS: dict[str, Callable[[], None]] = {
"wf17-container-instance-creation": wf17_container_instance_creation,
"wf17-project-execution-env": wf17_project_execution_env,
"wf17-plan-with-container": wf17_plan_with_container,
"wf17-execute-routed-to-container": wf17_execute_routed_to_container,
"wf17-apply-from-container": wf17_apply_from_container,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print(f"Usage: helper_wf17_explicit_container.py <{'|'.join(_COMMANDS)}>")
return 1
cmd = sys.argv[1]
handler = _COMMANDS.get(cmd)
if handler is None:
print(f"Unknown command: {cmd}")
return 1
reset_global_state()
handler()
return 0
if __name__ == "__main__":
sys.exit(main())
+74
View File
@@ -0,0 +1,74 @@
*** Settings ***
Documentation Integration test for Specification Workflow Example 17:
... Explicit Container with Directory Mount.
...
... Exercises explicit container-instance creation with
... dual mounts (resource-ref rw + host-path ro), project
... execution environment override, and container tool
... routing using mocked LLM providers and mocked
... container operations.
Force Tags wf17 integration explicit-container
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment With Database Isolation
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_wf17_explicit_container.py
*** Test Cases ***
WF17 Step 1 Container Instance Creation
[Documentation] Register container-instance resource with dual
... mounts (resource-ref rw + host-path ro). Verify
... the resource is persisted with image and mount
... metadata.
[Timeout] 120s
${result}= Run Process ${PYTHON} ${HELPER} wf17-container-instance-creation cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} wf17-container-ok
WF17 Step 2 Project Execution Environment
[Documentation] Create project, link container-instance resource,
... and set execution environment with override
... priority (project-level ``container``).
[Timeout] 120s
${result}= Run Process ${PYTHON} ${HELPER} wf17-project-execution-env cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} wf17-env-ok
WF17 Step 3 Plan With Container
[Documentation] Create action and ``plan use`` with trusted
... automation profile targeting the container
... project. Verify plan is created in strategize
... phase.
[Timeout] 120s
${result}= Run Process ${PYTHON} ${HELPER} wf17-plan-with-container cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} wf17-plan-ok
WF17 Step 4 Execute Routed To Container
[Documentation] ``plan execute`` routes tool invocations to the
... container. Verify execution routes through mocked
... container operations and session ID propagation.
[Timeout] 120s
${result}= Run Process ${PYTHON} ${HELPER} wf17-execute-routed-to-container cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} wf17-exec-ok
WF17 Step 5 Apply From Container
[Documentation] ``plan apply --yes`` writes changes back to host
... via bind mount. Verify changes synced from
... container and container stopped after apply.
[Timeout] 120s
${result}= Run Process ${PYTHON} ${HELPER} wf17-apply-from-container cwd=${WORKSPACE} timeout=120s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} wf17-apply-ok