fix(review): address PR #16 code-review findings
#1 ingest: normalize the pinned caller id (_norm_cited_id) so a non-bare DOI caller cannot store a URL-form papers.id that defeats DQ-5 idempotency / the startswith(10.) recovery gates. #2 quality.section_label: collapse to a controlled bucket only for an EXACT canonical heading; descriptive titles ('Abstract Nonsense...') keep their real title instead of being mislabelled. #4 ra_grobid_backfill: release the read connection before the slow GROBID network loop, fresh connection for the write (no idle-in-transaction across the loop over the flaky tunnel). #5/#10 tex: flatten_inputs strips unresolved input/include at the depth cap (no literal leak on cycles); _norm_texkey strips only a single leading ./ . #6/#7 arxiv.fetch_source: keep non-.tex members resolvable for input; pick primary on an UN-commented documentclass line. #13 is_arxiv_id: also exclude http:// and arXiv-DOI forms. Tests added/updated for each. Left as deliberate decisions: #3 (pre-section text drop is pre-existing in extract_sections; abstract stored separately), #8 (script normalizer is intentionally self-contained, already documented), #9/#11/#12 (no current trigger / tightening the gzip heuristic would reject valid old LaTeX like documentstyle / title truncation is by design on a write-only column). ruff + mypy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -238,11 +238,15 @@ def test_ingest_paper_source_tex(tmp_path: Any) -> None:
|
||||
chunk_sql: str = chunk_insert_call[0][0]
|
||||
assert "INSERT INTO chunks" in chunk_sql
|
||||
|
||||
# R-F: heading → controlled bucket where it maps ("Introduction"→intro,
|
||||
# "Proof of…"→proof), else the cleaned real title ("discrete conformal maps")
|
||||
# instead of collapsing to "body".
|
||||
# R-F: a bare canonical heading → bucket ("Introduction"→intro); any other
|
||||
# heading is stored as its cleaned real title ("proof of the main theorem",
|
||||
# "discrete conformal maps") instead of being collapsed/mislabelled.
|
||||
chunk_rows = chunk_insert_call[0][1]
|
||||
assert [row[4] for row in chunk_rows] == ["intro", "proof", "discrete conformal maps"]
|
||||
assert [row[4] for row in chunk_rows] == [
|
||||
"intro",
|
||||
"proof of the main theorem",
|
||||
"discrete conformal maps",
|
||||
]
|
||||
assert result.chunks_upserted == len(section_pairs)
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,18 @@ class TestFlattenInputs:
|
||||
assert r"\input" not in out
|
||||
assert "A" in out and "B" in out
|
||||
|
||||
def test_input_cycle_does_not_leak_directive(self) -> None:
|
||||
# a -> b -> a: the depth cap must strip the unresolved directive, not leak it.
|
||||
files = {"a.tex": r"AA \input{b}", "b.tex": r"BB \input{a}"}
|
||||
out = flatten_inputs(files["a.tex"], files)
|
||||
assert r"\input" not in out
|
||||
assert "AA" in out and "BB" in out
|
||||
|
||||
def test_dot_slash_prefix_resolves_without_overstrip(self) -> None:
|
||||
# "./intro" resolves intro.tex; lstrip must not eat a real leading dot/run.
|
||||
files = {"main.tex": r"\input{./intro}", "intro.tex": "BODY"}
|
||||
assert "BODY" in flatten_inputs(files["main.tex"], files)
|
||||
|
||||
|
||||
class TestChunkSections:
|
||||
def test_pairs_carry_section_titles(self) -> None:
|
||||
|
||||
@@ -272,16 +272,25 @@ class TestCleanTitle:
|
||||
|
||||
|
||||
class TestSectionLabel:
|
||||
def test_title_maps_to_controlled_bucket(self):
|
||||
# Headings matching the controlled vocab keep the canonical bucket.
|
||||
def test_bare_canonical_heading_maps_to_bucket(self):
|
||||
# Only an EXACT canonical heading collapses to its controlled bucket.
|
||||
assert section_label("Introduction", "body text") == "intro"
|
||||
assert section_label("Proof of Theorem 3", "body text") == "proof"
|
||||
assert section_label("Proof", "body text") == "proof"
|
||||
assert section_label("References", "body text") == "bibliography"
|
||||
assert section_label("Abstract", "body text") == "abstract"
|
||||
|
||||
def test_descriptive_title_stored_verbatim(self):
|
||||
# R-F win: a non-vocab heading is stored as its cleaned title, not "body".
|
||||
# R-F: a heading that merely STARTS with a keyword keeps its real title,
|
||||
# not the bucket — fixes classify_section prefix-match over-bucketing.
|
||||
assert section_label("Preliminaries", "body text") == "preliminaries"
|
||||
assert section_label("3.1 Discrete Conformal Maps", "x") == "discrete conformal maps"
|
||||
assert section_label("Proof of Theorem 3", "x") == "proof of theorem 3"
|
||||
assert section_label("Abstract Nonsense and Categories", "x") == (
|
||||
"abstract nonsense and categories"
|
||||
)
|
||||
assert section_label("Introduction to Operator Algebras", "x") == (
|
||||
"introduction to operator algebras"
|
||||
)
|
||||
|
||||
def test_none_or_blank_title_falls_back_to_content(self):
|
||||
# .pdf/.txt path: unchanged content-based classification.
|
||||
|
||||
@@ -16,4 +16,6 @@ def test_is_arxiv_id_rejects_dois_and_wids() -> None:
|
||||
assert not is_arxiv_id("10.14279/depositonce-20357")
|
||||
assert not is_arxiv_id("W2971636899")
|
||||
assert not is_arxiv_id("https://openalex.org/W2971636899")
|
||||
assert not is_arxiv_id("http://openalex.org/W2971636899")
|
||||
assert not is_arxiv_id("10.48550/arXiv.2305.10988") # arXiv DOI form, not bare id
|
||||
assert not is_arxiv_id("")
|
||||
|
||||
@@ -110,6 +110,37 @@ def test_fetch_source_bare_gzip_single_file(monkeypatch: pytest.MonkeyPatch) ->
|
||||
assert "Old single-file paper body." in result
|
||||
|
||||
|
||||
def test_fetch_source_resolves_non_tex_include(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""\\input of a non-.tex body member (e.g. a .def file) is inlined, not dropped (R-C)."""
|
||||
main = b"\\documentclass{article}\\begin{document}\\input{body.def}\\end{document}"
|
||||
tar_bytes = _make_tar_gz({"main.tex": main, "body.def": b"NON TEX BODY TEXT"})
|
||||
|
||||
def mock_get(url: str, *, timeout: int = 60, follow_redirects: bool = True) -> httpx.Response:
|
||||
return httpx.Response(200, content=tar_bytes)
|
||||
|
||||
monkeypatch.setattr(httpx, "get", mock_get)
|
||||
result = arxiv.fetch_source("2301.07041")
|
||||
assert result is not None
|
||||
assert "NON TEX BODY TEXT" in result
|
||||
|
||||
|
||||
def test_fetch_source_ignores_commented_documentclass(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A commented %\\documentclass must not select a file as the primary document."""
|
||||
decoy = b"% \\documentclass{article}\n\\section{Stub}\nDECOY ONLY"
|
||||
real = b"\\documentclass{book}\\begin{document}REAL BODY HERE\\end{document}"
|
||||
# decoy sorts/inserts first; without the fix it would win on the substring match.
|
||||
tar_bytes = _make_tar_gz({"aaa_decoy.tex": decoy, "real.tex": real})
|
||||
|
||||
def mock_get(url: str, *, timeout: int = 60, follow_redirects: bool = True) -> httpx.Response:
|
||||
return httpx.Response(200, content=tar_bytes)
|
||||
|
||||
monkeypatch.setattr(httpx, "get", mock_get)
|
||||
result = arxiv.fetch_source("2301.07041")
|
||||
assert result is not None
|
||||
assert "REAL BODY HERE" in result
|
||||
assert "DECOY ONLY" not in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# fetch_source — 404 → None
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user