forked from HAL9000/cleveragents-core
3a2b134f3c
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
410 lines
13 KiB
Python
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
|