Skip to content

Commit 4b21226

Browse files
manavgupclaude
andcommitted
Fix MCP test failures and production code attribute bugs
Production fixes in search_result_enricher.py: - Fix QueryResult access: qr.text → qr.chunk.text with null safety - Fix DocumentMetadata attributes: doc_id → document_name, file_type → content_type - Remove non-existent file_name attribute access Test fixes in test_search_result_enricher.py: - Fix property mocking: use _mcp_client direct assignment instead of patch.object - Use MagicMock instead of Mock for proper async method support - Fix mock_search_output fixture to use proper QueryResult structure - Add DocumentChunkWithScore import for proper chunk construction All 50 MCP-related tests now pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 723d5b6 commit 4b21226

File tree

2 files changed

+130
-117
lines changed

2 files changed

+130
-117
lines changed

backend/rag_solution/services/search_result_enricher.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,10 @@ async def enrich_query_results(
225225
async def enrich_single(qr: QueryResult) -> MCPEnrichedSearchResult:
226226
async with semaphore:
227227
start_time = time.perf_counter()
228+
# Get text from chunk if available
229+
chunk_text = qr.chunk.text if qr.chunk and qr.chunk.text else ""
228230
args = {
229-
"text": qr.text,
231+
"text": chunk_text,
230232
**(tool_arguments or {}),
231233
}
232234

@@ -378,15 +380,15 @@ async def _invoke_enrichment_tool(
378380
"answer": search_output.answer,
379381
"documents": [
380382
{
381-
"doc_id": doc.doc_id,
382-
"file_name": doc.file_name,
383-
"file_type": doc.file_type,
383+
"document_name": doc.document_name,
384+
"title": doc.title,
385+
"content_type": getattr(doc, "content_type", None),
384386
}
385387
for doc in search_output.documents[:5] # Limit to top 5
386388
],
387389
"chunks": [
388390
{
389-
"text": qr.text[:500], # Limit text length
391+
"text": (qr.chunk.text[:500] if qr.chunk and qr.chunk.text else ""), # Limit text length
390392
"score": qr.score,
391393
}
392394
for qr in search_output.query_results[:5]

tests/unit/services/test_search_result_enricher.py

Lines changed: 123 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
MCPToolsResponse,
2222
)
2323
from rag_solution.schemas.search_schema import SearchOutput
24-
from vectordbs.data_types import DocumentMetadata, QueryResult
24+
from vectordbs.data_types import DocumentChunkWithScore, DocumentMetadata, QueryResult
2525

2626

2727
class TestSearchResultEnricher:
@@ -50,17 +50,19 @@ def mock_search_output(self):
5050
answer="This is a test answer",
5151
documents=[
5252
DocumentMetadata(
53-
doc_id="doc1",
54-
file_name="test.pdf",
55-
file_type="pdf",
56-
created_at=datetime.utcnow(),
53+
document_name="test.pdf",
54+
title="Test Document",
55+
creation_date=datetime.utcnow(),
5756
)
5857
],
5958
query_results=[
6059
QueryResult(
6160
score=0.95,
62-
text="This is relevant text from the document.",
63-
document_chunk_id="chunk1",
61+
chunk=DocumentChunkWithScore(
62+
chunk_id="chunk1",
63+
text="This is relevant text from the document.",
64+
document_id="doc1",
65+
),
6466
)
6567
],
6668
rewritten_query="test query",
@@ -102,15 +104,16 @@ async def test_enrich_config_disabled_returns_original(self, enricher, mock_sear
102104
@pytest.mark.asyncio
103105
async def test_enrich_gateway_unavailable(self, enricher, mock_search_output):
104106
"""Test enrichment handles unavailable gateway gracefully."""
105-
with patch.object(enricher, "mcp_client") as mock_client:
106-
mock_client.is_available = AsyncMock(return_value=False)
107+
mock_client = MagicMock()
108+
mock_client.is_available = AsyncMock(return_value=False)
109+
enricher._mcp_client = mock_client
107110

108-
result = await enricher.enrich(mock_search_output)
111+
result = await enricher.enrich(mock_search_output)
109112

110-
assert result.metadata is not None
111-
assert "mcp_enrichment" in result.metadata
112-
assert result.metadata["mcp_enrichment"]["success"] is False
113-
assert "unavailable" in result.metadata["mcp_enrichment"]["error"].lower()
113+
assert result.metadata is not None
114+
assert "mcp_enrichment" in result.metadata
115+
assert result.metadata["mcp_enrichment"]["success"] is False
116+
assert "unavailable" in result.metadata["mcp_enrichment"]["error"].lower()
114117

115118
@pytest.mark.asyncio
116119
async def test_enrich_success_with_tools(self, enricher, mock_search_output):
@@ -134,19 +137,20 @@ async def test_enrich_success_with_tools(self, enricher, mock_search_output):
134137
execution_time_ms=100.0,
135138
)
136139

137-
with patch.object(enricher, "mcp_client") as mock_client:
138-
mock_client.is_available = AsyncMock(return_value=True)
139-
mock_client.list_tools = AsyncMock(return_value=mock_tools_response)
140-
mock_client.invoke_tool = AsyncMock(return_value=mock_invocation_result)
140+
mock_client = MagicMock()
141+
mock_client.is_available = AsyncMock(return_value=True)
142+
mock_client.list_tools = AsyncMock(return_value=mock_tools_response)
143+
mock_client.invoke_tool = AsyncMock(return_value=mock_invocation_result)
144+
enricher._mcp_client = mock_client
141145

142-
result = await enricher.enrich(mock_search_output)
146+
result = await enricher.enrich(mock_search_output)
143147

144-
assert result.metadata is not None
145-
assert "mcp_enrichment" in result.metadata
146-
assert result.metadata["mcp_enrichment"]["success"] is True
147-
assert len(result.metadata["mcp_enrichment"]["tools"]) == 1
148-
assert result.metadata["mcp_enrichment"]["tools"][0]["name"] == "summarizer"
149-
assert result.metadata["mcp_enrichment"]["tools"][0]["success"] is True
148+
assert result.metadata is not None
149+
assert "mcp_enrichment" in result.metadata
150+
assert result.metadata["mcp_enrichment"]["success"] is True
151+
assert len(result.metadata["mcp_enrichment"]["tools"]) == 1
152+
assert result.metadata["mcp_enrichment"]["tools"][0]["name"] == "summarizer"
153+
assert result.metadata["mcp_enrichment"]["tools"][0]["success"] is True
150154

151155
@pytest.mark.asyncio
152156
async def test_enrich_with_specific_tools(self, enricher, mock_search_output):
@@ -164,16 +168,17 @@ async def test_enrich_with_specific_tools(self, enricher, mock_search_output):
164168
execution_time_ms=50.0,
165169
)
166170

167-
with patch.object(enricher, "mcp_client") as mock_client:
168-
mock_client.is_available = AsyncMock(return_value=True)
169-
mock_client.invoke_tool = AsyncMock(return_value=mock_invocation_result)
171+
mock_client = MagicMock()
172+
mock_client.is_available = AsyncMock(return_value=True)
173+
mock_client.invoke_tool = AsyncMock(return_value=mock_invocation_result)
174+
enricher._mcp_client = mock_client
170175

171-
result = await enricher.enrich(mock_search_output, config)
176+
result = await enricher.enrich(mock_search_output, config)
172177

173-
# Should use custom_tool from config
174-
mock_client.invoke_tool.assert_called_once()
175-
call_args = mock_client.invoke_tool.call_args
176-
assert call_args[0][0] == "custom_tool"
178+
# Should use custom_tool from config
179+
mock_client.invoke_tool.assert_called_once()
180+
call_args = mock_client.invoke_tool.call_args
181+
assert call_args[0][0] == "custom_tool"
177182

178183
@pytest.mark.asyncio
179184
async def test_enrich_parallel_execution(self, enricher, mock_search_output):
@@ -184,13 +189,6 @@ async def test_enrich_parallel_execution(self, enricher, mock_search_output):
184189
parallel=True,
185190
)
186191

