Add comprehensive test suite with 97% code coverage

Implemented full test coverage for core bot components:

Test Coverage:
- MentionDetector: 98% (28 tests) - mention detection, nickname handling, content extraction
- Logger: 98% (15 tests) - file logging, log levels, unicode support
- ConversationMemory: 91% (25 tests) - history management, time filtering, JSON persistence
- PerplexityProvider: 100% (22 tests) - API calls, error handling, statistics
- Config: 100% (26 tests) - env loading, validation, JSON config

Infrastructure:
- Added pytest, pytest-asyncio, pytest-mock, pytest-cov to requirements.txt
- Created pytest.ini with coverage configuration
- Created .coveragerc to exclude non-production files
- Added conftest.py with shared fixtures and test isolation

Test Features:
- 116 total tests, all passing
- Isolated test environment (clean env vars, logging handlers)
- Async testing support for PerplexityProvider
- Mocked HTTP requests to avoid real API calls
- Comprehensive edge case coverage
- Unicode/German character support testing

Total: 97% code coverage across 273 statements
This commit is contained in:
Claude 2026-01-02 22:10:14 +00:00
parent db467d774c
commit f6813b5fa5
No known key found for this signature in database
10 changed files with 1760 additions and 0 deletions

357
tests/test_utils.py Normal file
View file

