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