-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_error_paths.py
More file actions
444 lines (336 loc) · 15.9 KB
/
test_error_paths.py
File metadata and controls
444 lines (336 loc) · 15.9 KB
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
"""
Error Path and Exception Handling Tests
Tests error recovery, exception handling, and fallback behavior
across all services without requiring external dependencies.
Per Engineering Constitution Section 4.7: Debugging & Incident Response
- Test error paths systematically
- Verify graceful failure modes
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from typing import Dict, Any
class TestPolicyServiceErrorPaths:
"""Test error handling in PolicyService."""
@pytest.fixture
def mock_services(self):
"""Create mock services for testing."""
return {
'ollama_service': Mock(),
'rag_service': Mock(),
'guardrails_service': Mock(),
'hitl_service': Mock(),
'eval_manager': Mock(),
'translation_service': Mock(),
'tts_service': Mock()
}
@pytest.fixture
def policy_service(self, mock_services):
"""Create PolicyService with mocked dependencies."""
from app.services.policy_service import PolicyService
# Configure default mock behaviors
mock_services['guardrails_service'].validate_input.return_value = {
'is_valid': True
}
mock_services['rag_service'].enabled = True
mock_services['rag_service'].get_stats.return_value = {
'irdai_indexed': True
}
mock_services['hitl_service'].check_analysis_quality.return_value = {
'requires_review': False
}
return PolicyService(**mock_services)
def test_analyze_policy_guardrails_rejection(self, policy_service, mock_services):
"""Test that guardrails rejection returns proper error."""
mock_services['guardrails_service'].validate_input.return_value = {
'is_valid': False,
'reason': 'Content contains prohibited terms'
}
result = policy_service.analyze_policy("test policy text")
assert result['status'] == 'error'
assert 'Input validation failed' in result['error']
assert 'prohibited terms' in result['error']
def test_analyze_policy_llm_unavailable(self, mock_services):
"""Test graceful degradation when LLM is unavailable."""
from app.services.policy_service import PolicyService, ServiceMode
mock_services['ollama_service'] = None # LLM unavailable
mock_services['guardrails_service'].validate_input.return_value = {
'is_valid': True
}
service = PolicyService(**mock_services)
result = service.analyze_policy("test policy text")
# Should return minimal analysis, not crash
assert result['status'] == 'partial'
assert result['service_mode'] == ServiceMode.MINIMAL.value
assert 'degradation_notice' in result
def test_analyze_policy_llm_exception(self, policy_service, mock_services):
"""Test handling of LLM service exceptions."""
mock_services['ollama_service'].analyze_policy.side_effect = Exception(
"Connection timeout"
)
result = policy_service.analyze_policy("test policy text")
# Should handle gracefully
assert result['status'] in ['error', 'partial']
def test_analyze_policy_rag_indexing_failure(self, policy_service, mock_services):
"""Test that RAG indexing failure doesn't block analysis."""
mock_services['rag_service'].index_document.side_effect = Exception(
"ChromaDB connection failed"
)
mock_services['ollama_service'].analyze_policy.return_value = {
'summary': 'Test analysis'
}
result = policy_service.analyze_policy("test policy text")
# Analysis should still succeed
assert result['status'] == 'success'
# But should log the RAG error
assert 'rag_indexing_error' in result.get('metrics', {}) or result['status'] == 'success'
def test_answer_question_llm_unavailable(self, mock_services):
"""Test Q&A graceful degradation when LLM unavailable."""
from app.services.policy_service import PolicyService, ServiceMode
mock_services['ollama_service'] = None
service = PolicyService(**mock_services)
result = service.answer_question("policy text", "What is coverage?")
assert result['confidence'] == 0.0
assert 'unavailable' in result['answer'].lower()
def test_answer_question_rag_failure_uses_fallback(self, policy_service, mock_services):
"""Test that RAG failure triggers fallback text search."""
mock_services['rag_service'].query_document.side_effect = Exception(
"Vector search failed"
)
mock_services['ollama_service'].answer_question.return_value = "Test answer"
result = policy_service.answer_question(
"The coverage amount is Rs. 5,00,000 for hospitalization.",
"What is the coverage amount?"
)
# Should still return an answer using fallback
assert 'answer' in result
assert result['answer'] is not None
class TestRAGServiceErrorPaths:
"""Test error handling in RAGService."""
def test_get_embeddings_connection_error(self):
"""Test embedding generation handles connection errors."""
from app.services.rag_service import RAGService
with patch('requests.Session') as mock_session:
mock_session.return_value.post.side_effect = Exception("Connection refused")
service = RAGService(persist_directory="./test_data")
if service.enabled:
result = service.get_embeddings("test text")
assert result is None # Should return None, not crash
def test_chunk_text_empty_input(self):
"""Test chunking handles empty input gracefully."""
from app.services.rag_service import RAGService
service = RAGService(persist_directory="./test_data")
if service.enabled:
chunks = service.chunk_text("")
assert chunks == [] or chunks == ['.'] # Empty or minimal result
def test_chunk_text_exceeds_limit(self):
"""Test chunking rejects oversized input."""
from app.services.rag_service import RAGService
import os
# Set a small limit for testing
os.environ['MAX_CHUNK_TEXT_LENGTH'] = '1000'
service = RAGService(persist_directory="./test_data")
if service.enabled:
large_text = "A" * 2000 # Exceeds limit
with pytest.raises(ValueError) as exc_info:
service.chunk_text(large_text)
assert 'exceeds maximum' in str(exc_info.value)
# Cleanup
os.environ.pop('MAX_CHUNK_TEXT_LENGTH', None)
class TestGuardrailsServiceErrorPaths:
"""Test error handling in GuardrailsService."""
def test_validate_empty_input(self):
"""Test guardrails handles empty input."""
from app.services.guardrails_service import GuardrailsService
service = GuardrailsService()
result = service.validate_input("")
# Should handle gracefully (either reject or accept with warning)
assert 'is_valid' in result
def test_validate_none_input(self):
"""Test guardrails handles None input."""
from app.services.guardrails_service import GuardrailsService
service = GuardrailsService()
# Should not crash
try:
result = service.validate_input(None)
assert 'is_valid' in result
except (TypeError, AttributeError):
# Acceptable to raise type error for None
pass
def test_validate_very_long_input(self):
"""Test guardrails handles very long input."""
from app.services.guardrails_service import GuardrailsService
service = GuardrailsService()
long_text = "Valid policy text. " * 10000 # ~190KB
result = service.validate_input(long_text)
# Should complete without timeout/crash
assert 'is_valid' in result
class TestDocumentServiceErrorPaths:
"""Test error handling in DocumentService."""
def test_extract_text_invalid_file_type(self):
"""Test text extraction handles invalid file types."""
from app.services.document_service import DocumentService
import tempfile
import os
service = DocumentService()
# Create a temp file with unsupported extension
with tempfile.NamedTemporaryFile(mode='w', suffix='.xyz', delete=False) as f:
f.write("test content")
temp_path = f.name
try:
# Should raise ValueError for unsupported type
with pytest.raises(ValueError) as exc_info:
service.extract_text_from_file(temp_path)
assert 'unsupported' in str(exc_info.value).lower()
finally:
os.unlink(temp_path)
def test_extract_text_corrupted_pdf(self):
"""Test text extraction handles corrupted PDF."""
from app.services.document_service import DocumentService
import tempfile
import os
service = DocumentService()
# Create a temp file with corrupted PDF content
with tempfile.NamedTemporaryFile(mode='wb', suffix='.pdf', delete=False) as f:
f.write(b"%PDF-1.4 corrupted content here")
temp_path = f.name
try:
# Should either return empty text or raise an exception, not crash
try:
result = service.extract_text_from_file(temp_path)
# If it returns, result should be a tuple (text, pages)
assert isinstance(result, tuple)
except Exception as e:
# Acceptable to raise exception for corrupted file
assert True
finally:
os.unlink(temp_path)
class TestHITLServiceErrorPaths:
"""Test error handling in HITLService."""
def test_check_analysis_quality_empty_analysis(self):
"""Test HITL handles empty analysis."""
from app.services.hitl_service import HITLService
service = HITLService()
result = service.check_analysis_quality({}, "test text")
# Should handle gracefully
assert 'requires_review' in result
def test_check_analysis_quality_missing_fields(self):
"""Test HITL handles analysis with missing fields."""
from app.services.hitl_service import HITLService
service = HITLService()
incomplete_analysis = {'summary': 'Test'} # Missing other fields
result = service.check_analysis_quality(incomplete_analysis, "test text")
assert 'requires_review' in result
class TestServiceModeDetection:
"""Test graceful degradation mode detection."""
def test_full_mode_all_services_available(self):
"""Test FULL mode when all services are available."""
from app.services.policy_service import PolicyService, ServiceMode
mock_ollama = Mock()
mock_rag = Mock()
mock_rag.enabled = True
mock_rag.get_stats.return_value = {'irdai_indexed': True}
service = PolicyService(
ollama_service=mock_ollama,
rag_service=mock_rag,
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
mode_info = service.get_service_mode(force_check=True)
assert mode_info.mode == ServiceMode.FULL
def test_minimal_mode_no_llm(self):
"""Test MINIMAL mode when LLM unavailable."""
from app.services.policy_service import PolicyService, ServiceMode
service = PolicyService(
ollama_service=None, # No LLM
rag_service=Mock(),
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
mode_info = service.get_service_mode(force_check=True)
assert mode_info.mode == ServiceMode.MINIMAL
def test_degraded_no_rag_mode(self):
"""Test DEGRADED_NO_RAG mode when RAG unavailable."""
from app.services.policy_service import PolicyService, ServiceMode
mock_rag = Mock()
mock_rag.enabled = False # RAG disabled
service = PolicyService(
ollama_service=Mock(),
rag_service=mock_rag,
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
mode_info = service.get_service_mode(force_check=True)
assert mode_info.mode == ServiceMode.DEGRADED_NO_RAG
class TestFallbackTextSearch:
"""Test fallback text search functionality."""
def test_fallback_finds_relevant_paragraphs(self):
"""Test fallback search finds keyword matches."""
from app.services.policy_service import PolicyService
service = PolicyService(
ollama_service=Mock(),
rag_service=Mock(),
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
document = """
This is the first paragraph about general information.
The coverage amount is Rs. 5,00,000 for hospitalization expenses.
This includes room rent and nursing charges.
Exclusions include cosmetic surgery and dental treatment.
"""
question = "What is the coverage amount?"
context, excerpts = service._fallback_text_search(document, question)
# Should find the paragraph with "coverage"
assert len(context) > 0
assert any('coverage' in c.lower() or '5,00,000' in c for c in context)
def test_fallback_handles_no_matches(self):
"""Test fallback search handles no keyword matches."""
from app.services.policy_service import PolicyService
service = PolicyService(
ollama_service=Mock(),
rag_service=Mock(),
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
document = "This document has no relevant content."
question = "What is the xyz123 specification?"
context, excerpts = service._fallback_text_search(document, question)
# Should return empty, not crash
assert isinstance(context, list)
assert isinstance(excerpts, list)
def test_fallback_filters_stop_words(self):
"""Test fallback search filters common stop words."""
from app.services.policy_service import PolicyService
service = PolicyService(
ollama_service=Mock(),
rag_service=Mock(),
guardrails_service=Mock(),
hitl_service=Mock(),
eval_manager=Mock(),
translation_service=Mock(),
tts_service=Mock()
)
document = """
The premium amount is Rs. 10,000 annually.
What is the waiting period for claims?
"""
# Question with mostly stop words
question = "What is the premium?"
context, excerpts = service._fallback_text_search(document, question)
# Should still find "premium" paragraph
assert len(context) > 0 or len(excerpts) >= 0 # May or may not find depending on paragraph length