187-
mock_result = MCPInvocationOutput(
188-
tool_name="",
189-
status=MCPInvocationStatus.SUCCESS,
190-
result={"data": "result"},
191-
execution_time_ms=50.0,
192-
)
193-
194192
call_count = 0
195193

196194
async def mock_invoke(name, args, timeout=None):
@@ -203,15 +201,16 @@ async def mock_invoke(name, args, timeout=None):
203201
execution_time_ms=50.0,
204202
)
205203

206-
with patch.object(enricher, "mcp_client") as mock_client:
207-
mock_client.is_available = AsyncMock(return_value=True)
208-
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
204+
mock_client = MagicMock()
205+
mock_client.is_available = AsyncMock(return_value=True)
206+
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
207+
enricher._mcp_client = mock_client
209208

210-
result = await enricher.enrich(mock_search_output, config)
209+
result = await enricher.enrich(mock_search_output, config)
211210

212-
# All tools should be called
213-
assert call_count == 3
214-
assert len(result.metadata["mcp_enrichment"]["tools"]) == 3
211+
# All tools should be called
212+
assert call_count == 3
213+
assert len(result.metadata["mcp_enrichment"]["tools"]) == 3
215214

216215
@pytest.mark.asyncio
217216
async def test_enrich_sequential_execution(self, enricher, mock_search_output):
@@ -233,14 +232,15 @@ async def mock_invoke(name, args, timeout=None):
233232
execution_time_ms=50.0,
234233
)
235234

