Files
temp/features/steps/database_models_lifecycle_coverage_steps.py
T
CoreRasurae f2f7aa5dc9 feat(core): add v3 lifecycle models, automation levels, subplan support, and security hardening
Implement multiple Stage A/B/E/SEC milestones for the v3 lifecycle system:
- Stage A5.3+A5.4: Add LifecycleActionModel and LifecyclePlanModel SQLAlchemy
  models with to_domain()/from_domain() conversion methods
- Stage A5.6: Implement ActionRepository with full CRUD, namespace/state
  queries, referential integrity checks, and retry decorator
- Stage E1: Add subplan domain models (ExecutionMode, SubplanMergeStrategy,
  SubplanConfig, SubplanStatus, SubplanAttempt, SubplanFailureHandler) with
  computed properties on Plan (is_subplan, is_root_plan, depth, has_subplans)
- Stage A6: Add AutomationLevel enum (MANUAL, REVIEW_BEFORE_APPLY,
  FULL_AUTOMATION), settings integration, PlanLifecycleService auto-progression,
  pause/resume, and CLI commands (--automation-level, set-automation-level)
- Stage SEC1: Remove eval()/exec() from stream_router.py, replace with named
  operation and transform registries; code blocks and unregistered transforms
  now raise StreamRoutingError
- Add langchain-anthropic dependency
- Update BDD tests for security changes and relax ADR directory requirement
2026-02-12 20:19:42 +00:00

1124 lines
41 KiB
Python

