File size: 8,319 Bytes
7ab7df1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
"""Tests for network and API failure scenarios."""

from unittest.mock import MagicMock, Mock, patch

import pytest
import requests

from models import create_llm, create_embeddings
from qa_chain import QAChainWrapper
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.documents import Document


@patch("models.ChatOpenAI")
def test_llm_server_unavailable(mock_chat_openai):
    """Test graceful handling when LLM server is unavailable."""
    # Simulate connection error
    mock_chat_openai.side_effect = requests.exceptions.ConnectionError("Connection refused")

    with pytest.raises((requests.exceptions.ConnectionError, Exception)):
        create_llm()


@patch("models.ChatOpenAI")
def test_llm_server_timeout(mock_chat_openai):
    """Test handling of LLM server timeout."""
    mock_llm = MagicMock()
    mock_llm.stream.side_effect = requests.exceptions.Timeout("Request timed out")
    mock_chat_openai.return_value = mock_llm

    llm = create_llm(streaming=True)
    
    # Should raise timeout error when streaming
    with pytest.raises(requests.exceptions.Timeout):
        list(llm.stream("test"))


@patch("models.ChatOpenAI")
def test_llm_server_error_response(mock_chat_openai):
    """Test handling of LLM server error response."""
    mock_llm = MagicMock()
    mock_llm.invoke.side_effect = Exception("500 Internal Server Error")
    mock_chat_openai.return_value = mock_llm

    llm = create_llm(streaming=False)
    
    with pytest.raises(Exception) as exc_info:
        llm.invoke("test")
    assert "500" in str(exc_info.value) or "Error" in str(exc_info.value)


@patch("models.HuggingFaceEmbeddings")
def test_embedding_model_download_failure(mock_embeddings_class):
    """Test handling of embedding model download failure."""
    mock_embeddings_class.side_effect = Exception("Failed to download model")

    with pytest.raises(Exception) as exc_info:
        create_embeddings()
    assert "Failed" in str(exc_info.value) or "download" in str(exc_info.value).lower()


@patch("qa_chain.create_llm")
@patch("qa_chain.format_chat_history")
def test_rag_query_with_llm_failure(mock_format_history, mock_create_llm, mock_vectorstore):
    """Test RAG query when LLM fails during streaming."""
    from qa_chain import create_qa_chain

    mock_format_history.return_value = ""
    mock_llm = MagicMock()
    mock_llm.stream.side_effect = requests.exceptions.ConnectionError("LLM server unavailable")
    mock_create_llm.return_value = mock_llm

    mock_retriever = MagicMock()
    mock_doc = Document(page_content="Test", metadata={"source": "test.pdf", "page": 1})
    mock_retriever.invoke.return_value = [mock_doc]
    mock_vectorstore.as_retriever.return_value = mock_retriever

    qa_chain = create_qa_chain(mock_vectorstore)

    # Mock the chain operator
    mock_chain = MagicMock()
    mock_chain.stream.side_effect = requests.exceptions.ConnectionError("LLM server unavailable")
    qa_chain._prompt.__or__ = MagicMock(return_value=mock_chain)

    inputs = {
        "question": "test question",
        "chat_history": [],
    }

    # Should handle error gracefully
    results = list(qa_chain.stream(inputs))
    
    # Should return error message in chunk
    assert len(results) > 0
    chunk_text = str(results[0].get("chunk", ""))
    # The error handling in qa_chain.py catches exceptions and yields error message
    assert len(chunk_text) > 0  # Should have some content (either response or error)


@patch("qa_chain.create_llm")
@patch("qa_chain.format_chat_history")
def test_rag_query_with_retriever_failure(mock_format_history, mock_create_llm, mock_vectorstore):
    """Test RAG query when retriever fails."""
    from qa_chain import QAChainWrapper
    from langchain_core.prompts import ChatPromptTemplate

    prompt = ChatPromptTemplate.from_template("Test: {question}")
    qa_chain_wrapper = QAChainWrapper(mock_vectorstore, prompt)

    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

    # Simulate retriever failure
    mock_retriever = MagicMock()
    mock_retriever.invoke.side_effect = Exception("ChromaDB connection failed")
    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": "test question",
        "chat_history": [],
    }

    # Should handle retriever error - may raise or handle gracefully
    try:
        results = list(qa_chain_wrapper.stream(inputs))
        # If it doesn't raise, should have error in response or empty results
        if results:
            chunk = results[0].get("chunk", "")
            assert "Error" in str(chunk).lower() or len(results) == 0 or len(chunk) > 0
    except Exception:
        # Exception is acceptable for retriever failure
        pass


@patch("ui.handlers.create_llm")
def test_vanilla_llm_network_failure(mock_create_llm):
    """Test vanilla LLM mode with network failure."""
    from ui.handlers import create_stream_chat_response

    mock_llm = MagicMock()
    mock_llm.stream.side_effect = requests.exceptions.ConnectionError("Server unavailable")
    mock_create_llm.return_value = mock_llm

    stream_fn = create_stream_chat_response(MagicMock())
    
    results = list(
        stream_fn(
            "test question",
            [],
            "Vanilla LLM",
        )
    )

    # Should return error message
    assert len(results) > 0
    assert "Error" in results[-1][0] or "unavailable" in results[-1][0].lower()


@patch("qa_chain.create_llm")
@patch("qa_chain.format_chat_history")
def test_stream_interruption_handling(mock_format_history, mock_create_llm, mock_vectorstore):
    """Test handling of stream interruption."""
    from qa_chain import QAChainWrapper
    from langchain_core.prompts import ChatPromptTemplate

    prompt = ChatPromptTemplate.from_template("Test: {question}")
    qa_chain_wrapper = QAChainWrapper(mock_vectorstore, prompt)

    mock_format_history.return_value = ""
    
    # Simulate stream that gets interrupted
    mock_llm = MagicMock()
    mock_chunk1 = MagicMock()
    mock_chunk1.content = "Partial "
    mock_chunk2 = MagicMock()
    mock_chunk2.content = "response"
    
    def interrupted_stream(*args, **kwargs):
        yield mock_chunk1
        raise KeyboardInterrupt("Stream interrupted")
    
    mock_llm.stream.side_effect = interrupted_stream
    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.side_effect = interrupted_stream
    qa_chain_wrapper._prompt.__or__ = MagicMock(return_value=mock_chain)

    inputs = {
        "question": "test question",
        "chat_history": [],
    }

    # Should handle interruption gracefully
    try:
        results = list(qa_chain_wrapper.stream(inputs))
        # If it completes, should have partial response
        if results:
            assert len(results) > 0
    except KeyboardInterrupt:
        # Interruption is acceptable
        pass


@patch("retrievers.BM25Okapi")
def test_hybrid_search_with_semantic_failure(mock_bm25, mock_vectorstore):
    """Test hybrid search when semantic search fails."""
    from retrievers import HybridRetriever
    from langchain_core.documents import Document

    mock_vectorstore.get.return_value = {
        "documents": ["Doc 1"],
        "metadatas": [{"source": "test.pdf", "page": 1}],
    }

    # Simulate semantic search failure
    mock_vectorstore.similarity_search_with_score.side_effect = Exception("Vector search failed")

    mock_bm25_instance = MagicMock()
    mock_bm25_instance.get_scores.return_value = [0.8]
    mock_bm25.return_value = mock_bm25_instance

    retriever = HybridRetriever(mock_vectorstore)
    
    # Should handle semantic failure gracefully
    results = retriever.hybrid_search("test query", k=1)
    
    # Should still return results based on BM25 only
    assert isinstance(results, list)