236-
with patch.object(enricher, "mcp_client") as mock_client:
237-
mock_client.is_available = AsyncMock(return_value=True)
238-
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
235+
mock_client = MagicMock()
236+
mock_client.is_available = AsyncMock(return_value=True)
237+
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
238+
enricher._mcp_client = mock_client
239239

240-
await enricher.enrich(mock_search_output, config)
240+
await enricher.enrich(mock_search_output, config)
241241

242-
# Should be in order for sequential execution
243-
assert execution_order == ["tool1", "tool2"]
242+
# Should be in order for sequential execution
243+
assert execution_order == ["tool1", "tool2"]
244244

245245
@pytest.mark.asyncio
246246
async def test_enrich_handles_tool_failure(self, enricher, mock_search_output):
@@ -266,22 +266,23 @@ async def mock_invoke(name, args, timeout=None):
266266
execution_time_ms=50.0,
267267
)
268268

269-
with patch.object(enricher, "mcp_client") as mock_client:
270-
mock_client.is_available = AsyncMock(return_value=True)
271-
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
269+
mock_client = MagicMock()
270+
mock_client.is_available = AsyncMock(return_value=True)
271+
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
272+
enricher._mcp_client = mock_client
272273

273-
result = await enricher.enrich(mock_search_output, config)
274+
result = await enricher.enrich(mock_search_output, config)
274275

275-
# Should still have results, with one success and one failure
276-
tools = result.metadata["mcp_enrichment"]["tools"]
277-
assert len(tools) == 2
276+
# Should still have results, with one success and one failure
277+
tools = result.metadata["mcp_enrichment"]["tools"]
278+
assert len(tools) == 2
278279

279-
working = next(t for t in tools if t["name"] == "working_tool")
280-
failing = next(t for t in tools if t["name"] == "failing_tool")
280+
working = next(t for t in tools if t["name"] == "working_tool")
281+
failing = next(t for t in tools if t["name"] == "failing_tool")
281282

282-
assert working["success"] is True
283-
assert failing["success"] is False
284-
assert failing["error"] == "Tool failed"
283+
assert working["success"] is True
284+
assert failing["success"] is False
285+
assert failing["error"] == "Tool failed"
285286

