"""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 ( 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, 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, 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 profile should be none") def step_t11(ctx: Context) -> None: """Automation profile defaults to None.""" assert ctx.subplan_plan.automation_profile is None @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()