Files
cleveragents-core/features/steps/execution_environment_steps.py
T
freemo ad5f737721
CI / lint (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 25s
CI / typecheck (pull_request) Successful in 35s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 38s
CI / build (pull_request) Successful in 21s
CI / unit_tests (pull_request) Successful in 3m42s
CI / docker (pull_request) Successful in 42s
CI / coverage (pull_request) Successful in 4m9s
CI / integration_tests (pull_request) Successful in 4m33s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 18s
CI / typecheck (push) Successful in 34s
CI / build (push) Successful in 19s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 48s
CI / integration_tests (push) Successful in 2m55s
CI / unit_tests (push) Successful in 3m10s
CI / docker (push) Successful in 59s
CI / coverage (push) Successful in 4m46s
CI / benchmark-publish (push) Successful in 14m18s
CI / benchmark-regression (pull_request) Successful in 26m2s
feat(execution): add execution environment routing
Add ExecutionEnvironment enum (host/container) to domain models, implement
execution environment resolution with priority chain (tool > plan > project
> default), wire the tool runner to check execution_environment before
execution, and add CLI flags to plan use/execute and project context set.

When container is selected but no container resource is available, a clear
ContainerUnavailableError is raised with an actionable message.

Closes #512
2026-03-03 14:48:55 -05:00

386 lines
13 KiB
Python

"""Step definitions for execution environment routing feature tests."""
from __future__ import annotations
from enum import StrEnum
from typing import Any
from behave import given, then, when
from behave.runner import Context
# ---------------------------------------------------------------------------
# Enum steps
# ---------------------------------------------------------------------------
@given("I import ExecutionEnvironment")
def step_import_enum(context: Context) -> None:
from cleveragents.domain.models.core.plan import ExecutionEnvironment
context.exec_env_cls = ExecutionEnvironment
@then('the HOST value should be "{value}"')
def step_host_value(context: Context, value: str) -> None:
assert context.exec_env_cls.HOST.value == value
@then('the CONTAINER value should be "{value}"')
def step_container_value(context: Context, value: str) -> None:
assert context.exec_env_cls.CONTAINER.value == value
@then("ExecutionEnvironment should be a StrEnum subclass")
def step_is_strenum(context: Context) -> None:
assert issubclass(context.exec_env_cls, StrEnum)
# ---------------------------------------------------------------------------
# Plan model steps
# ---------------------------------------------------------------------------
@given("I create a plan with default fields")
def step_plan_default(context: Context) -> None:
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
)
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
description="Test plan",
action_name="local/test-action",
definition_of_done="All tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
created_by="test",
reusable=True,
read_only=False,
)
@given('I create a plan with execution_environment "{env}"')
def step_plan_with_env(context: Context, env: str) -> None:
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
)
context.plan = Plan(
identity=PlanIdentity(plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV"),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
description="Test plan",
action_name="local/test-action",
execution_environment=env,
definition_of_done="All tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
created_by="test",
reusable=True,
read_only=False,
)
@then("the plan execution_environment should be None")
def step_plan_env_none(context: Context) -> None:
assert context.plan.execution_environment is None
@then('the plan execution_environment should be "{env}"')
def step_plan_env_value(context: Context, env: str) -> None:
assert context.plan.execution_environment == env
# ---------------------------------------------------------------------------
# ContextConfig model steps
# ---------------------------------------------------------------------------
@given("I create a ContextConfig with default fields")
def step_context_config_default(context: Context) -> None:
from cleveragents.domain.models.core.project import ContextConfig
context.context_config = ContextConfig()
@given('I create a ContextConfig with execution_environment "{env}"')
def step_context_config_with_env(context: Context, env: str) -> None:
from cleveragents.domain.models.core.project import ContextConfig
context.context_config = ContextConfig(execution_environment=env)
@then("the context config execution_environment should be None")
def step_context_config_env_none(context: Context) -> None:
assert context.context_config.execution_environment is None
@then('the context config execution_environment should be "{env}"')
def step_context_config_env_value(context: Context, env: str) -> None:
assert context.context_config.execution_environment == env
# ---------------------------------------------------------------------------
# Resolver steps
# ---------------------------------------------------------------------------
@given("I create an ExecutionEnvironmentResolver")
def step_create_resolver(context: Context) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ExecutionEnvironmentResolver,
)
context.resolver = ExecutionEnvironmentResolver()
context.resolved_env = None
context.raised_error: Exception | None = None
@when("I resolve with no overrides")
def step_resolve_no_overrides(context: Context) -> None:
context.resolved_env = context.resolver.resolve()
@when('I resolve with default "{default}"')
def step_resolve_default(context: Context, default: str) -> None:
context.resolved_env = context.resolver.resolve(default=default)
@when('I resolve with project_env "{project_env}"')
def step_resolve_project(context: Context, project_env: str) -> None:
context.resolved_env = context.resolver.resolve(project_env=project_env)
@when('I resolve with plan_env "{plan_env}" and project_env "{project_env}"')
def step_resolve_plan_project(
context: Context, plan_env: str, project_env: str
) -> None:
context.resolved_env = context.resolver.resolve(
plan_env=plan_env, project_env=project_env
)
@when('I resolve with tool_env "{tool_env}" and plan_env "{plan_env}"')
def step_resolve_tool_plan(context: Context, tool_env: str, plan_env: str) -> None:
context.resolved_env = context.resolver.resolve(
tool_env=tool_env, plan_env=plan_env
)
@when(
'I resolve the full chain with tool_env "{tool_env}" and plan_env '
'"{plan_env}" and project_env "{project_env}"'
)
def step_resolve_full_chain(
context: Context, tool_env: str, plan_env: str, project_env: str
) -> None:
context.resolved_env = context.resolver.resolve(
tool_env=tool_env, plan_env=plan_env, project_env=project_env
)
@when('I resolve with tool_env "{tool_env}"')
def step_resolve_tool_only(context: Context, tool_env: str) -> None:
try:
context.resolved_env = context.resolver.resolve(tool_env=tool_env)
except ValueError as exc:
context.raised_error = exc
@then('the resolved environment should be "{expected}"')
def step_resolved_env(context: Context, expected: str) -> None:
assert context.resolved_env is not None
assert context.resolved_env.value == expected
@then("exec-env a ValueError should be raised")
def step_value_error_raised(context: Context) -> None:
assert isinstance(context.raised_error, ValueError)
# ---------------------------------------------------------------------------
# Container availability steps
# ---------------------------------------------------------------------------
@when('I validate container available with types "{types_str}"')
def step_validate_container(context: Context, types_str: str) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
types = [t.strip() for t in types_str.split(",")]
try:
context.resolver.validate_container_available(types)
context.validation_ok = True
context.raised_error = None
except ContainerUnavailableError as exc:
context.validation_ok = False
context.raised_error = exc
@then("validation should succeed")
def step_validation_ok(context: Context) -> None:
assert context.validation_ok is True
@then("a ContainerUnavailableError should be raised")
def step_container_error_raised(context: Context) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
assert isinstance(context.raised_error, ContainerUnavailableError)
@when('I resolve_and_validate with project_env "{project_env}" and types "{types_str}"')
def step_resolve_validate_project(
context: Context, project_env: str, types_str: str
) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
types = [t.strip() for t in types_str.split(",")]
try:
context.resolved_env = context.resolver.resolve_and_validate(
linked_resource_types=types,
project_env=project_env,
)
context.raised_error = None
except ContainerUnavailableError as exc:
context.raised_error = exc
@when('I resolve_and_validate with plan_env "{plan_env}" and types "{types_str}"')
def step_resolve_validate_plan(context: Context, plan_env: str, types_str: str) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
types = [t.strip() for t in types_str.split(",")]
try:
context.resolved_env = context.resolver.resolve_and_validate(
linked_resource_types=types,
plan_env=plan_env,
)
context.raised_error = None
except ContainerUnavailableError as exc:
context.raised_error = exc
# ---------------------------------------------------------------------------
# ToolRunner integration steps
# ---------------------------------------------------------------------------
@given("I create a ToolRunner with a mock registry")
def step_create_tool_runner(context: Context) -> None:
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.runtime import ToolSpec
registry = ToolRegistry()
def echo_handler(inputs: dict[str, Any]) -> dict[str, Any]:
return {"echo": inputs}
spec = ToolSpec(
name="test/echo",
description="Echo tool",
input_schema={"type": "object", "properties": {}},
handler=echo_handler,
)
registry.register(spec)
context.runner = ToolRunner(registry)
context.tool_result = None
@when("I execute a tool with no environment overrides")
def step_execute_no_overrides(context: Context) -> None:
context.tool_result = context.runner.execute("test/echo", {"key": "value"})
@then("the tool should execute successfully on host")
def step_tool_success_host(context: Context) -> None:
assert context.tool_result is not None
assert context.tool_result.success is True
@when('I execute a tool with plan_env "{plan_env}" and no container resources')
def step_execute_container_no_resource(context: Context, plan_env: str) -> None:
context.tool_result = context.runner.execute(
"test/echo",
{"key": "value"},
plan_env=plan_env,
linked_resource_types=["git-checkout"],
)
@then("the tool result should have an error about container unavailable")
def step_tool_container_error(context: Context) -> None:
assert context.tool_result is not None
assert context.tool_result.success is False
assert "Container resource unavailable" in (context.tool_result.error or "")
# ---------------------------------------------------------------------------
# CONTAINER_RESOURCE_TYPES steps
# ---------------------------------------------------------------------------
@given("I import CONTAINER_RESOURCE_TYPES")
def step_import_types(context: Context) -> None:
from cleveragents.application.services.execution_environment_resolver import (
CONTAINER_RESOURCE_TYPES,
)
context.container_types = CONTAINER_RESOURCE_TYPES
@then('it should contain "{value}"')
def step_types_contain(context: Context, value: str) -> None:
assert value in context.container_types
@then('it should not contain "{value}"')
def step_types_not_contain(context: Context, value: str) -> None:
assert value not in context.container_types
# ---------------------------------------------------------------------------
# ContainerUnavailableError steps
# ---------------------------------------------------------------------------
@when('I create a ContainerUnavailableError for project "{name}"')
def step_create_error_with_project(context: Context, name: str) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
context.container_error = ContainerUnavailableError(name)
@when("I create a ContainerUnavailableError without project name")
def step_create_error_no_project(context: Context) -> None:
from cleveragents.application.services.execution_environment_resolver import (
ContainerUnavailableError,
)
context.container_error = ContainerUnavailableError()
@then('exec-env the error message should contain "{text}"')
def step_error_contains(context: Context, text: str) -> None:
assert text in str(context.container_error)
@then('exec-env the error message should not contain "{text}"')
def step_error_not_contains(context: Context, text: str) -> None:
assert text not in str(context.container_error)