import pytest from unittest.mock import AsyncMock, MagicMock, patch from tools.search_tool import EuropePMCTool, PubMedTool, UnpaywallTool from tools.wikipedia_tool import WikipediaTool, WikidataTool from tools.database_tool import Neo4jTool, MongoTool def _client_ctx(responses): """Build a mocked httpx.AsyncClient whose GET returns `responses` in order.""" client = MagicMock() ctx = MagicMock() it = iter(responses) async def get(*args, **kwargs): return next(it) ctx.get = AsyncMock(side_effect=get) client.__aenter__ = AsyncMock(return_value=ctx) client.__aexit__ = AsyncMock(return_value=None) return client def _resp(status=200, json=None, text=""): r = MagicMock() r.status_code = status r.json.return_value = json if json is not None else {} r.text = text r.raise_for_status = MagicMock() if status >= 400: r.raise_for_status.side_effect = RuntimeError(f"HTTP {status}") return r @pytest.mark.asyncio async def test_europepmc_tool_returns_papers(): tool = EuropePMCTool() mock_response = { "resultList": { "result": [ { "title": "A Study on Federated Learning", "authorString": "Alice A, Bob B", "pubYear": "2023", "abstractText": "We study federated learning.", "citedByCount": 12, "doi": "10.1234/test", "pmcid": "PMC1234567", "id": "PMC1234567", } ] } } with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])): result = await tool.run(query="federated learning", limit=5) assert result.success assert len(result.data) == 1 assert result.data[0]["title"] == "A Study on Federated Learning" assert result.data[0]["url"] == "https://europepmc.org/article/PMC/PMC1234567" assert result.data[0]["citation_count"] == 12 @pytest.mark.asyncio async def test_pubmed_tool_returns_papers(): tool = PubMedTool() esearch_json = {"esearchresult": {"idlist": ["33000001"]}} efetch_xml = """ 33000001
Neural networks for medical imaging We train CNNs on scans. 2021 JaneDoe 10.9999/medimg
""" with patch( "httpx.AsyncClient", return_value=_client_ctx([_resp(json=esearch_json), _resp(text=efetch_xml)]), ): result = await tool.run(query="medical imaging", limit=5) assert result.success assert len(result.data) == 1 assert result.data[0]["title"] == "Neural networks for medical imaging" assert result.data[0]["abstract"] == "We train CNNs on scans." assert result.data[0]["doi"] == "10.9999/medimg" assert result.data[0]["url"].startswith("https://pubmed.ncbi.nlm.nih.gov/") @pytest.mark.asyncio async def test_pubmed_tool_no_ids_returns_empty(): tool = PubMedTool() with patch( "httpx.AsyncClient", return_value=_client_ctx([_resp(json={"esearchresult": {"idlist": []}})]), ): result = await tool.run(query="nothing found", limit=5) assert result.success assert result.data == [] @pytest.mark.asyncio async def test_unpaywall_lookup_returns_oa_url(): tool = UnpaywallTool() mock_response = { "is_oa": True, "best_oa_location": {"url": "https://oa.example.com/paper.pdf"}, } with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])): result = await tool.run("10.1234/test") assert result.success assert result.data["is_oa"] is True assert result.data["url"] == "https://oa.example.com/paper.pdf" @pytest.mark.asyncio async def test_unpaywall_lookup_not_oa_404(): tool = UnpaywallTool() with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(status=404)])): result = await tool.run("10.1234/missing") assert result.success assert result.data["is_oa"] is False assert result.data["url"] == "" @pytest.mark.asyncio async def test_wikipedia_tool_returns_snippets(): tool = WikipediaTool() search_json = { "query": { "search": [ {"title": "Momentum Investing", "snippet": "Momentum investing basics"}, ] } } extracts_json = { "query": { "pages": { "1": {"title": "Momentum Investing", "extract": "Momentum investing is a strategy..."} } } } with patch( "httpx.AsyncClient", return_value=_client_ctx([_resp(json=search_json), _resp(json=extracts_json)]), ): result = await tool.run(query="momentum investing", limit=3) assert result.success assert len(result.data) == 1 assert result.data[0]["title"] == "Momentum Investing" assert result.data[0]["url"] == "https://en.wikipedia.org/wiki/Momentum_Investing" assert "Momentum investing is a strategy" in result.data[0]["snippet"] @pytest.mark.asyncio async def test_wikidata_tool_returns_entities(): tool = WikidataTool() mock_response = { "search": [ {"id": "Q42", "label": "Douglas Adams", "description": "English writer and humorist"}, ] } with patch("httpx.AsyncClient", return_value=_client_ctx([_resp(json=mock_response)])): result = await tool.run(query="douglas adams", limit=3) assert result.success assert result.data[0]["id"] == "Q42" assert result.data[0]["url"] == "https://www.wikidata.org/wiki/Q42" @pytest.mark.asyncio async def test_academic_tools_serialize_parallel_calls(): """Concurrent keyless academic searches must be spaced out (rate-limit regression).""" import asyncio import time import tools.search_tool as st original_interval = st._ACADEMIC_MIN_INTERVAL original_last = st._academic_last_request_at st._ACADEMIC_MIN_INTERVAL = 0.2 st._academic_last_request_at = 0.0 starts = [] empty_ok = {"resultList": {"result": []}} def make_ctx(): client = MagicMock() ctx = MagicMock() async def get(*args, **kwargs): starts.append(time.monotonic()) r = MagicMock() r.json.return_value = empty_ok r.raise_for_status = MagicMock() return r ctx.get = AsyncMock(side_effect=get) client.__aenter__ = AsyncMock(return_value=ctx) client.__aexit__ = AsyncMock(return_value=None) return client try: with patch("httpx.AsyncClient", return_value=make_ctx()): tool = EuropePMCTool() results = await asyncio.gather(*[tool.run(query="q", limit=5) for _ in range(3)]) finally: st._ACADEMIC_MIN_INTERVAL = original_interval st._academic_last_request_at = original_last assert all(r.success for r in results) assert len(starts) == 3 deltas = [starts[i + 1] - starts[i] for i in range(2)] assert all(d >= 0.18 for d in deltas) @pytest.mark.asyncio async def test_neo4j_tool_query(mock_neo4j): mock_neo4j.run.return_value = [{"n": "value"}] tool = Neo4jTool(neo4j=mock_neo4j) result = await tool.run(query="MATCH (n) RETURN n LIMIT 1") assert result.success assert result.data == [{"n": "value"}]