"""Step definitions for AutoDebug integration tests.""" import os from unittest.mock import MagicMock, Mock, patch from behave import given, then, when from behave.runner import Context from cleveragents.application.container import get_container, reset_container @given('I have a current plan with instruction "{instruction}"') def step_create_plan_with_instruction(context: Context, instruction: str) -> None: """Create a plan with given instruction.""" from cleveragents.cli.commands.plan import tell_command # Ensure we have a project first if not hasattr(context, "project"): container = get_container() project_service = container.project_service() context.project = project_service.get_current_project() tell_command(prompt=instruction, name=None) # Get the plan from container container = get_container() plan_service = container.plan_service() context.plan = plan_service.get_current_plan(context.project) @given("the AI provider will generate code with a syntax error") def step_ai_generates_syntax_error(context: Context) -> None: """Configure mock AI to generate code with syntax error.""" context.mock_error_type = "syntax" context.mock_build_error = "SyntaxError: invalid syntax on line 5" context.mock_generated_code = ( "def foo():\n print('hello'\n" # Missing closing paren ) @given("the AutoDebugAgent can fix the syntax error") def step_autodebug_can_fix_syntax(context: Context) -> None: """Configure AutoDebugAgent to successfully fix syntax error.""" context.autodebug_can_fix = True context.autodebug_fix_code = "def foo():\n print('hello')\n" @given("the AI provider will generate code with an import error") def step_ai_generates_import_error(context: Context) -> None: """Configure mock AI to generate code with import error.""" context.mock_error_type = "import" context.mock_build_error = "ImportError: No module named 'nonexistent'" context.mock_generated_code = "import nonexistent\n\ndef foo():\n pass\n" @given("the AutoDebugAgent can fix the import error") def step_autodebug_can_fix_import(context: Context) -> None: """Configure AutoDebugAgent to successfully fix import error.""" context.autodebug_can_fix = True context.autodebug_fix_code = "# Removed bad import\n\ndef foo():\n pass\n" @given("the AI provider will generate code with an unfixable error") def step_ai_generates_unfixable_error(context: Context) -> None: """Configure mock AI to generate code with unfixable error.""" context.mock_error_type = "unfixable" context.mock_build_error = "RuntimeError: System configuration error" context.mock_generated_code = "# This error cannot be fixed by code changes\n" @given("the AutoDebugAgent cannot fix the error") def step_autodebug_cannot_fix(context: Context) -> None: """Configure AutoDebugAgent to fail at fixing error.""" context.autodebug_can_fix = False @given("the AI provider will generate code with a validation error") def step_ai_generates_validation_error(context: Context) -> None: """Configure mock AI to generate code with validation error.""" context.mock_error_type = "validation" context.mock_build_error = "ValidationError: Code does not meet requirements" context.mock_generated_code = "# Code that fails validation\n" @when("I run auto_debug_build with max attempts {max_attempts:d}") def step_run_auto_debug_build(context: Context, max_attempts: int) -> None: """Run auto_debug_build with specified max attempts.""" from cleveragents.application.container import get_container container = get_container() plan_service = container.plan_service() # Mock the build_plan to simulate failure original_build = plan_service.build_plan def mock_build(project, progress_callback=None): if hasattr(context, "mock_build_error") and not getattr( context, "build_succeeded", False ): # First attempt fails if getattr(context, "autodebug_can_fix", False): # Mark that next attempt should succeed context.build_succeeded = True raise Exception(context.mock_build_error) # Subsequent attempts succeed if AutoDebug can fix return original_build(project, progress_callback=progress_callback) # Create mock LLM that returns deterministic responses mock_llm = MagicMock() mock_response = Mock() mock_response.content = "Mock LLM response" mock_llm.invoke = MagicMock(return_value=mock_response) # Inject mock LLM into the plan service so AutoDebugAgent receives it plan_service._llm = mock_llm # Patch build_plan and the AutoDebugAgent class with ( patch.object(plan_service, "build_plan", side_effect=mock_build), patch("cleveragents.agents.AutoDebugAgent") as agent_cls, ): agent_instance = MagicMock() agent_instance.invoke.return_value = { "result": {"success": True, "fix": {}}, } agent_cls.return_value = agent_instance try: success, changes, error = plan_service.auto_debug_build( project=context.project, max_attempts=max_attempts ) context.auto_debug_success = success context.auto_debug_changes = changes context.auto_debug_error = error except Exception as e: context.auto_debug_success = False context.auto_debug_error = str(e) context.auto_debug_changes = [] @then("the build should succeed after debugging") def step_build_succeeds(context: Context) -> None: """Verify build succeeded after auto-debug.""" assert context.auto_debug_success, f"Build failed: {context.auto_debug_error}" @then("the build should fail after {attempts:d} attempts") def step_build_fails_after_attempts(context: Context, attempts: int) -> None: """Verify build failed after specified attempts.""" assert not context.auto_debug_success, "Build should have failed but succeeded" @then("debug attempts should be saved to database") def step_debug_attempts_saved(context: Context) -> None: """Verify debug attempts were saved to database.""" from cleveragents.application.container import get_container container = get_container() with container.unit_of_work().transaction() as uow: attempts = uow.debug_attempts.get_for_plan(context.plan.id) assert len(attempts) > 0, "No debug attempts saved to database" context.saved_debug_attempts = attempts @then("the final attempt should be marked as successful") def step_final_attempt_successful(context: Context) -> None: """Verify final debug attempt is marked successful.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" # Find the highest attempt number max_attempt = max(a.attempt_number for a in context.saved_debug_attempts) final_attempt = next( a for a in context.saved_debug_attempts if a.attempt_number == max_attempt ) assert final_attempt.success, "Final attempt not marked as successful" @then("at least {count:d} debug attempt should exist") def step_at_least_n_attempts(context: Context, count: int) -> None: """Verify at least N debug attempts exist.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" assert len(context.saved_debug_attempts) >= count, ( f"Expected at least {count} attempts, got {len(context.saved_debug_attempts)}" ) @then("all {count:d} debug attempts should be marked as unsuccessful") def step_all_attempts_unsuccessful(context: Context, count: int) -> None: """Verify all attempts are marked unsuccessful.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" assert len(context.saved_debug_attempts) == count, ( f"Expected {count} attempts, got {len(context.saved_debug_attempts)}" ) for attempt in context.saved_debug_attempts: assert not attempt.success, ( f"Attempt {attempt.attempt_number} should be unsuccessful" ) @then("each attempt should have an error message") def step_attempts_have_error_messages(context: Context) -> None: """Verify each attempt has an error message.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" for attempt in context.saved_debug_attempts: assert attempt.error_message, ( f"Attempt {attempt.attempt_number} missing error message" ) @then("each attempt should have an attempt number") def step_attempts_have_numbers(context: Context) -> None: """Verify each attempt has an attempt number.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" for attempt in context.saved_debug_attempts: assert attempt.attempt_number is not None, "Attempt missing attempt number" assert attempt.attempt_number >= 1, ( f"Invalid attempt number: {attempt.attempt_number}" ) @then("each attempt should have a timestamp") def step_attempts_have_timestamps(context: Context) -> None: """Verify each attempt has a timestamp.""" assert hasattr(context, "saved_debug_attempts"), "No debug attempts found" for attempt in context.saved_debug_attempts: assert attempt.created_at, f"Attempt {attempt.attempt_number} missing timestamp" def after_scenario(context: Context, scenario) -> None: """Clean up after each scenario.""" # Change back to original directory if hasattr(context, "original_cwd"): os.chdir(context.original_cwd) # Clean up temp directory if hasattr(context, "test_dir"): import shutil shutil.rmtree(context.test_dir, ignore_errors=True) # Reset container reset_container() # Clean up environment os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)