"""Tests for agent failure detection in container logs."""
import pytest
from preloop.agents.container import ContainerAgentExecutor
from preloop.agents.base import AgentStatus
class TestAgentFailureDetection:
"""Test suite for detecting agent failures from container logs."""
def setup_method(self):
"""Set up test fixtures."""
self.agent = ContainerAgentExecutor(
agent_type="test", config={}, image="test-image:latest"
)
def test_detect_litellm_bad_request_error(self):
"""Test detection of litellm.BadRequestError in logs."""
logs = """
Starting agent execution...
litellm.BadRequestError: OpenAIException - {
"error": {
"message": "Unsupported parameter: 'temperature' is not supported with this model.",
"type": "invalid_request_error",
"param": "temperature",
"code": null
}
}
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_detect_authentication_error(self):
"""Test detection of authentication errors."""
logs = """
Connecting to API...
litellm.AuthenticationError: Invalid API key provided
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_detect_traceback(self):
"""Test detection of Python tracebacks."""
logs = """
Running task...
Traceback (most recent call last):
File "main.py", line 42, in run
result = do_something()
ValueError: Invalid input
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_no_error_in_success_logs(self):
"""Test that successful execution logs don't trigger false positives."""
logs = """
Starting agent execution...
Processing task...
Task completed successfully
Agent finished with exit code 0
"""
assert self.agent._detect_error_in_logs(logs) is False
def test_extract_error_from_logs(self):
"""Test extraction of error messages from logs."""
logs = """
Starting execution
Processing request
litellm.BadRequestError: Unsupported parameter
Additional context line
More context
"""
error_message = self.agent._extract_error_from_logs(logs)
assert "litellm.BadRequestError" in error_message
assert "Unsupported parameter" in error_message
def test_extract_error_with_context(self):
"""Test that error extraction includes surrounding context."""
logs = """
Line 1
Line 2
Error occurred: Connection failed
Line 4
Line 5
Line 6
Line 7
"""
error_message = self.agent._extract_error_from_logs(logs)
# Should include lines around the error
assert "Line 2" in error_message or "Line 1" in error_message
assert "Error occurred" in error_message
assert "Line 4" in error_message or "Line 5" in error_message
def test_no_error_pattern_returns_last_lines(self):
"""Test that logs without explicit errors return last lines as context.
When an execution fails, the last few lines often contain relevant
context even if they don't match explicit error patterns.
"""
logs = """
Starting execution
Processing task
Task completed
Process terminated unexpectedly
"""
error_message = self.agent._extract_error_from_logs(logs)
# Should return last content lines as fallback context
assert "terminated unexpectedly" in error_message
def test_detect_openai_exception(self):
"""Test detection of OpenAI exceptions."""
logs = """
Making API call...
OpenAIException: Rate limit exceeded
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_detect_fatal_error(self):
"""Test detection of FATAL ERROR messages."""
logs = """
Initializing system...
FATAL ERROR: Cannot connect to database
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_detect_critical_error(self):
"""Test detection of CRITICAL log level errors."""
logs = """
Running checks...
CRITICAL: System resource exhausted
"""
assert self.agent._detect_error_in_logs(logs) is True
def test_benign_no_commits_message(self):
"""Test that 'no commits' message doesn't trigger failure."""
logs = """
Checking for commits...
No commits on feature-branch, skipping push
Agent finished successfully
"""
assert self.agent._detect_error_in_logs(logs) is False
def test_benign_nothing_to_commit(self):
"""Test that 'nothing to commit' message doesn't trigger failure."""
logs = """
Running git status...
On branch main
nothing to commit, working tree clean
"""
assert self.agent._detect_error_in_logs(logs) is False
def test_benign_pr_already_exists(self):
"""Test that PR/MR creation failure doesn't trigger failure."""
logs = """
Creating pull request...
Failed to create PR (may already exist)
Continuing with execution...
"""
assert self.agent._detect_error_in_logs(logs) is False
def test_benign_up_to_date(self):
"""Test that 'up to date' git messages don't trigger failure."""
logs = """
Pushing to origin...
Everything up-to-date
Push completed
"""
assert self.agent._detect_error_in_logs(logs) is False
def test_single_error_message_not_critical(self):
"""Test that a single ERROR: message doesn't trigger failure."""
logs = """
Starting agent...
Processing task...
DEBUG: Task processing complete
INFO: Results saved
"""
# Add a single error that's not critical
logs_with_error = logs + "\nINFO: Operation completed with 0 errors.\n"
assert self.agent._detect_error_in_logs(logs_with_error) is False
def test_multiple_errors_trigger_failure(self):
"""Test that multiple ERROR: messages trigger failure."""
logs = """
Starting agent...
ERROR: Connection timeout on attempt 1
ERROR: Connection timeout on attempt 2
ERROR: Connection timeout on attempt 3
Failed to complete task
"""
assert self.agent._detect_error_in_logs(logs) is True
@pytest.mark.asyncio
class TestContainerFailureStatus:
"""Integration tests for container failure status detection."""
async def test_exit_code_zero_with_errors_marks_failed(self, mocker):
"""Test that exit code 0 with error logs is marked as FAILED."""
agent = ContainerAgentExecutor(
agent_type="test", config={}, image="test-image:latest"
)
# Mock Docker container with exit code 0 but error logs
mock_docker = mocker.AsyncMock()
mock_container = mocker.AsyncMock()
mock_container.show.return_value = {
"State": {"ExitCode": 0, "Status": "exited"}
}
error_logs = [
"Starting aider",
"litellm.BadRequestError: Unsupported parameter: 'temperature'",
"Failed to execute task",
]
mock_container.log.return_value = error_logs
mocker.patch.object(agent, "_get_docker_client", return_value=mock_docker)
mock_docker.containers.get.return_value = mock_container
# Mock get_logs to return error logs
mocker.patch.object(agent, "get_logs", return_value=error_logs)
# Mock get_status to return SUCCEEDED initially
mocker.patch.object(agent, "get_status", return_value=AgentStatus.SUCCEEDED)
result = await agent.get_result("test-container")
# Should override SUCCEEDED to FAILED due to error logs
assert result.status == AgentStatus.FAILED
assert result.error_message is not None
assert (
"BadRequestError" in result.error_message
or "Unsupported parameter" in result.error_message
)