diff --git a/codex/ingest.py b/codex/ingest.py index b63799d..b43fdd7 100644 --- a/codex/ingest.py +++ b/codex/ingest.py @@ -98,6 +98,14 @@ def ingest_paper( if paper is None: raise ValueError(f"Paper not found: {paper_id}") + # Canonical identity is the caller-supplied id — what the CLI / ingest scripts + # use and what papers.id is contracted to hold (arXiv id or DOI). OpenAlex's + # fetch_paper fills paper.id with the URL-form doi/id (e.g. + # "https://doi.org/10.48550/arxiv.math/0603097"), which broke bare-id lookups + # and cross-citation matching (audit C-7). The OpenAlex work id is retained + # separately as openalex_id and in the paper_identifiers alias table. + paper.id = paper_id.strip() + # Auto-generate bibkey when source has none (D-04). if paper.bibkey is None: paper.bibkey = _make_bibkey(paper) diff --git a/tests/ingest/test_ingest.py b/tests/ingest/test_ingest.py index 80e4d23..638a1ec 100644 --- a/tests/ingest/test_ingest.py +++ b/tests/ingest/test_ingest.py @@ -106,6 +106,37 @@ def test_ingest_paper_basic() -> None: ) +def test_ingest_canonicalises_paper_id_to_caller_arg() -> None: + """papers.id is the caller-supplied id, not OpenAlex's URL-form id (audit C-7). + + Real OpenAlex returns the doi/id as a full URL; ingest must key the paper on + the bare id the caller passed (matching ingest scripts and CLI lookups), and + keep the OpenAlex work id only as openalex_id. + """ + oa_paper = Paper( + id="https://doi.org/10.48550/arxiv.math/0603097", # URL form, as OpenAlex emits + title="A Test Paper", + openalex_id="https://openalex.org/W4301005794", + authors=["Alice", "Bob"], + year=2008, + abstract="Test abstract.", + ) + mock_conn = MagicMock() + + with ( + patch("codex.ingest.openalex.fetch_paper", return_value=oa_paper), + patch("codex.ingest.openalex.fetch_citations", return_value=[]), + patch("codex.ingest.get_embedder", return_value=_fake_embedder()), + patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)), + ): + result = ingest_paper("math/0603097") + + papers_params = mock_conn.execute.call_args_list[0][0][1] + assert papers_params["id"] == "math/0603097", "papers.id must be the caller's bare id" + assert papers_params["openalex_id"] == "https://openalex.org/W4301005794" + assert result.paper_id == "math/0603097" + + # --------------------------------------------------------------------------- # 2. test_ingest_paper_source_tex # --------------------------------------------------------------------------- diff --git a/tests/sources/test_openalex.py b/tests/sources/test_openalex.py index d481aa2..5a78b99 100644 --- a/tests/sources/test_openalex.py +++ b/tests/sources/test_openalex.py @@ -15,7 +15,8 @@ from codex.sources.openalex import _resolve_id _SAMPLE_WORK = { "id": "https://openalex.org/W2741809807", - "doi": "10.1145/3592430", + # Real OpenAlex returns the DOI as a full URL, not a bare DOI (audit T-3). + "doi": "https://doi.org/10.1145/3592430", "title": "Conformal Prediction: A Review", "publication_year": 2023, "authorships": [ @@ -90,6 +91,10 @@ def test_fetch_paper_success(monkeypatch: pytest.MonkeyPatch) -> None: assert paper.authors == ["Alice Smith", "Bob Jones"] assert paper.abstract == "Conformal prediction is great" assert paper.openalex_id == "https://openalex.org/W2741809807" + # fetch_paper maps id from the (URL-form) doi; ingest later canonicalises + # papers.id to the caller's bare id (audit C-7). Pin the real mapping here so + # the fixture cannot silently misrepresent OpenAlex again (audit T-3). + assert paper.id == "https://doi.org/10.1145/3592430" def test_fetch_paper_arxiv_id_prefixed(monkeypatch: pytest.MonkeyPatch) -> None: