"""Tests for structured logging configuration and error tracking. Tests cover JSON formatting, log rotation, error ID generation, Discord error message formatting, and privacy protection. """ import json import logging import tempfile from pathlib import Path from unittest.mock import Mock, patch import pytest from claude_coordinator.logging_config import ( JSONFormatter, ErrorTracker, setup_logging, get_log_directory, format_error_for_discord, log_and_format_error ) class TestJSONFormatter: """Tests for JSONFormatter class.""" def test_json_formatter_produces_valid_json(self): """Test that JSONFormatter outputs valid JSON. Validates that the formatter produces parseable JSON with correct structure and no syntax errors. """ formatter = JSONFormatter() record = logging.LogRecord( name='test', level=logging.INFO, pathname='/test/path.py', lineno=42, msg='Test message', args=(), exc_info=None ) output = formatter.format(record) # Should parse as valid JSON parsed = json.loads(output) assert isinstance(parsed, dict) def test_json_formatter_includes_required_fields(self): """Test that JSON logs include all required fields. Validates that timestamp, level, module, function, line, and message are present in every log entry. """ formatter = JSONFormatter() record = logging.LogRecord( name='test', level=logging.ERROR, pathname='/test/path.py', lineno=123, msg='Error message', args=(), exc_info=None ) output = formatter.format(record) parsed = json.loads(output) # Check required fields assert 'timestamp' in parsed assert 'level' in parsed assert parsed['level'] == 'ERROR' assert 'module' in parsed assert 'function' in parsed assert 'line' in parsed assert parsed['line'] == 123 assert 'message' in parsed assert parsed['message'] == 'Error message' def test_json_formatter_includes_exception_info(self): """Test that exceptions are properly included in JSON logs. Validates that exc_info is formatted and included when present. """ formatter = JSONFormatter() try: raise ValueError("Test exception") except ValueError: import sys exc_info = sys.exc_info() record = logging.LogRecord( name='test', level=logging.ERROR, pathname='/test/path.py', lineno=456, msg='Exception occurred', args=(), exc_info=exc_info ) output = formatter.format(record) parsed = json.loads(output) # Should include exception traceback assert 'exception' in parsed assert 'ValueError' in parsed['exception'] assert 'Test exception' in parsed['exception'] def test_json_formatter_includes_extra_fields(self): """Test that extra fields are included in JSON output. Validates that additional context passed via logging.extra is properly included in the output. """ formatter = JSONFormatter() record = logging.LogRecord( name='test', level=logging.INFO, pathname='/test/path.py', lineno=789, msg='Test with extra', args=(), exc_info=None ) # Add extra fields record.channel_id = '12345' record.session_id = 'abc-def-ghi' record.cost_usd = 0.05 output = formatter.format(record) parsed = json.loads(output) # Extra fields should be present assert parsed['channel_id'] == '12345' assert parsed['session_id'] == 'abc-def-ghi' assert parsed['cost_usd'] == 0.05 class TestErrorTracker: """Tests for ErrorTracker class.""" def test_generate_error_id_returns_string(self): """Test that error ID generation returns a string. Validates basic return type and format of error IDs. """ error_id = ErrorTracker.generate_error_id() assert isinstance(error_id, str) assert len(error_id) == 8 # First 8 chars of UUID def test_generate_error_id_is_unique(self): """Test that error IDs are unique across calls. Validates that multiple calls produce different IDs. """ ids = [ErrorTracker.generate_error_id() for _ in range(100)] # All IDs should be unique assert len(ids) == len(set(ids)) def test_log_error_with_id_includes_error_id(self): """Test that error logging includes the error ID in extra fields. Validates that the generated error ID is properly attached to the log record for tracking. """ logger = logging.getLogger('test') with patch.object(logger, 'log') as mock_log: error_id = ErrorTracker.log_error_with_id( logger, logging.ERROR, "Test error", channel_id="12345" ) # Should have called log with error_id in extra mock_log.assert_called_once() call_kwargs = mock_log.call_args[1] assert 'extra' in call_kwargs assert call_kwargs['extra']['error_id'] == error_id assert call_kwargs['extra']['channel_id'] == "12345" class TestSetupLogging: """Tests for setup_logging function.""" def test_setup_logging_creates_log_directory(self): """Test that log directory is created if it doesn't exist. Validates directory creation for log file storage. """ with tempfile.TemporaryDirectory() as tmpdir: log_file = Path(tmpdir) / "subdir" / "test.log" setup_logging( log_level="INFO", log_file=str(log_file), json_logs=True ) # Directory should be created assert log_file.parent.exists() def test_setup_logging_configures_handlers(self): """Test that logging handlers are properly configured. Validates that both file and console handlers are set up. """ with tempfile.TemporaryDirectory() as tmpdir: log_file = Path(tmpdir) / "test.log" setup_logging( log_level="DEBUG", log_file=str(log_file), json_logs=True ) root_logger = logging.getLogger() # Should have handlers assert len(root_logger.handlers) >= 2 # Check for RotatingFileHandler handler_types = [type(h).__name__ for h in root_logger.handlers] assert 'RotatingFileHandler' in handler_types assert 'StreamHandler' in handler_types class TestFormatErrorForDiscord: """Tests for Discord error message formatting.""" def test_format_error_includes_error_id(self): """Test that Discord error messages include error ID. Validates that error ID is displayed for user reference. """ error_id = "abc12345" msg = format_error_for_discord( error_type="timeout", error_id=error_id ) assert error_id in msg assert "Error ID" in msg def test_format_error_handles_known_types(self): """Test that known error types produce appropriate messages. Validates that different error types have user-friendly messages. """ # Test timeout error msg = format_error_for_discord(error_type="timeout") assert "⏱️" in msg assert "Timeout" in msg # Test Claude error msg = format_error_for_discord(error_type="claude_error") assert "❌" in msg assert "Claude Error" in msg # Test config error msg = format_error_for_discord(error_type="config_error") assert "⚙️" in msg assert "Configuration Error" in msg def test_format_error_handles_unknown_types(self): """Test that unknown error types produce generic message. Validates fallback behavior for unexpected error types. """ msg = format_error_for_discord(error_type="unknown_type_xyz") assert "❌" in msg assert "Error" in msg def test_format_error_does_not_expose_internal_details(self): """Test that error messages don't expose sensitive information. Validates privacy protection - no stack traces, paths, or internal implementation details in Discord messages. """ # Create exception with stack trace try: raise ValueError("Internal error details") except ValueError as e: msg = format_error_for_discord( error_type="claude_error", error=e, error_id="test123" ) # Should NOT contain exception details or stack traces assert "ValueError" not in msg assert "Internal error details" not in msg assert "Traceback" not in msg # Should contain user-friendly message assert "❌" in msg assert "Error ID" in msg class TestLogAndFormatError: """Tests for log_and_format_error convenience function.""" def test_log_and_format_error_returns_tuple(self): """Test that function returns (error_id, discord_message) tuple. Validates return value structure for use in error handling. """ logger = logging.getLogger('test') with patch.object(logger, 'log'): error_id, discord_msg = log_and_format_error( logger, error_type="timeout", message="Test timeout", channel_id="12345" ) assert isinstance(error_id, str) assert isinstance(discord_msg, str) assert len(error_id) == 8 assert error_id in discord_msg def test_log_and_format_error_logs_with_context(self): """Test that error is logged with full context. Validates that all context fields are passed to logger. """ logger = logging.getLogger('test') with patch.object(logger, 'log') as mock_log: error_id, discord_msg = log_and_format_error( logger, error_type="timeout", message="Test timeout", channel_id="12345", session_id="abc-def", duration=300 ) # Should log with all context mock_log.assert_called_once() call_kwargs = mock_log.call_args[1] assert call_kwargs['extra']['channel_id'] == "12345" assert call_kwargs['extra']['session_id'] == "abc-def" assert call_kwargs['extra']['duration'] == 300 assert call_kwargs['extra']['error_id'] == error_id if __name__ == "__main__": pytest.main([__file__, "-v"])