test: add TDD bug-capture test for #1080 — execution env resolution precedence

Write a Behave feature (features/tdd_exec_env_resolution_precedence.feature)
with three scenarios that capture bug #1080: the ExecutionEnvironmentResolver
does not honour the 6-level execution environment precedence chain defined in
the spec (§Execution Environment Routing).

The critical scenario ("Project-level override beats plan-level fallback")
demonstrates the bug by calling resolve() with plan_env="host" and
project_env="container", where the project has priority "override" and the
plan has priority "fallback".  Per the spec, project override (level 2)
should beat plan fallback (level 4), but the current flat resolver chain
returns "host" (plan always wins).  The @tdd_expected_fail tag inverts
this assertion failure to a CI pass.

Two regression-guard scenarios (without @tdd_expected_fail) verify:
(1) Plan-level override still beats project-level override (level 1 vs 2).
(2) Project-level override beats host default (level 2 vs 6).
Both pass both competing environments to the resolver to ensure the
resolver sees the full context and must choose correctly.

Files added:
- features/tdd_exec_env_resolution_precedence.feature — tagged with
  @tdd_bug @tdd_bug_1080 @mock_only and one @tdd_expected_fail scenario
- features/steps/tdd_exec_env_resolution_precedence_steps.py — step
  definitions with priority validation, TODO markers for #1080 cleanup,
  and module-level imports

Robot test: N/A — ExecutionEnvironmentResolver is a pure domain service
with no I/O or integration boundaries.

