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