#1: assert a URL-form / mixed-case caller id is normalized to the bare canonical form in the papers upsert (the fix's whole point; existing DQ-5 test only used a bare caller). #4: assert ra_grobid_backfill.main() opens two separate connections (read released before the GROBID loop, fresh one for the write) rather than holding one across the loop. Both pass; ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
"""Tests for scripts.ra_grobid_backfill — the R-A GROBID references backfill.
|
|
|
|
Only the pure, offline logic is covered (id normalization + Citation assembly);
|
|
``extract_references`` is mocked so no GROBID server or DB is required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from codex.models import Citation
|
|
from scripts.ra_grobid_backfill import _normalize_cited_id, build_grobid_citations, main
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _normalize_cited_id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_normalize_doi_url_to_bare_lowercase() -> None:
|
|
assert _normalize_cited_id(doi="https://doi.org/10.1007/S00454-019-00132-8") == (
|
|
"10.1007/s00454-019-00132-8"
|
|
)
|
|
|
|
|
|
def test_normalize_doi_prefixes() -> None:
|
|
assert _normalize_cited_id(doi="http://doi.org/10.1/X") == "10.1/x"
|
|
assert _normalize_cited_id(doi="doi:10.1/X") == "10.1/x"
|
|
assert _normalize_cited_id(doi="10.1/x") == "10.1/x"
|
|
|
|
|
|
def test_normalize_arxiv_prefix_stripped() -> None:
|
|
assert _normalize_cited_id(arxiv_id="arXiv:2305.10988") == "2305.10988"
|
|
assert _normalize_cited_id(arxiv_id="2305.10988") == "2305.10988"
|
|
# Legacy ids keep their slash.
|
|
assert _normalize_cited_id(arxiv_id="math/0603097") == "math/0603097"
|
|
|
|
|
|
def test_normalize_doi_takes_precedence_over_arxiv() -> None:
|
|
assert _normalize_cited_id(doi="10.5/y", arxiv_id="2305.10988") == "10.5/y"
|
|
|
|
|
|
def test_normalize_empty_returns_none() -> None:
|
|
assert _normalize_cited_id() is None
|
|
assert _normalize_cited_id(doi=" ", arxiv_id="") is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# build_grobid_citations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _ref(doi: str = "", arxiv_id: str = "") -> dict[str, str]:
|
|
return {"title": "t", "authors": "a", "year": "2020", "doi": doi, "arxiv_id": arxiv_id}
|
|
|
|
|
|
def test_build_citations_normalizes_dedups_and_drops_self() -> None:
|
|
paper_id = "10.1007/s00454-019-00132-8"
|
|
refs = [
|
|
_ref(doi="https://doi.org/10.1/A"), # → 10.1/a
|
|
_ref(doi="DOI:10.1/a"), # duplicate of the above after normalization
|
|
_ref(arxiv_id="arXiv:2305.10988"), # → 2305.10988
|
|
_ref(doi="", arxiv_id=""), # no id → skipped
|
|
_ref(doi="https://doi.org/10.1007/S00454-019-00132-8"), # self-cite → dropped
|
|
]
|
|
with patch("scripts.ra_grobid_backfill.extract_references", return_value=refs) as mock_ex:
|
|
cits = build_grobid_citations(paper_id, "some.pdf", grobid_url="http://grobid")
|
|
|
|
mock_ex.assert_called_once_with("some.pdf", grobid_url="http://grobid", timeout=60.0)
|
|
assert all(isinstance(c, Citation) for c in cits)
|
|
assert all(c.citing_id == paper_id for c in cits)
|
|
assert [c.cited_id for c in cits] == ["10.1/a", "2305.10988"]
|
|
|
|
|
|
def test_build_citations_empty_when_no_refs() -> None:
|
|
with patch("scripts.ra_grobid_backfill.extract_references", return_value=[]):
|
|
assert build_grobid_citations("10.1/x", "x.pdf") == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# main — connection scope (review finding #4)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_main_uses_separate_connections_for_read_and_write(tmp_path: Any) -> None:
|
|
"""Review #4: the read connection is released BEFORE the slow GROBID loop, and a
|
|
fresh connection is opened for the write — so no idle-in-transaction connection
|
|
spans the per-paper network loop (which would abort on an SSH-tunnel drop)."""
|
|
(tmp_path / "paper.pdf").write_bytes(b"%PDF-1.4")
|
|
conns: list[MagicMock] = []
|
|
|
|
@contextmanager
|
|
def fake_get_conn() -> Any:
|
|
c = MagicMock()
|
|
c.execute.return_value.fetchall.return_value = [
|
|
{"id": "10.1/x", "source_path": "/d/paper.txt", "out_edges": 0}
|
|
]
|
|
c.execute.return_value.fetchone.return_value = {"papers": 1, "citing": 1, "edges": 1}
|
|
conns.append(c)
|
|
yield c
|
|
|
|
with (
|
|
patch("scripts.ra_grobid_backfill.get_conn", side_effect=fake_get_conn),
|
|
patch("scripts.ra_grobid_backfill.build_grobid_citations", return_value=[]),
|
|
):
|
|
rc = main(["--pdf-dir", str(tmp_path), "--write"])
|
|
|
|
assert rc == 0
|
|
# Two DISTINCT get_conn() context managers (read, then write) — not one held
|
|
# open across the GROBID loop.
|
|
assert len(conns) == 2
|