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
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
250 lines
9.7 KiB
Python
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)
|