RSCE / tests /test_integration.py
LP1205's picture
Deploy RSCE backend to Hugging Face Space
5e952bb
Raw
History Blame Contribute Delete
11 kB
import os
import tempfile
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.models.paper import Paper
from src.models.claim import ExtractedClaim, ClaimType, Polarity, StudyDesign, Entity, EntityType, ClaimExtractionResponse
from src.models.contradiction import ContradictionType
from src.pipeline import run_full_pipeline
from src.config import settings
@pytest.fixture
def temp_db():
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.fixture
def temp_faiss():
fd, path = tempfile.mkstemp(suffix=".faiss")
os.close(fd)
yield path
if os.path.exists(path):
os.remove(path)
@pytest.mark.asyncio
async def test_end_to_end_pipeline_api_integration(temp_db, temp_faiss):
"""End-to-end integration test exercising the pipeline execution,
database persistence, and API REST endpoints serving the results.
"""
# Override settings.db_path and settings.faiss_index_path with temporary fixtures
with patch.object(settings, "db_path", temp_db), \
patch.object(settings, "faiss_index_path", temp_faiss):
# Initialize the database
from src.storage.database import init_db
init_db(db_path=temp_db)
# 1. Mock papers list
mock_papers = [
Paper(
pmid="11111",
title="Study 1 on Metformin",
authors=["John Adams", "Co-Author One"],
year=2020,
journal="Journal of Diabetes",
abstract_text="A clinical trial showed that Metformin reduces cancer risk in humans."
),
Paper(
pmid="22222",
title="Study 2 on Metformin",
authors=["Alice Baker"],
year=2023,
journal="Cancer Letters",
abstract_text="Another trial showed that Metformin increases cancer risk in humans."
)
]
# 2. Mock extracted claims
claim_1 = ExtractedClaim(
text="Metformin reduces cancer risk in humans.",
polarity=Polarity.NEGATIVE,
population="humans",
context="clinical trial",
quote_anchor="Metformin reduces cancer risk in humans",
claim_type=ClaimType.CAUSAL,
study_design=StudyDesign.RCT,
entities=[Entity(text="Metformin", entity_type=EntityType.DRUG)]
)
claim_2 = ExtractedClaim(
text="Metformin increases cancer risk in humans.",
polarity=Polarity.POSITIVE,
population="humans",
context="clinical trial",
quote_anchor="Metformin increases cancer risk in humans",
claim_type=ClaimType.CAUSAL,
study_design=StudyDesign.RCT,
entities=[Entity(text="Metformin", entity_type=EntityType.DRUG)]
)
# 3. Patch external pipeline modules
with patch("src.pipeline.search_pubmed", new_callable=AsyncMock) as mock_search, \
patch("src.pipeline.fetch_abstracts", new_callable=AsyncMock) as mock_fetch, \
patch("src.pipeline.enrich_papers_with_full_text", new_callable=AsyncMock):
mock_search.return_value = ["11111", "22222"]
mock_fetch.return_value = mock_papers
# Construct mock LLM responses
from src.detection.llm_judge import JudgeResponse
mock_llm = MagicMock()
mock_llm.model_name = "mock-integration-llm"
async def generate_structured_side_effect(prompt, response_schema, temperature=0.1):
if response_schema == ClaimExtractionResponse:
if "Study 1 on Metformin" in prompt or "reduces cancer risk" in prompt:
return ClaimExtractionResponse(claims=[claim_1])
else:
return ClaimExtractionResponse(claims=[claim_2])
elif response_schema == JudgeResponse:
return JudgeResponse(
is_same_topic=True,
is_contradiction=True,
is_genuine=True,
contradiction_type=ContradictionType.DIRECTION_REVERSAL,
explanation="Opposing findings on cancer risk.",
scope_note=""
)
else:
raise ValueError(f"Unexpected response_schema in test: {response_schema}")
mock_llm.generate_structured = AsyncMock(side_effect=generate_structured_side_effect)
mock_llm.generate_text = AsyncMock(
return_value="Metformin reduces cancer risk in humans [Adams et al., 2020], but Baker contradicts this [Baker, 2023]."
)
# Create a mock EntityNormalizer to decouple from scispaCy, synonym_map.json, and LLM fallbacks
class MockEntityNormalizer:
async def normalize_entities(self, claims):
for claim in claims:
for entity in claim.entities:
if entity.text.lower() == "metformin":
entity.text = "Metformin"
entity.canonical_id = "MeSH:D008687"
return claims
# Patch get_llm to return our mock LLM and EntityNormalizer
with patch("src.pipeline.get_llm", return_value=mock_llm), \
patch("src.detection.contradiction_detector.get_llm", return_value=mock_llm), \
patch("src.pipeline.EntityNormalizer", return_value=MockEntityNormalizer()):
# Execute full pipeline end-to-end (updates temporary database)
state = await run_full_pipeline(
"metformin cancer",
max_papers=2
)
# Verify pipeline returned successfully
assert state.status == "COMPLETED"
assert len(state.papers) == 2
assert len(state.claims) == 2
assert len(state.contradictions) == 1
# Now, test the REST API endpoints using FastAPI's TestClient
from fastapi.testclient import TestClient
from api.app import app
# Initialize the TestClient
client = TestClient(app)
# Test 1: Get Status Endpoint
status_res = client.get(f"/api/status/{state.run_id}")
assert status_res.status_code == 200
status_data = status_res.json()
assert status_data["run_id"] == state.run_id
assert status_data["status"] == "COMPLETED"
assert status_data["papers_fetched"] == 2
assert status_data["claims_extracted"] == 2
assert status_data["contradictions_found"] == 1
# Test 2: Get Results Endpoint
results_res = client.get(f"/api/results/{state.run_id}")
assert results_res.status_code == 200
results_data = results_res.json()
assert results_data["total_papers"] == 2
assert results_data["total_claims"] == 2
assert len(results_data["contradictions"]) == 1
assert "Baker, 2023" in results_data["summary"]
assert "Adams, 2020" in results_data["summary"]
# Test 3: Get Claims Endpoint
claims_res = client.get(f"/api/claims/{state.run_id}")
assert claims_res.status_code == 200
claims_data = claims_res.json()
assert len(claims_data) == 2
assert claims_data[0]["text"] == "Metformin reduces cancer risk in humans."
assert claims_data[1]["text"] == "Metformin increases cancer risk in humans."
# Test 4: Get Graph Endpoint
graph_res = client.get(f"/api/graph/{state.run_id}")
assert graph_res.status_code == 200
graph_data = graph_res.json()
assert "elements" in graph_data
assert "nodes" in graph_data["elements"]
assert "edges" in graph_data["elements"]
# Verify nodes by checking types rather than a strict total count
nodes = graph_data["elements"]["nodes"]
assert len(nodes) >= 4
node_types = [n["data"]["type"] for n in nodes]
assert node_types.count("paper") == 2
assert node_types.count("claim") == 2
assert node_types.count("entity") >= 1
# Test 5: WebSocket Endpoint (GET /api/ws/{run_id})
# FastAPI TestClient websocket_connect context manager connects synchronously.
# Since the test is async, we can await broadcast_status on the event loop,
# which will push the message to the socket for TestClient to read.
with client.websocket_connect(f"/api/ws/{state.run_id}") as websocket:
# Verify immediate state broadcast on connection
initial_data = websocket.receive_json()
assert initial_data["run_id"] == state.run_id
assert initial_data["status"] == "COMPLETED"
assert initial_data["papers_fetched"] == 2
assert initial_data["claims_extracted"] == 2
assert initial_data["contradictions_found"] == 1
# Verify manager broadcast updates are received by active websocket connections
from api.routes.analysis import manager
test_payload = {
"run_id": state.run_id,
"status": "RUNNING",
"status_message": "WebSocket broadcast test",
"papers_fetched": 3,
"claims_extracted": 4,
"contradictions_found": 2
}
await manager.broadcast_status(state.run_id, test_payload)
broadcast_data = websocket.receive_json()
assert broadcast_data["run_id"] == state.run_id
assert broadcast_data["status"] == "RUNNING"
assert broadcast_data["status_message"] == "WebSocket broadcast test"
assert broadcast_data["papers_fetched"] == 3
assert broadcast_data["claims_extracted"] == 4
assert broadcast_data["contradictions_found"] == 2