项目文件夹

文件
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 行
7.9 KiB
Python

"""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 1–10 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
)