新增 scripts/http_utils.py 统一封装 HTTP 请求的限流重试(429/5xx 指数退避, 优先遵守服务端 Retry-After),search_arxiv/crossref/semantic_scholar.py 和 verify_citation.py 都已接入,不再各自裸调 urllib。 新增 scripts/search_openalex.py 作为第四个检索源:免费、无需 API key,覆盖面 比单独的 Crossref 更广,还能拿到开放获取PDF直链;已接入 literature_search.py 的主检索流程。verify_citation.py 的跨源标题核查同步改为同时查 Semantic Scholar 和 OpenAlex 两个独立源、任一命中相似度达标即通过,不再单点依赖 S2—— 这是针对"S2 被限流导致整批候选退化成 unverified"这个实际发生过的问题的直接 修复,已用真实网络请求验证:复测中 S2 确实当场返回了 429,靠 OpenAlex 兜底 最终判定仍然是 verified。 顺带修了 archive_references.py 的 slugify(),之前中文主题名会被正则全部 过滤掉、退化成通用的 "references",导致不同中文主题的归档目录互相冲突。 scripts/tests/ 下补了 37 个 unittest(全部 mock 网络请求,不发真实请求), 覆盖 verify_citation 的三档判定和双源核查合并逻辑、archive_references 的 bib 解析边界情况(嵌套花括号、中文主题名)、literature_search 的候选去重 合并与 BibTeX 生成、http_utils 的重试逻辑。用标准库 unittest 而不是 pytest, 和这些脚本本身不引入第三方依赖的原则保持一致。 CLAUDE.md 的下一步计划里,这一项已标记为完成。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.7 KiB
Python
68 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit tests for literature_search.py's candidate merging and BibTeX generation.
|
|
|
|
Run: python -m unittest discover -s .claude/skills/literature-search-verify/scripts
|
|
"""
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
import literature_search as ls
|
|
|
|
|
|
class TestMergeCandidates(unittest.TestCase):
|
|
def test_dedupes_same_paper_across_sources_and_fills_missing_fields(self):
|
|
items = [
|
|
{"source": "arxiv", "title": "Deep Learning for Aeromagnetic Compensation",
|
|
"arxiv_id": "2401.00001", "doi": None, "year": "2024"},
|
|
{"source": "crossref", "title": "Deep Learning for Aeromagnetic Compensation",
|
|
"arxiv_id": None, "doi": "10.1/x", "year": 2024, "venue": "Some Journal"},
|
|
]
|
|
merged = ls.merge_candidates(items)
|
|
self.assertEqual(len(merged), 1)
|
|
self.assertEqual(merged[0]["arxiv_id"], "2401.00001")
|
|
self.assertEqual(merged[0]["doi"], "10.1/x")
|
|
self.assertEqual(sorted(merged[0]["sources"]), ["arxiv", "crossref"])
|
|
|
|
def test_distinct_titles_are_not_merged(self):
|
|
items = [
|
|
{"source": "arxiv", "title": "Paper About Cats"},
|
|
{"source": "crossref", "title": "Paper About Dogs"},
|
|
]
|
|
self.assertEqual(len(ls.merge_candidates(items)), 2)
|
|
|
|
|
|
class TestBibtexKey(unittest.TestCase):
|
|
def test_collision_gets_letter_suffix(self):
|
|
used = set()
|
|
key1 = ls.make_bibtex_key({"authors": ["Jane Smith"], "year": "2020"}, used)
|
|
key2 = ls.make_bibtex_key({"authors": ["John Smith"], "year": "2020"}, used)
|
|
self.assertEqual(key1, "smith2020")
|
|
self.assertEqual(key2, "smith2020a")
|
|
|
|
def test_no_authors_falls_back_to_unknown(self):
|
|
key = ls.make_bibtex_key({"authors": [], "year": "2020"}, set())
|
|
self.assertEqual(key, "unknown2020")
|
|
|
|
|
|
class TestToBibtex(unittest.TestCase):
|
|
def test_arxiv_only_candidate_uses_misc_with_eprint(self):
|
|
cand = {"authors": ["A B"], "title": "T", "year": "2024",
|
|
"arxiv_id": "2401.00001", "venue": None, "doi": None}
|
|
bib = ls.to_bibtex(cand, "b2024")
|
|
self.assertTrue(bib.startswith("@misc{b2024,"))
|
|
self.assertIn("eprint = {2401.00001}", bib)
|
|
|
|
def test_candidate_with_venue_uses_article_with_doi(self):
|
|
cand = {"authors": ["A B"], "title": "T", "year": "2024",
|
|
"arxiv_id": None, "venue": "Some Journal", "doi": "10.1/x"}
|
|
bib = ls.to_bibtex(cand, "b2024")
|
|
self.assertTrue(bib.startswith("@article{b2024,"))
|
|
self.assertIn("doi = {10.1/x}", bib)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|