Files
letta-server/tests/test_exception_logging.py
Kian Jones 25d54dd896 chore: enable F821, F401, W293 (#9503)
* auto fixes

* auto fix pt2 and transitive deps and undefined var checking locals()

* manual fixes (ignored or letta-code fixed)

* fix circular import
2026-02-24 10:55:08 -08:00

245 lines
7.8 KiB
Python

"""
Tests for global exception logging system.
"""
import asyncio
from unittest.mock import patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from letta.exceptions.logging import add_exception_context, log_and_raise, log_exception
from letta.server.rest_api.middleware.logging import LoggingMiddleware
@pytest.fixture
def app_with_exception_middleware():
"""Create a test FastAPI app with logging middleware."""
app = FastAPI()
app.add_middleware(LoggingMiddleware)
@app.get("/test-error")
def test_error():
raise ValueError("Test error message")
@app.get("/test-error-with-context")
def test_error_with_context():
exc = ValueError("Test error with context")
exc = add_exception_context(
exc,
user_id="test-user-123",
operation="test_operation",
)
raise exc
@app.get("/test-success")
def test_success():
return {"status": "ok"}
return app
def test_exception_middleware_logs_basic_exception(app_with_exception_middleware):
"""Test that the middleware logs exceptions with basic context."""
client = TestClient(app_with_exception_middleware, raise_server_exceptions=False)
with patch("letta.server.rest_api.middleware.logging.logger") as mock_logger:
response = client.get("/test-error")
# Should return 500
assert response.status_code == 500
# Should log the error
assert mock_logger.error.called
call_args = mock_logger.error.call_args
# Check the message
assert "ValueError" in call_args[0][0]
assert "Test error message" in call_args[0][0]
# Check the extra context
extra = call_args[1]["extra"]
assert extra["exception_type"] == "ValueError"
assert extra["exception_message"] == "Test error message"
assert "request" in extra
assert extra["request"]["method"] == "GET"
assert "/test-error" in extra["request"]["path"]
def test_exception_middleware_logs_custom_context(app_with_exception_middleware):
"""Test that the middleware logs custom context attached to exceptions."""
client = TestClient(app_with_exception_middleware, raise_server_exceptions=False)
with patch("letta.server.rest_api.middleware.logging.logger") as mock_logger:
response = client.get("/test-error-with-context")
# Should return 500
assert response.status_code == 500
# Should log the error with custom context
assert mock_logger.error.called
call_args = mock_logger.error.call_args
extra = call_args[1]["extra"]
# Check custom context
assert "custom_context" in extra
assert extra["custom_context"]["user_id"] == "test-user-123"
assert extra["custom_context"]["operation"] == "test_operation"
def test_exception_middleware_does_not_interfere_with_success(app_with_exception_middleware):
"""Test that the middleware doesn't interfere with successful requests."""
client = TestClient(app_with_exception_middleware)
response = client.get("/test-success")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_add_exception_context():
"""Test that add_exception_context properly attaches context to exceptions."""
exc = ValueError("Test error")
# Add context
exc_with_context = add_exception_context(
exc,
user_id="user-123",
agent_id="agent-456",
operation="test_op",
)
# Should be the same exception object
assert exc_with_context is exc
# Should have context attached
assert hasattr(exc, "__letta_context__")
assert exc.__letta_context__["user_id"] == "user-123"
assert exc.__letta_context__["agent_id"] == "agent-456"
assert exc.__letta_context__["operation"] == "test_op"
def test_add_exception_context_multiple_times():
"""Test that add_exception_context can be called multiple times."""
exc = ValueError("Test error")
# Add context in multiple calls
add_exception_context(exc, user_id="user-123")
add_exception_context(exc, agent_id="agent-456")
# Both should be present
assert exc.__letta_context__["user_id"] == "user-123"
assert exc.__letta_context__["agent_id"] == "agent-456"
def test_log_and_raise():
"""Test that log_and_raise logs and then raises the exception."""
exc = ValueError("Test error")
with patch("letta.exceptions.logging.logger") as mock_logger:
with pytest.raises(ValueError, match="Test error"):
log_and_raise(
exc,
"Operation failed",
context={"user_id": "user-123"},
)
# Should have logged
assert mock_logger.error.called
call_args = mock_logger.error.call_args
# Check message
assert "Operation failed" in call_args[0][0]
assert "ValueError" in call_args[0][0]
assert "Test error" in call_args[0][0]
# Check extra context
extra = call_args[1]["extra"]
assert extra["exception_type"] == "ValueError"
assert extra["user_id"] == "user-123"
def test_log_exception():
"""Test that log_exception logs without raising."""
exc = ValueError("Test error")
with patch("letta.exceptions.logging.logger") as mock_logger:
# Should not raise
log_exception(
exc,
"Operation failed, using fallback",
context={"user_id": "user-123"},
)
# Should have logged
assert mock_logger.error.called
call_args = mock_logger.error.call_args
# Check message
assert "Operation failed, using fallback" in call_args[0][0]
assert "ValueError" in call_args[0][0]
# Check extra context
extra = call_args[1]["extra"]
assert extra["exception_type"] == "ValueError"
assert extra["user_id"] == "user-123"
def test_log_exception_with_different_levels():
"""Test that log_exception respects different log levels."""
exc = ValueError("Test error")
with patch("letta.exceptions.logging.logger") as mock_logger:
# Test warning level
log_exception(exc, "Warning message", level="warning")
assert mock_logger.warning.called
# Test info level
log_exception(exc, "Info message", level="info")
assert mock_logger.info.called
@pytest.mark.asyncio
async def test_global_exception_handler_setup():
"""Test that global exception handlers can be set up without errors."""
from letta.server.global_exception_handler import setup_global_exception_handlers
# Should not raise
setup_global_exception_handlers()
# Verify sys.excepthook was modified
import sys
assert sys.excepthook != sys.__excepthook__
@pytest.mark.asyncio
async def test_asyncio_exception_handler():
"""Test that asyncio exception handler can be set up."""
from letta.server.global_exception_handler import setup_asyncio_exception_handler
loop = asyncio.get_event_loop()
# Should not raise
setup_asyncio_exception_handler(loop)
def test_exception_middleware_preserves_traceback(app_with_exception_middleware):
"""Test that the middleware preserves traceback information."""
client = TestClient(app_with_exception_middleware, raise_server_exceptions=False)
with patch("letta.server.rest_api.middleware.logging.logger") as mock_logger:
response = client.get("/test-error")
assert response.status_code == 500
call_args = mock_logger.error.call_args
# Check that exc_info was passed (enables traceback)
assert call_args[1]["exc_info"] is True
# Check that traceback is in extra
extra = call_args[1]["extra"]
assert "traceback" in extra
assert "ValueError" in extra["traceback"]
assert "test_error" in extra["traceback"]