#!/usr/bin/env python3
"""#431 — exact-title-or-bust acceptance regression tests.
The v5 architecture pivot (spec docs/design/2026-06-13-431-title-match-hardening-spec.md
§0.12): a NON-exact title is never promoted to `matched` on title-similarity +
year/author alone — that high-overlap-same-author signal is the shared signature
of an author's own related-but-distinct works (a correction and its original, a
reply and its target, Part I / Part II, a no-ordinal companion). The only
disambiguator all four resolvers can compute is the exact normalized title.
These tests pin the behavior the pre-existing client tests did NOT cover: the
pre-#431 clients returned the highest-ratio candidate regardless of exactness,
so a high-ratio related work was wrongly `matched`. Each client test below feeds
a candidate that clears the 0.70 ratio but is NOT an exact normalized title and
asserts the title fallback now yields no match; the legitimate exact / acronym /
case variants still match; F3 ordering reaches a correct exact #2 past a
non-exact #1; an exact-but-generic title with no corroborating ID stays
unmatched; and the resolver→reducer chain maps a title-keyed miss to
`unresolvable` (never a false `false`).
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT / "scripts") not in sys.path:
sys.path.insert(0, str(REPO_ROOT / "scripts"))
def _json_resp(payload: dict) -> MagicMock:
resp = MagicMock()
resp.read.return_value = json.dumps(payload).encode("utf-8")
resp.__enter__ = MagicMock(return_value=resp)
resp.__exit__ = MagicMock(return_value=None)
return resp
def _atom(*titles: str) -> bytes:
entries = "".join(
""
"http://arxiv.org/abs/1706.03762v5"
f"{t}"
"2017-06-12T00:00:00Z"
""
for t in titles
)
body = (
''
''
+ entries
+ ""
)
return body.encode("utf-8")
def _atom_resp(*titles: str) -> MagicMock:
resp = MagicMock()
resp.read.return_value = _atom(*titles)
resp.__enter__ = MagicMock(return_value=resp)
resp.__exit__ = MagicMock(return_value=None)
return resp
# A real R4 false-positive (spec §0.12): same first 4 words, ratio ~0.90, but a
# DISTINCT work — different topic in the tail. Non-exact ⇒ must NOT match.
_CITED = "Federated Learning for Mobile Keyboard Prediction"
_RELATED_WORK = "Federated Learning for Mobile Health Prediction"
class CrossrefExactOrBustTest(unittest.TestCase):
def _search(self, *cand_titles, year=None):
from crossref_client import CrossrefClient
payload = {"message": {"items": [{"title": [t]} for t in cand_titles]}}
with patch("urllib.request.urlopen", return_value=_json_resp(payload)):
return CrossrefClient().title_search(_CITED, year=year)
def test_non_exact_high_ratio_does_not_match(self):
self.assertIsNone(self._search(_RELATED_WORK))
def test_exact_title_matches(self):
result = self._search(_CITED)
self.assertIsNotNone(result)
self.assertEqual(result["title"], [_CITED])
def test_acronym_variant_matches(self):
from crossref_client import CrossrefClient
payload = {"message": {"items": [
{"title": ["RAG: Retrieval Augmented Generation"]}
]}}
with patch("urllib.request.urlopen", return_value=_json_resp(payload)):
result = CrossrefClient().title_search(
"R.A.G.: Retrieval Augmented Generation"
)
self.assertIsNotNone(result) # dotted-acronym normalizes byte-equal
def test_f3_ordering_reaches_exact_second_candidate(self):
# #1 is the high-ratio related work (non-exact), #2 is the exact work.
# Pre-#431 returned #1 and dropped the real work to unresolvable.
result = self._search(_RELATED_WORK, _CITED)
self.assertIsNotNone(result)
self.assertEqual(result["title"], [_CITED])
def test_generic_exact_title_no_id_does_not_match(self):
# §0.12.2: a bare generic title is never promoted on the title path
# (no ID can corroborate here). Even an exact 'Editorial'/'Editorial'
# returns no match.
from crossref_client import CrossrefClient
payload = {"message": {"items": [{"title": ["Editorial"]}]}}
with patch("urllib.request.urlopen", return_value=_json_resp(payload)):
result = CrossrefClient().title_search("Editorial")
self.assertIsNone(result)
class OpenAlexExactOrBustTest(unittest.TestCase):
def _search(self, *cand_titles, year=None):
from openalex_client import OpenAlexClient
payload = {"results": [{"title": t} for t in cand_titles]}
with patch("urllib.request.urlopen", return_value=_json_resp(payload)):
return OpenAlexClient().title_search(_CITED, year=year)
def test_non_exact_high_ratio_does_not_match(self):
self.assertIsNone(self._search(_RELATED_WORK))
def test_exact_title_matches(self):
result = self._search(_CITED)
self.assertIsNotNone(result)
self.assertEqual(result["title"], _CITED)
def test_generic_exact_title_no_id_does_not_match(self):
from openalex_client import OpenAlexClient
payload = {"results": [{"title": "Editorial"}]}
with patch("urllib.request.urlopen", return_value=_json_resp(payload)):
self.assertIsNone(OpenAlexClient().title_search("Editorial"))
class ArxivExactOrBustTest(unittest.TestCase):
def _search(self, *cand_titles, year=None):
from arxiv_client import ArxivClient
with patch("urllib.request.urlopen", return_value=_atom_resp(*cand_titles)):
return ArxivClient().title_search(_CITED, year=year)
def test_non_exact_high_ratio_does_not_match(self):
self.assertIsNone(self._search(_RELATED_WORK))
def test_exact_title_matches(self):
result = self._search(_CITED)
self.assertIsNotNone(result)
self.assertEqual(result["title"], _CITED)
def test_generic_exact_title_no_id_does_not_match(self):
from arxiv_client import ArxivClient
with patch("urllib.request.urlopen", return_value=_atom_resp("Editorial")):
self.assertIsNone(ArxivClient().title_search("Editorial"))
class SemanticScholarExactOrBustTest(unittest.TestCase):
def _search(self, *cand_titles, year=None):
import semantic_scholar_client as ssc
payload = {"data": [{"title": t, "paperId": f"p{i}"}
for i, t in enumerate(cand_titles)]}
resp = _json_resp(payload)
with patch("urllib.request.urlopen", return_value=resp):
client = ssc.SemanticScholarClient(sleep=MagicMock())
return client._lookup_by_title(_CITED, year)
def test_non_exact_high_ratio_does_not_match(self):
self.assertIs(self._search(_RELATED_WORK)["matched"], False)
def test_exact_title_matches(self):
self.assertIs(self._search(_CITED)["matched"], True)
def test_f3_ordering_reaches_exact_second_candidate(self):
result = self._search(_RELATED_WORK, _CITED)
self.assertIs(result["matched"], True)
def test_generic_exact_title_no_id_does_not_match(self):
self.assertIs(self._search("Editorial")["matched"], False)
class ExactNormalizedTitleHelperTest(unittest.TestCase):
"""Direct pins on the exact_normalized_title helper (#431 §0.12.1). The
acronym pre-pass must only ADD matches over the base normalization, never
drop one the base form already had — codex round-6 P2 caught that an
acronym-only equality check regressed a punctuation-only variant where
exactly one side is a contiguous initialism."""
def test_base_equal_punctuation_variant_matches(self):
from _text_similarity import exact_normalized_title
# base form makes these byte-equal ('d h lawrence'), but the acronym
# form does not ('dh' vs 'd h') — must still be exact via base equality.
self.assertTrue(exact_normalized_title("D.H. Lawrence", "D. H. Lawrence"))
self.assertTrue(exact_normalized_title("U.S. policy", "U. S. policy"))
self.assertTrue(
exact_normalized_title("U.S.A. policy", "U. S. A. policy")
)
def test_acronym_only_equal_variant_matches(self):
from _text_similarity import exact_normalized_title
# base form differs ('r a g x' vs 'rag x'); acronym collapse makes them
# equal — the original #431 acronym carve-out.
self.assertTrue(exact_normalized_title("R.A.G.: x", "RAG: x"))
def test_distinct_initialisms_do_not_match(self):
from _text_similarity import exact_normalized_title
# different initials are a different work — neither form equal.
self.assertFalse(
exact_normalized_title("D.H. Lawrence", "D.K. Lawrence")
)
class ResolverVerdictNarrowingTest(unittest.TestCase):
"""End-to-end (spec §0.5 / §0.12.1): a non-exact title fallback on a
title-only entry (no resolvable ID) must reduce to `unresolvable`, NEVER
`false`. `false` is reserved for an ID-keyed miss (C-V6(a))."""
def test_title_only_non_exact_reduces_to_unresolvable(self):
import contamination_signals as cs
from citation_verification_summary import reduce_lookup_verified
# No DOI ⇒ queried_by='title'. Client title_search returns None (the v5
# non-exact verdict). The resolver flow reports unmatched + title-keyed.
client = MagicMock()
client.title_search.return_value = None
entry = {"citation_key": "x", "title": _CITED} # no doi
unmatched, matched_by, queried_by = cs._resolve_doi_then_title(entry, client)
self.assertIs(unmatched, True)
self.assertIsNone(matched_by)
self.assertEqual(queried_by, "title")
# The reducer maps a title-keyed unmatched to `unresolvable`, not false.
verdict = reduce_lookup_verified({
"crossref": {"status": "unmatched", "queried_by": "title"},
})
self.assertEqual(verdict, "unresolvable")
if __name__ == "__main__":
unittest.main()