Initial release prep

This commit is contained in:
Justin Visser 2026-02-27 10:46:05 +01:00
parent c9b9d892f4
commit 92395b2b4b
100 changed files with 405 additions and 401 deletions

View file

@ -11,9 +11,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.runtime_deps import get_scholar_source
from app.main import app
from app.services.domains.publications.types import PublicationListItem
from app.services.domains.scholar.source import FetchResult
from app.services.domains.settings import application as user_settings_service
from app.services.publications.types import PublicationListItem
from app.services.scholar.source import FetchResult
from app.services.settings import application as user_settings_service
from app.settings import settings
from tests.integration.helpers import insert_user, login_user
@ -473,7 +473,7 @@ async def test_api_admin_dbops_pdf_queue_requeue_endpoint(
fingerprint=f"{(target_user_id + 73):064x}",
)
monkeypatch.setattr(
"app.services.domains.publications.pdf_queue._schedule_rows",
"app.services.publications.pdf_queue._schedule_rows",
lambda **_kwargs: None,
)
client = TestClient(app)
@ -517,7 +517,7 @@ async def test_api_admin_dbops_pdf_queue_requeue_all_endpoint(
fingerprint=f"{(target_user_id + 83):064x}",
)
monkeypatch.setattr(
"app.services.domains.publications.pdf_queue._schedule_rows",
"app.services.publications.pdf_queue._schedule_rows",
lambda **_kwargs: None,
)
client = TestClient(app)
@ -1290,12 +1290,16 @@ async def test_api_settings_get_and_update(db_session: AsyncSession) -> None:
_assert_safety_state_contract(settings_payload["safety_state"])
assert settings_payload["safety_state"]["cooldown_active"] is False
policy = settings_payload["policy"]
run_interval_minutes = max(45, int(policy["min_run_interval_minutes"]))
request_delay_seconds = max(6, int(policy["min_request_delay_seconds"]))
update_response = client.put(
"/api/v1/settings",
json={
"auto_run_enabled": True,
"run_interval_minutes": 45,
"request_delay_seconds": 6,
"run_interval_minutes": run_interval_minutes,
"request_delay_seconds": request_delay_seconds,
"nav_visible_pages": ["dashboard", "scholars", "publications", "settings", "runs"],
},
headers=headers,
@ -1303,8 +1307,8 @@ async def test_api_settings_get_and_update(db_session: AsyncSession) -> None:
assert update_response.status_code == 200
updated = update_response.json()["data"]
assert updated["auto_run_enabled"] is True
assert updated["run_interval_minutes"] == 45
assert updated["request_delay_seconds"] == 6
assert updated["run_interval_minutes"] == run_interval_minutes
assert updated["request_delay_seconds"] == request_delay_seconds
assert updated["nav_visible_pages"] == [
"dashboard",
"scholars",
@ -2404,7 +2408,7 @@ async def test_api_publications_list_schedules_background_enrichment(
return 1
monkeypatch.setattr(
"app.services.domains.publications.application.schedule_missing_pdf_enrichment_for_user",
"app.services.publications.application.schedule_missing_pdf_enrichment_for_user",
_fake_schedule,
)
@ -2484,7 +2488,7 @@ async def test_api_publication_retry_pdf_queues_resolution_job(
return True
monkeypatch.setattr(
"app.services.domains.publications.application.schedule_retry_pdf_enrichment_for_row",
"app.services.publications.application.schedule_retry_pdf_enrichment_for_row",
_fake_retry_scheduler,
)
@ -2514,7 +2518,7 @@ async def test_api_publication_retry_pdf_queues_resolution_job(
]
monkeypatch.setattr(
"app.services.domains.publications.application.hydrate_pdf_enrichment_state",
"app.services.publications.application.hydrate_pdf_enrichment_state",
_fake_hydrate,
)

View file

@ -4,7 +4,7 @@ import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.domains.dbops import run_publication_link_repair
from app.services.dbops import run_publication_link_repair
from tests.integration.helpers import insert_user

View file

@ -4,7 +4,7 @@ import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.domains.dbops.integrity import collect_integrity_report
from app.services.dbops.integrity import collect_integrity_report
def _check_counts(report: dict) -> dict[str, int]:

View file

@ -8,8 +8,8 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import CrawlRun, Publication, RunStatus, RunTriggerType, ScholarProfile, ScholarPublication
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.domains.openalex.types import OpenAlexWork
from app.services.ingestion.application import ScholarIngestionService
from app.services.openalex.types import OpenAlexWork
from tests.integration.helpers import insert_user
@ -84,9 +84,9 @@ async def test_deferred_enrichment_sweeps_previous_runs(db_session: AsyncSession
# We patch the client at its source, and also mock arXiv to avoid real HTTP calls
with (
patch("app.services.domains.openalex.client.OpenAlexClient") as MockClient,
patch("app.services.openalex.client.OpenAlexClient") as MockClient,
patch(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication", new=AsyncMock(return_value=None)
"app.services.arxiv.application.discover_arxiv_id_for_publication", new=AsyncMock(return_value=None)
),
):
mock_instance = MockClient.return_value

View file

@ -8,8 +8,8 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import RunStatus, RunTriggerType
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.domains.scholar.source import FetchResult
from app.services.ingestion.application import ScholarIngestionService
from app.services.scholar.source import FetchResult
from tests.integration.helpers import insert_user
REGRESSION_FIXTURE_DIR = Path("tests/fixtures/scholar/regression")

View file

@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.api.runtime_deps import get_ingestion_service
from app.db.models import CrawlRun, Publication, RunStatus, RunTriggerType
from app.main import app
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.ingestion.application import ScholarIngestionService
from tests.integration.helpers import insert_user, login_user
@ -214,7 +214,7 @@ async def test_background_enrichment_preserves_canceled_status(
return []
monkeypatch.setattr(
"app.services.domains.openalex.client.OpenAlexClient",
"app.services.openalex.client.OpenAlexClient",
_OpenAlexClientStub,
)
@ -339,7 +339,7 @@ async def test_partial_discovery_exception_keeps_new_pub_count_consistent(
monkeypatch.setattr(service, "_resolve_publication", _resolve_publication_stub)
from app.db.models import ScholarProfile
from app.services.domains.scholar.parser_types import PublicationCandidate
from app.services.scholar.parser_types import PublicationCandidate
scholar = await db_session.get(ScholarProfile, scholar_profile_id)
assert scholar is not None

View file

@ -9,13 +9,13 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import ArxivQueryCacheEntry
from app.services.domains.arxiv.cache import (
from app.services.arxiv.cache import (
build_query_fingerprint,
get_cached_feed,
run_with_inflight_dedupe,
set_cached_feed,
)
from app.services.domains.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
from app.services.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
def _sample_feed(arxiv_id: str = "1234.5678") -> ArxivFeed:

View file

@ -7,10 +7,10 @@ import httpx
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.domains.arxiv import client as arxiv_client_module
from app.services.domains.arxiv.client import ArxivClient
from app.services.domains.arxiv.errors import ArxivClientValidationError, ArxivRateLimitError
from app.services.domains.arxiv.rate_limit import ArxivCooldownStatus
from app.services.arxiv import client as arxiv_client_module
from app.services.arxiv.client import ArxivClient
from app.services.arxiv.errors import ArxivClientValidationError, ArxivRateLimitError
from app.services.arxiv.rate_limit import ArxivCooldownStatus
_CLIENT_FEED_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
@ -160,7 +160,7 @@ async def test_client_logs_cache_hit_and_miss(
def _capture_log(_msg: str, *args, **kwargs) -> None:
logged.append({"event": _msg, **(kwargs.get("extra") or {})})
monkeypatch.setattr("app.services.domains.arxiv.client.logger.info", _capture_log)
monkeypatch.setattr("app.services.arxiv.client.logger.info", _capture_log)
client = ArxivClient(request_fn=_request_fn, cache_enabled=True)
await client.search(query="ti:test-cache")
await client.search(query="ti:test-cache")
@ -196,7 +196,7 @@ async def test_request_feed_skips_live_call_when_global_cooldown_is_active(
monkeypatch.setattr(arxiv_client_module, "get_arxiv_cooldown_status", _cooldown_status)
monkeypatch.setattr(arxiv_client_module, "run_with_global_arxiv_limit", _unexpected_limit_call)
monkeypatch.setattr("app.services.domains.arxiv.client.logger.warning", _capture_warning)
monkeypatch.setattr("app.services.arxiv.client.logger.warning", _capture_warning)
with pytest.raises(ArxivRateLimitError):
await arxiv_client_module._request_arxiv_feed(

View file

@ -4,9 +4,9 @@ from types import SimpleNamespace
import pytest
from app.services.domains.arxiv import application as arxiv_application
from app.services.domains.arxiv import gateway as arxiv_gateway
from app.services.domains.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
from app.services.arxiv import application as arxiv_application
from app.services.arxiv import gateway as arxiv_gateway
from app.services.arxiv.types import ArxivEntry, ArxivFeed, ArxivOpenSearchMeta
from app.settings import settings

View file

@ -2,9 +2,9 @@ from __future__ import annotations
from datetime import UTC, datetime
from app.services.domains.arxiv.guards import arxiv_skip_reason_for_item
from app.services.domains.publication_identifiers.types import DisplayIdentifier
from app.services.domains.publications.types import PublicationListItem
from app.services.arxiv.guards import arxiv_skip_reason_for_item
from app.services.publication_identifiers.types import DisplayIdentifier
from app.services.publications.types import PublicationListItem
def _item(

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import pytest
from app.services.domains.arxiv.errors import ArxivParseError
from app.services.domains.arxiv.parser import parse_arxiv_feed
from app.services.arxiv.errors import ArxivParseError
from app.services.arxiv.parser import parse_arxiv_feed
_VALID_FEED_XML = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"

View file

@ -9,9 +9,9 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import ArxivRuntimeState
from app.services.domains.arxiv.constants import ARXIV_RUNTIME_STATE_KEY
from app.services.domains.arxiv.errors import ArxivRateLimitError
from app.services.domains.arxiv.rate_limit import get_arxiv_cooldown_status, run_with_global_arxiv_limit
from app.services.arxiv.constants import ARXIV_RUNTIME_STATE_KEY
from app.services.arxiv.errors import ArxivRateLimitError
from app.services.arxiv.rate_limit import get_arxiv_cooldown_status, run_with_global_arxiv_limit
from app.settings import settings
@ -98,7 +98,7 @@ async def test_arxiv_rate_limit_logs_request_scheduled_and_completed(
def _capture_log(_msg: str, *args, **kwargs) -> None:
logged.append({"event": _msg, **(kwargs.get("extra") or {})})
monkeypatch.setattr("app.services.domains.arxiv.rate_limit.logger.info", _capture_log)
monkeypatch.setattr("app.services.arxiv.rate_limit.logger.info", _capture_log)
await run_with_global_arxiv_limit(fetch=_fetch, source_path="search")
scheduled = [entry for entry in logged if entry.get("event") == "arxiv.request_scheduled"]
@ -128,7 +128,7 @@ async def test_arxiv_rate_limit_logs_cooldown_activation(
def _capture_warning(_msg: str, *args, **kwargs) -> None:
logged_warning.append({"event": _msg, **(kwargs.get("extra") or {})})
monkeypatch.setattr("app.services.domains.arxiv.rate_limit.logger.warning", _capture_warning)
monkeypatch.setattr("app.services.arxiv.rate_limit.logger.warning", _capture_warning)
with pytest.raises(ArxivRateLimitError):
await run_with_global_arxiv_limit(fetch=_fetch, source_path="lookup_ids")
finally:

View file

@ -1,4 +1,4 @@
from app.services.domains.openalex.types import OpenAlexWork
from app.services.openalex.types import OpenAlexWork
def test_parse_openalex_work_from_api_dict() -> None:

View file

@ -1,5 +1,5 @@
from app.services.domains.openalex.matching import find_best_match
from app.services.domains.openalex.types import OpenAlexWork
from app.services.openalex.matching import find_best_match
from app.services.openalex.types import OpenAlexWork
def test_find_best_match_exact_title():

View file

@ -8,8 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from app.db.models import ScholarPublication
from app.services.domains.publications import dedup as dedup_service
from app.services.domains.publications.dedup import (
from app.services.publications import dedup as dedup_service
from app.services.publications.dedup import (
NearDuplicateCluster,
NearDuplicateMember,
find_identifier_duplicate_pairs,
@ -87,15 +87,15 @@ async def test_merge_duplicate_publication_migrates_links_and_identifiers() -> N
with (
patch(
"app.services.domains.publications.dedup._load_publication",
"app.services.publications.dedup._load_publication",
new=AsyncMock(side_effect=[winner, dup]),
),
patch(
"app.services.domains.publications.dedup._migrate_scholar_links",
"app.services.publications.dedup._migrate_scholar_links",
new=AsyncMock(),
) as mock_links,
patch(
"app.services.domains.publications.dedup._migrate_identifiers",
"app.services.publications.dedup._migrate_identifiers",
new=AsyncMock(),
) as mock_identifiers,
):
@ -112,7 +112,7 @@ async def test_merge_duplicate_publication_rejects_missing_publications() -> Non
with (
patch(
"app.services.domains.publications.dedup._load_publication",
"app.services.publications.dedup._load_publication",
new=AsyncMock(side_effect=[None, None]),
),
pytest.raises(ValueError),
@ -160,7 +160,7 @@ async def test_migrate_scholar_links_drops_conflicts() -> None:
@pytest.mark.asyncio
async def test_sweep_returns_zero_when_no_pairs() -> None:
with patch(
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
"app.services.publications.dedup.find_identifier_duplicate_pairs",
new=AsyncMock(return_value=[]),
):
session = AsyncMock()
@ -174,11 +174,11 @@ async def test_sweep_returns_zero_when_no_pairs() -> None:
async def test_sweep_returns_merge_count() -> None:
with (
patch(
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
"app.services.publications.dedup.find_identifier_duplicate_pairs",
new=AsyncMock(return_value=[(1, 2), (3, 4)]),
),
patch(
"app.services.domains.publications.dedup.merge_duplicate_publication",
"app.services.publications.dedup.merge_duplicate_publication",
new=AsyncMock(),
) as mock_merge,
):
@ -194,11 +194,11 @@ async def test_sweep_returns_merge_count() -> None:
async def test_sweep_merges_each_dup_only_once() -> None:
with (
patch(
"app.services.domains.publications.dedup.find_identifier_duplicate_pairs",
"app.services.publications.dedup.find_identifier_duplicate_pairs",
new=AsyncMock(return_value=[(1, 2), (1, 2)]),
),
patch(
"app.services.domains.publications.dedup.merge_duplicate_publication",
"app.services.publications.dedup.merge_duplicate_publication",
new=AsyncMock(),
) as mock_merge,
):
@ -227,7 +227,7 @@ async def test_find_near_duplicate_clusters_groups_similar_titles() -> None:
assert second is not None
with patch(
"app.services.domains.publications.dedup._load_near_duplicate_candidates",
"app.services.publications.dedup._load_near_duplicate_candidates",
new=AsyncMock(return_value=[first, second]),
):
clusters = await find_near_duplicate_clusters(AsyncMock())
@ -255,7 +255,7 @@ async def test_find_near_duplicate_clusters_skips_unrelated_titles() -> None:
assert second is not None
with patch(
"app.services.domains.publications.dedup._load_near_duplicate_candidates",
"app.services.publications.dedup._load_near_duplicate_candidates",
new=AsyncMock(return_value=[first, second]),
):
clusters = await find_near_duplicate_clusters(AsyncMock())
@ -277,7 +277,7 @@ async def test_merge_near_duplicate_cluster_merges_non_winner_members() -> None:
)
with patch(
"app.services.domains.publications.dedup.merge_duplicate_publication",
"app.services.publications.dedup.merge_duplicate_publication",
new=AsyncMock(),
) as mock_merge:
merged = await merge_near_duplicate_cluster(AsyncMock(), cluster=cluster)

View file

@ -4,7 +4,7 @@ from types import SimpleNamespace
import pytest
from app.services.domains.crossref import application as crossref_app
from app.services.crossref import application as crossref_app
def _item(*, title: str, year: int | None, scholar_label: str = "Shinya Yamanaka"):

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from app.services.domains.doi.normalize import first_doi_from_texts, normalize_doi
from app.services.doi.normalize import first_doi_from_texts, normalize_doi
def test_normalize_doi_extracts_and_lowercases() -> None:

View file

@ -1,12 +1,12 @@
from __future__ import annotations
from app.services.domains.ingestion.fingerprints import (
from app.services.ingestion.fingerprints import (
_dedupe_publication_candidates,
canonical_title_for_dedup,
fuzzy_titles_match,
normalize_title,
)
from app.services.domains.scholar.parser import PublicationCandidate
from app.services.scholar.parser import PublicationCandidate
def _candidate(

View file

@ -4,9 +4,9 @@ from types import SimpleNamespace
import pytest
from app.services.domains.arxiv.errors import ArxivRateLimitError
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.domains.publication_identifiers import application as identifier_service
from app.services.arxiv.errors import ArxivRateLimitError
from app.services.ingestion.application import ScholarIngestionService
from app.services.publication_identifiers import application as identifier_service
@pytest.mark.asyncio

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import pytest
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.domains.ingestion.types import RunProgress, ScholarProcessingOutcome
from app.services.ingestion.application import ScholarIngestionService
from app.services.ingestion.types import RunProgress, ScholarProcessingOutcome
def _outcome(*, scholar_profile_id: int, outcome_label: str) -> ScholarProcessingOutcome:

View file

@ -5,10 +5,10 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import Publication, PublicationIdentifier
from app.services.domains.arxiv import application as arxiv_service
from app.services.domains.publication_identifiers import application as identifier_service
from app.services.domains.publication_identifiers.types import IdentifierKind
from app.services.domains.publications.types import UnreadPublicationItem
from app.services.arxiv import application as arxiv_service
from app.services.publication_identifiers import application as identifier_service
from app.services.publication_identifiers.types import IdentifierKind
from app.services.publications.types import UnreadPublicationItem
def test_derive_display_identifier_prefers_doi_over_arxiv() -> None:
@ -47,7 +47,7 @@ def test_derive_display_identifier_uses_pmcid_when_present() -> None:
def test_normalize_arxiv_id_handles_urls() -> None:
from app.services.domains.publication_identifiers.normalize import normalize_arxiv_id
from app.services.publication_identifiers.normalize import normalize_arxiv_id
assert normalize_arxiv_id("https://arxiv.org/abs/1504.08025") == "1504.08025"
assert normalize_arxiv_id("http://arxiv.org/pdf/1504.08025v2.pdf") == "1504.08025v2"
@ -59,7 +59,7 @@ def test_normalize_arxiv_id_handles_urls() -> None:
def test_normalize_arxiv_id_handles_raw_text() -> None:
from app.services.domains.publication_identifiers.normalize import normalize_arxiv_id
from app.services.publication_identifiers.normalize import normalize_arxiv_id
assert normalize_arxiv_id("1504.08025v1") == "1504.08025v1"
assert normalize_arxiv_id("arXiv:1504.08025") == "1504.08025"
@ -69,7 +69,7 @@ def test_normalize_arxiv_id_handles_raw_text() -> None:
def test_normalize_pmcid_handles_urls_and_text() -> None:
from app.services.domains.publication_identifiers.normalize import normalize_pmcid
from app.services.publication_identifiers.normalize import normalize_pmcid
assert normalize_pmcid("https://pmc.ncbi.nlm.nih.gov/articles/PMC2175868/") == "PMC2175868"
assert normalize_pmcid("http://www.ncbi.nlm.nih.gov/pmc/articles/PMC1234567") == "PMC1234567"
@ -79,7 +79,7 @@ def test_normalize_pmcid_handles_urls_and_text() -> None:
def test_normalize_pmid_handles_urls() -> None:
from app.services.domains.publication_identifiers.normalize import normalize_pmid
from app.services.publication_identifiers.normalize import normalize_pmid
assert normalize_pmid("https://pubmed.ncbi.nlm.nih.gov/12345678/") == "12345678"
assert normalize_pmid("http://pubmed.ncbi.nlm.nih.gov/12345678") == "12345678"
@ -117,11 +117,11 @@ async def test_discover_and_sync_skips_arxiv_when_crossref_finds_doi(
raise AssertionError("arXiv should be skipped after strong DOI evidence.")
monkeypatch.setattr(
"app.services.domains.crossref.application.discover_doi_for_publication",
"app.services.crossref.application.discover_doi_for_publication",
_fake_crossref,
)
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fail_arxiv,
)
@ -158,11 +158,11 @@ async def test_discover_and_sync_skips_arxiv_for_low_quality_title(
return "1234.5678"
monkeypatch.setattr(
"app.services.domains.crossref.application.discover_doi_for_publication",
"app.services.crossref.application.discover_doi_for_publication",
_fake_crossref,
)
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fake_arxiv,
)
@ -190,11 +190,11 @@ async def test_discover_and_sync_calls_arxiv_when_item_is_eligible(
return "2501.00001v2"
monkeypatch.setattr(
"app.services.domains.crossref.application.discover_doi_for_publication",
"app.services.crossref.application.discover_doi_for_publication",
_fake_crossref,
)
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fake_arxiv,
)

View file

@ -6,9 +6,9 @@ from types import SimpleNamespace
import pytest
from app.db.models import PublicationPdfJob
from app.services.domains.publications import pdf_queue
from app.services.domains.publications.pdf_resolution_pipeline import PipelineOutcome
from app.services.domains.unpaywall.application import OaResolutionOutcome
from app.services.publications import pdf_queue
from app.services.publications.pdf_resolution_pipeline import PipelineOutcome
from app.services.unpaywall.application import OaResolutionOutcome
def _job(

View file

@ -5,10 +5,10 @@ from types import SimpleNamespace
import pytest
from app.services.domains.arxiv.errors import ArxivRateLimitError
from app.services.domains.publication_identifiers.types import DisplayIdentifier
from app.services.domains.publications import pdf_resolution_pipeline as pipeline
from app.services.domains.unpaywall.application import OaResolutionOutcome
from app.services.arxiv.errors import ArxivRateLimitError
from app.services.publication_identifiers.types import DisplayIdentifier
from app.services.publications import pdf_resolution_pipeline as pipeline
from app.services.unpaywall.application import OaResolutionOutcome
def _row(*, display_identifier: DisplayIdentifier | None = None) -> SimpleNamespace:
@ -165,7 +165,7 @@ async def test_arxiv_outcome_skips_when_strong_doi_identifier(
raise AssertionError("arXiv lookup should be skipped when DOI evidence is strong.")
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fail_discover,
)
outcome = await pipeline._arxiv_outcome(row, request_email="user@example.com")
@ -183,7 +183,7 @@ async def test_arxiv_outcome_skips_when_title_quality_is_low(
raise AssertionError("arXiv lookup should be skipped for low-quality titles.")
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fail_discover,
)
outcome = await pipeline._arxiv_outcome(row, request_email="user@example.com")
@ -198,7 +198,7 @@ async def test_arxiv_outcome_calls_arxiv_when_eligible(
return "1234.5678"
monkeypatch.setattr(
"app.services.domains.arxiv.application.discover_arxiv_id_for_publication",
"app.services.arxiv.application.discover_arxiv_id_for_publication",
_fake_discover,
)
row = _row()

View file

@ -2,8 +2,8 @@ from __future__ import annotations
from datetime import UTC, datetime
from app.services.domains.ingestion import scheduler as scheduler_module
from app.services.domains.ingestion.application import ScholarIngestionService
from app.services.ingestion import scheduler as scheduler_module
from app.services.ingestion.application import ScholarIngestionService
from app.settings import settings

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
from app.db.models import UserSetting
from app.services.domains.ingestion import safety as run_safety
from app.services.ingestion import safety as run_safety
def test_apply_run_safety_outcome_triggers_blocked_cooldown() -> None:

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from app.services.domains.runs.application import extract_run_summary
from app.services.runs.application import extract_run_summary
def test_extract_run_summary_includes_extended_metrics() -> None:

View file

@ -4,13 +4,13 @@ from pathlib import Path
import pytest
from app.services.domains.scholar.parser import (
from app.services.scholar.parser import (
ParseState,
ScholarDomInvariantError,
parse_author_search_page,
parse_profile_page,
)
from app.services.domains.scholar.source import FetchResult
from app.services.scholar.source import FetchResult
def _fixture(name: str) -> str:

View file

@ -3,9 +3,9 @@ from __future__ import annotations
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.domains.scholar.parser import ParseState
from app.services.domains.scholar.source import FetchResult
from app.services.domains.scholars import application as scholar_service
from app.services.scholar.parser import ParseState
from app.services.scholar.source import FetchResult
from app.services.scholars import application as scholar_service
pytestmark = [pytest.mark.integration, pytest.mark.db]

View file

@ -1,7 +1,7 @@
import pytest
from app.services.domains.scholar import rate_limit as scholar_rate_limit
from app.services.domains.scholar.source import FetchResult, LiveScholarSource, _build_profile_url
from app.services.scholar import rate_limit as scholar_rate_limit
from app.services.scholar.source import FetchResult, LiveScholarSource, _build_profile_url
def test_build_profile_url_includes_pagesize_for_initial_page() -> None:

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from app.services.domains.unpaywall import pdf_discovery
from app.services.unpaywall import pdf_discovery
def test_looks_like_pdf_url_detects_path_and_query_variants() -> None:

View file

@ -5,9 +5,9 @@ from datetime import UTC, datetime
import pytest
from app.services.domains.publication_identifiers.types import DisplayIdentifier
from app.services.domains.publications.types import PublicationListItem
from app.services.domains.unpaywall import application as unpaywall_app
from app.services.publication_identifiers.types import DisplayIdentifier
from app.services.publications.types import PublicationListItem
from app.services.unpaywall import application as unpaywall_app
class _DummyAsyncClient:

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from app.services.domains.settings.application import (
from app.services.settings.application import (
DEFAULT_NAV_VISIBLE_PAGES,
HARD_MIN_REQUEST_DELAY_SECONDS,
HARD_MIN_RUN_INTERVAL_MINUTES,