Files
cleveragents-core/features/steps/auto_debug_integration_steps.py
freemo a074b4846f
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 19s
CI / quality (pull_request) Successful in 28s
CI / security (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 56s
CI / integration_tests (pull_request) Successful in 4m51s
CI / unit_tests (pull_request) Successful in 19m29s
CI / docker (pull_request) Successful in 39s
CI / benchmark-regression (pull_request) Successful in 26m10s
CI / coverage (pull_request) Successful in 47m42s
CI / lint (push) Successful in 13s
CI / quality (push) Successful in 17s
CI / build (push) Successful in 23s
CI / typecheck (push) Successful in 30s
CI / security (push) Successful in 30s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 2m52s
CI / unit_tests (push) Successful in 10m8s
CI / docker (push) Successful in 1m19s
CI / benchmark-publish (push) Successful in 11m54s
CI / coverage (push) Failing after 39m51s
fix(provider): remove FakeListLLM defaults
Remove FakeListLLM as a silent fallback in agent graph constructors
(plan_generation.py, context_analysis.py, auto_debug.py). All three now
raise ValueError when llm=None, making missing-provider errors explicit.

Add Settings.mock_providers flag and validate_provider_availability()
method. Update container.get_ai_provider() to check Settings.mock_providers
first, with env-var fallback for backward compatibility.

Add resolve_provider_by_name() helper to the provider registry and export
it from cleveragents.providers. Add structlog trace logging to
ProviderRegistry.get_default_provider_type() to record selection reasoning.

Update all existing behave step files, robot tests, and benchmarks that
relied on the implicit FakeListLLM default to pass an explicit LLM
instance instead.

Add new BDD tests (features/provider_fixes.feature with 17 scenarios),
Robot Framework integration tests (robot/provider_detection_smoke.robot),
and ASV benchmarks (benchmarks/provider_selection_bench.py).

ISSUES CLOSED: #323
2026-02-27 09:47:10 -05:00

250 lines
9.7 KiB
Python

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