"""Step definitions for lifecycle data persistence and retrieval tests."""
import json
from datetime import datetime
from behave import given, then, when
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.infrastructure.database.models import (
Base,
LifecycleActionModel,
LifecyclePlanModel,
)
# ---------------------------------------------------------------------------
# Background steps
# ---------------------------------------------------------------------------
@given("the lifecycle database is ready")
def step_import_lifecycle_modules(context):
"""Set up an in-memory database with lifecycle tables."""
context.database_url = "sqlite:///:memory:"
context.engine = create_engine(context.database_url)
context.SessionLocal = sessionmaker(
bind=context.engine, autoflush=False, autocommit=False
)
Base.metadata.create_all(context.engine)
@given("a lifecycle database session is open")
def step_create_lifecycle_test_session(context):
"""Open a database session for the lifecycle tests."""
if not hasattr(context, "SessionLocal"):
step_import_lifecycle_modules(context)
context.db_session = context.SessionLocal()
# ---------------------------------------------------------------------------
# Helper: create a fully populated LifecycleActionModel
# ---------------------------------------------------------------------------
VALID_ULID_1 = "01HGZ6FE0AQDYTR4BXVQZ6E001"
VALID_ULID_2 = "01HGZ6FE0AQDYTR4BXVQZ6E002"
VALID_ULID_3 = "01HGZ6FE0AQDYTR4BXVQZ6E003"
VALID_ULID_4 = "01HGZ6FE0AQDYTR4BXVQZ6E004"
VALID_ULID_5 = "01HGZ6FE0AQDYTR4BXVQZ6E005"
def _make_action_model(
action_id=VALID_ULID_1,
name="local/test-action",
namespace="local",
short_name="test-action",
short_description="A short description",
long_description="A long description of the action",
definition_of_done="All tests pass and coverage > 80%",
strategy_actor="local/strategy-actor",
execution_actor="local/execution-actor",
estimation_actor="local/estimation-actor",
review_actor="local/review-actor",
inputs_schema="[]",
state="draft",
reusable=True,
read_only=False,
created_at="2025-06-01T12:00:00",
updated_at="2025-06-01T13:00:00",
created_by="test-user",
tags="[]",
):
"""Create a LifecycleActionModel with sensible defaults."""
return LifecycleActionModel(
action_id=action_id,
name=name,
namespace=namespace,
short_name=short_name,
short_description=short_description,
long_description=long_description,
definition_of_done=definition_of_done,
strategy_actor=strategy_actor,
execution_actor=execution_actor,
estimation_actor=estimation_actor,
review_actor=review_actor,
inputs_schema=inputs_schema,
state=state,
reusable=reusable,
read_only=read_only,
created_at=created_at,
updated_at=updated_at,
created_by=created_by,
tags=tags,
)
def _make_plan_model(
plan_id=VALID_ULID_1,
parent_plan_id=None,
root_plan_id=None,
action_id=VALID_ULID_1,
phase="action",
state="draft",
attempt=1,
automation_level="manual",
namespaced_name="local/test-plan",
description="A test plan description",
definition_of_done="Tests pass",
project_ids="[]",
strategy_actor="local/strategy-actor",
execution_actor="local/execution-actor",
error_message=None,
created_at="2025-06-01T12:00:00",
updated_at="2025-06-01T13:00:00",
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,
created_by="test-user",
tags="[]",
reusable=True,
read_only=False,
):
"""Create a LifecyclePlanModel with sensible defaults."""
return LifecyclePlanModel(
plan_id=plan_id,
parent_plan_id=parent_plan_id,
root_plan_id=root_plan_id,
action_id=action_id,
phase=phase,
state=state,
attempt=attempt,
automation_level=automation_level,
namespaced_name=namespaced_name,
description=description,
definition_of_done=definition_of_done,
project_ids=project_ids,
strategy_actor=strategy_actor,
execution_actor=execution_actor,
error_message=error_message,
created_at=created_at,
updated_at=updated_at,
completed_at=completed_at,
strategize_started_at=strategize_started_at,
strategize_completed_at=strategize_completed_at,
execute_started_at=execute_started_at,
execute_completed_at=execute_completed_at,
apply_started_at=apply_started_at,
applied_at=applied_at,
created_by=created_by,
tags=tags,
reusable=reusable,
read_only=read_only,
)
# ---------------------------------------------------------------------------
# Action: loading a stored record as a domain object
# ---------------------------------------------------------------------------
@given("an action record exists with valid attributes and tags")
def step_create_action_model_valid(context):
"""Persist a valid action record with tags."""
context.action_model = _make_action_model(
tags='["tag1", "tag2"]',
)
context.db_session.add(context.action_model)
context.db_session.commit()
@when("the action record is loaded as a domain object")
def step_call_to_domain_on_action(context):
"""Load the persisted action record as a domain object."""
context.action_domain = context.action_model.to_domain()
@then("the loaded action should preserve its original identifier")
def step_verify_action_domain_id(context):
"""Verify the loaded action retains the original identifier."""
assert context.action_domain.action_id == VALID_ULID_1
@then("the loaded action should preserve its namespace and short name")
def step_verify_action_domain_namespaced_name(context):
"""Verify the loaded action retains its namespace and short name."""
assert context.action_domain.namespaced_name.namespace == "local"
assert context.action_domain.namespaced_name.name == "test-action"
@then("the loaded action should preserve its description fields")
def step_verify_action_domain_descriptions(context):
"""Verify the loaded action retains all description fields."""
assert context.action_domain.short_description == "A short description"
assert context.action_domain.long_description == "A long description of the action"
assert (
context.action_domain.definition_of_done == "All tests pass and coverage > 80%"
)
@then("the loaded action should preserve its actor assignments")
def step_verify_action_domain_actors(context):
"""Verify the loaded action retains all actor assignments."""
assert context.action_domain.strategy_actor == "local/strategy-actor"
assert context.action_domain.execution_actor == "local/execution-actor"
assert context.action_domain.estimation_actor == "local/estimation-actor"
assert context.action_domain.review_actor == "local/review-actor"
@then("the loaded action should preserve its state and flags")
def step_verify_action_domain_state_flags(context):
"""Verify the loaded action retains its state and boolean flags."""
from cleveragents.domain.models.core.plan import ActionState
assert context.action_domain.state == ActionState.DRAFT
assert context.action_domain.reusable is True
assert context.action_domain.read_only is False
@then("the loaded action should preserve its timestamps")
def step_verify_action_domain_timestamps(context):
"""Verify the loaded action retains its timestamps."""
assert context.action_domain.created_at == datetime.fromisoformat(
"2025-06-01T12:00:00"
)
assert context.action_domain.updated_at == datetime.fromisoformat(
"2025-06-01T13:00:00"
)
@then("the loaded action should preserve its tags")
def step_verify_action_domain_tags(context):
"""Verify the loaded action retains its tags."""
assert context.action_domain.tags == ["tag1", "tag2"]
# ---------------------------------------------------------------------------
# Action: loading a stored record with arguments
# ---------------------------------------------------------------------------
@given("an action record exists with a structured arguments schema")
def step_create_action_model_with_args(context):
"""Persist an action record that has structured input arguments."""
args_json = json.dumps(
[
{
"name": "target_coverage",
"arg_type": "int",
"requirement": "required",
"description": "Target coverage percentage",
},
{
"name": "framework",
"arg_type": "str",
"requirement": "optional",
"description": "Test framework to use",
},
]
)
context.action_model = _make_action_model(inputs_schema=args_json)
context.db_session.add(context.action_model)
context.db_session.commit()
@then("the loaded action should contain the expected argument entries")
def step_verify_action_arguments_parsed(context):
"""Verify the loaded action contains the expected number of arguments."""
assert len(context.action_domain.arguments) == 2
@then("each argument entry should have the correct name and type")
def step_verify_action_argument_details(context):
"""Verify each argument entry has the correct name."""
args = context.action_domain.arguments
assert args[0].name == "target_coverage"
assert args[1].name == "framework"
# ---------------------------------------------------------------------------
# Action: loading a record with no inputs or tags
# ---------------------------------------------------------------------------
@given("an action record exists with no inputs and no tags")
def step_create_action_model_empty_inputs_tags(context):
"""Persist an action record with empty inputs and tags."""
context.action_model = _make_action_model(inputs_schema="[]", tags="[]")
context.db_session.add(context.action_model)
context.db_session.commit()
@then("the loaded action should have an empty arguments collection")
def step_verify_empty_arguments(context):
"""Verify the loaded action has no arguments."""
assert context.action_domain.arguments == []
@then("the loaded action should have an empty tags collection")
def step_verify_empty_tags(context):
"""Verify the loaded action has no tags."""
assert context.action_domain.tags == []
# ---------------------------------------------------------------------------
# Action: storing a domain object as a database record
# ---------------------------------------------------------------------------
@given("a complete action domain object is prepared")
def step_create_action_domain_object(context):
"""Prepare a fully populated action domain object."""
from cleveragents.domain.models.core.action import Action, ActionArgument
from cleveragents.domain.models.core.plan import ActionState, NamespacedName
context.action_domain_input = Action(
action_id=VALID_ULID_1,
namespaced_name=NamespacedName(namespace="local", name="my-action"),
short_description="Short desc",
long_description="Long desc",
definition_of_done="All tests pass",
strategy_actor="local/strategy",
execution_actor="local/executor",
estimation_actor="local/estimator",
review_actor="local/reviewer",
arguments=[
ActionArgument(
name="coverage",
arg_type="int",
requirement="required",
description="Coverage target",
),
],
reusable=True,
read_only=False,
state=ActionState.DRAFT,
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
created_by="test-user",
tags=["ci", "testing"],
)
@when("the action domain object is stored as a database record")
def step_call_from_domain_on_action(context):
"""Store the action domain object as a database record."""
context.action_model_result = LifecycleActionModel.from_domain(
context.action_domain_input
)
@then("the stored record should preserve the action identifier")
def step_verify_from_domain_action_id(context):
"""Verify the stored record retains the action identifier."""
assert context.action_model_result.action_id == VALID_ULID_1
@then("the stored record should preserve the name components")
def step_verify_from_domain_name_fields(context):
"""Verify the stored record retains name, namespace, and short name."""
assert context.action_model_result.name == "local/my-action"
assert context.action_model_result.namespace == "local"
assert context.action_model_result.short_name == "my-action"
@then("the stored record should preserve the description fields")
def step_verify_from_domain_descriptions(context):
"""Verify the stored record retains all description fields."""
assert context.action_model_result.short_description == "Short desc"
assert context.action_model_result.long_description == "Long desc"
assert context.action_model_result.definition_of_done == "All tests pass"
@then("the stored record should preserve the actor assignments")
def step_verify_from_domain_actors(context):
"""Verify the stored record retains all actor assignments."""
assert context.action_model_result.strategy_actor == "local/strategy"
assert context.action_model_result.execution_actor == "local/executor"
assert context.action_model_result.estimation_actor == "local/estimator"
assert context.action_model_result.review_actor == "local/reviewer"
@then("the stored record should serialize the arguments as JSON")
def step_verify_from_domain_inputs_schema(context):
"""Verify the stored record has arguments serialized as JSON."""
parsed = json.loads(context.action_model_result.inputs_schema)
assert len(parsed) == 1
assert parsed[0]["name"] == "coverage"
@then("the stored record should serialize the tags as JSON")
def step_verify_from_domain_tags_json(context):
"""Verify the stored record has tags serialized as JSON."""
parsed = json.loads(context.action_model_result.tags)
assert parsed == ["ci", "testing"]
@then("the stored record should preserve the state value")
def step_verify_from_domain_state(context):
"""Verify the stored record retains the state value."""
assert context.action_model_result.state == "draft"
@then("the stored record should format timestamps as ISO strings")
def step_verify_from_domain_timestamps(context):
"""Verify the stored record formats timestamps as ISO strings."""
assert context.action_model_result.created_at == "2025-06-01T12:00:00"
assert context.action_model_result.updated_at == "2025-06-01T13:00:00"
# ---------------------------------------------------------------------------
# Action: storing with an enumerated state
# ---------------------------------------------------------------------------
@given("an action domain object is prepared with an enumerated state")
def step_create_action_with_enum_state(context):
"""Prepare an action domain object that uses an ActionState enum value."""
from cleveragents.domain.models.core.action import Action
from cleveragents.domain.models.core.plan import ActionState, NamespacedName
context.action_domain_input = Action(
action_id=VALID_ULID_1,
namespaced_name=NamespacedName(namespace="local", name="enum-action"),
definition_of_done="Tests pass",
strategy_actor="local/strategy",
execution_actor="local/executor",
state=ActionState.AVAILABLE,
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
)
@then("the stored record state should equal the enum value")
def step_verify_enum_state_value(context):
"""Verify the stored record state is the string value of the enum."""
assert context.action_model_result.state == "available"
# ---------------------------------------------------------------------------
# Action: storing with a custom string state
# ---------------------------------------------------------------------------
@given("an action-like object is prepared with a custom string state")
def step_create_action_with_string_state(context):
"""Prepare an action-like object with a plain string state."""
from types import SimpleNamespace
from cleveragents.domain.models.core.plan import NamespacedName
context.action_domain_string_state = SimpleNamespace(
action_id=VALID_ULID_1,
namespaced_name=NamespacedName(namespace="local", name="str-action"),
short_description="Desc",
long_description="Long",
definition_of_done="Done",
strategy_actor="local/strategy",
execution_actor="local/executor",
estimation_actor=None,
review_actor=None,
arguments=[],
reusable=True,
read_only=False,
state="custom-state",
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
created_by=None,
tags=[],
)
@when("the custom-state action is stored as a database record")
def step_call_from_domain_string_state(context):
"""Store the custom-state action as a database record."""
context.action_model_result = LifecycleActionModel.from_domain(
context.action_domain_string_state
)
@then("the stored record state should equal the custom string")
def step_verify_plain_string_state(context):
"""Verify the stored record state is the plain custom string."""
assert context.action_model_result.state == "custom-state"
# ---------------------------------------------------------------------------
# Plan: parsing ISO timestamps
# ---------------------------------------------------------------------------
@when("a valid ISO-8601 string is parsed as a timestamp")
def step_call_parse_iso_valid(context):
"""Parse a valid ISO-8601 string as a timestamp."""
context.parse_iso_result = LifecyclePlanModel._parse_iso("2025-06-01T12:00:00")
@then("the parsed timestamp should be the expected datetime value")
def step_verify_parse_iso_datetime(context):
"""Verify the parsed timestamp matches the expected datetime."""
assert isinstance(context.parse_iso_result, datetime)
assert context.parse_iso_result == datetime(2025, 6, 1, 12, 0, 0)
@when("an absent value is parsed as a timestamp")
def step_call_parse_iso_none(context):
"""Parse an absent (None) value as a timestamp."""
context.parse_iso_result = LifecyclePlanModel._parse_iso(None)
@then("the parsed timestamp should be absent")
def step_verify_parse_iso_none(context):
"""Verify parsing an absent value returns nothing."""
assert context.parse_iso_result is None
# ---------------------------------------------------------------------------
# Plan: formatting datetimes as ISO strings
# ---------------------------------------------------------------------------
@when("a datetime value is formatted as an ISO string")
def step_call_to_iso_datetime(context):
"""Format a datetime value as an ISO string."""
context.to_iso_result = LifecyclePlanModel._to_iso(datetime(2025, 6, 1, 12, 0, 0))
@then("the formatted string should match ISO-8601 format")
def step_verify_to_iso_string(context):
"""Verify the formatted string matches ISO-8601 format."""
assert context.to_iso_result == "2025-06-01T12:00:00"
@when("an absent datetime is formatted as an ISO string")
def step_call_to_iso_none(context):
"""Format an absent (None) datetime as an ISO string."""
context.to_iso_result = LifecyclePlanModel._to_iso(None)
@then("the formatted value should be absent")
def step_verify_to_iso_none(context):
"""Verify formatting an absent datetime returns nothing."""
assert context.to_iso_result is None
# ---------------------------------------------------------------------------
# Plan: loading a stored record in the action phase
# ---------------------------------------------------------------------------
@given("a plan record exists in the action phase with draft state")
def step_create_plan_model_action_draft(context):
"""Persist a plan record in the action phase with draft state."""
action_model = _make_action_model()
context.db_session.add(action_model)
context.db_session.commit()
context.plan_model = _make_plan_model(
phase="action",
state="draft",
project_ids='["proj-1", "proj-2"]',
tags='["important"]',
)
context.db_session.add(context.plan_model)
context.db_session.commit()
@when("the plan record is loaded as a domain object")
def step_call_to_domain_on_plan(context):
"""Load the persisted plan record as a domain object."""
context.plan_domain = context.plan_model.to_domain()
@then("the loaded plan should preserve its identity")
def step_verify_plan_identity(context):
"""Verify the loaded plan retains its identifier and attempt."""
assert context.plan_domain.identity.plan_id == VALID_ULID_1
assert context.plan_domain.identity.attempt == 1
@then("the loaded plan should be in the action phase")
def step_verify_plan_action_phase(context):
"""Verify the loaded plan is in the action phase."""
from cleveragents.domain.models.core.plan import PlanPhase
assert context.plan_domain.phase == PlanPhase.ACTION
@then("the loaded plan action state should be draft")
def step_verify_plan_action_state_draft(context):
"""Verify the loaded plan action state is draft."""
from cleveragents.domain.models.core.plan import ActionState
assert context.plan_domain.action_state == ActionState.DRAFT
@then("the loaded plan processing state should be absent")
def step_verify_plan_processing_state_none(context):
"""Verify the loaded plan has no processing state."""
assert context.plan_domain.processing_state is None
@then("the loaded plan should preserve its description")
def step_verify_plan_description(context):
"""Verify the loaded plan retains its description."""
assert context.plan_domain.description == "A test plan description"
@then("the loaded plan should preserve its timestamps")
def step_verify_plan_timestamps(context):
"""Verify the loaded plan retains its timestamps."""
assert context.plan_domain.timestamps.created_at == datetime.fromisoformat(
"2025-06-01T12:00:00"
)
assert context.plan_domain.timestamps.updated_at == datetime.fromisoformat(
"2025-06-01T13:00:00"
)
@then("the loaded plan should preserve its metadata")
def step_verify_plan_metadata(context):
"""Verify the loaded plan retains its metadata fields."""
assert context.plan_domain.created_by == "test-user"
assert context.plan_domain.reusable is True
assert context.plan_domain.read_only is False
# ---------------------------------------------------------------------------
# Plan: loading a stored record in the strategize phase
# ---------------------------------------------------------------------------
@given("a plan record exists in the strategize phase with processing state")
def step_create_plan_model_strategize(context):
"""Persist a plan record in the strategize phase."""
existing = (
context.db_session.query(LifecycleActionModel)
.filter_by(action_id=VALID_ULID_1)
.first()
)
if not existing:
action_model = _make_action_model()
context.db_session.add(action_model)
context.db_session.commit()
context.plan_model = _make_plan_model(
plan_id=VALID_ULID_2,
phase="strategize",
state="processing",
)
context.db_session.add(context.plan_model)
context.db_session.commit()
@then("the loaded plan should be in the strategize phase")
def step_verify_plan_strategize_phase(context):
"""Verify the loaded plan is in the strategize phase."""
from cleveragents.domain.models.core.plan import PlanPhase
assert context.plan_domain.phase == PlanPhase.STRATEGIZE
@then("the loaded plan processing state should be processing")
def step_verify_plan_processing_state(context):
"""Verify the loaded plan processing state is processing."""
from cleveragents.domain.models.core.plan import ProcessingState
assert context.plan_domain.processing_state == ProcessingState.PROCESSING
@then("the loaded plan action state should be absent")
def step_verify_plan_action_state_none(context):
"""Verify the loaded plan has no action state."""
assert context.plan_domain.action_state is None
# ---------------------------------------------------------------------------
# Plan: loading a record with all phase timestamps
# ---------------------------------------------------------------------------
@given("a plan record exists with all phase timestamps populated")
def step_create_plan_model_all_timestamps(context):
"""Persist a plan record with all phase timestamps filled."""
existing = (
context.db_session.query(LifecycleActionModel)
.filter_by(action_id=VALID_ULID_1)
.first()
)
if not existing:
action_model = _make_action_model()
context.db_session.add(action_model)
context.db_session.commit()
context.plan_model = _make_plan_model(
plan_id=VALID_ULID_3,
phase="apply",
state="complete",
strategize_started_at="2025-06-01T14:00:00",
strategize_completed_at="2025-06-01T14:30:00",
execute_started_at="2025-06-01T15:00:00",
execute_completed_at="2025-06-01T15:30:00",
apply_started_at="2025-06-01T16:00:00",
applied_at="2025-06-01T16:30:00",
)
context.db_session.add(context.plan_model)
context.db_session.commit()
@then("the loaded plan timestamps should include the strategize window")
def step_verify_strategize_timestamps(context):
"""Verify the loaded plan has correct strategize timestamps."""
ts = context.plan_domain.timestamps
assert ts.strategize_started_at == datetime(2025, 6, 1, 14, 0, 0)
assert ts.strategize_completed_at == datetime(2025, 6, 1, 14, 30, 0)
@then("the loaded plan timestamps should include the execute window")
def step_verify_execute_timestamps(context):
"""Verify the loaded plan has correct execute timestamps."""
ts = context.plan_domain.timestamps
assert ts.execute_started_at == datetime(2025, 6, 1, 15, 0, 0)
assert ts.execute_completed_at == datetime(2025, 6, 1, 15, 30, 0)
@then("the loaded plan timestamps should include the apply window")
def step_verify_apply_timestamps(context):
"""Verify the loaded plan has correct apply timestamps."""
ts = context.plan_domain.timestamps
assert ts.apply_started_at == datetime(2025, 6, 1, 16, 0, 0)
assert ts.applied_at == datetime(2025, 6, 1, 16, 30, 0)
# ---------------------------------------------------------------------------
# Plan: loading a record with project IDs and tags
# ---------------------------------------------------------------------------
@given("a plan record exists with associated project identifiers and tags")
def step_create_plan_model_with_ids_tags(context):
"""Persist a plan record with project identifiers and tags."""
existing = (
context.db_session.query(LifecycleActionModel)
.filter_by(action_id=VALID_ULID_1)
.first()
)
if not existing:
action_model = _make_action_model()
context.db_session.add(action_model)
context.db_session.commit()
context.plan_model = _make_plan_model(
plan_id=VALID_ULID_4,
phase="strategize",
state="queued",
project_ids='["proj-a", "proj-b", "proj-c"]',
tags='["urgent", "backend"]',
)
context.db_session.add(context.plan_model)
context.db_session.commit()
@then("the loaded plan should contain the expected project identifiers")
def step_verify_plan_project_ids(context):
"""Verify the loaded plan contains the expected project identifiers."""
assert context.plan_domain.project_ids == ["proj-a", "proj-b", "proj-c"]
@then("the loaded plan should contain the expected tags")
def step_verify_plan_tags(context):
"""Verify the loaded plan contains the expected tags."""
assert context.plan_domain.tags == ["urgent", "backend"]
# ---------------------------------------------------------------------------
# Plan: storing a domain object with action state
# ---------------------------------------------------------------------------
def _make_plan_domain(
phase="action",
action_state=None,
processing_state=None,
plan_id=VALID_ULID_1,
project_ids=None,
tags=None,
timestamps=None,
automation_level=None,
):
"""Create a Plan domain object for testing storage."""
from cleveragents.domain.models.core.plan import (
AutomationLevel,
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
)
if project_ids is None:
project_ids = ["proj-1"]
if tags is None:
tags = ["test"]
if timestamps is None:
timestamps = PlanTimestamps(
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
)
resolved_automation = (
automation_level if automation_level is not None else AutomationLevel.MANUAL
)
plan_kwargs = dict(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
description="Test plan description",
definition_of_done="Tests pass",
phase=PlanPhase(phase),
automation_level=resolved_automation,
strategy_actor="local/strategy",
execution_actor="local/executor",
project_ids=project_ids,
timestamps=timestamps,
created_by="test-user",
tags=tags,
reusable=True,
read_only=False,
)
if action_state is not None:
plan_kwargs["action_state"] = action_state
if processing_state is not None:
plan_kwargs["processing_state"] = processing_state
return Plan(**plan_kwargs)
@given("a plan domain object is prepared in the action phase with an available state")
def step_create_plan_domain_action_available(context):
"""Prepare a plan domain object in the action phase with available state."""
from cleveragents.domain.models.core.plan import ActionState
context.plan_domain_input = _make_plan_domain(
phase="action",
action_state=ActionState.AVAILABLE,
project_ids=["proj-1", "proj-2"],
tags=["ci", "test"],
)
@when("the plan domain object is stored as a database record")
def step_call_from_domain_on_plan(context):
"""Store the plan domain object as a database record."""
context.plan_model_result = LifecyclePlanModel.from_domain(
context.plan_domain_input
)
@then('the stored plan record should have state "{expected_state}"')
def step_verify_from_domain_plan_state(context, expected_state):
"""Verify the stored plan record has the expected state."""
assert context.plan_model_result.state == expected_state
@then("the stored plan record should preserve the plan identifier")
def step_verify_from_domain_plan_id(context):
"""Verify the stored plan record retains the plan identifier."""
assert context.plan_model_result.plan_id == VALID_ULID_1
@then("the stored plan record should preserve the phase")
def step_verify_from_domain_plan_phase(context):
"""Verify the stored plan record retains the phase."""
assert context.plan_model_result.phase == "action"
@then("the stored plan record should serialize the project identifiers as JSON")
def step_verify_from_domain_project_ids(context):
"""Verify the stored plan record has project identifiers as JSON."""
parsed = json.loads(context.plan_model_result.project_ids)
assert parsed == ["proj-1", "proj-2"]
@then("the stored plan record should serialize the plan tags as JSON")
def step_verify_from_domain_plan_tags(context):
"""Verify the stored plan record has tags as JSON."""
parsed = json.loads(context.plan_model_result.tags)
assert parsed == ["ci", "test"]
@then("the stored plan record should format plan timestamps as ISO strings")
def step_verify_from_domain_plan_timestamps(context):
"""Verify the stored plan record has timestamps as ISO strings."""
assert context.plan_model_result.created_at == "2025-06-01T12:00:00"
assert context.plan_model_result.updated_at == "2025-06-01T13:00:00"
# ---------------------------------------------------------------------------
# Plan: storing a domain object with processing state
# ---------------------------------------------------------------------------
@given("a plan domain object is prepared in the strategize phase with a queued state")
def step_create_plan_domain_strategize_queued(context):
"""Prepare a plan domain object in the strategize phase with queued state."""
from cleveragents.domain.models.core.plan import ProcessingState
context.plan_domain_input = _make_plan_domain(
phase="strategize",
processing_state=ProcessingState.QUEUED,
)
@then('the stored plan record phase should be "{expected_phase}"')
def step_verify_from_domain_plan_phase_value(context, expected_phase):
"""Verify the stored plan record has the expected phase."""
assert context.plan_model_result.phase == expected_phase
# ---------------------------------------------------------------------------
# Plan: storing a domain object with no explicit state
# ---------------------------------------------------------------------------
@given("a plan domain object is prepared with neither action nor processing state")
def step_create_plan_domain_null_states(context):
"""Prepare a plan-like object with no explicit state to test the fallback."""
from types import SimpleNamespace
from cleveragents.domain.models.core.plan import (
NamespacedName,
PlanIdentity,
PlanPhase,
PlanTimestamps,
)
context.plan_domain_stateless = SimpleNamespace(
identity=PlanIdentity(plan_id=VALID_ULID_1),
namespaced_name=NamespacedName(namespace="local", name="null-plan"),
description="Null state plan",
definition_of_done="Done",
phase=PlanPhase.STRATEGIZE,
action_state=None,
processing_state=None,
automation_level="manual",
strategy_actor="local/strategy",
execution_actor="local/executor",
project_ids=["proj-1"],
timestamps=PlanTimestamps(
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
),
error_message=None,
created_by=None,
tags=[],
reusable=True,
read_only=False,
)
@when("the stateless plan domain object is stored as a database record")
def step_call_from_domain_stateless(context):
"""Store the stateless plan as a database record."""
context.plan_model_result = LifecyclePlanModel.from_domain(
context.plan_domain_stateless
)
# ---------------------------------------------------------------------------
# Plan: storing a domain object with all phase timestamps
# ---------------------------------------------------------------------------
@given("a plan domain object is prepared with all phase timestamps")
def step_create_plan_domain_all_timestamps(context):
"""Prepare a plan domain object with all phase timestamps."""
from cleveragents.domain.models.core.plan import (
ActionState,
PlanTimestamps,
)
timestamps = PlanTimestamps(
created_at=datetime(2025, 6, 1, 12, 0, 0),
updated_at=datetime(2025, 6, 1, 13, 0, 0),
strategize_started_at=datetime(2025, 6, 1, 14, 0, 0),
strategize_completed_at=datetime(2025, 6, 1, 14, 30, 0),
execute_started_at=datetime(2025, 6, 1, 15, 0, 0),
execute_completed_at=datetime(2025, 6, 1, 15, 30, 0),
apply_started_at=datetime(2025, 6, 1, 16, 0, 0),
applied_at=datetime(2025, 6, 1, 16, 30, 0),
)
context.plan_domain_input = _make_plan_domain(
phase="action",
action_state=ActionState.AVAILABLE,
timestamps=timestamps,
)
@then("the stored plan record should have all phase timestamps as ISO strings")
def step_verify_from_domain_all_timestamps(context):
"""Verify all phase timestamp columns are formatted as ISO strings."""
m = context.plan_model_result
assert m.strategize_started_at == "2025-06-01T14:00:00"
assert m.strategize_completed_at == "2025-06-01T14:30:00"
assert m.execute_started_at == "2025-06-01T15:00:00"
assert m.execute_completed_at == "2025-06-01T15:30:00"
assert m.apply_started_at == "2025-06-01T16:00:00"
assert m.applied_at == "2025-06-01T16:30:00"
assert m.completed_at == "2025-06-01T16:30:00"
# ---------------------------------------------------------------------------
# Plan: storing a domain object with non-default automation level
# ---------------------------------------------------------------------------
@given('a plan domain object is prepared with automation level "{level}"')
def step_create_plan_domain_with_automation_level(context, level):
"""Prepare a plan domain object with a specific automation level."""
from cleveragents.domain.models.core.plan import ActionState, AutomationLevel
context.plan_domain_input = _make_plan_domain(
phase="action",
action_state=ActionState.AVAILABLE,
automation_level=AutomationLevel(level),
)
@then('the stored plan record automation level should be "{expected_level}"')
def step_verify_from_domain_automation_level(context, expected_level):
"""Verify the stored plan record has the expected automation level."""
assert context.plan_model_result.automation_level == expected_level
# ---------------------------------------------------------------------------
# Plan: storing a domain object with an explicit action_id
# ---------------------------------------------------------------------------
VALID_ULID_ACTION = "01JACTION000000000000ACTION"
@when("the plan domain object is stored with an explicit action identifier")
def step_call_from_domain_with_action_id(context):
"""Store the plan domain object with an explicit action_id."""
context.plan_model_result = LifecyclePlanModel.from_domain(
context.plan_domain_input,
action_id=VALID_ULID_ACTION,
)
@then("the stored plan record should preserve the supplied action identifier")
def step_verify_from_domain_action_id(context):
"""Verify the stored plan record retains the supplied action_id."""
assert context.plan_model_result.action_id == VALID_ULID_ACTION
# ---------------------------------------------------------------------------
# Round-trip persistence tests
# ---------------------------------------------------------------------------
@when("the action is saved to the database and reloaded as a domain object")
def step_convert_and_persist_action(context):
"""Save the action to the database and reload it as a domain object."""
context.action_model_persisted = LifecycleActionModel.from_domain(
context.action_domain_input
)
context.db_session.add(context.action_model_persisted)
context.db_session.commit()
context.action_model_retrieved = (
context.db_session.query(LifecycleActionModel)
.filter_by(action_id=context.action_model_persisted.action_id)
.first()
)
assert context.action_model_retrieved is not None
context.action_round_tripped = context.action_model_retrieved.to_domain()
@then("the reloaded action should match the original action")
def step_verify_round_trip_action(context):
"""Verify the reloaded action matches the original."""
original = context.action_domain_input
result = context.action_round_tripped
assert result.action_id == original.action_id
assert str(result.namespaced_name) == str(original.namespaced_name)
assert result.definition_of_done == original.definition_of_done
assert result.strategy_actor == original.strategy_actor
assert result.execution_actor == original.execution_actor
assert result.reusable == original.reusable
assert result.read_only == original.read_only
assert result.tags == original.tags
assert len(result.arguments) == len(original.arguments)
@when("the plan record is saved and then reloaded as a domain object")
def step_persist_and_reload_plan(context):
"""Save the plan record and reload it as a domain object."""
# Plan was already persisted in the Given step
context.plan_model_retrieved = (
context.db_session.query(LifecyclePlanModel)
.filter_by(plan_id=context.plan_model.plan_id)
.first()
)
assert context.plan_model_retrieved is not None
context.plan_round_tripped = context.plan_model_retrieved.to_domain()
@then("the reloaded plan should preserve its identity and phase")
def step_verify_round_trip_plan(context):
"""Verify the reloaded plan retains its identity and phase."""
from cleveragents.domain.models.core.plan import PlanPhase
result = context.plan_round_tripped
assert result.identity.plan_id == VALID_ULID_1
assert result.phase == PlanPhase.ACTION
assert result.description == "A test plan description"
assert result.created_by == "test-user"