Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

491 lines
17 KiB
Python

"""
Tests for RISExporter._create_ris_entry() edge cases.
Existing tests cover: basic ID, year extraction, DOI from metadata,
GitHub publisher, and arXiv publisher.
This file adds coverage for the many remaining branches:
- Author parsing (split by 'and', '&', commas)
- DOI extraction from text (not just metadata)
- Publisher detection for Reddit, YouTube, Medium, PyPI, generic domains
- www. prefix stripping
- Title cleaning (DOI, "Published in", "Volume", "Pages" patterns)
- Empty/minimal inputs
- Author match overlapping with title cleaning
"""
import re
from freezegun import freeze_time
from local_deep_research.text_optimization.citation_formatter import RISExporter
class TestRISEntryAuthorParsing:
"""Tests for author extraction from 'by Author1, Author2' patterns."""
def _make(self):
return RISExporter()
def test_single_author(self):
"""Extracts a single author."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Some Title\nWritten by Jane Doe."
)
assert "AU - Jane Doe" in result
def test_authors_separated_by_and(self):
"""Authors separated by 'and' are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith and Bob Jones."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
def test_authors_separated_by_ampersand(self):
"""Authors separated by '&' are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith & Bob Jones."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
def test_authors_separated_by_comma(self):
"""Authors separated by commas are split correctly."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice Smith, Bob Jones, Carol White."
)
assert "AU - Alice Smith" in result
assert "AU - Bob Jones" in result
assert "AU - Carol White" in result
def test_authors_mixed_separators(self):
"""Authors with mixed 'and' and comma separators."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nby Alice, Bob and Carol."
)
assert "AU - Alice" in result
assert "AU - Bob" in result
assert "AU - Carol" in result
def test_no_authors(self):
"""No author line produces no AU entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no authors")
assert "AU -" not in result
def test_by_keyword_case_insensitive(self):
"""The 'by' keyword match is case-insensitive."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title\nBY John Smith.")
assert "AU - John Smith" in result
def test_empty_author_parts_filtered(self):
"""Empty parts from splitting are filtered out."""
exporter = self._make()
# Trailing comma leaves an empty part
result = exporter._create_ris_entry("1", "Title\nby Alice, , Bob.")
lines = [
line for line in result.split("\n") if line.startswith("AU -")
]
# Should only have real authors, not empty strings
for line in lines:
assert line.strip() != "AU -"
class TestRISEntryDOIExtraction:
"""Tests for DOI extraction from metadata and text."""
def _make(self):
return RISExporter()
def test_doi_from_metadata_takes_precedence(self):
"""DOI from metadata dict is used over DOI in text."""
exporter = self._make()
result = exporter._create_ris_entry(
"1",
"Title DOI: 10.9999/text-doi",
metadata={"doi": "10.1234/meta-doi"},
)
assert "DO - 10.1234/meta-doi" in result
assert (
"10.9999/text-doi" not in result.split("DO - ")[1].split("\n")[0]
)
def test_doi_from_text(self):
"""DOI extracted from text when not in metadata."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\nDOI: 10.1234/test.5678"
)
assert "DO - 10.1234/test.5678" in result
def test_doi_from_text_case_insensitive(self):
"""DOI extraction from text is case-insensitive."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title\ndoi: 10.1234/lowercase"
)
assert "DO - 10.1234/lowercase" in result
def test_no_doi(self):
"""No DOI produces no DO entry."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no DOI")
assert "DO -" not in result
def test_metadata_none_falls_back_to_text(self):
"""When metadata is None, DOI is extracted from text."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title\nDOI: 10.5555/fallback")
assert "DO - 10.5555/fallback" in result
def test_metadata_without_doi_key(self):
"""Metadata dict without 'doi' key falls back to text."""
exporter = self._make()
result = exporter._create_ris_entry(
"1",
"Title\nDOI: 10.7777/textdoi",
metadata={"other": "value"},
)
assert "DO - 10.7777/textdoi" in result
class TestRISEntryPublisherDetection:
"""Tests for publisher extraction from URL domains."""
def _make(self):
return RISExporter()
def test_reddit_publisher(self):
"""Reddit URL produces Reddit publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.reddit.com/r/python/post"
)
assert "PB - Reddit" in result
def test_reddit_subdomain(self):
"""Reddit subdomain also detected."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://old.reddit.com/r/science"
)
assert "PB - Reddit" in result
def test_youtube_publisher(self):
"""YouTube URL produces YouTube publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.youtube.com/watch?v=abc123"
)
assert "PB - YouTube" in result
def test_youtube_mobile(self):
"""Mobile YouTube URL produces YouTube publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://m.youtube.com/watch?v=abc123"
)
assert "PB - YouTube" in result
def test_medium_publisher(self):
"""Medium URL produces Medium publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://medium.com/@author/article"
)
assert "PB - Medium" in result
def test_medium_subdomain(self):
"""Medium subdomain (custom publication) detected."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://blog.medium.com/post"
)
assert "PB - Medium" in result
def test_pypi_publisher(self):
"""PyPI URL produces PyPI publisher."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://pypi.org/project/requests/"
)
assert "PB - Python Package Index (PyPI)" in result
def test_generic_domain_as_publisher(self):
"""Unknown domain used as publisher name."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://example.com/article"
)
assert "PB - example.com" in result
def test_www_stripped_from_domain(self):
"""www. prefix is stripped from domain."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://www.example.org/page"
)
assert "PB - example.org" in result
def test_no_url_no_publisher(self):
"""No URL produces no PB or UR entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "PB -" not in result
assert "UR -" not in result
def test_empty_url_no_publisher(self):
"""Empty string URL produces no PB or UR entries."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title", url="")
assert "PB -" not in result
assert "UR -" not in result
def test_github_subdomain(self):
"""GitHub subdomain detected as GitHub."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://pages.github.com/project"
)
assert "PB - GitHub" in result
def test_arxiv_subdomain(self):
"""arXiv subdomain detected as arXiv."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://export.arxiv.org/abs/1234"
)
assert "PB - arXiv" in result
class TestRISEntryTitleCleaning:
"""Tests for title cleaning patterns."""
def _make(self):
return RISExporter()
def test_doi_removed_from_title(self):
"""DOI text is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper DOI: 10.1234/test\nMore content"
)
# The title line should not contain DOI
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "DOI:" not in ti_line
def test_published_in_removed_from_title(self):
"""'Published in ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Published in Nature 2023\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Published in" not in ti_line
def test_volume_removed_from_title(self):
"""'Volume ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Volume 42 Issue 3\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Volume" not in ti_line
def test_pages_removed_from_title(self):
"""'Pages ...' is removed from the title."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "My Paper Pages 100-120\nContent"
)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "Pages" not in ti_line
def test_fallback_to_original_title_when_clean_empty(self):
"""If cleaning empties the title, original title is used."""
exporter = self._make()
# Title that's entirely "by Author" where author match starts at 0
result = exporter._create_ris_entry("1", "by John Smith\nContent")
# Should still have a TI entry (either cleaned or fallback)
ti_line = [
line for line in result.split("\n") if line.startswith("TI -")
][0]
assert "TI - " in ti_line
class TestRISEntryStructure:
"""Tests for overall RIS entry structure."""
def _make(self):
return RISExporter()
def test_contains_ty_elec(self):
"""Entry contains TY - ELEC type marker."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "TY - ELEC" in result
def test_ends_with_er(self):
"""Entry ends with ER marker."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
lines = result.split("\n")
assert lines[-1] == "ER - "
def test_contains_language(self):
"""Entry contains language tag."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "LA - en" in result
@freeze_time("2025-06-15")
def test_contains_access_year(self):
"""Entry contains current access year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "Y1 - 2025" in result
@freeze_time("2025-06-15")
def test_contains_access_date(self):
"""Entry contains formatted access date."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title")
assert "DA - 2025/06/15" in result
def test_ref_id_format(self):
"""Reference ID formatted as 'refN'."""
exporter = self._make()
result = exporter._create_ris_entry("42", "Title")
assert "ID - ref42" in result
class TestRISEntryYearExtraction:
"""Tests for year extraction from text."""
def _make(self):
return RISExporter()
def test_year_1990s(self):
"""Extracts 1990s year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Published in 1995")
assert "PY - 1995" in result
def test_year_2000s(self):
"""Extracts 2000s year."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Released 2003")
assert "PY - 2003" in result
def test_no_year(self):
"""No year in text produces no PY entry."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Title with no date")
assert "PY -" not in result
def test_first_year_extracted(self):
"""First valid year in text is extracted."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Written in 2020, revised in 2023"
)
assert "PY - 2020" in result
def test_year_boundary_1900(self):
"""Year 1900 is valid (19xx pattern)."""
exporter = self._make()
result = exporter._create_ris_entry("1", "Historical text from 1900")
assert "PY - 1900" in result
def test_non_year_four_digit_ignored(self):
"""Four-digit numbers outside 19xx-20xx range are not matched."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Article number 3456 in series"
)
assert "PY -" not in result
class TestRISEntryURLHandling:
"""Tests for URL handling in RIS entries."""
def _make(self):
return RISExporter()
def test_url_included(self):
"""URL is included as UR entry."""
exporter = self._make()
result = exporter._create_ris_entry(
"1", "Title", url="https://example.com"
)
assert "UR - https://example.com" in result
def test_url_with_path(self):
"""Full URL with path is preserved."""
exporter = self._make()
url = "https://example.com/path/to/article?id=123"
result = exporter._create_ris_entry("1", "Title", url=url)
assert f"UR - {url}" in result
class TestRISEntryStructuralValidity:
"""Regression guard for the accumulator-clobber bug.
``_create_ris_entry`` initialized ``lines = []`` as the RIS-output
accumulator, then immediately reassigned ``lines = full_text.split("\\n")``
to read the title — overwriting the accumulator with the raw source body.
Every RIS field was then appended *after* those source lines, so each entry
emitted the source text before the mandatory leading ``TY - `` tag and
reference managers (Zotero/Mendeley/EndNote) rejected the record. The
pre-existing tests only used ``"<tag>" in result`` substring checks, so they
never caught the leaked prefix.
"""
# RIS records are newline-separated ``XX - value`` lines where the tag is
# two uppercase-or-digit chars (TY, ID, TI, AU, DO, PY, UR, PB, Y1, DA, LA,
# ER), two spaces, a hyphen, then a space.
_RIS_LINE = re.compile(r"^[A-Z][A-Z0-9] - ")
def test_entry_starts_with_ty_tag(self):
"""RIS requires ``TY`` to be the first tag of the record."""
exporter = RISExporter()
source = (
"Understanding Widgets\n"
"URL: https://example.com/widgets\n"
"Collection: Gadgets"
)
result = exporter._create_ris_entry(
"7", source, url="https://example.com/widgets"
)
assert result.startswith("TY - "), result
def test_no_raw_source_lines_leak(self):
"""Every non-blank output line is a RIS tag, never raw source body."""
exporter = RISExporter()
source = (
"My Title\n"
"BODYLEAKMARKER this descriptive line must never be emitted\n"
"Collection: Internal"
)
result = exporter._create_ris_entry("1", source)
assert "BODYLEAKMARKER" not in result
for line in result.split("\n"):
if not line.strip():
continue
assert self._RIS_LINE.match(line), f"non-RIS line leaked: {line!r}"