Files
temp/features/steps/database_models_lifecycle_coverage_steps.py
T

1148 lines
42 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 (
ActionArgumentModel,
Base,
LifecycleActionModel,
LifecyclePlanModel,
PlanProjectModel,
)
# ---------------------------------------------------------------------------
# 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(
name="local/test-action",
namespace="local",
short_name="test-action",
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="available",
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.
Maps the legacy helper parameters to the new spec-aligned column names:
- ``name`` -> ``namespaced_name`` (PK)
- ``short_name`` -> ``name``
- ``description`` -> ``description``
- ``inputs_schema`` -> ``inputs_schema_json``
- ``tags`` -> ``tags_json``
"""
return LifecycleActionModel(
namespaced_name=name,
namespace=namespace,
name=short_name,
description=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_json=inputs_schema if inputs_schema != "[]" else None,
state=state,
reusable=reusable,
read_only=read_only,
created_at=created_at,
updated_at=updated_at,
created_by=created_by,
tags_json=tags,
)
def _make_plan_model(
plan_id=VALID_ULID_1,
parent_plan_id=None,
root_plan_id=None,
action_name="local/test-action",
phase="strategize",
state="queued",
attempt=1,
namespaced_name="local/test-plan",
namespace="local",
description="A test plan description",
definition_of_done="Tests pass",
project_names=None,
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.
Maps legacy helper parameters to the new spec-aligned column names:
- ``action_id`` -> ``action_name`` (FK to actions.namespaced_name)
- ``state`` -> ``processing_state``
- ``project_ids`` removed (now uses ``plan_projects`` child table)
- ``tags`` -> ``tags_json``
- Added ``namespace`` column
"""
model = LifecyclePlanModel(
plan_id=plan_id,
parent_plan_id=parent_plan_id,
root_plan_id=root_plan_id,
action_name=action_name,
phase=phase,
processing_state=state,
attempt=attempt,
namespaced_name=namespaced_name,
namespace=namespace,
description=description,
definition_of_done=definition_of_done,
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_json=tags,
reusable=reusable,
read_only=read_only,
)
# Populate project links via child table
if project_names:
for pname in json.loads(project_names):
model.project_links_rel.append(
PlanProjectModel(
project_name=pname,
created_at=created_at,
)
)
return model
# ---------------------------------------------------------------------------
# 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 (namespaced_name)."""
assert str(context.action_domain.namespaced_name) == "local/test-action"
@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.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.action import ActionState
assert context.action_domain.state == ActionState.AVAILABLE
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 via child table."""
context.action_model = _make_action_model()
# Add arguments via the child table
context.action_model.arguments_rel.append(
ActionArgumentModel(
name="target_coverage",
arg_type="integer",
requirement="required",
description="Target coverage percentage",
position=0,
)
)
context.action_model.arguments_rel.append(
ActionArgumentModel(
name="framework",
arg_type="string",
requirement="optional",
description="Test framework to use",
position=1,
)
)
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,
ActionState,
)
from cleveragents.domain.models.core.plan import NamespacedName
context.action_domain_input = Action(
namespaced_name=NamespacedName(namespace="local", name="my-action"),
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.AVAILABLE,
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 (namespaced_name)."""
assert context.action_model_result.namespaced_name == "local/my-action"
@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.namespaced_name == "local/my-action"
assert context.action_model_result.namespace == "local"
assert context.action_model_result.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.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 stored via child table."""
args = context.action_model_result.arguments_rel
assert len(args) == 1
assert args[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_json)
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 == "available"
@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, ActionState
from cleveragents.domain.models.core.plan import NamespacedName
context.action_domain_input = Action(
namespaced_name=NamespacedName(namespace="local", name="enum-action"),
description="Enum state 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(
namespaced_name=NamespacedName(namespace="local", name="str-action"),
description="Desc",
long_description="Long",
definition_of_done="Done",
strategy_actor="local/strategy",
execution_actor="local/executor",
estimation_actor=None,
review_actor=None,
apply_actor=None,
invariant_actor=None,
arguments=[],
inputs_schema=None,
invariants=[],
automation_profile=None,
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 strategize phase
# ---------------------------------------------------------------------------
@given("a plan record exists in the strategize phase with queued state")
def step_create_plan_model_strategize_queued(context):
"""Persist a plan record in the strategize phase with queued state."""
action_model = _make_action_model()
context.db_session.add(action_model)
context.db_session.commit()
context.plan_model = _make_plan_model(
phase="strategize",
state="queued",
project_names='["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 processing state should be queued")
def step_verify_plan_processing_state_queued(context):
"""Verify the loaded plan has QUEUED processing state."""
from cleveragents.domain.models.core.plan import ProcessingState
assert context.plan_domain.processing_state == ProcessingState.QUEUED
@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(namespaced_name="local/test-action")
.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.state == ProcessingState.PROCESSING
# ---------------------------------------------------------------------------
# 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(namespaced_name="local/test-action")
.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(namespaced_name="local/test-action")
.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_names='["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."""
project_names = [link.project_name for link in context.plan_domain.project_links]
assert project_names == ["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 processing state
# ---------------------------------------------------------------------------
def _make_plan_domain(
phase="strategize",
processing_state=None,
plan_id=VALID_ULID_1,
project_links=None,
tags=None,
timestamps=None,
):
"""Create a Plan domain object for testing storage."""
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
if project_links is None:
project_links = [ProjectLink(project_name="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_processing = (
processing_state if processing_state is not None else ProcessingState.QUEUED
)
return Plan(
identity=PlanIdentity(plan_id=plan_id),
namespaced_name=NamespacedName(namespace="local", name="test-plan"),
action_name="local/test-action",
description="Test plan description",
definition_of_done="Tests pass",
phase=PlanPhase(phase),
processing_state=resolved_processing,
strategy_actor="local/strategy",
execution_actor="local/executor",
project_links=project_links,
timestamps=timestamps,
created_by="test-user",
tags=tags,
reusable=True,
read_only=False,
)
@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 strategize phase with queued state."""
from cleveragents.domain.models.core.plan import ProcessingState, ProjectLink
context.plan_domain_input = _make_plan_domain(
phase="strategize",
processing_state=ProcessingState.QUEUED,
project_links=[
ProjectLink(project_name="proj-1"),
ProjectLink(project_name="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 processing_state."""
assert context.plan_model_result.processing_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 == "strategize"
@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 links via child table."""
project_names = [
pl.project_name for pl in context.plan_model_result.project_links_rel
]
assert project_names == ["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 serialized as JSON."""
parsed = json.loads(context.plan_model_result.tags_json)
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, ProjectLink
context.plan_domain_input = _make_plan_domain(
phase="strategize",
processing_state=ProcessingState.QUEUED,
project_links=[
ProjectLink(project_name="proj-1"),
ProjectLink(project_name="proj-2"),
],
tags=["ci", "test"],
)
@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"),
action_name="local/test-action",
description="Null state plan",
definition_of_done="Done",
phase=PlanPhase.STRATEGIZE,
processing_state=None,
automation_profile=None,
strategy_actor="local/strategy",
execution_actor="local/executor",
review_actor=None,
apply_actor=None,
estimation_actor=None,
invariant_actor=None,
project_links=[],
invariants=[],
arguments={},
arguments_order=[],
changeset_id=None,
sandbox_refs=[],
validation_summary=None,
decision_root_id=None,
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 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="strategize",
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 (automation_level removed, uses profile)."""
context.plan_domain_input = _make_plan_domain(
phase="strategize",
)
@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 (automation_level column removed)."""
# automation_level column was removed; this step is a no-op
pass
# ---------------------------------------------------------------------------
# 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_name."""
context.plan_model_result = LifecyclePlanModel.from_domain(
context.plan_domain_input,
action_name=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_name == VALID_ULID_ACTION
) # still passed explicitly
# ---------------------------------------------------------------------------
# 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(namespaced_name=context.action_model_persisted.namespaced_name)
.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 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.STRATEGIZE
assert result.description == "A test plan description"
assert result.created_by == "test-user"