"""Unit tests for ChatService boundary conditions. These tests verify behavior at limit boundaries and edge cases. Based on constants in the source code. """ from unittest.mock import MagicMock, patch from contextlib import contextmanager class TestMessageBoundaries: """Tests for message content boundaries.""" def test_message_with_empty_content_accepted(self): """Test that empty message content is accepted.""" from src.local_deep_research.chat.service import ChatService mock_session_obj = MagicMock() mock_session_obj.id = "test-session" mock_session_obj.message_count = 0 added_messages = [] @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() def mock_add(msg): added_messages.append(msg) mock_session.add = mock_add mock_session.commit = MagicMock() mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") message_id = service.add_message( session_id="test-session", role="user", content="", message_type="query", ) assert message_id is not None assert len(added_messages) == 1 assert added_messages[0].content == "" def test_message_with_none_content_is_rejected(self): """chat_messages.content is NOT NULL. add_message must reject content=None at the application layer.""" import pytest from src.local_deep_research.chat.service import ChatService service = ChatService(username="testuser") with pytest.raises(ValueError, match="content is required"): service.add_message( session_id="test-session", role="assistant", content=None, message_type="response", research_id="research-123", ) class TestContextBoundaries: """Tests for accumulated context boundaries.""" def test_accumulated_context_entities_limited_to_50(self): """Test that key_entities are limited to 50 items.""" from src.local_deep_research.chat.service import ChatService mock_session_obj = MagicMock() mock_session_obj.id = "test-session" mock_session_obj.accumulated_context = { "key_entities": [f"existing_{i}" for i in range(40)], "topics": [], "summary": "", } @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Try to add 20 more entities (40 + 20 = 60 > 50) service.update_accumulated_context( "test-session", new_entities=[f"new_{i}" for i in range(20)], ) # Should be capped at 50 entities = mock_session_obj.accumulated_context["key_entities"] assert len(entities) <= 50 def test_accumulated_context_topics_limited_to_20(self): """Test that topics are limited to 20 items.""" from src.local_deep_research.chat.service import ChatService mock_session_obj = MagicMock() mock_session_obj.id = "test-session" mock_session_obj.accumulated_context = { "key_entities": [], "topics": [f"existing_topic_{i}" for i in range(15)], "summary": "", } @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Try to add 10 more topics (15 + 10 = 25 > 20) service.update_accumulated_context( "test-session", new_topics=[f"new_topic_{i}" for i in range(10)], ) # Should be capped at 20 topics = mock_session_obj.accumulated_context["topics"] assert len(topics) <= 20 def test_accumulated_summary_truncated_to_8000_chars(self): """Test that summary is truncated to 8000 characters.""" from src.local_deep_research.chat.service import ChatService mock_session_obj = MagicMock() mock_session_obj.id = "test-session" mock_session_obj.accumulated_context = { "key_entities": [], "topics": [], "summary": "A" * 7000, # Already 7000 chars } @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Add 2000 more chars (7000 + 2000 + separator = ~9000+ > 8000) service.update_accumulated_context( "test-session", summary_addition="B" * 2000, ) # Should be capped at 8000 summary = mock_session_obj.accumulated_context["summary"] assert len(summary) <= 8000 class TestSessionBoundaries: """Tests for session creation boundaries.""" def test_session_title_at_max_100_chars(self): """Test that title from query is limited to 100 chars + ellipsis.""" from src.local_deep_research.chat.service import ChatService added_sessions = [] @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() def mock_add(session): added_sessions.append(session) mock_session.add = mock_add mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Create query longer than 100 chars long_query = "A" * 150 service.create_session(initial_query=long_query) assert len(added_sessions) == 1 title = added_sessions[0].title # _fallback_title truncates over-long queries to a fixed 100-char # display budget: query[:97].strip() + "...". The ellipsis lives # *inside* the 100-char cap, not on top of it — keeps sidebar # widths predictable. assert len(title) == 100 assert title.endswith("...") def test_session_title_exactly_100_chars_no_ellipsis(self): """Test that 100-char query doesn't get ellipsis.""" from src.local_deep_research.chat.service import ChatService added_sessions = [] @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() def mock_add(session): added_sessions.append(session) mock_session.add = mock_add mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Create query exactly 100 chars exact_query = "A" * 100 service.create_session(initial_query=exact_query) title = added_sessions[0].title # Title should be exactly the query (no ellipsis) assert len(title) == 100 assert not title.endswith("...") class TestContextManagerBoundaries: """Tests for ChatContextManager boundaries.""" def test_findings_limited_to_5(self): """Test that findings are limited to MAX_FINDINGS_TO_INCLUDE (5).""" from src.local_deep_research.chat.context import ChatContextManager # Create 10 assistant messages with research_id messages = [ { "id": f"msg-{i}", "role": "assistant", "content": f"Finding {i}", "message_type": "response", "research_id": f"research-{i}", } for i in range(10) ] manager = ChatContextManager( session_id="test-session", messages=messages, accumulated_context={}, ) findings = manager._extract_findings_from_history() # Findings is a combined string, but it should only have 5 messages worth # Count the number of "Finding" occurrences finding_count = findings.count("Finding") assert finding_count <= 5 def test_create_summary_truncates_long_paragraphs_to_300(self): """Test that _create_summary truncates paragraphs to 300 chars.""" from src.local_deep_research.chat.context import ChatContextManager long_paragraph = "Z" * 500 manager = ChatContextManager( session_id="test-session", messages=[], accumulated_context={}, ) summary = manager._create_summary(long_paragraph) # Should be truncated to 300 + "..." assert len(summary) == 303 assert summary.endswith("...") class TestEdgeCases: """Tests for various edge cases.""" def test_entity_deduplication(self): """Test that duplicate entities are deduplicated.""" from src.local_deep_research.chat.service import ChatService mock_session_obj = MagicMock() mock_session_obj.id = "test-session" mock_session_obj.accumulated_context = { "key_entities": ["quantum", "computing"], "topics": [], "summary": "", } @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_session_obj mock_session.query.return_value.filter_by.return_value.first.return_value = mock_session_obj mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): service = ChatService(username="testuser") # Add duplicate entity service.update_accumulated_context( "test-session", new_entities=["quantum", "superposition"], ) entities = mock_session_obj.accumulated_context["key_entities"] # quantum should appear only once assert entities.count("quantum") == 1 # All three unique entities should be present assert set(entities) == {"quantum", "computing", "superposition"}