Files
placeholder/features/steps/exec_env_project_override_steps.py
hamza.khyari cd9cb9e889 fix(cli): honour project-level execution-env-priority in resolution
Thread plan_env and project_env through the tool execution chain so
the ExecutionEnvironmentResolver receives project-level execution
environment values stored in ContextConfig.execution_environment.

The resolver's precedence logic (tool > plan > project > default) was
already correct, but callers never passed project_env — tools always
fell through to the global HOST default, ignoring project-level
overrides entirely.

Changes:
- PlanExecutionContext: add plan_env and project_env fields + properties
- ToolCallRouter: accept plan_env/project_env in constructor, pass to
  runner.execute() in route() and route_streaming()
- ToolCallingRuntime: accept plan_env/project_env, pass to
  runner.execute() in the direct-runner fallback path
- Update monkey-patched execute stubs in tool_router_steps.py to accept
  explicit plan_env/project_env keyword arguments
- Add 4 BDD scenarios proving plan_env/project_env reach the resolver
  via ToolCallRouter (project-only, plan-only, both, neither)

Dependency: requires PR #1135 to be merged first (adds CLI flag and
persistence for project-level execution-env-priority).

Closes #1080

ISSUES CLOSED: #1080
2026-03-31 11:31:00 +00:00

130 lines
4.3 KiB
Python

"""Step definitions for exec_env_project_override.feature.
Verifies that plan_env and project_env are threaded from
ToolCallRouter through to the ExecutionEnvironmentResolver
inside ToolRunner.execute().
Issue #1080.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from behave.runner import Context
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.router import ToolCallRouter
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.runtime import ToolSpec
__all__: list[str] = []
def _echo_tool(arguments: dict[str, Any]) -> dict[str, Any]:
"""Simple echo tool for testing."""
return {"echoed": arguments}
@given("ee1080- a tool registry with a registered echo tool")
def step_ee1080_registry(context: Context) -> None:
context.ee1080_registry = ToolRegistry()
spec = ToolSpec(
name="test/echo",
description="Echo tool for testing",
input_schema={"type": "object", "properties": {}},
handler=_echo_tool,
)
context.ee1080_registry.register(spec)
@given("ee1080- a tool runner and resolver spy")
def step_ee1080_runner(context: Context) -> None:
context.ee1080_runner = ToolRunner(context.ee1080_registry)
# Spy on _env_resolver.resolve to capture what it receives
context.ee1080_resolve_calls: list[dict[str, Any]] = [] # type: ignore[annotation-unchecked]
original_rv = context.ee1080_runner._env_resolver.resolve_and_validate
def spy_resolve_and_validate(**kwargs: Any) -> tuple[str, str | None]:
context.ee1080_resolve_calls.append(
{
"plan_env": kwargs.get("plan_env"),
"project_env": kwargs.get("project_env"),
}
)
return original_rv(**kwargs)
context.ee1080_runner._env_resolver.resolve_and_validate = spy_resolve_and_validate # type: ignore[method-assign]
@given('ee1080- a ToolCallRouter with project_env "{env}"')
def step_ee1080_router_project(context: Context, env: str) -> None:
context.ee1080_router = ToolCallRouter(
registry=context.ee1080_registry,
runner=context.ee1080_runner,
plan_id="ee1080-plan",
project_env=env,
)
@given('ee1080- a ToolCallRouter with plan_env "{env}"')
def step_ee1080_router_plan(context: Context, env: str) -> None:
context.ee1080_router = ToolCallRouter(
registry=context.ee1080_registry,
runner=context.ee1080_runner,
plan_id="ee1080-plan",
plan_env=env,
)
@given(
'ee1080- a ToolCallRouter with both plan_env "{plan}" and project_env "{project}"'
)
def step_ee1080_router_both(context: Context, plan: str, project: str) -> None:
context.ee1080_router = ToolCallRouter(
registry=context.ee1080_registry,
runner=context.ee1080_runner,
plan_id="ee1080-plan",
plan_env=plan,
project_env=project,
)
@given("ee1080- a ToolCallRouter without env params")
def step_ee1080_router_none(context: Context) -> None:
context.ee1080_router = ToolCallRouter(
registry=context.ee1080_registry,
runner=context.ee1080_runner,
plan_id="ee1080-plan",
)
@when("ee1080- I route a tool call through the router")
def step_ee1080_route(context: Context) -> None:
payload = {
"name": "test/echo",
"arguments": {"msg": "hello"},
}
context.ee1080_result = context.ee1080_router.route(payload)
@then('ee1080- the resolver should have received project_env "{expected}"')
def step_ee1080_check_project_env(context: Context, expected: str) -> None:
expected_val = None if expected == "None" else expected
calls = context.ee1080_resolve_calls
assert len(calls) > 0, "Resolver was never called"
actual = calls[-1]["project_env"]
assert actual == expected_val, (
f"Expected project_env={expected_val!r}, got {actual!r}"
)
@then('ee1080- the resolver should have received plan_env "{expected}"')
def step_ee1080_check_plan_env(context: Context, expected: str) -> None:
expected_val = None if expected == "None" else expected
calls = context.ee1080_resolve_calls
assert len(calls) > 0, "Resolver was never called"
actual = calls[-1]["plan_env"]
assert actual == expected_val, f"Expected plan_env={expected_val!r}, got {actual!r}"