forked from HAL9000/cleveragents-core
051ee7c290
Added 52 new .feature files and corresponding _steps.py files targeting previously uncovered code paths in the following areas: - TUI layer: app, commands, persona (state/schema/registry), widgets, input (shell_exec, reference_parser) - Application services: plan lifecycle/service/executor, session, project, repo indexing, correction, checkpoint, actor, llm_actors, strategy coordinator, resource file watcher, service retry wiring - CLI commands: session, resource, repl, plan, db, automation_profile - Domain models: retry_policy, resource_type, cost_budget, docker_compose_analyzer, detail_level, _sql_string_aware, _postgresql_helpers - Core: circuit_breaker, retry_service_patterns - Infrastructure: repositories, transaction_sandbox, strategy_registry, plugins/loader, container - Config: settings - Agents: plan_generation, context_analysis, auto_debug - A2A: facade All new tests follow the Behave/Gherkin BDD standard. Resolved step definition collisions with unique prefixes. Fixed Alembic fileConfig logger disabling issue (disable_existing_loggers=False). ISSUES CLOSED: #1068
348 lines
13 KiB
Python
348 lines
13 KiB
Python
"""Step definitions for auto_debug_coverage_boost.feature.
|
|
|
|
These steps target specific uncovered lines in
|
|
src/cleveragents/agents/graphs/auto_debug.py:
|
|
|
|
- Line 124: _analyze_error when content == "Mock LLM response" (true branch)
|
|
- Lines 128-130: _analyze_error exception handler
|
|
- Lines 200-203: _generate_fix when content == "Mock LLM response" (true branch)
|
|
- Lines 209-213: _generate_fix JSONDecodeError fallback
|
|
- Lines 215-220: _generate_fix exception handler
|
|
- Line 266: _validate_fix when content == "Mock LLM response"
|
|
- Lines 269-270: _validate_fix JSON parse with is_valid
|
|
- Lines 271-276: _validate_fix JSONDecodeError keyword scan
|
|
- Lines 277-279: _validate_fix exception handler
|
|
- Lines 283-286: _validate_fix invalid → append to attempted_fixes
|
|
- Lines 290-295: _should_retry_fix routing logic
|
|
- Lines 297-305: _finalize result construction
|
|
"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.agents.graphs.auto_debug import AutoDebugAgent, AutoDebugState
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers: mock LLM classes
|
|
# ---------------------------------------------------------------------------
|
|
class _MockResponse:
|
|
"""Minimal response object with a content attribute."""
|
|
|
|
def __init__(self, content: str) -> None:
|
|
self.content = content
|
|
|
|
|
|
class _MockLLM:
|
|
"""Stub LLM that returns a fixed content string."""
|
|
|
|
def __init__(self, content: str = "Mock LLM response") -> None:
|
|
self._content = content
|
|
|
|
def invoke(self, messages: Any) -> _MockResponse:
|
|
return _MockResponse(self._content)
|
|
|
|
|
|
class _FailingLLM:
|
|
"""Stub LLM whose invoke always raises an exception."""
|
|
|
|
def invoke(self, messages: Any) -> None:
|
|
raise RuntimeError("LLM service unavailable for testing")
|
|
|
|
|
|
def _base_state(**overrides: Any) -> AutoDebugState:
|
|
"""Create a minimal valid AutoDebugState with optional overrides."""
|
|
state: AutoDebugState = {
|
|
"messages": [],
|
|
"context": {},
|
|
"result": None,
|
|
"error": None,
|
|
"metadata": {},
|
|
"error_message": "NameError: name 'x' is not defined",
|
|
"code_context": "print(x)",
|
|
"attempted_fixes": [],
|
|
"current_fix": {},
|
|
"fix_validated": False,
|
|
}
|
|
state.update(overrides) # type: ignore[typeddict-item]
|
|
return state
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
@given("the auto debug graph module is imported")
|
|
def step_auto_debug_module_imported(context):
|
|
"""Verify the auto_debug module is importable."""
|
|
assert AutoDebugAgent is not None
|
|
assert AutoDebugState is not None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Agent construction helpers
|
|
# ---------------------------------------------------------------------------
|
|
@given('an auto debug agent with a mock LLM returning "{content}"')
|
|
def step_create_agent_mock_llm(context, content):
|
|
"""Create an AutoDebugAgent with a mock LLM returning the given content."""
|
|
context.ad_agent = AutoDebugAgent(llm=_MockLLM(content), max_fix_attempts=3)
|
|
|
|
|
|
@given(
|
|
'an auto debug agent with a mock LLM returning "{content}"'
|
|
" and max {n:d} fix attempts"
|
|
)
|
|
def step_create_agent_mock_llm_max_attempts(context, content, n):
|
|
"""Create an AutoDebugAgent with a mock LLM and custom max_fix_attempts."""
|
|
context.ad_agent = AutoDebugAgent(llm=_MockLLM(content), max_fix_attempts=n)
|
|
|
|
|
|
@given("an auto debug agent with a failing LLM")
|
|
def step_create_agent_failing_llm(context):
|
|
"""Create an AutoDebugAgent with an LLM that always raises."""
|
|
context.ad_agent = AutoDebugAgent(llm=_FailingLLM(), max_fix_attempts=3)
|
|
|
|
|
|
@given("an auto debug agent with a mock LLM returning valid fix JSON")
|
|
def step_create_agent_valid_fix_json(context):
|
|
"""Create an agent whose LLM returns well-formed fix JSON."""
|
|
fix_json = json.dumps(
|
|
{
|
|
"description": "Parsed JSON fix",
|
|
"code": "print('fixed')",
|
|
"files_to_modify": ["main.py"],
|
|
}
|
|
)
|
|
context.ad_agent = AutoDebugAgent(llm=_MockLLM(fix_json), max_fix_attempts=3)
|
|
|
|
|
|
@given("an auto debug agent with a mock LLM returning valid validation JSON true")
|
|
def step_create_agent_valid_validation_json_true(context):
|
|
"""Create an agent whose LLM returns validation JSON with is_valid=true."""
|
|
validation_json = json.dumps(
|
|
{
|
|
"is_valid": True,
|
|
"reasoning": "Fix resolves the error",
|
|
"issues": [],
|
|
}
|
|
)
|
|
context.ad_agent = AutoDebugAgent(llm=_MockLLM(validation_json), max_fix_attempts=3)
|
|
|
|
|
|
@given("an auto debug agent with a mock LLM returning valid validation JSON false")
|
|
def step_create_agent_valid_validation_json_false(context):
|
|
"""Create an agent whose LLM returns validation JSON with is_valid=false."""
|
|
validation_json = json.dumps(
|
|
{
|
|
"is_valid": False,
|
|
"reasoning": "Fix does not resolve the error",
|
|
"issues": ["error persists"],
|
|
}
|
|
)
|
|
context.ad_agent = AutoDebugAgent(llm=_MockLLM(validation_json), max_fix_attempts=3)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State preparation helpers
|
|
# ---------------------------------------------------------------------------
|
|
@given("a state with an existing error analysis message")
|
|
def step_state_with_analysis(context):
|
|
"""Prepare a state that already has an error_analysis message."""
|
|
context.ad_state = _base_state(
|
|
messages=[
|
|
{
|
|
"role": "assistant",
|
|
"content": "Detected a NameError due to undefined variable",
|
|
"type": "error_analysis",
|
|
}
|
|
]
|
|
)
|
|
|
|
|
|
@given("a state with two previous attempted fixes")
|
|
def step_state_with_previous_attempts(context):
|
|
"""Prepare a state with two prior attempted fixes."""
|
|
context.ad_state = _base_state(
|
|
messages=[
|
|
{
|
|
"role": "assistant",
|
|
"content": "Analysis result",
|
|
"type": "error_analysis",
|
|
}
|
|
],
|
|
attempted_fixes=[
|
|
{"description": "First attempt", "code": "# v1", "files_to_modify": []},
|
|
{"description": "Second attempt", "code": "# v2", "files_to_modify": []},
|
|
],
|
|
)
|
|
|
|
|
|
@given("a state with a current fix to validate")
|
|
def step_state_with_current_fix(context):
|
|
"""Prepare a state that has a current_fix ready for validation."""
|
|
context.ad_state = _base_state(
|
|
current_fix={
|
|
"description": "Proposed fix",
|
|
"code": "x = 42\nprint(x)",
|
|
"files_to_modify": ["main.py"],
|
|
}
|
|
)
|
|
|
|
|
|
@given("a finalize state with fix_validated {validated} and {n:d} attempted fixes")
|
|
def step_finalize_state(context, validated, n):
|
|
"""Prepare a state for _finalize with the given validation status."""
|
|
is_valid = validated.lower() == "true"
|
|
fixes = [
|
|
{"description": f"Attempt {i + 1}", "code": f"# v{i + 1}"} for i in range(n)
|
|
]
|
|
context.ad_state = _base_state(
|
|
fix_validated=is_valid,
|
|
attempted_fixes=fixes,
|
|
current_fix={"description": "Latest fix", "code": "# latest"},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When: call individual node methods
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call _analyze_error with an error state")
|
|
def step_call_analyze_error(context):
|
|
"""Invoke _analyze_error directly on the agent."""
|
|
state = _base_state()
|
|
context.ad_result_state = context.ad_agent._analyze_error(state)
|
|
|
|
|
|
@when("I call _generate_fix with the prepared state")
|
|
def step_call_generate_fix(context):
|
|
"""Invoke _generate_fix directly on the agent."""
|
|
context.ad_result_state = context.ad_agent._generate_fix(context.ad_state)
|
|
|
|
|
|
@when("I call _validate_fix with the prepared state")
|
|
def step_call_validate_fix(context):
|
|
"""Invoke _validate_fix directly on the agent."""
|
|
context.ad_result_state = context.ad_agent._validate_fix(context.ad_state)
|
|
|
|
|
|
@when("I call _finalize with the prepared state")
|
|
def step_call_finalize(context):
|
|
"""Invoke _finalize directly on the agent."""
|
|
context.ad_result_state = context.ad_agent._finalize(context.ad_state)
|
|
|
|
|
|
@when("I check should_retry_fix with validated {validated} and {n:d} attempt")
|
|
def step_check_should_retry_singular(context, validated, n):
|
|
"""Invoke _should_retry_fix with a crafted state (singular form)."""
|
|
is_valid = validated.lower() == "true"
|
|
fixes = [{"description": f"fix {i + 1}"} for i in range(n)]
|
|
state = _base_state(fix_validated=is_valid, attempted_fixes=fixes)
|
|
context.routing_decision = context.ad_agent._should_retry_fix(state)
|
|
|
|
|
|
@when("I check should_retry_fix with validated {validated} and {n:d} attempts")
|
|
def step_check_should_retry_plural(context, validated, n):
|
|
"""Invoke _should_retry_fix with a crafted state (plural form)."""
|
|
is_valid = validated.lower() == "true"
|
|
fixes = [{"description": f"fix {i + 1}"} for i in range(n)]
|
|
state = _base_state(fix_validated=is_valid, attempted_fixes=fixes)
|
|
context.routing_decision = context.ad_agent._should_retry_fix(state)
|
|
|
|
|
|
@when("I try to create an AutoDebugAgent with no LLM")
|
|
def step_create_agent_no_llm(context):
|
|
"""Attempt to create an AutoDebugAgent with llm=None."""
|
|
try:
|
|
AutoDebugAgent(llm=None)
|
|
context.constructor_error = None
|
|
except ValueError as exc:
|
|
context.constructor_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then: assertions
|
|
# ---------------------------------------------------------------------------
|
|
@then('the analysis message should be "{expected}"')
|
|
def step_verify_analysis_message(context, expected):
|
|
"""Verify the last error_analysis message content."""
|
|
messages = context.ad_result_state.get("messages", [])
|
|
analysis_msgs = [m for m in messages if m.get("type") == "error_analysis"]
|
|
assert analysis_msgs, "No error_analysis message found in state"
|
|
actual = analysis_msgs[-1]["content"]
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then('the current fix description should be "{expected}"')
|
|
def step_verify_fix_description(context, expected):
|
|
"""Verify the current_fix description."""
|
|
fix = context.ad_result_state.get("current_fix", {})
|
|
actual = fix.get("description", "")
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then('the current fix code should be "{expected}"')
|
|
def step_verify_fix_code(context, expected):
|
|
"""Verify the current_fix code."""
|
|
fix = context.ad_result_state.get("current_fix", {})
|
|
actual = fix.get("code", "")
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then("the fix should be marked as validated")
|
|
def step_fix_validated_true(context):
|
|
"""Verify fix_validated is True."""
|
|
assert context.ad_result_state["fix_validated"] is True
|
|
|
|
|
|
@then("the fix should not be marked as validated")
|
|
def step_fix_validated_false(context):
|
|
"""Verify fix_validated is False."""
|
|
assert context.ad_result_state["fix_validated"] is False
|
|
|
|
|
|
@then("the current fix should be appended to attempted fixes")
|
|
def step_fix_appended_to_attempted(context):
|
|
"""Verify the current fix was moved to attempted_fixes."""
|
|
attempted = context.ad_result_state.get("attempted_fixes", [])
|
|
assert len(attempted) > 0, "Expected at least one attempted fix"
|
|
last = attempted[-1]
|
|
assert last.get("description") == "Proposed fix", (
|
|
f"Expected 'Proposed fix', got '{last.get('description')}'"
|
|
)
|
|
|
|
|
|
@then('the routing decision should be "{expected}"')
|
|
def step_verify_routing(context, expected):
|
|
"""Verify the _should_retry_fix routing result."""
|
|
assert context.routing_decision == expected, (
|
|
f"Expected '{expected}', got '{context.routing_decision}'"
|
|
)
|
|
|
|
|
|
@then("the result should indicate success {expected}")
|
|
def step_verify_result_success(context, expected):
|
|
"""Verify the result dict success field."""
|
|
is_success = expected.lower() == "true"
|
|
result = context.ad_result_state.get("result")
|
|
assert result is not None, "No result dict found in state"
|
|
assert result["success"] is is_success, (
|
|
f"Expected success={is_success}, got {result['success']}"
|
|
)
|
|
|
|
|
|
@then("the result attempts count should be {n:d}")
|
|
def step_verify_result_attempts(context, n):
|
|
"""Verify the result dict attempts count."""
|
|
result = context.ad_result_state.get("result")
|
|
assert result is not None, "No result dict found in state"
|
|
assert result["attempts"] == n, f"Expected {n} attempts, got {result['attempts']}"
|
|
|
|
|
|
@then("a ValueError should be raised with the missing provider message")
|
|
def step_verify_constructor_valueerror(context):
|
|
"""Verify the constructor raised a ValueError about missing LLM."""
|
|
assert context.constructor_error is not None, "Expected ValueError but none raised"
|
|
assert isinstance(context.constructor_error, ValueError)
|
|
assert "No LLM provider configured" in str(context.constructor_error)
|