"""Step definitions for models_lifecycle_coverage_r2.feature. Provides branch coverage for ``LifecycleActionModel`` and ``LifecyclePlanModel`` conversion helpers and plan helper functions in ``cleveragents.infrastructure.database.models``. All step-text uses an ``r2mod-`` prefix to avoid collisions with existing step definition files. """ from __future__ import annotations from datetime import UTC, datetime from typing import Any from behave import given, then, when from sqlalchemy.orm import attributes as sa_attr from cleveragents.infrastructure.database.models import ( ActionArgumentModel, ActionInvariantModel, LifecycleActionModel, LifecyclePlanModel, PlanArgumentModel, ) # =================================================================== # Helpers # =================================================================== _NOW_ISO: str = datetime.now(tz=UTC).isoformat() # Valid ULID constant for use in test models _ULID_PLAN = "01ARZ3NDEKTSV4RRFFQ69G5FA2" def _set_rel_none(model: Any, attr: str) -> None: """Bypass SQLAlchemy's instrumented setter to force a relationship to None. This lets us test the ``or []`` fallback branches in ``to_domain()``. """ state = sa_attr.instance_state(model) state.dict[attr] = None def _make_action_model( *, arguments: list[ActionArgumentModel] | None = None, invariants: list[ActionInvariantModel] | None = None, tags_json: str | None = "[]", inputs_schema_json: str | None = None, ) -> LifecycleActionModel: """Create a minimal ``LifecycleActionModel``.""" model = LifecycleActionModel( namespaced_name="local/test-action", namespace="local", name="test-action", description="Test action", long_description=None, definition_of_done="tests pass", strategy_actor="local/strat", execution_actor="local/exec", review_actor=None, apply_actor=None, estimation_actor=None, invariant_actor=None, automation_profile=None, reusable=True, read_only=False, inputs_schema_json=inputs_schema_json, state="available", created_by=None, tags_json=tags_json, created_at=_NOW_ISO, updated_at=_NOW_ISO, ) if arguments is not None: model.arguments_rel = arguments else: model.arguments_rel = [] if invariants is not None: model.invariants_rel = invariants else: model.invariants_rel = [] return model def _make_plan_model( *, automation_profile: str | None = None, sandbox_refs_json: str | None = None, validation_summary_json: str | None = None, tags_json: str | None = None, action_name: str | None = None, error_details_json: str | None = None, error_message: str | None = None, arguments: list[PlanArgumentModel] | None = None, ) -> LifecyclePlanModel: """Create a minimal ``LifecyclePlanModel``.""" model = LifecyclePlanModel( plan_id=_ULID_PLAN, parent_plan_id=None, root_plan_id=_ULID_PLAN, action_name=action_name or "", namespaced_name="local/test-plan", namespace="local", phase="action", processing_state="queued", attempt=1, description="Test plan", definition_of_done=None, strategy_actor=None, execution_actor=None, review_actor=None, apply_actor=None, estimation_actor=None, invariant_actor=None, automation_profile=automation_profile or "balanced", effective_profile_snapshot="{}", reusable=True, read_only=False, inputs_schema_json=None, changeset_id=None, sandbox_refs_json=sandbox_refs_json, validation_summary_json=validation_summary_json, decision_root_id=None, error_message=error_message, error_details_json=error_details_json, created_by=None, tags_json=tags_json or "[]", created_at=_NOW_ISO, updated_at=_NOW_ISO, completed_at=None, strategize_started_at=None, strategize_completed_at=None, execute_started_at=None, execute_completed_at=None, apply_started_at=None, applied_at=None, ) model.project_links_rel = [] model.invariants_rel = [] if arguments is not None: model.arguments_rel = arguments else: model.arguments_rel = [] return model # =================================================================== # LifecycleActionModel.to_domain() # =================================================================== @given("a r2mod-ActionModel with an argument having None defaults") def step_action_arg_none_defaults(context: Any) -> None: arg = ActionArgumentModel( name="arg1", arg_type="string", requirement="required", description="desc", default_value_json=None, min_value=None, max_value=None, validation_pattern=None, position=0, ) context.r2_action_model = _make_action_model(arguments=[arg]) @given("a r2mod-ActionModel with None tags_json and None inputs_schema_json") def step_action_none_tags_inputs(context: Any) -> None: context.r2_action_model = _make_action_model( tags_json=None, inputs_schema_json=None ) @given("a r2mod-ActionModel with None rels") def step_action_none_rels(context: Any) -> None: model = _make_action_model() _set_rel_none(model, "arguments_rel") _set_rel_none(model, "invariants_rel") context.r2_action_model = model @when("I r2mod-convert the ActionModel to domain") def step_action_to_domain(context: Any) -> None: context.r2_action_domain = context.r2_action_model.to_domain() @then("the r2mod-action first argument default_value should be None") def step_action_arg_default_none(context: Any) -> None: assert context.r2_action_domain.arguments[0].default_value is None @then("the r2mod-action first argument min_value should be None") def step_action_arg_min_none(context: Any) -> None: assert context.r2_action_domain.arguments[0].min_value is None @then("the r2mod-action first argument max_value should be None") def step_action_arg_max_none(context: Any) -> None: assert context.r2_action_domain.arguments[0].max_value is None @then("the r2mod-action tags should be empty") def step_action_tags_empty(context: Any) -> None: assert context.r2_action_domain.tags == [] @then("the r2mod-action inputs_schema should be None") def step_action_inputs_none(context: Any) -> None: assert context.r2_action_domain.inputs_schema is None @then("the r2mod-action arguments should be empty") def step_action_args_empty(context: Any) -> None: assert context.r2_action_domain.arguments == [] @then("the r2mod-action invariants should be empty") def step_action_invs_empty(context: Any) -> None: assert context.r2_action_domain.invariants == [] # =================================================================== # LifecyclePlanModel.to_domain() # =================================================================== @given("a r2mod-PlanModel with all optional fields set to None") def step_plan_all_none(context: Any) -> None: model = _make_plan_model( automation_profile=None, sandbox_refs_json=None, validation_summary_json=None, tags_json=None, action_name="local/test-action", # required non-empty by Plan domain error_details_json=None, ) _set_rel_none(model, "project_links_rel") _set_rel_none(model, "invariants_rel") _set_rel_none(model, "arguments_rel") context.r2_plan_model = model @given("a r2mod-PlanModel with None action_name") def step_plan_none_action(context: Any) -> None: context.r2_plan_model = _make_plan_model(action_name=None) @when("I r2mod-attempt to convert the PlanModel to domain") def step_plan_to_domain_attempt(context: Any) -> None: try: context.r2_plan_domain = context.r2_plan_model.to_domain() context.r2_error = None except Exception as exc: context.r2_error = exc @then('a r2mod-ValidationError should have been raised with "{fragment}"') def step_r2_validation_error_fragment(context: Any, fragment: str) -> None: assert context.r2_error is not None, "Expected an error but none was raised" assert fragment in str(context.r2_error), ( f"Expected '{fragment}' in error: {context.r2_error}" ) @given("a r2mod-PlanModel with an argument having None value_json") def step_plan_arg_none_value(context: Any) -> None: arg = PlanArgumentModel( name="test_arg", value_json=None, value_type="string", position=0, ) context.r2_plan_model = _make_plan_model( arguments=[arg], action_name="local/test-action" ) @when("I r2mod-convert the PlanModel to domain") def step_plan_to_domain(context: Any) -> None: context.r2_plan_domain = context.r2_plan_model.to_domain() @then("the r2mod-plan automation_profile should be None") def step_plan_profile_none(context: Any) -> None: assert context.r2_plan_domain.automation_profile is None @then("the r2mod-plan validation_summary should be None") def step_plan_validation_none(context: Any) -> None: assert context.r2_plan_domain.validation_summary is None @then("the r2mod-plan sandbox_refs should be empty") def step_plan_sandbox_empty(context: Any) -> None: assert context.r2_plan_domain.sandbox_refs == [] @then("the r2mod-plan tags should be empty") def step_plan_tags_empty(context: Any) -> None: assert context.r2_plan_domain.tags == [] @then("the r2mod-plan action_name should be empty string") def step_plan_action_empty(context: Any) -> None: assert context.r2_plan_domain.action_name == "" @then("the r2mod-plan error_details should be None") def step_plan_err_none(context: Any) -> None: assert context.r2_plan_domain.error_details is None @then('the r2mod-plan argument "{name}" should be None') def step_plan_arg_value_none(context: Any, name: str) -> None: assert context.r2_plan_domain.arguments[name] is None # =================================================================== # LifecycleActionModel.from_domain() # =================================================================== @when("I r2mod-create ActionModel from domain with None inputs_schema") def step_action_from_domain_none_schema(context: Any) -> None: from cleveragents.domain.models.core.action import Action, ActionState from cleveragents.domain.models.core.plan import NamespacedName action = Action( namespaced_name=NamespacedName(namespace="local", name="test-act"), description="test", definition_of_done="tests pass", strategy_actor="local/strat", execution_actor="local/exec", inputs_schema=None, state=ActionState.AVAILABLE, ) context.r2_created_action = LifecycleActionModel.from_domain(action) @then("the r2mod-created ActionModel inputs_schema_json should be None") def step_action_created_schema_none(context: Any) -> None: assert context.r2_created_action.inputs_schema_json is None @when("I r2mod-create ActionModel from domain with argument having None default") def step_action_from_domain_arg_none_default(context: Any) -> None: from cleveragents.domain.models.core.action import ( Action, ActionArgument, ActionState, ArgumentRequirement, ArgumentType, ) from cleveragents.domain.models.core.plan import NamespacedName arg = ActionArgument( name="myarg", arg_type=ArgumentType.STRING, requirement=ArgumentRequirement.OPTIONAL, description="no default", default_value=None, ) action = Action( namespaced_name=NamespacedName(namespace="local", name="test-act"), description="test", definition_of_done="tests pass", strategy_actor="local/strat", execution_actor="local/exec", arguments=[arg], state=ActionState.AVAILABLE, ) context.r2_created_action = LifecycleActionModel.from_domain(action) @then("the r2mod-created ActionModel first argument default_value_json should be None") def step_action_created_arg_default_none(context: Any) -> None: assert context.r2_created_action.arguments_rel[0].default_value_json is None # =================================================================== # LifecyclePlanModel helpers # =================================================================== @then("r2mod-_parse_iso with None should return None") def step_parse_iso_none(context: Any) -> None: assert LifecyclePlanModel._parse_iso(None) is None @then("r2mod-_to_iso with None should return None") def step_to_iso_none(context: Any) -> None: assert LifecyclePlanModel._to_iso(None) is None @then('r2mod-_parse_iso with "{iso}" should return a datetime') def step_parse_iso_value(context: Any, iso: str) -> None: result = LifecyclePlanModel._parse_iso(iso) assert isinstance(result, datetime) @then("r2mod-_to_iso with a datetime should return an ISO string") def step_to_iso_value(context: Any) -> None: now = datetime.now(tz=UTC) result = LifecyclePlanModel._to_iso(now) assert isinstance(result, str) assert "T" in result