"""Tests for codex.sources.semanticscholar.""" from __future__ import annotations import httpx import pytest from codex.models import Citation from codex.sources import semanticscholar # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _SAMPLE_REFS_RESPONSE = { "data": [ { "citedPaper": { "paperId": "abc123", "externalIds": {"DOI": "10.1000/xyz1"}, }, "contexts": ["This approach was first described in [1]."], }, { "citedPaper": { "paperId": "def456", "externalIds": {"ArXiv": "2301.07041"}, }, "contexts": [], }, ] } _SAMPLE_RECS_RESPONSE = { "recommendedPapers": [ {"paperId": "rec_id_1"}, {"paperId": "rec_id_2"}, {"paperId": "rec_id_3"}, ] } # --------------------------------------------------------------------------- # fetch_references # --------------------------------------------------------------------------- def test_fetch_references_returns_citations(monkeypatch: pytest.MonkeyPatch) -> None: """fetch_references maps API response to Citation objects correctly.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: return httpx.Response(200, json=_SAMPLE_REFS_RESPONSE) monkeypatch.setattr(semanticscholar, "_get", mock_get) citations = semanticscholar.fetch_references("abc123") assert len(citations) == 2 assert all(isinstance(c, Citation) for c in citations) # First citation uses DOI and has context assert citations[0].citing_id == "abc123" assert citations[0].cited_id == "10.1000/xyz1" assert citations[0].context == "This approach was first described in [1]." # Second citation falls back to ArXiv ID, no context assert citations[1].citing_id == "abc123" assert citations[1].cited_id == "2301.07041" assert citations[1].context is None def test_fetch_references_404_returns_empty(monkeypatch: pytest.MonkeyPatch) -> None: """A 404 response should return an empty list.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: raise httpx.HTTPStatusError( "Not Found", request=httpx.Request("GET", url), response=httpx.Response(404, request=httpx.Request("GET", url)), ) monkeypatch.setattr(semanticscholar, "_get", mock_get) result = semanticscholar.fetch_references("nonexistent") assert result == [] # --------------------------------------------------------------------------- # fetch_recommendations # --------------------------------------------------------------------------- def test_fetch_recommendations_returns_list_of_str( monkeypatch: pytest.MonkeyPatch, ) -> None: """fetch_recommendations returns a flat list of paperId strings.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: return httpx.Response(200, json=_SAMPLE_RECS_RESPONSE) monkeypatch.setattr(semanticscholar, "_get", mock_get) result = semanticscholar.fetch_recommendations("abc123", limit=3) assert isinstance(result, list) assert len(result) == 3 assert all(isinstance(r, str) for r in result) assert result == ["rec_id_1", "rec_id_2", "rec_id_3"] def test_fetch_recommendations_404_returns_empty( monkeypatch: pytest.MonkeyPatch, ) -> None: """A 404 response should return an empty list.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: raise httpx.HTTPStatusError( "Not Found", request=httpx.Request("GET", url), response=httpx.Response(404, request=httpx.Request("GET", url)), ) monkeypatch.setattr(semanticscholar, "_get", mock_get) result = semanticscholar.fetch_recommendations("nonexistent") assert result == [] # --------------------------------------------------------------------------- # fetch_references — {"data": null} guard (DQ-1 regression) # --------------------------------------------------------------------------- def test_fetch_references_null_data_returns_empty(monkeypatch: pytest.MonkeyPatch) -> None: """S2 returns HTTP 200 with ``{"data": null}`` for a known paper that has no parsed references; this must yield [] rather than raising TypeError.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: return httpx.Response(200, json={"data": None, "citingPaperInfo": {}}) monkeypatch.setattr(semanticscholar, "_get", mock_get) assert semanticscholar.fetch_references("DOI:10.14279/depositonce-5415") == [] # --------------------------------------------------------------------------- # fetch_abstract (DQ-2) # --------------------------------------------------------------------------- def test_fetch_abstract_returns_text(monkeypatch: pytest.MonkeyPatch) -> None: """fetch_abstract returns the abstract string when present.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: return httpx.Response(200, json={"abstract": "We study ideal hyperbolic polyhedra."}) monkeypatch.setattr(semanticscholar, "_get", mock_get) assert semanticscholar.fetch_abstract("DOI:10.1007/s00454-019-00132-8") == ( "We study ideal hyperbolic polyhedra." ) def test_fetch_abstract_null_returns_none(monkeypatch: pytest.MonkeyPatch) -> None: """A null/absent abstract yields None.""" def mock_get(url: str, params: dict[str, object] | None = None) -> httpx.Response: return httpx.Response(200, json={"abstract": None}) monkeypatch.setattr(semanticscholar, "_get", mock_get) assert semanticscholar.fetch_abstract("arXiv:1234.5678") is None