@ -0,0 +1,357 @@
"""
Tests for utility classes: MentionDetector and Logger
"""
import pytest
from pathlib import Path
import logging
from utils import MentionDetector, Logger
class TestMentionDetector:
"""Test MentionDetector functionality"""
def test_init_generates_nicknames(self):
"""Test that nicknames are generated from bot name"""
detector = MentionDetector("kenearosmd")
assert "kene" in detector.nicknames
assert "kenearos" in detector.nicknames
def test_init_short_name_generates_partial_nicknames(self):
"""Test nickname generation with shorter names"""
detector = MentionDetector("Eugen")
# Only 5 chars, so only first 4 chars nickname
assert "Euge" in detector.nicknames
def test_mention_with_at_symbol(self):
"""Test detection of @mention format"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("@Eugen what's up?")
assert detector.is_mentioned("Hey @Eugen can you help?")
assert detector.is_mentioned("@eugen") # case insensitive
def test_mention_with_colon(self):
"""Test detection of name: format"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("Eugen: what do you think?")
assert detector.is_mentioned("eugen: hello") # case insensitive
def test_mention_with_punctuation(self):
"""Test detection with various punctuation"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("Eugen! Hey there!")
assert detector.is_mentioned("Eugen? Are you there?")
assert detector.is_mentioned("Eugen, can you help?")
assert detector.is_mentioned("Eugen. Listen up")
def test_mention_at_start(self):
"""Test detection of name at message start"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("Eugen what's the weather?")
def test_mention_anywhere_as_whole_word(self):
"""Test detection of name as whole word anywhere"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("Hey Eugen how are you?")
assert detector.is_mentioned("Is Eugen online?")
def test_no_false_positive_partial_match(self):
"""Test that partial matches don't trigger detection"""
detector = MentionDetector("Eugen")
# "Eugene" contains "Eugen" but shouldn't match due to word boundary
assert not detector.is_mentioned("Eugene is a different name")
def test_nickname_detection(self):
"""Test that nicknames are detected"""
detector = MentionDetector("kenearosmd")
assert detector.is_mentioned("@kene what's up?")
assert detector.is_mentioned("kenearos: hello")
def test_case_insensitive_detection(self):
"""Test case insensitivity"""
detector = MentionDetector("Eugen")
assert detector.is_mentioned("EUGEN")
assert detector.is_mentioned("eugen")
assert detector.is_mentioned("EuGeN")
def test_empty_message_returns_false(self):
"""Test that empty messages return False"""
detector = MentionDetector("Eugen")
assert not detector.is_mentioned("")
assert not detector.is_mentioned(None)
def test_no_mention_returns_false(self):
"""Test messages without mentions return False"""
detector = MentionDetector("Eugen")
assert not detector.is_mentioned("Just a regular message")
assert not detector.is_mentioned("Nothing to see here")
def test_ambiguous_greeting_detection(self):
"""Test detection of ambiguous greetings"""
detector = MentionDetector("Eugen")
assert detector.is_ambiguous_greeting("hi")
assert detector.is_ambiguous_greeting("Hi")
assert detector.is_ambiguous_greeting("hello")
assert detector.is_ambiguous_greeting("hallo")
assert detector.is_ambiguous_greeting("hey there")
assert detector.is_ambiguous_greeting("moin")
assert detector.is_ambiguous_greeting("servus")
def test_ambiguous_greeting_german(self):
"""Test German greetings"""
detector = MentionDetector("Eugen")
assert detector.is_ambiguous_greeting("wie gehts")
assert detector.is_ambiguous_greeting("wie geht's")
assert detector.is_ambiguous_greeting("alles klar")
def test_ambiguous_greeting_english(self):
"""Test English greetings"""
detector = MentionDetector("Eugen")
assert detector.is_ambiguous_greeting("how are you")
assert detector.is_ambiguous_greeting("everything ok")
def test_ambiguous_greeting_not_if_mentioned(self):
"""Test that clear mentions don't count as ambiguous"""
detector = MentionDetector("Eugen")
assert not detector.is_ambiguous_greeting("@Eugen hi")
assert not detector.is_ambiguous_greeting("Eugen: hello")
def test_ambiguous_greeting_empty_message(self):
"""Test ambiguous greeting with empty message"""
detector = MentionDetector("Eugen")
assert not detector.is_ambiguous_greeting("")
assert not detector.is_ambiguous_greeting(None)
def test_extract_content_removes_at_mention(self):
"""Test content extraction removes @mention"""
detector = MentionDetector("Eugen")
assert detector.extract_content("@Eugen what's up?") == "what's up?"
assert detector.extract_content("@eugen hello") == "hello"
def test_extract_content_removes_name_with_colon(self):
"""Test content extraction removes name: format"""
detector = MentionDetector("Eugen")
assert detector.extract_content("Eugen: what's the weather?") == "what's the weather?"
assert detector.extract_content("eugen, help me") == "help me"
def test_extract_content_removes_name_at_start(self):
"""Test content extraction removes name at start"""
detector = MentionDetector("Eugen")
assert detector.extract_content("Eugen what's up?") == "what's up?"
def test_extract_content_removes_name_at_end(self):
"""Test content extraction removes name at end"""
detector = MentionDetector("Eugen")
result = detector.extract_content("what's up Eugen")
assert result.strip() == "what's up"
def test_extract_content_removes_nickname(self):
"""Test content extraction removes nicknames"""
detector = MentionDetector("kenearosmd")
assert detector.extract_content("@kene what's up?") == "what's up?"
assert detector.extract_content("kenearos: hello") == "hello"
def test_extract_content_empty_message(self):
"""Test content extraction with empty message"""
detector = MentionDetector("Eugen")
assert detector.extract_content("") == ""
assert detector.extract_content(None) == ""
def test_extract_content_only_mention(self):
"""Test content extraction when message is only the mention"""
detector = MentionDetector("Eugen")
assert detector.extract_content("@Eugen") == ""
assert detector.extract_content("Eugen") == ""
def test_extract_content_preserves_other_content(self):
"""Test that extraction doesn't remove legitimate content"""
detector = MentionDetector("Eugen")
# Should not remove "Eugene" from content
result = detector.extract_content("@Eugen tell me about Eugene")
assert "Eugene" in result
class TestLogger:
"""Test Logger functionality"""
def _flush_logger(self, logger):
"""Helper to flush logger handlers"""
for handler in logger.main_logger.handlers:
handler.flush()
for handler in logger.api_logger.handlers:
handler.flush()
def test_logger_creates_log_directory(self, temp_dir):
"""Test that logger creates log directory"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
assert log_dir.exists()
def test_logger_creates_main_log_file(self, temp_dir):
"""Test that main log file is created"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.info("test message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
assert main_log.exists()
def test_logger_creates_api_log_file(self, temp_dir):
"""Test that API log file is created"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.api_call("test", "model", 5)
self._flush_logger(logger)
api_log = log_dir / "api_debug.log"
assert api_log.exists()
def test_logger_info_writes_message(self, temp_dir):
"""Test that info messages are written"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.info("test info message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "test info message" in content
assert "INFO" in content
def test_logger_error_writes_message(self, temp_dir):
"""Test that error messages are written"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.error("test error message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "test error message" in content
assert "ERROR" in content
def test_logger_warning_writes_message(self, temp_dir):
"""Test that warning messages are written"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.warning("test warning message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "test warning message" in content
assert "WARNING" in content
def test_logger_debug_mode_writes_debug(self, temp_dir):
"""Test that debug messages are written in debug mode"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.debug("test debug message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "test debug message" in content
assert "DEBUG" in content
def test_logger_non_debug_mode_skips_debug(self, temp_dir):
"""Test that debug messages are not written when debug_mode=False"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.debug("test debug message")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
# Debug messages shouldn't appear in INFO level logging
assert "test debug message" not in content
def test_logger_api_call_logging(self, temp_dir):
"""Test API call logging"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.api_call("https://api.test.com", "sonar-pro", 10)
self._flush_logger(logger)
api_log = log_dir / "api_debug.log"
content = api_log.read_text()
assert "API CALL" in content
assert "sonar-pro" in content
assert "10" in content
def test_logger_api_response_logging(self, temp_dir):
"""Test API response logging"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.api_response(200, 75, 1.234, "Test response content")
self._flush_logger(logger)
api_log = log_dir / "api_debug.log"
content = api_log.read_text()
assert "API RESPONSE" in content
assert "200" in content
assert "75" in content
assert "1.23" in content
assert "Test response" in content
def test_logger_api_error_logging(self, temp_dir):
"""Test API error logging"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.api_error(401, "Unauthorized access")
self._flush_logger(logger)
api_log = log_dir / "api_debug.log"
content = api_log.read_text()
assert "API ERROR" in content
assert "401" in content
assert "Unauthorized" in content
def test_logger_chat_message_in_debug_mode(self, temp_dir):
"""Test chat message logging in debug mode"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.chat_message("testuser", "Hello world!")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "CHAT" in content
assert "testuser" in content
assert "Hello world!" in content
def test_logger_chat_message_not_in_non_debug_mode(self, temp_dir):
"""Test chat message not logged when debug_mode=False"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=False)
logger.chat_message("testuser", "Hello world!")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "Hello world!" not in content
def test_logger_bot_response_in_debug_mode(self, temp_dir):
"""Test bot response logging in debug mode"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.bot_response("testuser", "Here's my response")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text()
assert "BOT RESPONSE" in content
assert "testuser" in content
assert "Here's my response" in content
def test_logger_unicode_support(self, temp_dir):
"""Test that logger handles German/Unicode characters"""
log_dir = temp_dir / "logs"
logger = Logger(log_dir=str(log_dir), debug_mode=True)
logger.info("Äpfel, Öle, Über, ß")
self._flush_logger(logger)
main_log = log_dir / "eugen.log"
content = main_log.read_text(encoding='utf-8')
assert "Äpfel" in content
assert "Öle" in content
assert "Über" in content
assert "ß" in content