"""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"