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

211 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit tests for the pure-function scoring helpers in
``journal_quality.scoring``.
These cover the rules baked into ``derive_quality_score`` and
``institution_score_from_h_index``. They live in their own file so the
scoring rubric is documented and locked in independently of the DB build
pipeline.
"""
import pytest
from local_deep_research.journal_quality.scoring import (
derive_quality_score,
institution_score_from_h_index,
normalize_name,
)
class TestDeriveQualityScore:
"""The 110 quality score rubric."""
def test_predatory_not_in_doaj_returns_one(self):
# Auto-remove threshold — caller treats score 1 as "drop this".
assert derive_quality_score(is_predatory=True, is_in_doaj=False) == 1
def test_predatory_in_doaj_falls_through(self):
# DOAJ membership rescues the journal — predatory flag is ignored
# because the whitelist override at build time clears it.
# `derive_quality_score` itself only applies the auto-remove rule
# when NOT in DOAJ; otherwise it uses the normal scoring.
assert derive_quality_score(is_predatory=True, is_in_doaj=True) == 5
@pytest.mark.parametrize(
"h_index,expected",
[
(151, 10), # Elite (Nature/Science/NEJM)
(76, 8),
(41, 7),
(21, 6),
(11, 5),
(1, 4),
(10, 4), # 10 is NOT > 10 → falls into the "default" branch
],
)
def test_h_index_thresholds(self, h_index, expected):
assert derive_quality_score(h_index=h_index) == expected
def test_doaj_listed_returns_five(self):
# There used to be a DOAJ Seal tier (score 8) above this; DOAJ
# retired the Seal in April 2025 so listing is the only DOAJ
# signal now.
assert derive_quality_score(is_in_doaj=True) == 5
def test_h_index_takes_precedence_over_doaj(self):
# If we have an h-index, it wins over DOAJ — h-index is the
# canonical bibliometric and DOAJ is the OA fallback.
assert (
derive_quality_score(h_index=80, is_in_doaj=True)
== 8 # h_index branch returns 8 (> 75)
)
def test_conference_default(self):
assert derive_quality_score(source_type="conference") == 5
def test_repository_capped_at_acceptable(self):
"""Preprint repositories (arXiv, bioRxiv, ...) are not peer-
reviewed — their h-index reflects aggregate paper citations,
not venue rigor. arXiv has h=674 + Q1 from OpenAlex but must
NOT rank as ELITE/Q1 in our scoring.
"""
# arXiv's real OpenAlex stats — should NOT produce 10.
assert (
derive_quality_score(
h_index=674,
quartile="Q1",
source_type="repository",
)
== 5
)
# Predatory check still fires first for repositories flagged
# predatory (unlikely but defensive).
assert (
derive_quality_score(
is_predatory=True,
source_type="repository",
)
== 1
)
def test_returns_none_when_no_signal(self):
# No h-index, not in DOAJ, not a conference → caller should
# treat as "unknown" and fall through to the next tier.
assert derive_quality_score() is None
assert derive_quality_score(h_index=0) is None
def test_h_index_zero_treated_as_no_signal(self):
# h-index=0 means "newly indexed, not meaningful" — should NOT
# trigger the h-index branch.
assert (
derive_quality_score(h_index=0, is_in_doaj=True)
== 5 # falls through to DOAJ-listed scoring
)
class TestInstitutionScoreFromHIndex:
"""Single source of truth for institution h-index → score mapping.
`db.score_from_affiliations` delegates here, so this also covers the
consolidation of the previously-duplicated scoring path in db.py.
"""
@pytest.mark.parametrize(
"h_index,expected",
[
(None, None),
(0, 4), # below threshold → default
(49, 4),
(50, 4), # NOT > 50 → default
(51, 5),
(100, 5), # the historically duplicated branch — confirmed merged
(250, 5), # NOT > 250 → still 5
(251, 6), # top tier
(1000, 6), # cap at 6 — institution alone never beats venue
],
)
def test_thresholds(self, h_index, expected):
assert institution_score_from_h_index(h_index) == expected
class TestNormalizeName:
"""Helper used by the build pipeline + the runtime accessor."""
def test_lowercases(self):
assert normalize_name("Nature") == "nature"
def test_strips_whitespace(self):
assert normalize_name(" Nature ") == "nature"
def test_nfkc_normalization(self):
# Half-width and full-width characters should compare equal.
assert normalize_name("NATURE") == normalize_name("NATURE")
class TestDeriveQualityScoreQuartile:
"""Tests for the new ``quartile`` parameter on ``derive_quality_score``.
Quartile is the canonical librarian-facing journal quality signal.
These tests document the rubric: Q1 → strong (or elite if h_index
also tops the elite threshold), Q2 → very_good, Q3 → good, Q4 →
acceptable. Predatory always wins. Quartile takes precedence over
DOAJ but defers to the predatory check.
"""
@pytest.mark.parametrize(
"quartile,expected",
[
("Q1", 8), # JOURNAL_QUALITY_STRONG
("Q2", 7), # JOURNAL_QUALITY_VERY_GOOD
("Q3", 6), # JOURNAL_QUALITY_GOOD
("Q4", 5), # JOURNAL_QUALITY_ACCEPTABLE
],
)
def test_quartile_maps_to_score(self, quartile, expected):
assert derive_quality_score(quartile=quartile) == expected
def test_q1_with_elite_h_index_returns_elite(self):
# Q1 + h-index above the elite threshold (150) should bump to 10
# so Nature stays distinguishable from a typical Q1.
assert derive_quality_score(quartile="Q1", h_index=200) == 10
def test_q1_with_low_h_index_stays_strong(self):
# h-index below the elite threshold doesn't downgrade Q1.
assert derive_quality_score(quartile="Q1", h_index=50) == 8
@pytest.mark.parametrize("quartile", ["q1", " Q1 ", "Q1\n", "q2"])
def test_quartile_is_normalized(self, quartile):
# Lowercase / trailing whitespace / odd casing should still match.
# We're verifying `derive_quality_score` does the .upper().strip()
# itself rather than relying on the caller.
result = derive_quality_score(quartile=quartile)
assert result is not None
assert 5 <= result <= 10 # All Q* should map to a valid score
def test_unknown_quartile_falls_through_to_h_index(self):
# Garbage quartile input should NOT silently use the h-index;
# it should fall through to the next branch (h_index→default).
# Tests document the safety: a typo in quartile data doesn't
# corrupt the score.
assert (
derive_quality_score(quartile="ZZ", h_index=200) == 10
) # Falls through to h_index branch
assert (
derive_quality_score(quartile="ZZ") is None
) # No fallback signal at all
def test_quartile_takes_precedence_over_doaj(self):
# When both quartile and DOAJ are present, quartile wins (it's
# a stronger signal than just being in DOAJ).
# Q4 (5) is the same value as DOAJ_QUALITY_LISTED (5), so use
# Q3 (6) which is distinct from the DOAJ-listed score.
assert derive_quality_score(quartile="Q3", is_in_doaj=True) == 6
def test_predatory_overrides_quartile(self):
# Even a Q1 journal gets dropped if predatory and not in DOAJ.
assert (
derive_quality_score(
quartile="Q1", is_predatory=True, is_in_doaj=False
)
== 1
)