Spaces:
Runtime error
Runtime error
| """Tests for input validation and edge cases.""" | |
| from unittest.mock import MagicMock, Mock, patch | |
| import pytest | |
| from qa_chain import QAChainWrapper | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from langchain_core.documents import Document | |
| from ui.handlers import create_stream_chat_response, create_respond_handler | |
| def qa_chain_wrapper(mock_vectorstore): | |
| """Create a QAChainWrapper instance.""" | |
| prompt = ChatPromptTemplate.from_template("Test: {question}") | |
| return QAChainWrapper(mock_vectorstore, prompt) | |
| def test_very_long_query(mock_format_history, mock_create_llm, qa_chain_wrapper): | |
| """Test handling of extremely long queries (>10k characters).""" | |
| very_long_query = "What is " + "RAG? " * 2000 # ~10k+ characters | |
| mock_format_history.return_value = "" | |
| mock_llm = MagicMock() | |
| mock_chunk = MagicMock() | |
| mock_chunk.content = "Response" | |
| mock_llm.stream.return_value = [mock_chunk] | |
| mock_create_llm.return_value = mock_llm | |
| mock_retriever = MagicMock() | |
| mock_retriever.invoke.return_value = [Document(page_content="Test", metadata={})] | |
| qa_chain_wrapper._retriever = mock_retriever | |
| mock_chain = MagicMock() | |
| mock_chain.stream.return_value = [mock_chunk] | |
| qa_chain_wrapper._prompt.__or__ = MagicMock(return_value=mock_chain) | |
| inputs = { | |
| "question": very_long_query, | |
| "chat_history": [], | |
| } | |
| # Should handle long query without crashing | |
| results = list(qa_chain_wrapper.stream(inputs)) | |
| assert len(results) > 0 | |
| def test_empty_whitespace_query(): | |
| """Test handling of whitespace-only queries.""" | |
| from ui.handlers import create_respond_handler | |
| stream_fn = MagicMock() | |
| respond_fn = create_respond_handler(stream_fn) | |
| # Empty string | |
| results = list(respond_fn("", [], False, "mmr", "All Documents", False, False, 70)) | |
| assert results[0][0] == "" # Should return empty immediately | |
| # Whitespace only - now also returns empty (treated as empty after strip) | |
| results = list(respond_fn(" ", [], False, "mmr", "All Documents", False, False, 70)) | |
| assert results[0][0] == "" # Should return empty immediately | |
| # Newlines only - also returns empty | |
| results = list(respond_fn("\n\n\n", [], False, "mmr", "All Documents", False, False, 70)) | |
| assert results[0][0] == "" # Should return empty immediately | |
| def test_special_characters_in_query(mock_format_history, mock_create_llm, qa_chain_wrapper): | |
| """Test handling of special characters in queries.""" | |
| special_chars_query = "What is RAG? @#$%^&*()[]{}|\\/<>?~`" | |
| mock_format_history.return_value = "" | |
| mock_llm = MagicMock() | |
| mock_chunk = MagicMock() | |
| mock_chunk.content = "Response" | |
| mock_llm.stream.return_value = [mock_chunk] | |
| mock_create_llm.return_value = mock_llm | |
| mock_retriever = MagicMock() | |
| mock_retriever.invoke.return_value = [Document(page_content="Test", metadata={})] | |
| qa_chain_wrapper._retriever = mock_retriever | |
| mock_chain = MagicMock() | |
| mock_chain.stream.return_value = [mock_chunk] | |
| qa_chain_wrapper._prompt.__or__ = MagicMock(return_value=mock_chain) | |
| inputs = { | |
| "question": special_chars_query, | |
| "chat_history": [], | |
| } | |
| # Should handle special characters without crashing | |
| results = list(qa_chain_wrapper.stream(inputs)) | |
| assert len(results) > 0 | |
| def test_unicode_emoji_in_query(mock_format_history, mock_create_llm, qa_chain_wrapper): | |
| """Test handling of Unicode and emoji in queries.""" | |
| unicode_query = "What is RAG? π δ½ ε₯½ Ω Ψ±ΨΨ¨Ψ§" | |
| mock_format_history.return_value = "" | |
| mock_llm = MagicMock() | |
| mock_chunk = MagicMock() | |
| mock_chunk.content = "Response" | |
| mock_llm.stream.return_value = [mock_chunk] | |
| mock_create_llm.return_value = mock_llm | |
| mock_retriever = MagicMock() | |
| mock_retriever.invoke.return_value = [Document(page_content="Test", metadata={})] | |
| qa_chain_wrapper._retriever = mock_retriever | |
| mock_chain = MagicMock() | |
| mock_chain.stream.return_value = [mock_chunk] | |
| qa_chain_wrapper._prompt.__or__ = MagicMock(return_value=mock_chain) | |
| inputs = { | |
| "question": unicode_query, | |
| "chat_history": [], | |
| } | |
| # Should handle Unicode/emoji without crashing | |
| results = list(qa_chain_wrapper.stream(inputs)) | |
| assert len(results) > 0 | |
| def test_invalid_document_filter(): | |
| """Test handling of invalid document filter selection.""" | |
| from ui.handlers import create_stream_chat_response | |
| mock_qa_chain = MagicMock() | |
| mock_qa_chain.stream.return_value = [ | |
| { | |
| "chunk": "Response", | |
| "source_documents": [], | |
| "docs_with_scores": None, | |
| "rewritten_query": None, | |
| "hybrid_scores": None, | |
| } | |
| ] | |
| # Pass valid sources - nonexistent.pdf is NOT in the list | |
| stream_fn = create_stream_chat_response(mock_qa_chain, available_sources=["valid.pdf"]) | |
| # Invalid filter (document that doesn't exist in available_sources) | |
| results = list( | |
| stream_fn( | |
| "test question", | |
| [], | |
| "RAG", | |
| doc_filter="nonexistent.pdf", # Invalid filter - not in available_sources | |
| search_type="mmr", | |
| ) | |
| ) | |
| # Should handle invalid filter gracefully | |
| assert len(results) > 0 | |
| # Invalid filter should be ignored (no filter passed to chain) | |
| call_args = mock_qa_chain.stream.call_args[0][0] | |
| assert "filter" not in call_args # Filter is NOT passed for invalid sources | |
| def test_malformed_chat_history(): | |
| """Test handling of malformed chat history.""" | |
| from ui.handlers import messages_to_tuples | |
| # Missing role - should handle gracefully | |
| try: | |
| malformed_history1 = [ | |
| {"content": "Message without role"}, | |
| ] | |
| tuples1 = messages_to_tuples(malformed_history1) | |
| assert isinstance(tuples1, list) | |
| # Should return empty (no valid user/assistant pairs) | |
| assert tuples1 == [] | |
| except (KeyError, TypeError): | |
| # Exception is acceptable for malformed input | |
| pass | |
| # Missing content - should handle gracefully | |
| try: | |
| malformed_history2 = [ | |
| {"role": "user"}, | |
| ] | |
| tuples2 = messages_to_tuples(malformed_history2) | |
| assert isinstance(tuples2, list) | |
| except (KeyError, TypeError): | |
| # Exception is acceptable for malformed input | |
| pass | |
| # Invalid role - should handle gracefully | |
| malformed_history3 = [ | |
| {"role": "invalid", "content": "Message"}, | |
| ] | |
| tuples3 = messages_to_tuples(malformed_history3) | |
| # Should return empty (only processes user/assistant pairs) | |
| assert isinstance(tuples3, list) | |
| assert tuples3 == [] | |
| def test_query_rewriting_with_very_short_query(mock_create_llm, qa_chain_wrapper): | |
| """Test query rewriting with very short query.""" | |
| very_short_query = "RAG?" | |
| mock_llm = MagicMock() | |
| mock_response = MagicMock() | |
| # Make rewritten query shorter than 30% of original | |
| # "RAG?" is 4 chars, 30% = 1.2, so "R" (1 char) should trigger fallback | |
| mock_response.content = "R" | |
| mock_llm.invoke.return_value = mock_response | |
| mock_create_llm.return_value = mock_llm | |
| result = qa_chain_wrapper.rewrite_query(very_short_query) | |
| # Should return original if rewritten is too short (< 30% of original length) | |
| assert result == very_short_query | |
| def test_hybrid_search_with_extreme_alpha(mock_format_history, mock_create_llm, qa_chain_wrapper): | |
| """Test hybrid search with extreme alpha values.""" | |
| from langchain_core.documents import Document | |
| mock_format_history.return_value = "" | |
| mock_llm = MagicMock() | |
| mock_chunk = MagicMock() | |
| mock_chunk.content = "Response" | |
| mock_llm.stream.return_value = [mock_chunk] | |
| mock_create_llm.return_value = mock_llm | |
| mock_retriever = MagicMock() | |
| mock_retriever.invoke.return_value = [Document(page_content="Test", metadata={})] | |
| qa_chain_wrapper._retriever = mock_retriever | |
| mock_chain = MagicMock() | |
| mock_chain.stream.return_value = [mock_chunk] | |
| qa_chain_wrapper._prompt.__or__ = MagicMock(return_value=mock_chain) | |
| # Test with alpha = 0.0 (pure keyword) | |
| inputs = { | |
| "question": "test", | |
| "chat_history": [], | |
| "search_type": "hybrid", | |
| "hybrid_alpha": 0.0, | |
| } | |
| results = list(qa_chain_wrapper.stream(inputs)) | |
| assert len(results) > 0 | |
| # Test with alpha = 1.0 (pure semantic) | |
| inputs["hybrid_alpha"] = 1.0 | |
| results = list(qa_chain_wrapper.stream(inputs)) | |
| assert len(results) > 0 | |
| def test_rerank_with_very_few_documents(qa_chain_wrapper, sample_documents): | |
| """Test re-ranking with very few documents.""" | |
| # Re-rank with only 1 document | |
| result = qa_chain_wrapper.rerank_documents("query", sample_documents[:1], top_k=5) | |
| # Should return the single document | |
| assert len(result) == 1 | |
| # Re-rank with more documents than top_k | |
| result = qa_chain_wrapper.rerank_documents("query", sample_documents[:10], top_k=3) | |
| # Should return only top_k documents | |
| assert len(result) == 3 | |