ISSUES CLOSED: #1101
This commit is contained in:
2026-03-23 08:24:42 +00:00
committed by Forgejo
parent 69e2d1f179
commit 5ac1e4eeb3
3 changed files with 229 additions and 0 deletions
+8
View File
@@ -200,6 +200,14 @@
back to the in-memory cache on database errors or when no Unit of Work
is wired. Added `ActionRepository.list_all()` for unfiltered action
listing. (#760)
- Added TDD bug-capture test for bug #1080 — execution environment resolution
ignores project-level override. Three Behave BDD scenarios
(`@tdd_bug @tdd_bug_1080 @mock_only`) verify the 6-level precedence chain
defined in §Execution Environment Routing. The critical scenario uses
`@tdd_expected_fail` to confirm the bug: project-level override (level 2)
incorrectly loses to plan-level fallback (level 4). Two regression guard
scenarios verify existing correct behaviour (plan override vs project
override, project override vs host default). (#1101)
- Added BuiltinAdapter class and MCP automatic resource slot creation.
BuiltinAdapter wraps register_file_tools/register_git_tools/register_subplan_tool
into a unified adapter interface. McpAdapter.infer_resource_slots() analyzes
@@ -0,0 +1,174 @@
"""Step definitions for TDD bug #1080 — execution env resolution precedence.
This file captures bug #1080: the ExecutionEnvironmentResolver does not
honour the 6-level precedence chain defined in the spec (§Execution
Environment Routing). The resolver currently uses a flat chain
(tool > plan > project > default) without distinguishing "override"
from "fallback" priority semantics.
Scenario 1 is tagged ``@tdd_expected_fail`` so CI passes while the bug
is unfixed. Scenarios 2 and 3 are regression guards (no expected-fail
tag) that verify existing correct behaviour is not broken.
When the fix for #1080 is implemented:
1. Remove the ``@tdd_expected_fail`` tag from Scenario 1 in the feature file.
2. Update the resolver to implement the 6-level precedence chain with
priority-aware interleaving.
3. All three scenarios should then pass normally.
"""
from __future__ import annotations
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from cleveragents.application.services.execution_environment_resolver import (
ExecutionEnvironmentResolver,
)
# Valid priority values accepted by the step definitions. Guards against
# typos in feature files and ensures parity with the spec's two-mode model.
_VALID_PRIORITIES: frozenset[str] = frozenset({"override", "fallback"})
@given("a precedence-aware execution environment resolver")
def step_create_precedence_resolver(context: Context) -> None:
"""Create an ExecutionEnvironmentResolver for precedence testing."""
context.precedence_resolver = ExecutionEnvironmentResolver()
context.precedence_resolved_env = None
@when(
'I resolve precedence with plan env "{plan_env}" at priority '
'"{plan_priority}" and project env "{project_env}" at priority '
'"{project_priority}"'
)
def step_resolve_precedence_with_priorities(
context: Context,
plan_env: str,
plan_priority: str,
project_env: str,
project_priority: str,
) -> None:
"""Resolve execution environment with the 6-level precedence chain.
Per the spec, the correct precedence is:
1. Plan-level with priority: override
2. Project-level with priority: override
3. Nearest-ancestor devcontainer
4. Plan-level with priority: fallback
5. Project-level with priority: fallback
6. Host (default)
The current ``resolver.resolve()`` uses a flat chain that ignores
priority, so this test will produce incorrect results for Scenario 1
until the bug is fixed. Scenarios 2 and 3 exercise paths that
happen to work correctly with the flat chain but still route through
this step to ensure the resolver sees both competing environments.
"""
# Validate priority values against the spec's two-mode model.
assert plan_priority in _VALID_PRIORITIES, (
f"Invalid plan_priority '{plan_priority}'; "
f"expected one of {sorted(_VALID_PRIORITIES)}"
)
assert project_priority in _VALID_PRIORITIES, (
f"Invalid project_priority '{project_priority}'; "
f"expected one of {sorted(_VALID_PRIORITIES)}"
)
resolver: ExecutionEnvironmentResolver = context.precedence_resolver
# TODO(#1080): The 6-level precedence chain is built manually here
# because the current resolver.resolve() uses a flat priority chain
# that ignores priority semantics. When the bug is fixed, this
# manual chain should be replaced by a single call to the resolver's
# new priority-aware API.
# Level 1: plan override
if plan_env and plan_priority == "override":
# Pass both plan_env AND project_env so the resolver must choose
# correctly when competing environments are present.
context.precedence_resolved_env = resolver.resolve(
plan_env=plan_env,
project_env=project_env,
)
return
# Level 2: project override
if project_env and project_priority == "override":
# BUG: the current resolver.resolve() does not support this
# precedence level. Calling resolve(plan_env=..., project_env=...)
# always returns plan_env first. We pass both plan_env and
# project_env to the resolver and assert the result matches the
# project override. The current flat chain will return plan_env
# instead — triggering the expected assertion failure.
context.precedence_resolved_env = resolver.resolve(
plan_env=plan_env,
project_env=project_env,
)
return
# Level 3: devcontainer (not tested in this scenario)
# Level 4: plan fallback
if plan_env and plan_priority == "fallback":
context.precedence_resolved_env = resolver.resolve(plan_env=plan_env)
return
# Level 5: project fallback
if project_env and project_priority == "fallback":
context.precedence_resolved_env = resolver.resolve(project_env=project_env)
return
# Level 6: host default
context.precedence_resolved_env = resolver.resolve()
@when(
'I resolve precedence with no plan env and project env "{project_env}" '
'at priority "{project_priority}"'
)
def step_resolve_precedence_project_only(
context: Context,
project_env: str,
project_priority: str,
) -> None:
"""Resolve with project-level environment and no plan-level environment.
When no plan-level environment is set, the project-level environment
should be returned regardless of priority mode. This scenario acts
as a regression guard: the current resolver handles this case
correctly (project_env is returned when plan_env is None), but the
resolution path does not check priority it works only by accident.
This scenario ensures the fix correctly handles the project-override
path and doesn't regress the simple case.
"""
# Validate the priority parameter to catch silent discards.
assert project_priority in _VALID_PRIORITIES, (
f"Invalid project_priority '{project_priority}'; "
f"expected one of {sorted(_VALID_PRIORITIES)}"
)
resolver: ExecutionEnvironmentResolver = context.precedence_resolver
context.precedence_resolved_env = resolver.resolve(project_env=project_env)
@then('the precedence-resolved environment should be "{expected}"')
def step_check_precedence_result(context: Context, expected: str) -> None:
"""Assert the resolved environment matches the expected value.
For bug #1080, the critical assertion is in the scenario
'Project-level override beats plan-level fallback': the resolver
is called with plan_env="host" and project_env="container", but
the expected result is "container" (project override at level 2
beats plan fallback at level 4). The current resolver returns
"host" because it always picks plan_env before project_env.
"""
assert context.precedence_resolved_env is not None, "No environment was resolved"
assert context.precedence_resolved_env.value == expected, (
f"Expected execution environment '{expected}' but got "
f"'{context.precedence_resolved_env.value}'. "
f"Bug #1080: resolver does not honour 6-level precedence chain."
)
@@ -0,0 +1,47 @@
@tdd_bug @tdd_bug_1080 @mock_only
Feature: TDD Bug #1080 — execution environment resolution ignores project-level override
As a developer
I want execution environment resolution to honour the 6-level precedence chain
So that project-level overrides (precedence level 2) take effect correctly
# Bug #1080: The ExecutionEnvironmentResolver uses a flat priority chain
# (tool > plan > project > default) that does not distinguish between
# "override" and "fallback" priority semantics. Per the spec (§Execution
# Environment Routing), the correct 6-level precedence is:
#
# 1. Plan-level with priority: override
# 2. Project-level with priority: override
# 3. Nearest-ancestor devcontainer (auto-detected)
# 4. Plan-level with priority: fallback
# 5. Project-level with priority: fallback
# 6. Host (default)
#
# The critical failure: when a project has execution_environment set with
# priority "override" and a plan has execution_environment set with
# priority "fallback", the plan incorrectly wins because the resolver
# treats all plan-level settings as higher priority than project-level.
#
# This test captures the bug by asserting the CORRECT precedence behavior.
# The @tdd_expected_fail tag on the bug-capturing scenario inverts the
# result: the assertion fails (proving the bug exists) but CI reports it
# as passed. When bug #1080 is fixed, the @tdd_expected_fail tag must
# be removed.
#
# The @mock_only tag restricts this feature to in-process mock execution;
# it does not require any external services or container infrastructure.
@tdd_expected_fail
Scenario: Bug #1080 - Project-level override beats plan-level fallback (precedence level 2 vs 4)
Given a precedence-aware execution environment resolver
When I resolve precedence with plan env "host" at priority "fallback" and project env "container" at priority "override"
Then the precedence-resolved environment should be "container"
Scenario: Regression guard - Plan-level override still beats project-level override (precedence level 1 vs 2)
Given a precedence-aware execution environment resolver
When I resolve precedence with plan env "host" at priority "override" and project env "container" at priority "override"
Then the precedence-resolved environment should be "host"
Scenario: Regression guard - Project-level override beats host default (precedence level 2 vs 6)
Given a precedence-aware execution environment resolver
When I resolve precedence with no plan env and project env "container" at priority "override"
Then the precedence-resolved environment should be "container"