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

385 lines
12 KiB
Python

"""Tests for citation_normalizer — engine-specific dict → CSL-JSON metadata."""
from local_deep_research.utilities.citation_normalizer import (
detect_engine,
normalize_citation,
normalize_issn,
_parse_authors_list,
_parse_name,
_extract_doi,
_extract_arxiv_id,
)
class TestNormalizeIssn:
def test_dashed_form(self):
assert normalize_issn("1522-9645") == "15229645"
def test_undashed_form(self):
assert normalize_issn("15229645") == "15229645"
def test_round_trip_equality(self):
assert normalize_issn("1522-9645") == normalize_issn("15229645")
def test_uppercase_x_check_digit(self):
assert normalize_issn("1234-567x") == "1234567X"
def test_already_uppercase_x(self):
assert normalize_issn("1234-567X") == "1234567X"
def test_whitespace_stripped(self):
assert normalize_issn(" 1522-9645 ") == "15229645"
def test_empty_returns_none(self):
assert normalize_issn("") is None
def test_none_returns_none(self):
assert normalize_issn(None) is None
def test_too_short_returns_none(self):
assert normalize_issn("1234") is None
def test_too_long_returns_none(self):
assert normalize_issn("123456789") is None
def test_non_digit_returns_none(self):
assert normalize_issn("abcd-efgh") is None
class TestDetectEngine:
def test_arxiv_url(self):
assert (
detect_engine({"link": "https://arxiv.org/abs/2401.12345"})
== "arxiv"
)
def test_openalex_url(self):
assert (
detect_engine({"link": "https://openalex.org/W12345"}) == "openalex"
)
def test_pubmed_url(self):
assert (
detect_engine({"link": "https://pubmed.ncbi.nlm.nih.gov/12345"})
== "pubmed"
)
def test_explicit_source(self):
assert detect_engine({"source": "arXiv"}) == "arxiv"
def test_web_returns_none(self):
assert detect_engine({"link": "https://example.com/blog"}) is None
def test_empty_returns_none(self):
assert detect_engine({}) is None
class TestParseAuthors:
def test_string_list(self):
result = _parse_authors_list(["John Smith", "Jane Doe"])
assert len(result) == 2
assert result[0]["given"] == "John"
assert result[0]["family"] == "Smith"
def test_comma_string(self):
result = _parse_authors_list("John Smith, Jane Doe")
assert len(result) == 2
def test_dict_with_name(self):
result = _parse_authors_list([{"name": "John Smith"}])
assert result[0]["family"] == "Smith"
def test_already_csl(self):
csl = [{"family": "Smith", "given": "John"}]
result = _parse_authors_list(csl)
assert result == csl
def test_empty(self):
assert _parse_authors_list(None) is None
assert _parse_authors_list([]) is None
assert _parse_authors_list("") is None
def test_normalize_citation_prefers_authors_csl(self):
"""When both authors_csl (structured) and authors (display string)
are present, the structured list wins so 'Last, First' name pairs
survive the round-trip — the bare comma-joined string would split
into 4 singletons instead of 2 author pairs.
"""
from local_deep_research.utilities.citation_normalizer import (
normalize_citation,
)
source = {
"source_engine": "nasa_ads",
"title": "Test",
"authors": "Smith, John, Doe, Jane", # ambiguous joined form
"authors_csl": [
{"family": "Smith", "given": "John"},
{"family": "Doe", "given": "Jane"},
],
"year": 2023,
}
result = normalize_citation(source)
assert result is not None
assert len(result["authors"]) == 2
assert result["authors"][0] == {"family": "Smith", "given": "John"}
assert result["authors"][1] == {"family": "Doe", "given": "Jane"}
class TestParseName:
def test_first_last(self):
assert _parse_name("John Smith") == {
"given": "John",
"family": "Smith",
}
def test_last_comma_first(self):
assert _parse_name("Smith, John") == {
"family": "Smith",
"given": "John",
}
def test_single_name(self):
assert _parse_name("Aristotle") == {"literal": "Aristotle"}
class TestExtractDoi:
def test_bare_doi(self):
assert _extract_doi({"doi": "10.1234/test"}) == "10.1234/test"
def test_url_doi(self):
assert (
_extract_doi({"doi": "https://doi.org/10.1234/test"})
== "10.1234/test"
)
def test_doi_list(self):
assert (
_extract_doi({"doi": ["10.1234/test", "10.5678/other"]})
== "10.1234/test"
)
def test_none(self):
assert _extract_doi({}) is None
def test_external_ids_doi(self):
# Semantic Scholar exposes DOIs through `external_ids`. After
# consolidating `_extract_doi` into citation_normalizer, this is
# the canonical home for DOI extraction.
assert (
_extract_doi({"external_ids": {"DOI": "10.1234/test"}})
== "10.1234/test"
)
def test_externalIds_camelCase_doi(self):
# Some Semantic Scholar payloads use camelCase `externalIds`.
assert (
_extract_doi({"externalIds": {"DOI": "10.1234/test"}})
== "10.1234/test"
)
def test_lowercase_doi_key_in_external_ids(self):
# Tolerant of lowercase variant.
assert (
_extract_doi({"external_ids": {"doi": "10.1234/test"}})
== "10.1234/test"
)
def test_link_with_doi_org(self):
# ArXiv-style result that embeds the DOI in the link URL.
assert (
_extract_doi({"link": "https://doi.org/10.1234/test"})
== "10.1234/test"
)
def test_link_with_dx_doi_org(self):
# The legacy `dx.doi.org` redirect form.
assert (
_extract_doi({"link": "https://dx.doi.org/10.1234/test"})
== "10.1234/test"
)
def test_link_with_http_doi_org(self):
# http (not https) variant.
assert (
_extract_doi({"link": "http://doi.org/10.1234/test"})
== "10.1234/test"
)
def test_doi_field_takes_priority_over_external_ids(self):
# When both are present, the explicit `doi` field wins.
assert (
_extract_doi(
{
"doi": "10.1234/explicit",
"external_ids": {"DOI": "10.5678/from-external"},
}
)
== "10.1234/explicit"
)
def test_link_without_doi_returns_none(self):
# A link that doesn't contain a doi.org URL must NOT be matched
# (CodeQL py/incomplete-url-substring-sanitization guard).
assert (
_extract_doi(
{"link": "https://attacker.example/doi.org/10.1234/test"}
)
is None
)
class TestExtractArxivId:
def test_from_url(self):
assert (
_extract_arxiv_id({"link": "https://arxiv.org/abs/2401.12345"})
== "2401.12345"
)
def test_explicit(self):
assert _extract_arxiv_id({"arxiv_id": "2401.12345"}) == "2401.12345"
def test_old_format_hyphen_archive(self):
assert (
_extract_arxiv_id(
{"link": "https://arxiv.org/abs/cond-mat/0501001"}
)
== "cond-mat/0501001"
)
def test_old_format_dotted_subject_class(self):
assert (
_extract_arxiv_id({"link": "https://arxiv.org/abs/math.AG/0601001"})
== "math.AG/0601001"
)
def test_old_format_hep_th(self):
assert (
_extract_arxiv_id({"link": "http://arxiv.org/abs/hep-th/9802150"})
== "hep-th/9802150"
)
def test_new_format_5digit_sequence(self):
assert (
_extract_arxiv_id({"link": "https://arxiv.org/abs/1501.00001"})
== "1501.00001"
)
def test_new_format_with_version(self):
assert (
_extract_arxiv_id({"link": "https://arxiv.org/abs/2501.12345v2"})
== "2501.12345v2"
)
class TestNormalizeCitation:
def test_arxiv_result(self):
source = {
"title": "Attention Is All You Need",
"link": "https://arxiv.org/abs/1706.03762",
"authors": ["Ashish Vaswani", "Noam Shazeer"],
"published": "2017-06-12",
"journal_ref": None,
"source": "arXiv",
"doi": "10.5555/3295222.3295349",
}
result = normalize_citation(source)
assert result is not None
assert result["source_engine"] == "arxiv"
assert result["arxiv_id"] == "1706.03762"
assert result["doi"] == "10.5555/3295222.3295349"
assert result["year"] == 2017
assert len(result["authors"]) == 2
assert result["item_type"] == "article" # No journal_ref = preprint
assert "csl_json" in result
def test_arxiv_published_result(self):
source = {
"title": "Some Paper",
"link": "https://arxiv.org/abs/2401.12345",
"journal_ref": "Nature 612, 34-41 (2023)",
"source": "arXiv",
}
result = normalize_citation(source)
assert result["item_type"] == "article-journal"
assert result["container_title"] == "Nature 612, 34-41 (2023)"
def test_openalex_result(self):
source = {
"title": "A Study",
"link": "https://doi.org/10.1234/test",
"authors": "John Smith, Jane Doe et al.",
"year": 2023,
"journal": "Nature",
"journal_ref": "Nature",
"openalex_source_id": "S12345",
"source_type": "journal",
"doi": "https://doi.org/10.1234/test",
"source": "openalex",
}
result = normalize_citation(source)
assert result is not None
assert result["source_engine"] == "openalex"
assert result["doi"] == "10.1234/test"
assert result["container_title"] == "Nature"
assert result["year"] == 2023
def test_web_result_returns_none(self):
source = {
"title": "Blog Post",
"link": "https://example.com/blog",
}
assert normalize_citation(source) is None
def test_unknown_placeholder_is_filtered(self):
"""OpenAlex / NASA ADS used to emit ``journal="unknown"`` when no
venue was indexed. The normalizer must strip that literal so it
doesn't become a container_title — there's an actual OpenAlex
source named "unknown" (Q1, h_index=5) that would otherwise get
matched by the reputation filter's name-based lookup.
"""
source = {
"title": "Paper",
"link": "https://doi.org/10.1/x",
"journal": "unknown",
"journal_ref": None,
"source": "openalex",
"doi": "10.1/x",
}
result = normalize_citation(source)
assert result is not None
assert result.get("container_title") is None
def test_unknown_placeholder_case_insensitive(self):
"""Same filter catches "Unknown", "UNKNOWN", " unknown " variants."""
for placeholder in ("Unknown", "UNKNOWN", " unknown "):
source = {
"title": "Paper",
"link": "https://doi.org/10.1/x",
"journal": placeholder,
"source": "openalex",
"doi": "10.1/x",
}
result = normalize_citation(source)
assert result is not None
assert result.get("container_title") is None, (
f"placeholder {placeholder!r} leaked through"
)
def test_csl_json_built(self):
source = {
"title": "Test Paper",
"link": "https://arxiv.org/abs/2401.12345",
"authors": ["John Smith"],
"published": "2024-01-15",
"source": "arXiv",
}
result = normalize_citation(source)
csl = result["csl_json"]
assert csl["title"] == "Test Paper"
assert csl["type"] == "article"
assert csl["author"][0]["family"] == "Smith"
assert csl["issued"]["date-parts"] == [[2024, 1, 15]]
assert csl["URL"] == "https://arxiv.org/abs/2401.12345"