Files
cleveragents-core/features/steps/subplan_model_steps.py
T

485 lines
15 KiB
Python

"""Step definitions for subplan model hierarchy, defaults, and guardrails."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.plan import (
AutomationLevel,
ExecutionMode,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
ProcessingState,
SubplanConfig,
SubplanFailureHandler,
SubplanMergeStrategy,
SubplanStatus,
can_transition,
)
_R = "01ARZ3NDEKTSV4RRFFQ69G5FAV" # root ULID
_P = "01ARZ3NDEKTSV4RRFFQ69G5FAW" # parent ULID
_C = "01ARZ3NDEKTSV4RRFFQ69G5FAX" # child ULID
_G = "01ARZ3NDEKTSV4RRFFQ69G5FAY" # grandchild ULID
_S = "01KH72MDGQMPRW3R5WPZVB8KC1" # status ULID
def _plan(
pid: str = _R,
par: str | None = None,
root: str | None = None,
att: int = 1,
ph: PlanPhase = PlanPhase.STRATEGIZE,
ps: ProcessingState = ProcessingState.QUEUED,
al: AutomationLevel = AutomationLevel.MANUAL,
sc: SubplanConfig | None = None,
ss: list[SubplanStatus] | None = None,
) -> Plan:
"""Build a Plan with sensible defaults for subplan testing."""
return Plan(
identity=PlanIdentity(
plan_id=pid,
parent_plan_id=par,
root_plan_id=root,
attempt=att,
),
namespaced_name=NamespacedName(
server=None,
namespace="local",
name="subplan-test",
),
description="Subplan test plan",
action_name="local/subplan-action",
phase=ph,
processing_state=ps,
automation_level=al,
subplan_config=sc,
subplan_statuses=ss or [],
)
def _st(err: str | None = None, att: int = 1) -> SubplanStatus:
"""Build a SubplanStatus."""
return SubplanStatus(
subplan_id=_S,
action_name="local/sub-task",
status=ProcessingState.ERRORED if err else ProcessingState.QUEUED,
error=err,
attempt_number=att,
target_resources=["src/main.py"],
)
# -- Given: plan creation --
@given("I create a subplan-test root plan")
def step_g01(ctx: Context) -> None:
"""Create root plan."""
ctx.subplan_plan = _plan()
@given("I create a subplan-test plan with root_plan_id equal to plan_id")
def step_g02(ctx: Context) -> None:
"""Create root plan with explicit root_plan_id."""
ctx.subplan_plan = _plan(root=_R)
@given("I create a subplan-test child plan with a parent")
def step_g03(ctx: Context) -> None:
"""Create child plan."""
ctx.subplan_plan = _plan(pid=_C, par=_P, root=_R)
@given("I create a subplan-test three-level hierarchy")
def step_g05(ctx: Context) -> None:
"""Create root/parent/grandchild."""
ctx.subplan_root = _plan(pid=_R, root=_R)
ctx.subplan_parent = _plan(pid=_P, par=_R, root=_R)
ctx.subplan_grandchild = _plan(pid=_G, par=_P, root=_R)
@given("I create a subplan-test default plan")
def step_g06(ctx: Context) -> None:
"""Create default plan."""
ctx.subplan_plan = _plan()
@given("I create a subplan-test plan with sequential subplan config")
def step_g07(ctx: Context) -> None:
"""Plan with sequential config."""
ctx.subplan_plan = _plan(
sc=SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
merge_strategy=SubplanMergeStrategy.GIT_THREE_WAY,
fail_fast=False,
)
)
@given("I create a subplan-test plan with parallel subplan config")
def step_g08(ctx: Context) -> None:
"""Plan with parallel config."""
ctx.subplan_plan = _plan(
sc=SubplanConfig(
execution_mode=ExecutionMode.PARALLEL,
max_parallel=10,
retry_failed=True,
max_retries=3,
)
)
@given("I create a subplan-test default subplan config")
def step_g09(ctx: Context) -> None:
"""Standalone SubplanConfig with defaults."""
ctx.subplan_config = SubplanConfig()
@given("I create a subplan-test plan with one subplan status")
def step_g10(ctx: Context) -> None:
"""Plan with one status."""
ctx.subplan_plan = _plan(ss=[_st()])
@given("I create a subplan-test fail-fast config with errored status")
def step_g13(ctx: Context) -> None:
"""Fail-fast config + errored status."""
ctx.subplan_handler_config = SubplanConfig(
execution_mode=ExecutionMode.PARALLEL, fail_fast=True
)
ctx.subplan_handler_status = _st("TimeoutError: timed out")
@given("I create a subplan-test parallel-no-fail-fast config with errored status")
def step_g14(ctx: Context) -> None:
"""Parallel (no fail_fast) + errored status."""
ctx.subplan_handler_config = SubplanConfig(
execution_mode=ExecutionMode.PARALLEL, fail_fast=False
)
ctx.subplan_handler_status = _st("TimeoutError: timed out")
@given("I create a subplan-test config with retriable timeout error")
def step_g15(ctx: Context) -> None:
"""Config + retriable TimeoutError."""
ctx.subplan_handler_config = SubplanConfig(retry_failed=True, max_retries=2)
ctx.subplan_handler_status = _st("TimeoutError: op timed out", 1)
@given("I create a subplan-test config with non-retriable configuration error")
def step_g16(ctx: Context) -> None:
"""Config + non-retriable ConfigurationError."""
ctx.subplan_handler_config = SubplanConfig(retry_failed=True, max_retries=2)
ctx.subplan_handler_status = _st("ConfigurationError: bad", 1)
@given("I create a subplan-test config with retries disabled and retriable error")
def step_g17(ctx: Context) -> None:
"""Config (retries off) + retriable error."""
ctx.subplan_handler_config = SubplanConfig(retry_failed=False)
ctx.subplan_handler_status = _st("TimeoutError: timed out", 1)
@given("I create a subplan-test config with max retries exceeded")
def step_g18(ctx: Context) -> None:
"""Config + attempt > max_retries."""
ctx.subplan_handler_config = SubplanConfig(retry_failed=True, max_retries=2)
ctx.subplan_handler_status = _st("TimeoutError: timed out", 3)
@given("I build a subplan-test plan with the child identity")
def step_g19(ctx: Context) -> None:
"""Alias plan for CLI dict assertion."""
ctx.subplan_cli_plan = ctx.subplan_plan
@given("I build a subplan-test plan with the root identity")
def step_g20(ctx: Context) -> None:
"""Alias plan for CLI dict assertion."""
ctx.subplan_cli_plan = ctx.subplan_plan
# -- When: validation errors --
@when("I try to create a subplan-test identity with invalid parent ULID")
def step_w01(ctx: Context) -> None:
"""Invalid parent_plan_id."""
ctx.error = None
try:
PlanIdentity(plan_id=_R, parent_plan_id="not-a-valid-ulid")
except ValidationError as e:
ctx.error = e
@when("I try to create a subplan-test config with max_parallel {v:d}")
def step_w02(ctx: Context, v: int) -> None:
"""Invalid max_parallel."""
ctx.error = None
try:
SubplanConfig(max_parallel=v)
except ValidationError as e:
ctx.error = e
@when("I try to create a subplan-test config with max_retries {v:d}")
def step_w03(ctx: Context, v: int) -> None:
"""Invalid max_retries."""
ctx.error = None
try:
SubplanConfig(max_retries=v)
except ValidationError as e:
ctx.error = e
# -- Then: plan identity --
@then("the subplan-test plan should not be a subplan")
def step_t01(ctx: Context) -> None:
"""Not a subplan."""
assert not ctx.subplan_plan.is_subplan
@then("the subplan-test plan should be a subplan")
def step_t02(ctx: Context) -> None:
"""Is a subplan."""
assert ctx.subplan_plan.is_subplan
@then("the subplan-test plan should be a root plan")
def step_t03(ctx: Context) -> None:
"""Is root."""
assert ctx.subplan_plan.is_root_plan
@then("the subplan-test plan should not be a root plan")
def step_t04(ctx: Context) -> None:
"""Not root."""
assert not ctx.subplan_plan.is_root_plan
@then("the subplan-test plan depth should be {e:d}")
def step_t05(ctx: Context, e: int) -> None:
"""Depth check."""
assert ctx.subplan_plan.depth == e
@then("the subplan-test plan attempt should be {e:d}")
def step_t06(ctx: Context, e: int) -> None:
"""Attempt check."""
assert ctx.subplan_plan.identity.attempt == e
@then('the subplan-test plan phase should be "{e}"')
def step_t07(ctx: Context, e: str) -> None:
"""Phase check."""
assert ctx.subplan_plan.phase.value == e
@then('the subplan-test plan processing state should be "{e}"')
def step_t08(ctx: Context, e: str) -> None:
"""Processing state check."""
assert ctx.subplan_plan.processing_state.value == e
@then("the subplan-test plan should not be terminal")
def step_t09(ctx: Context) -> None:
"""Not terminal."""
assert not ctx.subplan_plan.is_terminal
@then("the subplan-test plan should not be errored")
def step_t10(ctx: Context) -> None:
"""Not errored."""
assert not ctx.subplan_plan.is_errored
@then('the subplan-test plan automation level should be "{e}"')
def step_t11(ctx: Context, e: str) -> None:
"""Automation level check."""
assert ctx.subplan_plan.automation_level.value == e
@then("the subplan-test plan subplan_config should be none")
def step_t12(ctx: Context) -> None:
"""No subplan config."""
assert ctx.subplan_plan.subplan_config is None
@then("the subplan-test plan should have {c:d} subplan statuses")
def step_t13(ctx: Context, c: int) -> None:
"""Status count check."""
assert len(ctx.subplan_plan.subplan_statuses) == c
# -- Then: subplan config on plan --
@then('the subplan-test plan subplan_config execution_mode should be "{e}"')
def step_t14(ctx: Context, e: str) -> None:
"""Config execution_mode."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.execution_mode.value == e
@then('the subplan-test plan subplan_config merge_strategy should be "{e}"')
def step_t15(ctx: Context, e: str) -> None:
"""Config merge_strategy."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.merge_strategy.value == e
@then("the subplan-test plan subplan_config fail_fast should be {e}")
def step_t16(ctx: Context, e: str) -> None:
"""Config fail_fast."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.fail_fast is (e.lower() == "true")
@then("the subplan-test plan subplan_config max_parallel should be {e:d}")
def step_t17(ctx: Context, e: int) -> None:
"""Config max_parallel."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.max_parallel == e
@then("the subplan-test plan subplan_config retry_failed should be {e}")
def step_t18(ctx: Context, e: str) -> None:
"""Config retry_failed."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.retry_failed is (e.lower() == "true")
@then("the subplan-test plan subplan_config max_retries should be {e:d}")
def step_t19(ctx: Context, e: int) -> None:
"""Config max_retries."""
assert ctx.subplan_plan.subplan_config is not None
assert ctx.subplan_plan.subplan_config.max_retries == e
# -- Then: standalone SubplanConfig defaults --
@then('the subplan-test subplan config execution_mode should be "{e}"')
def step_t20(ctx: Context, e: str) -> None:
"""Standalone config execution_mode."""
assert ctx.subplan_config.execution_mode.value == e
@then('the subplan-test subplan config merge_strategy should be "{e}"')
def step_t21(ctx: Context, e: str) -> None:
"""Standalone config merge_strategy."""
assert ctx.subplan_config.merge_strategy.value == e
@then("the subplan-test subplan config max_parallel should be {e:d}")
def step_t22(ctx: Context, e: int) -> None:
"""Standalone config max_parallel."""
assert ctx.subplan_config.max_parallel == e
@then("the subplan-test subplan config fail_fast should be {e}")
def step_t23(ctx: Context, e: str) -> None:
"""Standalone config fail_fast."""
assert ctx.subplan_config.fail_fast is (e.lower() == "true")
@then("the subplan-test subplan config retry_failed should be {e}")
def step_t24(ctx: Context, e: str) -> None:
"""Standalone config retry_failed."""
assert ctx.subplan_config.retry_failed is (e.lower() == "true")
@then("the subplan-test subplan config max_retries should be {e:d}")
def step_t25(ctx: Context, e: int) -> None:
"""Standalone config max_retries."""
assert ctx.subplan_config.max_retries == e
@then("the subplan-test subplan config timeout should be none")
def step_t26(ctx: Context) -> None:
"""Standalone config timeout is None."""
assert ctx.subplan_config.timeout_per_subplan_seconds is None
# -- Then: has_subplans --
@then("the subplan-test plan should have subplans")
def step_t27(ctx: Context) -> None:
"""Has subplans."""
assert ctx.subplan_plan.has_subplans
@then("the subplan-test plan should not have subplans")
def step_t28(ctx: Context) -> None:
"""No subplans."""
assert not ctx.subplan_plan.has_subplans
# -- Then: hierarchy --
@then("the subplan-test grandchild root_plan_id should match the root plan_id")
def step_t29(ctx: Context) -> None:
"""Grandchild root matches root."""
assert (
ctx.subplan_grandchild.identity.root_plan_id
== ctx.subplan_root.identity.plan_id
)
# -- Then: transitions --
@then('subplan-test transition from "{f}" to "{t}" should be valid')
def step_t30(ctx: Context, f: str, t: str) -> None:
"""Valid transition."""
assert can_transition(PlanPhase(f), PlanPhase(t))
@then('subplan-test transition from "{f}" to "{t}" should be invalid')
def step_t31(ctx: Context, f: str, t: str) -> None:
"""Invalid transition."""
assert not can_transition(PlanPhase(f), PlanPhase(t))
# -- Then: failure handler --
@then("the subplan-test failure handler should stop others")
def step_t34(ctx: Context) -> None:
"""Handler stops others."""
h = SubplanFailureHandler()
assert h.should_stop_others(ctx.subplan_handler_config, ctx.subplan_handler_status)
@then("the subplan-test failure handler should not stop others")
def step_t35(ctx: Context) -> None:
"""Handler does not stop others."""
h = SubplanFailureHandler()
assert not h.should_stop_others(
ctx.subplan_handler_config, ctx.subplan_handler_status
)
@then("the subplan-test failure handler should retry")
def step_t36(ctx: Context) -> None:
"""Handler retries."""
h = SubplanFailureHandler()
assert h.should_retry(ctx.subplan_handler_config, ctx.subplan_handler_status)
@then("the subplan-test failure handler should not retry")
def step_t37(ctx: Context) -> None:
"""Handler does not retry."""
h = SubplanFailureHandler()
assert not h.should_retry(ctx.subplan_handler_config, ctx.subplan_handler_status)
# -- Then: CLI dict --
@then("the subplan-test cli dict should contain parent_plan_id")
def step_t38(ctx: Context) -> None:
"""CLI dict has parent_plan_id."""
assert "parent_plan_id" in ctx.subplan_cli_plan.as_cli_dict()
@then("the subplan-test cli dict should contain subplan_count of {e:d}")
def step_t39(ctx: Context, e: int) -> None:
"""CLI dict subplan_count."""
assert ctx.subplan_plan.as_cli_dict().get("subplan_count") == e
@then("the subplan-test cli dict should not contain parent_plan_id")
def step_t40(ctx: Context) -> None:
"""CLI dict no parent_plan_id."""
assert "parent_plan_id" not in ctx.subplan_cli_plan.as_cli_dict()