Enhanced test suite from 97% to 100% coverage with 173 passing tests. ## New Features ### 1. Error Handling Tests (3 tests) - test_add_message_handles_read_error_gracefully - test_add_message_handles_write_error_gracefully - test_clear_user_history_handles_permission_error - test_logger_reuses_existing_handlers Coverage: memory.py 91% → 100%, utils.py 98% → 100% ### 2. Integration Tests (9 tests) - test_integration.py - test_mention_to_response_workflow: Full message flow - test_conversation_context_preserved: Multi-turn conversations - test_config_to_components_integration: Component initialization - test_error_recovery_workflow: Graceful error handling - test_mention_detection_integration_with_nicknames - test_memory_limit_enforcement_in_workflow - test_ambiguous_greeting_workflow - test_logger_integration_with_memory - test_logger_integration_with_ai_provider - test_config_validation_workflow ### 3. Parameterized Tests (45 tests) - 18 mention detection test cases - 17 greeting detection test cases - 8 content extraction test cases - Covers edge cases, unicode, nicknames, false positives ### 4. GitHub Actions CI Workflow - Multi-Python version testing (3.9, 3.10, 3.11, 3.12) - Automated coverage reporting - Code quality checks (Black, isort, flake8) - Codecov integration - Coverage badge generation ## Test Statistics - Tests: 116 → 173 (+49% increase) - Coverage: 97% → 100% (+3pp) - Execution time: ~1.1 seconds - All components: 100% coverage ## Files Modified - tests/test_utils.py: Added parameterized tests and error handling - tests/test_memory.py: Added error handling tests - tests/test_integration.py: NEW - Full integration test suite - tests/README.md: Updated documentation - .github/workflows/test.yml: NEW - CI/CD automation
453 lines
19 KiB
Python
453 lines
19 KiB
Python
"""
|
|
Tests for utility classes: MentionDetector and Logger
|
|
"""
|
|
import pytest
|
|
from pathlib import Path
|
|
import logging
|
|
from utils import MentionDetector, Logger
|
|
|
|
|
|
# Parameterized test data
|
|
MENTION_TEST_CASES = [
|
|
# (message, bot_name, expected_result, description)
|
|
("@Eugen hello", "Eugen", True, "at-mention"),
|
|
("@eugen hello", "Eugen", True, "at-mention lowercase"),
|
|
("@EUGEN hello", "Eugen", True, "at-mention uppercase"),
|
|
("Eugen: what's up?", "Eugen", True, "colon format"),
|
|
("Eugen! hey", "Eugen", True, "exclamation format"),
|
|
("Eugen? are you there", "Eugen", True, "question format"),
|
|
("Eugen, help me", "Eugen", True, "comma format"),
|
|
("Eugen. listen", "Eugen", True, "period format"),
|
|
("Hey Eugen how are you", "Eugen", True, "mention in middle"),
|
|
("Is Eugen online?", "Eugen", True, "mention as word"),
|
|
("Eugene is different", "Eugen", False, "partial match should fail"),
|
|
("Eugenics is a topic", "Eugen", False, "false positive check"),
|
|
("Regular message", "Eugen", False, "no mention"),
|
|
("@kenearosmd hi", "kenearosmd", True, "long name at-mention"),
|
|
("@kene hi", "kenearosmd", True, "nickname 4 chars"),
|
|
("@kenearos hi", "kenearosmd", True, "nickname 8 chars"),
|
|
("kenearosmd: what", "kenearosmd", True, "long name colon"),
|
|
("kene: what", "kenearosmd", True, "nickname colon"),
|
|
]
|
|
|
|
GREETING_TEST_CASES = [
|
|
# (message, expected_ambiguous, description)
|
|
("hi", True, "simple hi"),
|
|
("Hi", True, "capitalized Hi"),
|
|
("HI", True, "uppercase HI"),
|
|
("hello", True, "hello"),
|
|
("hallo", True, "German hallo"),
|
|
("hey there", True, "hey there"),
|
|
("moin", True, "German moin"),
|
|
("servus", True, "German servus"),
|
|
("wie gehts", True, "German wie gehts"),
|
|
("wie geht's", True, "German with apostrophe"),
|
|
("alles klar", True, "German alles klar"),
|
|
("how are you", True, "English how are you"),
|
|
("everything ok", True, "English everything ok"),
|
|
("@Eugen hi", False, "clear mention not ambiguous"),
|
|
("Eugen: hello", False, "clear mention not ambiguous"),
|
|
("Just a message", False, "not a greeting"),
|
|
("high score", False, "hi in word should not match"),
|
|
]
|
|
|
|
CONTENT_EXTRACTION_CASES = [
|
|
# (message, bot_name, expected_content, description)
|
|
("@Eugen what's the weather?", "Eugen", "what's the weather?", "at-mention extraction"),
|
|
("Eugen: tell me more", "Eugen", "tell me more", "colon extraction"),
|
|
("Eugen, help please", "Eugen", "help please", "comma extraction"),
|
|
("what's up Eugen", "Eugen", "what's up", "mention at end"),
|
|
("@Eugen", "Eugen", "", "only mention"),
|
|
("Eugen", "Eugen", "", "only name"),
|
|
("@kene what's up", "kenearosmd", "what's up", "nickname extraction"),
|
|
("", "Eugen", "", "empty message"),
|
|
]
|
|
|
|
|
|
class TestMentionDetector:
|
|
"""Test MentionDetector functionality"""
|
|
|
|
@pytest.mark.parametrize("message,bot_name,expected,description", MENTION_TEST_CASES)
|
|
def test_mention_detection_comprehensive(self, message, bot_name, expected, description):
|
|
"""Parameterized test for comprehensive mention detection coverage"""
|
|
detector = MentionDetector(bot_name)
|
|
result = detector.is_mentioned(message)
|
|
assert result == expected, f"Failed for case: {description}"
|
|
|
|
@pytest.mark.parametrize("message,expected,description", GREETING_TEST_CASES)
|
|
def test_ambiguous_greeting_comprehensive(self, message, expected, description):
|
|
"""Parameterized test for comprehensive greeting detection"""
|
|
detector = MentionDetector("Eugen")
|
|
result = detector.is_ambiguous_greeting(message)
|
|
assert result == expected, f"Failed for case: {description}"
|
|
|
|
@pytest.mark.parametrize("message,bot_name,expected_content,description", CONTENT_EXTRACTION_CASES)
|
|
def test_content_extraction_comprehensive(self, message, bot_name, expected_content, description):
|
|
"""Parameterized test for comprehensive content extraction"""
|
|
detector = MentionDetector(bot_name)
|
|
result = detector.extract_content(message)
|
|
assert result.strip() == expected_content.strip(), f"Failed for case: {description}"
|
|
|
|
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
|
|
|
|
def test_logger_reuses_existing_handlers(self, temp_dir):
|
|
"""Test that logger doesn't create duplicate handlers"""
|
|
import logging
|
|
|
|
log_dir = temp_dir / "logs"
|
|
|
|
# Create first logger
|
|
logger1 = Logger(log_dir=str(log_dir), debug_mode=True)
|
|
handler_count_1 = len(logger1.main_logger.handlers)
|
|
|
|
# Create second logger with same settings - should reuse handlers
|
|
logger2 = Logger(log_dir=str(log_dir), debug_mode=True)
|
|
handler_count_2 = len(logger2.main_logger.handlers)
|
|
|
|
# Should have same number of handlers (reused, not duplicated)
|
|
assert handler_count_1 == handler_count_2
|
|
assert handler_count_1 > 0 # But should have at least one handler
|