import json import tempfile import unittest from pathlib import Path from core.skills import SkillRegistry from skills.literature.scripts.merge_publications import ( exact_key, main, merge_publications, normalize_doi, normalize_isbn, normalize_record, ) ROOT = Path(__file__).resolve().parents[1] class LiteratureSkillTests(unittest.TestCase): def test_literature_is_the_only_publication_search_skill(self) -> None: registry = SkillRegistry(ROOT / "skills") self.assertIn("literature", registry.skills) self.assertNotIn("research", registry.skills) self.assertNotIn("documents", registry.skills) class LiteratureNormalizationTests(unittest.TestCase): def test_normalizes_doi_and_isbn(self) -> None: self.assertEqual(normalize_doi("https://doi.org/10.1000/ABC.1."), "10.1000/abc.1") self.assertEqual(normalize_isbn("0-306-40615-2"), "9780306406157") def test_preserves_unknown_raw_type(self) -> None: record = normalize_record({"title": "A", "type": "future-object"}, "future") self.assertEqual(record["type"], "other") self.assertEqual(record["raw_type"], "future-object") def test_different_book_editions_have_different_keys(self) -> None: first = normalize_record( {"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "1st"}, "books", ) second = normalize_record( {"title": "Materials", "type": "book", "isbn": "9780306406157", "edition": "2nd"}, "books", ) self.assertNotEqual(exact_key(first), exact_key(second)) def test_preserves_date_and_relations(self) -> None: record = normalize_record( { "title": "Chapter", "type": "book-chapter", "publication_date": "2025-03-09", "is_part_of": "Book A", }, "books", ) self.assertEqual(record["issued"], {"year": 2025, "month": 3, "day": 9}) self.assertEqual(record["relations"]["is_part_of"], "Book A") class LiteratureMergeTests(unittest.TestCase): def test_merges_same_doi_and_preserves_sources(self) -> None: result = merge_publications([ ("paper_server", {"id": "p1", "doi": "10.1/ABC", "title": "Title", "abstract": "A"}), ("materials_library", {"id": "m1", "doi": "https://doi.org/10.1/abc", "title": "Title"}), ]) self.assertEqual(len(result["publications"]), 1) self.assertEqual( {source["backend"] for source in result["publications"][0]["sources"]}, {"paper_server", "materials_library"}, ) def test_does_not_merge_chapter_with_book(self) -> None: result = merge_publications([ ("source", {"title": "Hydration", "type": "book", "first_author": "Li", "year": 2024}), ("source", {"title": "Hydration", "type": "book_chapter", "first_author": "Li", "year": 2024}), ]) self.assertEqual(len(result["publications"]), 2) def test_keeps_standard_revisions_and_patent_family_members(self) -> None: result = merge_publications([ ("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2020}), ("standards", {"title": "Method", "type": "standard", "standard_number": "GB/T 1", "publisher": "SAC", "year": 2025}), ("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "CN100A", "patent_family": "F1"}), ("patents", {"title": "Binder", "type": "patent", "patent_publication_number": "US100B", "patent_family": "F1"}), ]) self.assertEqual(len(result["publications"]), 4) def test_marks_similar_titles_as_possible_duplicates(self) -> None: result = merge_publications([ ("a", {"title": "Hydration of low carbon cement systems", "type": "article", "year": 2024}), ("b", {"title": "Hydration of low-carbon cement system", "type": "article", "year": 2024}), ]) self.assertEqual(len(result["publications"]), 2) self.assertEqual(len(result["possible_duplicates"]), 1) def test_cli_writes_output(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) source = root / "source.json" output = root / "output.json" source.write_text(json.dumps([{"title": "A", "doi": "10.1/a"}]), encoding="utf-8") self.assertEqual(main(["--input", f"paper_server={source}", "--output", str(output)]), 0) self.assertEqual(len(json.loads(output.read_text(encoding="utf-8"))["publications"]), 1) if __name__ == "__main__": unittest.main()