Files
temp/features/steps/models_lifecycle_coverage_r2_steps.py
T
freemo 3a2b134f3c test(coverage): add Behave scenarios for remaining under-tested modules
Added Behave BDD feature files and step definitions targeting coverage
gaps in six modules:

- container.py: exercise get_database_url env-var fallback, AI provider
  None path, cached container singleton, override_providers edge cases
  (lines 66-69, 125-130; branches at 51, 57, 82, 87, 256, 284-288)
- correction_service.py: exercise exception-handling paths in
  execute_revert and execute_append via monkeypatched analyze_impact
  and ULID failures (lines 254-262, 320-328)
- plan_lifecycle_service.py: exercise _persisted UoW commit paths,
  InvalidPhaseTransitionError custom message branch, non-reusable
  action archive, and error_details merge logic (branches at 100,
  216, 237, 327, 461, 570, 576, 607)
- plan.py (CLI): exercise spec-dict optional field branches,
  _print_lifecycle_plan conditional rendering, use_action argument
  parsing, auto-resolve paths, legacy wrappers, and validation
  error branches across 66 scenarios
- skill.py (CLI): exercise singleton cache, timestamp-absent show,
  no-tools MCP, add/remove/list/show format and error branches
  across 26 scenarios
- models.py (DB): exercise to_domain/from_domain None-field branches
  in SkillModel, SessionModel, ToolModel, LifecycleActionModel,
  LifecyclePlanModel, NamespacedProjectModel, and SessionMessageModel
  across 41 scenarios

All 302 features, 6503 scenarios, 28271 steps pass (nox -e unit_tests).

ISSUES CLOSED: #446
2026-02-25 19:38:43 -05:00

410 lines
13 KiB
Python

"""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=None,
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,
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