feat(resource): implement 6-level execution environment precedence chain
CI / build (pull_request) Successful in 16s
CI / helm (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m47s
CI / typecheck (pull_request) Successful in 3m57s
CI / security (pull_request) Successful in 4m6s
CI / benchmark-publish (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 7m6s
CI / unit_tests (pull_request) Successful in 7m18s
CI / docker (pull_request) Successful in 1m19s
CI / coverage (pull_request) Successful in 11m49s
CI / e2e_tests (pull_request) Successful in 20m44s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 55m9s

Implement the spec's 6-level execution environment precedence chain
(spec lines 19324-19386):

1. Plan override (priority=override) — always wins
2. Project override (priority=override) — wins over devcontainer
3. Nearest-ancestor devcontainer — auto-discovered
4. Plan fallback (priority=fallback) — defers to devcontainer
5. Project fallback (priority=fallback) — defers to closer scopes
6. Host default — final fallback

- New resolve_with_precedence() API on ExecutionEnvironmentResolver
- Added execution_env_priority field to ContextConfig (project model)
- has_devcontainer() helper for devcontainer-instance detection
- Legacy 4-level resolve() preserved for backward compatibility
- _parse_priority() defaults missing priority to FALLBACK
- 13 new Behave scenarios testing all 6 levels + edge cases
- Updated CHANGELOG

ISSUES CLOSED: #877
This commit is contained in:
2026-03-19 04:09:42 +00:00
parent d27fb6d1f0
commit 5e96b4bf80
18 changed files with 712 additions and 96 deletions
@@ -383,3 +383,155 @@ def step_error_contains(context: Context, text: str) -> None:
@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