fix(ingest): D-04 bibkey heuristic + D-05a/b/c regression tests + __main__

D-04: add _make_bibkey(paper) — ConcatSurnames+Year, ≤3 authors full list,
      >3 authors FirstEtAlYear. Applied in ingest_paper when paper.bibkey is
      None. ON CONFLICT now fills NULL bibkeys via COALESCE(papers.bibkey,
      EXCLUDED.bibkey) while preserving manually-set values.

D-05b: add codex/__main__.py so `python -m codex` dispatches to the CLI.

D-05c: fix test_ingest.py:75 — rename var to raw_api_citations (the raw
       OpenAlex payload with citing_id=openalex_id) and add regression
       assertion: citing_id written to DB must equal paper.id (DOI), not
       paper.openalex_id.

D-05a (proxy): add TestEntryPoint.test_python_m_codex_help — runs
       `sys.executable -m codex --help` via subprocess, covering the
       non-uv-run entry path. 253 tests green, ruff+mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-15 03:07:06 +02:00
parent 335ca2c923
commit b87087d3c6
4 changed files with 139 additions and 5 deletions

View File

@@ -13,7 +13,7 @@ from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from codex.ingest import IngestResult, ingest_paper
from codex.ingest import IngestResult, _make_bibkey, ingest_paper
from codex.models import Citation, FigureChunk, FormulaChunk, Paper
# ---------------------------------------------------------------------------
@@ -71,13 +71,14 @@ def test_ingest_paper_basic() -> None:
mock_conn.executemany = MagicMock()
mock_conn.commit = MagicMock()
api_citations = [
# OpenAlex returns citing_id=openalex_id; ingest.py must rewrite to paper.id (D-05c).
raw_api_citations = [
Citation(citing_id=paper.openalex_id or "", cited_id="W999"),
]
with (
patch("codex.ingest.openalex.fetch_paper", return_value=paper) as mock_fetch,
patch("codex.ingest.openalex.fetch_citations", return_value=api_citations),
patch("codex.ingest.openalex.fetch_citations", return_value=raw_api_citations),
patch("codex.ingest.get_embedder", return_value=_fake_embedder()),
patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)),
):
@@ -94,7 +95,15 @@ def test_ingest_paper_basic() -> None:
assert isinstance(result, IngestResult)
assert result.paper_id == paper.id
assert result.chunks_upserted == 0
assert result.citations_upserted == len(api_citations)
assert result.citations_upserted == len(raw_api_citations)
# D-05c regression: citing_id must be rewritten to paper.id (DOI), not openalex_id.
mock_cursor = mock_conn.cursor.return_value.__enter__.return_value
cit_call = mock_cursor.executemany.call_args_list[0]
inserted_rows: list[tuple[str, str, Any]] = cit_call[0][1]
assert inserted_rows[0][0] == paper.id, (
f"citing_id must be paper.id='{paper.id}', not openalex_id='{paper.openalex_id}'"
)
# ---------------------------------------------------------------------------
@@ -404,6 +413,76 @@ def test_ingest_paper_rich_requires_pdf_source(tmp_path: Any) -> None:
assert result.figures_upserted == 0
# ---------------------------------------------------------------------------
# D-04 — bibkey auto-generation heuristic
# ---------------------------------------------------------------------------
def test_make_bibkey_single_author() -> None:
paper = Paper(id="doi:10.1/x", title="T", authors=["John Smith"], year=2023)
assert _make_bibkey(paper) == "Smith2023"
def test_make_bibkey_two_authors() -> None:
paper = Paper(id="doi:10.1/x", title="T", authors=["Alice Foo", "Bob Bar"], year=2015)
assert _make_bibkey(paper) == "FooBar2015"
def test_make_bibkey_three_authors() -> None:
paper = Paper(
id="doi:10.1/x",
title="T",
authors=["Alexander Bobenko", "Ulrich Pinkall", "Boris Springborn"],
year=2015,
)
assert _make_bibkey(paper) == "BobenkoPinkallSpringborn2015"
def test_make_bibkey_many_authors_uses_etal() -> None:
paper = Paper(
id="doi:10.1/x",
title="T",
authors=["A One", "B Two", "C Three", "D Four"],
year=2020,
)
assert _make_bibkey(paper) == "OneEtAl2020"
def test_make_bibkey_no_authors_returns_none() -> None:
paper = Paper(id="doi:10.1/x", title="T", authors=[], year=2023)
assert _make_bibkey(paper) is None
def test_make_bibkey_no_year_returns_none() -> None:
paper = Paper(id="doi:10.1/x", title="T", authors=["John Smith"], year=None)
assert _make_bibkey(paper) is None
def test_ingest_sets_bibkey_when_none(tmp_path: Any) -> None:
"""ingest_paper auto-generates bibkey when paper has none (D-04)."""
paper = _make_paper() # bibkey=None (default from _make_paper)
mock_conn = MagicMock()
mock_conn.execute = MagicMock()
mock_conn.executemany = MagicMock()
mock_conn.commit = MagicMock()
with (
patch("codex.ingest.openalex.fetch_paper", return_value=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)),
):
ingest_paper(paper.id)
upsert_params: dict[str, Any] = mock_conn.execute.call_args_list[0][0][1]
assert upsert_params["bibkey"] is not None, "bibkey must be auto-generated when paper has none"
# authors=["Alice", "Bob"], year=2023 → 2 authors ≤ 3 → concat last tokens
assert upsert_params["bibkey"] == "AliceBob2023"
# ---------------------------------------------------------------------------
def test_ingest_result_has_formula_figure_counts() -> None:
"""IngestResult has formulas_upserted and figures_upserted fields with default 0."""
result = IngestResult(paper_id="test", chunks_upserted=0, citations_upserted=0)