""" Property-based fuzz tests for chat components using Hypothesis. These tests verify that chat functions don't crash on arbitrary input and handle edge cases gracefully. """ import pytest from hypothesis import given, settings, strategies as st, assume, HealthCheck from unittest.mock import MagicMock, patch from contextlib import contextmanager # ============================================================================= # Custom Strategies for Chat Testing # ============================================================================= def message_roles(): """Generate valid message roles.""" return st.sampled_from(["user", "assistant"]) def message_types(): """Generate valid message types. The schema removed "step" from ChatMessageType (steps live in chat_progress_steps with their own enum-less schema); the chat context layer filters out any "step" rows before they reach here. """ return st.sampled_from(["query", "followup", "response"]) def safe_text(max_size=200): """Generate text without problematic Unicode.""" return st.text( alphabet=st.characters( blacklist_categories=("Cs",), # Exclude surrogates blacklist_characters=["\x00"], # Exclude null bytes ), max_size=max_size, ) def message_dict(): """Generate a message dictionary.""" return st.fixed_dictionaries( { "id": st.text( min_size=5, max_size=20, alphabet="abcdef0123456789-" ), "role": message_roles(), "content": safe_text(max_size=100), "message_type": message_types(), "research_id": st.one_of( st.none(), st.text(min_size=5, max_size=20, alphabet="abcdef0123456789-"), ), } ) def accumulated_context_dict(): """Generate an accumulated context dictionary.""" return st.fixed_dictionaries( { "key_entities": st.lists(safe_text(max_size=50), max_size=20), "topics": st.lists(safe_text(max_size=30), max_size=10), "summary": safe_text(max_size=200), } ) # ============================================================================= # ChatContextManager Fuzz Tests # ============================================================================= class TestChatContextManagerFuzz: """Fuzz tests for ChatContextManager.""" @given( messages=st.lists(message_dict(), max_size=10), accumulated=st.one_of(st.none(), accumulated_context_dict()), ) @settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow]) def test_build_context_handles_arbitrary_messages( self, messages, accumulated ): """Test that build_research_context never crashes on arbitrary input.""" from src.local_deep_research.chat.context import ChatContextManager try: manager = ChatContextManager( session_id="test-session", messages=messages, accumulated_context=accumulated, ) context = manager.build_research_context() # Verify result structure assert isinstance(context, dict) assert "session_id" in context assert "is_multi_turn" in context except Exception as e: pytest.fail( f"build_research_context crashed: {type(e).__name__}: {e}" ) @given(accumulated=accumulated_context_dict()) @settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow]) def test_accumulated_context_handles_arbitrary_data(self, accumulated): """Test that accumulated context with arbitrary data is handled.""" from src.local_deep_research.chat.context import ChatContextManager try: manager = ChatContextManager( session_id="test-session", messages=[], accumulated_context=accumulated, ) # Test key entity extraction entities = manager._get_key_entities() assert isinstance(entities, list) assert len(entities) <= 20 # Should be limited # Test topics extraction topics = manager._get_topics() assert isinstance(topics, list) assert len(topics) <= 10 # Should be limited except Exception as e: pytest.fail(f"Context handling crashed: {type(e).__name__}: {e}") @given(content=safe_text(max_size=500)) @settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow]) def test_create_summary_handles_arbitrary_content(self, content): """Test that summary creation handles arbitrary content.""" from src.local_deep_research.chat.context import ChatContextManager try: manager = ChatContextManager("test-session", [], {}) summary = manager._create_summary(content) # Should return a string assert isinstance(summary, str) # Should be limited in length assert len(summary) <= 400 # 300 + "..." except Exception as e: pytest.fail(f"Summary creation crashed: {type(e).__name__}: {e}") @given(content=safe_text(max_size=200)) @settings(max_examples=30, suppress_health_check=[HealthCheck.too_slow]) def test_extract_context_updates_handles_arbitrary_input(self, content): """Test that context update extraction handles arbitrary input.""" from src.local_deep_research.chat.context import ChatContextManager try: manager = ChatContextManager("test-session", [], {}) updates = manager.extract_context_updates(content) # Verify structure assert isinstance(updates, dict) assert "new_entities" in updates assert "new_topics" in updates assert "summary_addition" in updates except Exception as e: pytest.fail( f"Context update extraction crashed: {type(e).__name__}: {e}" ) # ============================================================================= # ChatService Fuzz Tests # ============================================================================= class TestChatServiceFuzz: """Fuzz tests for ChatService.""" @given(query=st.one_of(st.none(), safe_text(max_size=200))) @settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow]) def test_title_generation_handles_any_string(self, query): """Test that title generation handles any input string.""" from src.local_deep_research.chat.service import ChatService try: service = ChatService(username="test_user") title = service._generate_title(query) # Should return a string assert isinstance(title, str) # Should have reasonable length assert len(title) <= 150 # 100 + "..." except Exception as e: pytest.fail(f"Title generation crashed: {type(e).__name__}: {e}") @given( content=safe_text(max_size=100), role=message_roles(), message_type=message_types(), ) @settings(max_examples=30, suppress_health_check=[HealthCheck.too_slow]) def test_add_message_handles_unicode(self, content, role, message_type): """Test that add_message handles various Unicode content.""" from src.local_deep_research.chat.service import ChatService # Skip empty content for valid messages assume(len(content.strip()) > 0) @contextmanager def mock_get_user_db_session(username, password=None): mock_session = MagicMock() # Mock the session query to return a valid session mock_chat_session = MagicMock() mock_chat_session.message_count = 0 mock_session.query.return_value.filter_by.return_value.with_for_update.return_value.first.return_value = mock_chat_session mock_session.query.return_value.filter_by.return_value.first.return_value = mock_chat_session mock_session.add = MagicMock() mock_session.commit = MagicMock() yield mock_session with patch( "src.local_deep_research.chat.service.get_user_db_session", mock_get_user_db_session, ): try: service = ChatService(username="test_user") message_id = service.add_message( session_id="test-session", role=role, content=content, message_type=message_type, ) # Should return a UUID string assert isinstance(message_id, str) assert len(message_id) > 0 except ValueError: # Session not found is expected in some cases pass except Exception as e: pytest.fail(f"add_message crashed: {type(e).__name__}: {e}") # ============================================================================= # Chat API Fuzz Tests (Parametrized for performance) # ============================================================================= # Test data for API fuzzing - these are checked without Hypothesis # to avoid the fixture overhead with each hypothesis iteration SPECIAL_CONTENT_CASES = [ "Normal message", "", # Empty " " * 100, # Whitespace "Unicode: Привет 你好 🎉", "", "'\"<>&", # HTML special chars "Line1\nLine2\rLine3", "A" * 9999, # Near limit "\x00\x01\x02", # Control chars ] UNICODE_TITLE_CASES = [ "Normal Title", "", "Привет мир", "你好世界", "🎉🔥💯", "