"""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('the container types should contain "{value}"') def step_types_contain(context: Context, value: str) -> None: assert value in context.container_types @then('the container types 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) # ── 6-Level Precedence Chain Steps (#877) ──────────────── def _resolver(): from cleveragents.application.services.execution_environment_resolver import ( ExecutionEnvironmentResolver, ) return ExecutionEnvironmentResolver() @when( 'I resolve with plan_env "{plan}" plan_priority "{pp}" ' 'project_env "{proj}" project_priority "{projp}" ' "devcontainer {dc}" ) def step_resolve_6level( context: Context, plan: str, pp: str, proj: str, projp: str, dc: str ) -> None: context.resolved_env = _resolver().resolve_with_precedence( plan_env=plan, plan_priority=pp, project_env=proj, project_priority=projp, devcontainer_available=dc.lower() == "true", ) @when( 'I resolve with tool_env "{tool}" plan_env "{plan}" ' 'plan_priority "{pp}" devcontainer {dc}' ) def step_resolve_tool_wins( context: Context, tool: str, plan: str, pp: str, dc: str ) -> None: context.resolved_env = _resolver().resolve_with_precedence( tool_env=tool, plan_env=plan, plan_priority=pp, devcontainer_available=dc.lower() == "true", ) @when( 'I resolve with project_env "{proj}" project_priority "{projp}" devcontainer {dc}' ) def step_resolve_project_prio(context: Context, proj: str, projp: str, dc: str) -> None: context.resolved_env = _resolver().resolve_with_precedence( project_env=proj, project_priority=projp, devcontainer_available=dc.lower() == "true", ) @when("I resolve with devcontainer {dc}") def step_resolve_dc_only(context: Context, dc: str) -> None: context.resolved_env = _resolver().resolve_with_precedence( devcontainer_available=dc.lower() == "true", ) @when('I resolve with plan_env "{plan}" project_env "{proj}" devcontainer {dc}') def step_resolve_both_fallback(context: Context, plan: str, proj: str, dc: str) -> None: context.resolved_env = _resolver().resolve_with_precedence( plan_env=plan, project_env=proj, devcontainer_available=dc.lower() == "true", ) @when('I check has_devcontainer with types "{types}"') def step_has_devcontainer(context: Context, types: str) -> None: type_list = [t.strip() for t in types.split(",")] context.has_dc_result = _resolver().has_devcontainer(type_list) @then("exec-env has_devcontainer should be {expected}") def step_has_dc_assert(context: Context, expected: str) -> None: assert context.has_dc_result == (expected.lower() == "true") @when('I create a ContextConfig with execution_env_priority "{priority}"') def step_context_config_priority(context: Context, priority: str) -> None: from cleveragents.domain.models.core.project import ContextConfig config = ContextConfig( execution_environment="container", execution_env_priority=priority, ) context.ctx_config = config @then('exec-env the context config priority should be "{priority}"') def step_ctx_config_priority_check(context: Context, priority: str) -> None: assert context.ctx_config.execution_env_priority == priority # ── DAG walk and container ref steps (#877 subtasks 2-4) ── @when('I resolve with DAG walk from "{resource_id}" with devcontainer ancestor') def step_dag_walk_with_dc(context: Context, resource_id: str) -> None: resolver = _resolver() # Simulate a DAG: res-file -> res-dir -> res-dc (devcontainer-instance) linked_types = { resource_id: "fs-file", "res-dir": "fs-directory", "res-dc": "devcontainer-instance", } parent_map = { resource_id: ["res-dir"], "res-dir": ["res-dc"], } context.resolved_env = resolver.resolve_with_dag( resource_id=resource_id, linked_resource_types=linked_types, parent_map=parent_map, ) @when('I resolve with DAG walk from "{resource_id}" without devcontainer ancestor') def step_dag_walk_no_dc(context: Context, resource_id: str) -> None: resolver = _resolver() linked_types = { resource_id: "fs-file", "res-dir": "fs-directory", "res-checkout": "git-checkout", } parent_map = { resource_id: ["res-dir"], "res-dir": ["res-checkout"], } context.resolved_env = resolver.resolve_with_dag( resource_id=resource_id, linked_resource_types=linked_types, parent_map=parent_map, ) @when('I parse container ref "{ref}"') def step_parse_container_ref(context: Context, ref: str) -> None: resolver = _resolver() env, name = resolver.parse_container_ref(ref) context.resolved_env = env context.container_ref_name = name @then('exec-env the container ref name should be "{name}"') def step_container_ref_name(context: Context, name: str) -> None: assert context.container_ref_name == name