Files
cleveragents-core/features/steps/exec_env_precedence_steps.py
hamza.khyari 5e96b4bf80
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
feat(resource): implement 6-level execution environment precedence chain
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
2026-03-31 16:01:58 +00:00

175 lines
5.4 KiB
Python

"""Step definitions for exec_env_precedence.feature.
Self-contained step file for the 6-level execution environment
precedence chain. All imports are top-level and guarded.
"""
from __future__ import annotations
from behave import then, when
from cleveragents.application.services.execution_environment_resolver import (
ExecutionEnvironmentResolver,
)
def _resolver() -> ExecutionEnvironmentResolver:
return ExecutionEnvironmentResolver()
# ── 6-Level Precedence Chain ─────────────────────────────
@when(
'I resolve precedence with plan_env "{plan}" plan_priority "{pp}" '
'project_env "{proj}" project_priority "{projp}" '
"devcontainer {dc}"
)
def step_resolve_full(
context: object,
plan: str,
pp: str,
proj: str,
projp: str,
dc: str,
) -> None:
context.prec_resolved = _resolver().resolve_with_precedence(
plan_env=plan,
plan_priority=pp,
project_env=proj,
project_priority=projp,
devcontainer_available=dc.lower() == "true",
)
@when(
'I resolve precedence with tool_env "{tool}" plan_env "{plan}" '
'plan_priority "{pp}" devcontainer {dc}'
)
def step_resolve_tool(
context: object,
tool: str,
plan: str,
pp: str,
dc: str,
) -> None:
context.prec_resolved = _resolver().resolve_with_precedence(
tool_env=tool,
plan_env=plan,
plan_priority=pp,
devcontainer_available=dc.lower() == "true",
)
@when(
'I resolve precedence with project_env "{proj}" '
'project_priority "{projp}" devcontainer {dc}'
)
def step_resolve_proj(context: object, proj: str, projp: str, dc: str) -> None:
context.prec_resolved = _resolver().resolve_with_precedence(
project_env=proj,
project_priority=projp,
devcontainer_available=dc.lower() == "true",
)
@when("I resolve precedence with devcontainer {dc}")
def step_resolve_default(context: object, dc: str) -> None:
context.prec_resolved = _resolver().resolve_with_precedence(
devcontainer_available=dc.lower() == "true",
)
@when(
'I resolve precedence with plan_env "{plan}" project_env "{proj}" devcontainer {dc}'
)
def step_resolve_both_fb(context: object, plan: str, proj: str, dc: str) -> None:
context.prec_resolved = _resolver().resolve_with_precedence(
plan_env=plan,
project_env=proj,
devcontainer_available=dc.lower() == "true",
)
@then('the precedence resolved environment should be "{expected}"')
def step_check_resolved(context: object, expected: str) -> None:
actual = context.prec_resolved.value
assert actual == expected, f"Expected '{expected}', got '{actual}'"
# ── Devcontainer helpers ─────────────────────────────────
@when('I check has_devcontainer with types "{types}" for precedence')
def step_has_dc(context: object, types: str) -> None:
type_list = [t.strip() for t in types.split(",")]
context.prec_has_dc = _resolver().has_devcontainer(type_list)
@then("has_devcontainer should be {expected} for precedence")
def step_has_dc_check(context: object, expected: str) -> None:
assert context.prec_has_dc == (expected.lower() == "true")
# ── ContextConfig priority field ─────────────────────────
@when('I create a ContextConfig with priority "{priority}" for precedence')
def step_ctx_config(context: object, priority: str) -> None:
from cleveragents.domain.models.core.project import ContextConfig
context.prec_ctx_config = ContextConfig(
execution_environment="container",
execution_env_priority=priority,
)
@then('the context config priority should be "{priority}" for precedence')
def step_ctx_config_check(context: object, priority: str) -> None:
assert context.prec_ctx_config.execution_env_priority == priority
# ── DAG walk ─────────────────────────────────────────────
@when('I resolve with DAG walk from "{rid}" with devcontainer ancestor for precedence')
def step_dag_with_dc(context: object, rid: str) -> None:
context.prec_resolved = _resolver().resolve_with_dag(
resource_id=rid,
linked_resource_types={
rid: "fs-file",
"res-dir": "fs-directory",
"res-dc": "devcontainer-instance",
},
parent_map={rid: ["res-dir"], "res-dir": ["res-dc"]},
)
@when(
'I resolve with DAG walk from "{rid}" without devcontainer ancestor for precedence'
)
def step_dag_no_dc(context: object, rid: str) -> None:
context.prec_resolved = _resolver().resolve_with_dag(
resource_id=rid,
linked_resource_types={
rid: "fs-file",
"res-dir": "fs-directory",
"res-checkout": "git-checkout",
},
parent_map={rid: ["res-dir"], "res-dir": ["res-checkout"]},
)
# ── Container reference ──────────────────────────────────
@when('I parse container ref "{ref}" for precedence')
def step_parse_ref(context: object, ref: str) -> None:
_, name = _resolver().parse_container_ref(ref)
context.prec_ref_name = name
@then('the container ref name should be "{name}" for precedence')
def step_ref_name(context: object, name: str) -> None:
assert context.prec_ref_name == name