"""Step definitions for Automation Profile BDD tests. Tests the automation profile system: - AutomationProfile-based auto-progress (should_auto_progress, auto_progress) - PlanLifecycleService automation methods (pause_plan, resume_plan) - Profile resolution via automation_profile on Plan """ from behave import given, then, when from behave.runner import Context from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.config.settings import Settings from cleveragents.core.exceptions import PlanError from cleveragents.domain.models.core.automation_profile import BUILTIN_PROFILES from cleveragents.domain.models.core.plan import ( AutomationProfileProvenance, AutomationProfileRef, ProjectLink, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _create_service(context: Context) -> None: """Create a PlanLifecycleService for automation tests.""" settings = Settings() context.auto_service = PlanLifecycleService(settings=settings) context.auto_error = None def _create_available_action(context: Context, name: str = "local/auto-action") -> str: """Create and make available an action, return its namespaced name.""" action = context.auto_service.create_action( name=name, description="Automation test action", definition_of_done="Automation test definition", strategy_actor="openai/gpt-4", execution_actor="openai/gpt-4", ) action_name = str(action.namespaced_name) return action_name def _profile_name_for_level(level: str) -> str: """Map old level names to profile names for backward-compat step defs.""" mapping = { "manual": "manual", "review_before_apply": "auto", "full_automation": "full-auto", } return mapping.get(level, level) def _create_plan_at_phase( context: Context, profile_name: str, phase: str, state: str, ) -> None: """Create a plan and advance it to the requested phase/state. Sets the automation profile on the plan after creation. """ action_name = _create_available_action(context) plan = context.auto_service.use_action( action_name=action_name, project_links=[ProjectLink(project_name="project-auto")], ) plan_id = plan.identity.plan_id # Ensure manual profile so we can control progression plan.automation_profile = AutomationProfileRef( profile_name="manual", provenance=AutomationProfileProvenance.PLAN, ) # Advance through phases as needed if phase in ("strategize",): if state == "processing": context.auto_service.start_strategize(plan_id) elif state == "complete": context.auto_service.start_strategize(plan_id) context.auto_service.complete_strategize(plan_id) elif phase in ("execute",): context.auto_service.start_strategize(plan_id) context.auto_service.complete_strategize(plan_id) context.auto_service.execute_plan(plan_id) if state == "processing": context.auto_service.start_execute(plan_id) elif state == "complete": context.auto_service.start_execute(plan_id) context.auto_service.complete_execute(plan_id) elif phase in ("apply",): context.auto_service.start_strategize(plan_id) context.auto_service.complete_strategize(plan_id) context.auto_service.execute_plan(plan_id) context.auto_service.start_execute(plan_id) context.auto_service.complete_execute(plan_id) context.auto_service.apply_plan(plan_id) if state == "processing": context.auto_service.start_apply(plan_id) # Now set the desired automation profile resolved_profile = _profile_name_for_level(profile_name) plan = context.auto_service.get_plan(plan_id) plan.automation_profile = AutomationProfileRef( profile_name=resolved_profile, provenance=AutomationProfileProvenance.PLAN, ) context.auto_plan = plan # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("I have a plan lifecycle service with automation level support") def step_create_automation_service(context: Context) -> None: """Create a PlanLifecycleService for automation tests.""" _create_service(context) # --------------------------------------------------------------------------- # Plan creation at specific phase/state with automation profile # --------------------------------------------------------------------------- @given( 'I have a plan with automation level "{level}" in strategize phase with processing state' ) def step_plan_auto_strategize_processing(context: Context, level: str) -> None: """Create plan at strategize/processing with given automation profile.""" _create_plan_at_phase(context, level, "strategize", "processing") @given( 'I have a plan with automation level "{level}" in execute phase with processing state' ) def step_plan_auto_execute_processing(context: Context, level: str) -> None: """Create plan at execute/processing with given automation profile.""" _create_plan_at_phase(context, level, "execute", "processing") @given( 'I have a plan with automation level "{level}" in strategize phase with queued state' ) def step_plan_auto_strategize_queued(context: Context, level: str) -> None: """Create plan at strategize/queued with given automation profile.""" _create_plan_at_phase(context, level, "strategize", "queued") @given( 'I have a plan with automation level "{level}" in strategize phase with complete state for query' ) def step_plan_auto_strategize_complete_query(context: Context, level: str) -> None: """Create plan at strategize/complete for should_auto_progress query.""" _create_plan_at_phase(context, level, "strategize", "complete") @given( 'I have a plan with automation level "{level}" in execute phase with complete state for query' ) def step_plan_auto_execute_complete_query(context: Context, level: str) -> None: """Create plan at execute/complete for should_auto_progress query.""" _create_plan_at_phase(context, level, "execute", "complete") # --------------------------------------------------------------------------- # When: complete_strategize / complete_execute on automated plan # --------------------------------------------------------------------------- @when("I complete strategize on the automated plan") def step_complete_strategize_auto(context: Context) -> None: """Complete strategize and let auto_progress run.""" context.auto_plan = context.auto_service.complete_strategize( context.auto_plan.identity.plan_id ) @when("I complete execute on the automated plan") def step_complete_execute_auto(context: Context) -> None: """Complete execute and let auto_progress run.""" context.auto_plan = context.auto_service.complete_execute( context.auto_plan.identity.plan_id ) # --------------------------------------------------------------------------- # Then: check automated plan phase and state # --------------------------------------------------------------------------- @then('the automated plan phase should be "{expected}"') def step_check_auto_plan_phase(context: Context, expected: str) -> None: """Check the automated plan's current phase.""" actual = context.auto_plan.phase.value assert actual == expected, ( f"Expected automated plan phase '{expected}', got '{actual}'" ) @then('the automated plan processing state should be "{expected}"') def step_check_auto_plan_state(context: Context, expected: str) -> None: """Check the automated plan's current processing state.""" assert context.auto_plan.state is not None actual = context.auto_plan.state.value assert actual == expected, ( f"Expected automated plan processing state '{expected}', got '{actual}'" ) # --------------------------------------------------------------------------- # Plan-level override tests # --------------------------------------------------------------------------- @given('the global automation level is "{level}"') def step_set_global_automation_level(context: Context, level: str) -> None: """Set up service with the global default automation profile.""" _create_service(context) profile_name = _profile_name_for_level(level) # pydantic-settings BaseSettings doesn't accept constructor overrides for # fields with validation_alias, so we set the attribute after construction. context.auto_service.settings.default_automation_profile = profile_name @given('I have a plan created with explicit automation level "{level}"') def step_create_plan_explicit_level(context: Context, level: str) -> None: """Create a plan with an explicit automation profile.""" action_name = _create_available_action(context) context.auto_plan = context.auto_service.use_action( action_name=action_name, project_links=[ProjectLink(project_name="project-explicit")], ) profile_name = _profile_name_for_level(level) context.auto_plan.automation_profile = AutomationProfileRef( profile_name=profile_name, provenance=AutomationProfileProvenance.PLAN, ) @given("I have an available action for automation tests") def step_create_available_action_for_auto(context: Context) -> None: """Create an available action for use in automation tests.""" context.auto_action_name = _create_available_action(context) @when('I use the action with automation level "{level}" on project "{project_id}"') def step_use_action_with_level(context: Context, level: str, project_id: str) -> None: """Use an action and set an explicit automation profile.""" context.auto_plan = context.auto_service.use_action( action_name=context.auto_action_name, project_links=[ProjectLink(project_name=project_id)], ) profile_name = _profile_name_for_level(level) context.auto_plan.automation_profile = AutomationProfileRef( profile_name=profile_name, provenance=AutomationProfileProvenance.PLAN, ) @when('I use the action without explicit automation level on project "{project_id}"') def step_use_action_without_level(context: Context, project_id: str) -> None: """Use an action without specifying automation profile (uses default).""" context.auto_plan = context.auto_service.use_action( action_name=context.auto_action_name, project_links=[ProjectLink(project_name=project_id)], ) @then('the plan automation level should be "{expected}"') def step_check_plan_automation_level(context: Context, expected: str) -> None: """Check the plan's automation profile maps to expected level name.""" if context.auto_plan.automation_profile is not None: actual = context.auto_plan.automation_profile.profile_name else: actual = "manual" expected_profile = _profile_name_for_level(expected) assert actual == expected_profile, ( f"Expected plan automation profile '{expected_profile}', got '{actual}'" ) # --------------------------------------------------------------------------- # Change automation profile mid-plan # --------------------------------------------------------------------------- @when('I set the plan automation level to "{level}"') def step_set_plan_automation_level(context: Context, level: str) -> None: """Set automation profile on existing plan.""" profile_name = _profile_name_for_level(level) context.auto_plan.automation_profile = AutomationProfileRef( profile_name=profile_name, provenance=AutomationProfileProvenance.PLAN, ) @given("I have a terminal plan for automation tests") def step_create_terminal_plan_for_auto(context: Context) -> None: """Create a terminal (applied) plan.""" action_name = _create_available_action(context) plan = context.auto_service.use_action( action_name=action_name, project_links=[ProjectLink(project_name="project-terminal")], ) plan_id = plan.identity.plan_id context.auto_service.start_strategize(plan_id) context.auto_service.complete_strategize(plan_id) context.auto_service.execute_plan(plan_id) context.auto_service.start_execute(plan_id) context.auto_service.complete_execute(plan_id) context.auto_service.apply_plan(plan_id) context.auto_service.start_apply(plan_id) context.auto_service.complete_apply(plan_id) context.auto_plan = context.auto_service.get_plan(plan_id) @when('I try to set the plan automation level to "{level}"') def step_try_set_plan_automation_level(context: Context, level: str) -> None: """Try to set automation profile (may fail for terminal plans).""" context.auto_error = None try: if context.auto_plan.is_terminal: raise PlanError( f"Plan {context.auto_plan.identity.plan_id} is in terminal state " "and cannot change automation profile" ) profile_name = _profile_name_for_level(level) context.auto_plan.automation_profile = AutomationProfileRef( profile_name=profile_name, provenance=AutomationProfileProvenance.PLAN, ) except PlanError as e: context.auto_error = e @then("a plan error should be raised for automation") def step_check_plan_error_automation(context: Context) -> None: """Check that a plan error was raised.""" assert context.auto_error is not None, "Expected a PlanError to be raised" assert isinstance(context.auto_error, PlanError) # --------------------------------------------------------------------------- # should_auto_progress query # --------------------------------------------------------------------------- @then("should_auto_progress should return false") def step_check_should_not_auto_progress(context: Context) -> None: """Verify should_auto_progress returns False.""" result = context.auto_service.should_auto_progress(context.auto_plan) assert result is False, f"Expected should_auto_progress=False, got {result}" @then("should_auto_progress should return true") def step_check_should_auto_progress(context: Context) -> None: """Verify should_auto_progress returns True.""" result = context.auto_service.should_auto_progress(context.auto_plan) assert result is True, f"Expected should_auto_progress=True, got {result}" @then("should_auto_progress on terminal plan should return false") def step_check_should_not_auto_progress_terminal(context: Context) -> None: """Verify should_auto_progress returns False for terminal plan.""" result = context.auto_service.should_auto_progress(context.auto_plan) assert result is False, ( f"Expected should_auto_progress=False for terminal plan, got {result}" ) # --------------------------------------------------------------------------- # Pause and Resume # --------------------------------------------------------------------------- @when("I pause the plan") def step_pause_plan(context: Context) -> None: """Pause the plan.""" context.auto_plan = context.auto_service.pause_plan( context.auto_plan.identity.plan_id ) @when("I try to pause the terminal plan") def step_try_pause_terminal_plan(context: Context) -> None: """Try to pause a terminal plan (should fail).""" context.auto_error = None try: context.auto_plan = context.auto_service.pause_plan( context.auto_plan.identity.plan_id ) except PlanError as e: context.auto_error = e @given('I have a paused plan that was previously "{level}" in strategize phase') def step_create_paused_plan(context: Context, level: str) -> None: """Create a plan, set automation profile, then pause it.""" _create_plan_at_phase(context, level, "strategize", "queued") context.auto_service.pause_plan(context.auto_plan.identity.plan_id) context.auto_plan = context.auto_service.get_plan( context.auto_plan.identity.plan_id ) @when('I resume the plan with level "{level}"') def step_resume_plan_with_level(context: Context, level: str) -> None: """Resume the plan with a specific automation profile.""" profile_name = _profile_name_for_level(level) context.auto_plan = context.auto_service.resume_plan( context.auto_plan.identity.plan_id, automation_profile=profile_name, ) @when("I resume the plan without explicit level") def step_resume_plan_without_level(context: Context) -> None: """Resume the plan without specifying profile (defaults to auto).""" context.auto_plan = context.auto_service.resume_plan( context.auto_plan.identity.plan_id, ) @when("I try to resume the terminal plan") def step_try_resume_terminal_plan(context: Context) -> None: """Try to resume a terminal plan (should fail).""" context.auto_error = None try: context.auto_plan = context.auto_service.resume_plan( context.auto_plan.identity.plan_id ) except PlanError as e: context.auto_error = e @given("I have a paused plan in strategize phase with complete state") def step_create_paused_plan_strategize_complete(context: Context) -> None: """Create a plan at strategize/complete and pause it.""" _create_plan_at_phase(context, "full_automation", "strategize", "complete") context.auto_service.pause_plan(context.auto_plan.identity.plan_id) context.auto_plan = context.auto_service.get_plan( context.auto_plan.identity.plan_id ) # --------------------------------------------------------------------------- # Settings resolution # --------------------------------------------------------------------------- @then('the resolved default automation level should be "{expected}"') def step_check_resolved_default(context: Context, expected: str) -> None: """Check the resolved default automation profile from settings.""" profile = context.auto_service.settings.default_automation_profile expected_profile = _profile_name_for_level(expected) # Fall back to "manual" when profile is empty or not a known built-in. actual = profile if (profile and profile in BUILTIN_PROFILES) else "manual" assert actual == expected_profile, ( f"Expected resolved profile '{expected_profile}', got '{actual}'" )