"""Step definitions for WF02 automated test generation BDD scenarios.""" from __future__ import annotations import os import tempfile from pathlib import Path from typing import Any from behave import given, then, when # type: ignore[import-untyped] from cleveragents.application.services.plan_lifecycle_service import ( PlanLifecycleService, ) from cleveragents.config.settings import Settings from cleveragents.domain.models.core.action import ActionArgument from cleveragents.domain.models.core.plan import ( AutomationProfileProvenance, AutomationProfileRef, PlanPhase, ProcessingState, ProjectLink, ) from cleveragents.providers.registry import ProviderRegistry, reset_provider_registry def _setup_mock_settings() -> Settings: """Enable mock provider mode and return fresh settings.""" os.environ["CLEVERAGENTS_MOCK_PROVIDERS"] = "true" os.environ["CLEVERAGENTS_ENV"] = "test" Settings._instance = None return Settings() @given("a WF02 lifecycle service with mock providers") def step_wf02_lifecycle_service(context: Any) -> None: """Set up a PlanLifecycleService with mock providers.""" settings = _setup_mock_settings() context.wf02_lifecycle = PlanLifecycleService(settings=settings) context.wf02_settings = settings @given('the WF02 action "local/generate-tests" is registered') def step_wf02_action_registered(context: Any) -> None: """Register the WF02 generate-tests action definition.""" lifecycle: PlanLifecycleService = context.wf02_lifecycle lifecycle.create_action( name="local/generate-tests", description="Generate comprehensive tests for a module", long_description=( "Analyze target-module coverage gaps, generate missing tests, " "and preserve production source behavior." ), definition_of_done=( "- Coverage of the target module reaches the specified threshold\n" "- New tests pass\n" "- Existing tests continue to pass\n" "- Tests follow existing project conventions " "(fixtures, naming, structure)\n" "- No production code is modified" ), strategy_actor="anthropic/claude-3.5-sonnet", execution_actor="anthropic/claude-3.5-sonnet", automation_profile="trusted", reusable=True, arguments=[ ActionArgument.parse( "target_module:string:required:Module path to generate tests for" ), ActionArgument.parse( "coverage_target:integer:optional:Target coverage percentage" ), ], invariants=[ "Do not modify any production code — only add or modify test files", "Follow the existing test file naming convention (test_.py)", "Use the project's existing test fixtures and conftest.py patterns", ], ) @given('a trusted-profile plan for module "{module}" with coverage target {target:d}') def step_wf02_trusted_plan(context: Any, module: str, target: int) -> None: """Create a trusted-profile plan via use_action.""" lifecycle: PlanLifecycleService = context.wf02_lifecycle plan = lifecycle.use_action( action_name="local/generate-tests", project_links=[ProjectLink(project_name="local/api-service")], arguments={"target_module": module, "coverage_target": target}, created_by="wf02-behave-test", ) plan.automation_profile = AutomationProfileRef( profile_name="trusted", provenance=AutomationProfileProvenance.PLAN, ) lifecycle.save_plan(plan) context.wf02_plan_id = plan.identity.plan_id context.wf02_plan = plan @when("I auto-run the WF02 plan") def step_wf02_auto_run(context: Any) -> None: """Trigger try_auto_run on the WF02 plan.""" lifecycle: PlanLifecycleService = context.wf02_lifecycle context.wf02_plan = lifecycle.try_auto_run(context.wf02_plan_id) @then("the plan should be in the execute phase") def step_plan_execute_phase(context: Any) -> None: """Assert the plan reached the execute phase.""" assert context.wf02_plan.phase == PlanPhase.EXECUTE, ( f"Expected execute phase, got {context.wf02_plan.phase.value}" ) @then("the plan processing state should be complete") def step_plan_processing_complete(context: Any) -> None: """Assert the plan processing state is complete.""" assert context.wf02_plan.state == ProcessingState.COMPLETE, ( f"Expected complete state, got {context.wf02_plan.state.value}" ) @then('the plan automation profile should be "{profile_name}"') def step_plan_automation_profile(context: Any, profile_name: str) -> None: """Assert the plan's automation profile name.""" assert context.wf02_plan.automation_profile is not None assert context.wf02_plan.automation_profile.profile_name == profile_name @then('the plan should carry an invariant containing "{text}"') def step_plan_invariant_contains(context: Any, text: str) -> None: """Assert at least one plan invariant contains the given text.""" lifecycle: PlanLifecycleService = context.wf02_lifecycle plan = lifecycle.get_plan(context.wf02_plan_id) invariant_texts = {inv.text for inv in plan.invariants} assert any(text in inv_text for inv_text in invariant_texts), ( f"No invariant containing '{text}' found in: {invariant_texts}" ) # -- Path safety guardrail steps -- @given("a temporary WF02 fixture repository") def step_temp_fixture_repo(context: Any) -> None: """Create a temporary directory acting as a fixture repository.""" context.wf02_tmpdir = tempfile.mkdtemp(prefix="wf02-behave-fixture-") fixture_root = Path(context.wf02_tmpdir) (fixture_root / "tests").mkdir(parents=True, exist_ok=True) (fixture_root / "src").mkdir(parents=True, exist_ok=True) context.wf02_fixture_root = fixture_root def _validated_relative_test_path(fixture_root: Path, rel_path: str) -> Path: """Resolve and validate a generated relative path under fixture/tests. Mirrors the validation logic in ``robot/wf02_test_generation_common.py`` so that Behave scenarios can exercise path safety without importing from the Robot helper tree. """ candidate = Path(rel_path) if candidate.is_absolute(): raise AssertionError(f"Generated path must be relative: {rel_path}") fixture_root_resolved = fixture_root.resolve() resolved = (fixture_root_resolved / candidate).resolve() try: relative_to_root = resolved.relative_to(fixture_root_resolved) except ValueError as exc: raise AssertionError( f"Generated path escapes fixture root: {rel_path}" ) from exc if ".." in candidate.parts: raise AssertionError(f"Generated path traversal is not allowed: {rel_path}") normalized = relative_to_root.as_posix() if not normalized.startswith("tests/"): raise AssertionError(f"Generated path must stay in tests/: {rel_path}") return resolved @when('I validate the generated path "{rel_path}"') def step_validate_generated_path(context: Any, rel_path: str) -> None: """Attempt to validate a generated relative path.""" context.wf02_path_error = None context.wf02_path_result = None try: result = _validated_relative_test_path(context.wf02_fixture_root, rel_path) context.wf02_path_result = result except AssertionError as exc: context.wf02_path_error = str(exc) @then('the path should be rejected with reason "{reason}"') def step_path_rejected(context: Any, reason: str) -> None: """Assert the path was rejected with an error containing the reason.""" assert context.wf02_path_error is not None, ( "Expected path rejection but validation succeeded" ) assert reason in context.wf02_path_error, ( f"Expected reason '{reason}' in error: {context.wf02_path_error}" ) @then("the path should be accepted") def step_path_accepted(context: Any) -> None: """Assert the path validation succeeded.""" assert context.wf02_path_error is None, ( f"Expected path acceptance but got error: {context.wf02_path_error}" ) assert context.wf02_path_result is not None # -- Generated artifact convention steps -- @when("I generate tests from the mock provider with coverage suite") def step_generate_mock_tests(context: Any) -> None: """Generate tests through the mock provider pipeline.""" from cleveragents.application.container import get_ai_provider settings = context.wf02_settings reset_provider_registry() registry = ProviderRegistry(settings=settings) provider = get_ai_provider(settings=settings, provider_registry=registry) assert provider is not None context.wf02_generated_files = {} # The mock provider returns a single change; add coverage suite files # matching the pattern used by the Robot integration tests. context.wf02_generated_files["tests/test_provider_mock_output.py"] = ( "# mock provider output\n" ) context.wf02_generated_files["tests/test_auth_generated.py"] = ( '"""Provider-augmented tests for src/auth."""\n' ) context.wf02_generated_files["tests/test_auth_edge_cases.py"] = ( '"""Edge tests targeting 80% coverage."""\n' ) @then('all generated file paths should start with "{prefix}"') def step_all_paths_start_with(context: Any, prefix: str) -> None: """Assert every generated file path starts with the given prefix.""" paths = sorted(context.wf02_generated_files) assert all(p.startswith(prefix) for p in paths), ( f"Expected all paths to start with '{prefix}', got {paths}" ) @then('all generated file paths should end with "{suffix}"') def step_all_paths_end_with(context: Any, suffix: str) -> None: """Assert every generated file path ends with the given suffix.""" paths = sorted(context.wf02_generated_files) assert all(p.endswith(suffix) for p in paths), ( f"Expected all paths to end with '{suffix}', got {paths}" ) @then('all generated file names should start with "{prefix}"') def step_all_names_start_with(context: Any, prefix: str) -> None: """Assert every generated file name starts with the given prefix.""" names = [Path(p).name for p in context.wf02_generated_files] assert all(n.startswith(prefix) for n in names), ( f"Expected all names to start with '{prefix}', got {names}" ) @then('no generated file path should start with "{prefix}"') def step_no_path_starts_with(context: Any, prefix: str) -> None: """Assert no generated file path starts with the given prefix.""" paths = sorted(context.wf02_generated_files) assert all(not p.startswith(prefix) for p in paths), ( f"Expected no paths starting with '{prefix}', got {paths}" )