"""Полнота kit-registry vs боевой scheduler-dispatch (#2192). Гарантирует ship-dark инвариант: КАЖДЫЙ source, который обрабатывает боевой `app.services.scheduler.scheduler_loop` (if/elif + startswith), резолвится в kit-registry (`build_registry(product_handlers=build_product_handlers(ctx))`). Тест ПАДАЕТ, если source добавили в боевой dispatch, но забыли в registry — иначе после переключения USE_KIT_SCHEDULER=true такой source молча не запустится. Источник истины — сам файл боевого scheduler'а: парсим `source == "X"` (exact) и `source.startswith("X")` (wildcard), чтобы при дрейфе dispatch тест ловил рассинхрон автоматически, а не по захардкоженному списку. """ from __future__ import annotations import os import re from pathlib import Path os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from scraper_kit.orchestration.scheduler import ( SchedulerContext, build_registry, resolve_handler, ) import app.services.scheduler as _sched_mod from app.services.product_handlers import build_product_handlers from app.services.scraper_adapters import ( RealEnrichmentJobs, RealMatcherAdapter, RealScraperConfig, RealSessionFactory, ) def _production_sources() -> tuple[set[str], set[str]]: """(exact, prefixes) source'ов из боевого scheduler_loop dispatch.""" src = Path(_sched_mod.__file__).read_text(encoding="utf-8") exact = set(re.findall(r'source == "([^"]+)"', src)) prefixes = set(re.findall(r'source\.startswith\("([^"]+)"\)', src)) return exact, prefixes def _build_registry() -> dict: ctx = SchedulerContext( config=RealScraperConfig(), matcher=RealMatcherAdapter(), enrichment=RealEnrichmentJobs(), session_factory=RealSessionFactory(), ) return build_registry(product_handlers=build_product_handlers(ctx)) def test_production_dispatch_parse_sane() -> None: """Sanity: парсер видит непустой набор source'ов (защита от regex-дрейфа).""" exact, prefixes = _production_sources() # На момент #2192 боевой dispatch держит 25 exact + 1 wildcard-семейство. assert len(exact) >= 25, f"parse regressed — got {len(exact)} exact sources" assert "deactivate_stale_" in prefixes def test_every_production_source_resolves_in_kit_registry() -> None: """Каждый боевой exact-source резолвится в kit-registry.""" registry = _build_registry() exact, _ = _production_sources() missing = [s for s in sorted(exact) if resolve_handler(s, registry) is None] assert not missing, f"sources missing from kit registry: {missing}" def test_wildcard_family_resolves() -> None: """Каждый deactivate_stale_* член семейства резолвится через wildcard-Handler.""" registry = _build_registry() _, prefixes = _production_sources() for prefix in sorted(prefixes): for member in ("avito", "yandex", "cian"): source = f"{prefix}{member}" assert ( resolve_handler(source, registry) is not None ), f"wildcard source {source} did not resolve" def test_unknown_source_does_not_resolve() -> None: """Негативный контроль: несуществующий source → None (тест реально дискриминирует).""" registry = _build_registry() assert resolve_handler("totally_unknown_source_xyz", registry) is None