286287
@pytest.mark.asyncio
287288
async def test_enrich_preserves_original_output(self, enricher, mock_search_output):
@@ -292,30 +293,37 @@ async def test_enrich_preserves_original_output(self, enricher, mock_search_outp
292293

293294
config = MCPEnrichmentConfig(enabled=True, tools=["tool1"])
294295

295-
with patch.object(enricher, "mcp_client") as mock_client:
296-
mock_client.is_available = AsyncMock(return_value=True)
297-
mock_client.invoke_tool = AsyncMock(
298-
return_value=MCPInvocationOutput(
299-
tool_name="tool1",
300-
status=MCPInvocationStatus.SUCCESS,
301-
result={"modified": True},
302-
execution_time_ms=50.0,
303-
)
296+
mock_client = MagicMock()
297+
mock_client.is_available = AsyncMock(return_value=True)
298+
mock_client.invoke_tool = AsyncMock(
299+
return_value=MCPInvocationOutput(
300+
tool_name="tool1",
301+
status=MCPInvocationStatus.SUCCESS,
302+
result={"modified": True},
303+
execution_time_ms=50.0,
304304
)
305+
)
306+
enricher._mcp_client = mock_client
305307

306-
result = await enricher.enrich(mock_search_output, config)
308+
result = await enricher.enrich(mock_search_output, config)
307309

308-
# Original fields should be unchanged
309-
assert result.answer == original_answer
310-
assert result.documents == original_docs
311-
assert result.query_results == original_results
310+
# Original fields should be unchanged
311+
assert result.answer == original_answer
312+
assert result.documents == original_docs
313+
assert result.query_results == original_results
312314

313315
@pytest.mark.asyncio
314316
async def test_enrich_query_results_single_tool(self, enricher):
315317
"""Test enriching individual query results with a tool."""
316318
query_results = [
317-
QueryResult(score=0.9, text="Text 1", document_chunk_id="c1"),
318-
QueryResult(score=0.8, text="Text 2", document_chunk_id="c2"),
319+
QueryResult(
320+
score=0.9,
321+
chunk=DocumentChunkWithScore(chunk_id="c1", text="Text 1", document_id="d1"),
322+
),
323+
QueryResult(
324+
score=0.8,
325+
chunk=DocumentChunkWithScore(chunk_id="c2", text="Text 2", document_id="d2"),
326+
),
319327
]
320328

321329
async def mock_invoke(name, args, timeout=None):
@@ -326,20 +334,21 @@ async def mock_invoke(name, args, timeout=None):
326334
execution_time_ms=30.0,
327335
)
328336

329-
with patch.object(enricher, "mcp_client") as mock_client:
330-
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
337+
mock_client = MagicMock()
338+
mock_client.invoke_tool = AsyncMock(side_effect=mock_invoke)
339+
enricher._mcp_client = mock_client
331340

332-
results = await enricher.enrich_query_results(
333-
query_results,
334-
"entity_extractor",
335-
{"extract_types": ["person", "org"]},
336-
)
341+
results = await enricher.enrich_query_results(
342+
query_results,
343+
"entity_extractor",
344+
{"extract_types": ["person", "org"]},
345+
)
337346

338-
assert len(results) == 2
339-
assert results[0].original_score == 0.9
340-
assert results[1].original_score == 0.8
341-
assert len(results[0].enrichments) == 1
342-
assert results[0].enrichments[0].success is True
347+
assert len(results) == 2
348+
assert results[0].original_score == 0.9
349+
assert results[1].original_score == 0.8
350+
assert len(results[0].enrichments) == 1
351+
assert results[0].enrichments[0].success is True
343352

344353
@pytest.mark.asyncio
345354
async def test_enrich_empty_tools_list(self, enricher, mock_search_output):
@@ -350,14 +359,15 @@ async def test_enrich_empty_tools_list(self, enricher, mock_search_output):
350359
gateway_healthy=True,
351360
)
352361

353-
with patch.object(enricher, "mcp_client") as mock_client:
354-
mock_client.is_available = AsyncMock(return_value=True)
355-
mock_client.list_tools = AsyncMock(return_value=mock_tools_response)
362+
mock_client = MagicMock()
363+
mock_client.is_available = AsyncMock(return_value=True)
364+
mock_client.list_tools = AsyncMock(return_value=mock_tools_response)
365+
enricher._mcp_client = mock_client
356366

357-
result = await enricher.enrich(mock_search_output)
367+
result = await enricher.enrich(mock_search_output)
358368

359-
# Should return original without enrichment metadata
360-
assert result.answer == mock_search_output.answer
369+
# Should return original without enrichment metadata
370+
assert result.answer == mock_search_output.answer
361371

362372
@pytest.mark.asyncio
363373
async def test_enrich_merges_with_existing_metadata(self, enricher):
@@ -371,22 +381,23 @@ async def test_enrich_merges_with_existing_metadata(self, enricher):
371381

372382
config = MCPEnrichmentConfig(enabled=True, tools=["tool1"])
373383

374-
with patch.object(enricher, "mcp_client") as mock_client:
375-
mock_client.is_available = AsyncMock(return_value=True)
376-
mock_client.invoke_tool = AsyncMock(
377-
return_value=MCPInvocationOutput(
378-
tool_name="tool1",
379-
status=MCPInvocationStatus.SUCCESS,
380-
result={},
381-
execution_time_ms=50.0,
382-
)
384+
mock_client = MagicMock()
385+
mock_client.is_available = AsyncMock(return_value=True)
386+
mock_client.invoke_tool = AsyncMock(
387+
return_value=MCPInvocationOutput(
388+
tool_name="tool1",
389+
status=MCPInvocationStatus.SUCCESS,
390+
result={},
391+
execution_time_ms=50.0,
383392
)
393+
)
394+
enricher._mcp_client = mock_client
384395

385-
result = await enricher.enrich(search_output, config)
396+
result = await enricher.enrich(search_output, config)
386397

387-
# Both old and new metadata should exist
388-
assert result.metadata["existing_key"] == "existing_value"
389-
assert "mcp_enrichment" in result.metadata
398+
# Both old and new metadata should exist
399+
assert result.metadata["existing_key"] == "existing_value"
400+
assert "mcp_enrichment" in result.metadata
390401

391402

392403
class TestEnrichmentConfig:

0 commit comments

Comments
 (0)