All checks were successful
Deploy Trade-In / deploy (push) Successful in 58s
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 37s
Deploy Trade-In / test (push) Successful in 1m38s
Deploy Trade-In / build-backend (push) Successful in 50s
87 lines
3.7 KiB
Python
87 lines
3.7 KiB
Python
"""Полнота 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
|