test(tradein/scheduler): нетавтологичный kit registry-completeness guard (#2397 Part C)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 14s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m57s

Deep-review нашёл: удалённый test_kit_registry_completeness.py заменялся
test_routing_coverage_sets_match, но тот строил registry ИЗ хардкоженного
_PRODUCT_SOURCES через recording-stub и проверял тот же набор — тавтология,
не гоняющая реальный build_product_handlers(). Плюс _PRODUCT_SOURCES был
устаревшим: не хватало cian_history_backfill, deals_freshness_monitor,
osm_poi_ekb_refresh (все три есть в реальном build_product_handlers).

Фикс:
- _PRODUCT_SOURCES дополнен тремя пропущенными source'ами, сверен построчным
  grep '"...": Handler(' по product_handlers.py (20 exact + 1 wildcard).
  Список остаётся canonical hardcode (источник правды — сам product_handlers.py
  + scrape_schedules seed-миграции), с явным комментарием "поддерживается вручную".
- test_routing_coverage_sets_match удалён, заменён
  test_real_build_product_handlers_covers_all_scheduled_sources: зовёт РЕАЛЬНЫЙ
  product_handlers.build_product_handlers(ctx=None) + build_registry (не stub),
  ассертит resolve_handler для каждого source из _PRODUCT_SOURCES ∪
  _KIT_NATIVE_SOURCES. Уроненная handler-entry или scheduled source без
  handler'а теперь роняет CI.

Регресс-пруф (сделан вручную, откачен): временно закомментировал
"osm_poi_ekb_refresh" в build_product_handlers → новый тест упал с
"misses source=osm_poi_ekb_refresh"; вернул строку → тест снова зелёный
(git diff на product_handlers.py пуст после отката).

Verify: full pytest 3179 passed / 6 skipped / 1 known-unrelated fail
(test_search_cache_hit, #2208); ruff 0.7.4 чист на изменённых файлах.
This commit is contained in:
bot-backend 2026-07-04 12:59:59 +03:00
parent 85059aeb1b
commit c30581ae8d

View file

@ -5,9 +5,9 @@ Kit — единственный scheduler-путь (#2397 Part C убрал leg
reap_zombies machinery; сравнивать «golden-parity» больше не с чем). Этот файл проверяет
форму и инварианты kit-реестра самого по себе:
1. REGISTRY: kit-native sweep-handler'ы зарегистрированы (`build_registry`), продуктовые
source'ы резолвятся через `resolve_handler` (включая deactivate_stale_* wildcard),
неизвестный source None.
1. REGISTRY: реальный `build_product_handlers()` (НЕ stub) + `build_registry` покрывает
каждый canonical scheduled source (kit-native + продуктовые + deactivate_stale_*
wildcard-члены); неизвестный source None.
2. CLAIM: `_claim_run` happy-path, already-running skip, lock-busy skip, appeared-under-
lock rollback. Мокаем БД + ctx.runs.
3. ZOMBIE: `reap_zombies` возвращает число + commit.
@ -39,10 +39,17 @@ from scraper_kit.orchestration.scheduler import (
resolve_handler,
)
# ── продуктовые source'ы (НЕ kit-native), которые build_product_handlers регистрирует ──
# (было ключами таблицы source → старый trigger_*; сам старый dispatch удалён #2397 Part C,
# но список source-имён остаётся полезным для recording-registry ниже).
# ── продуктовые source'ы (НЕ kit-native) — CANONICAL, поддерживается ВРУЧНУЮ ─────────
# Источник истины: app.services.product_handlers.build_product_handlers() keys (минус
# wildcard "deactivate_stale_*", раскрытый в 3 конкретных member-source'а) + семейство
# scrape_schedules-сидов (data/sql/*scrape_schedules*seed*.sql, 158_seed_proxy_healthcheck_
# schedule.sql, 162_seed_deals_freshness_monitor.sql). Этот список НЕ выводится из кода —
# добавил/убрал источник в product_handlers.py? Обнови и здесь, иначе
# test_real_build_product_handlers_covers_all_scheduled_sources ничего не поймает.
# (2026-07-04 deep-review #2397 Part C: список был устаревшим — не хватало
# cian_history_backfill/deals_freshness_monitor/osm_poi_ekb_refresh; исправлено.)
_PRODUCT_SOURCES: set[str] = {
"cian_history_backfill",
"rosreestr_dkp_import",
"listing_source_snapshot",
"asking_to_sold_ratio_refresh",
@ -53,6 +60,7 @@ _PRODUCT_SOURCES: set[str] = {
"deactivate_stale_cian",
"sber_index_pull",
"rosreestr_quarter_poll",
"deals_freshness_monitor",
"newbuilding_enrich",
"yandex_newbuilding_sweep",
"geoportal_coords_backfill",
@ -60,6 +68,7 @@ _PRODUCT_SOURCES: set[str] = {
"avito_detail_backfill",
"yandex_detail_backfill",
"cadastral_geo_match",
"osm_poi_ekb_refresh",
"house_imv_backfill",
"house_dedup_merge",
"proxy_healthcheck",
@ -123,11 +132,27 @@ def _build_recording_registry() -> tuple[dict[str, Handler], dict[str, MagicMock
# ── 1. REGISTRY shape ────────────────────────────────────────────────────────
def test_routing_coverage_sets_match() -> None:
"""Множество source'ов kit-реестра ⊇ множество продуктовых source'ов."""
registry, _ = _build_recording_registry()
for source in _PRODUCT_SOURCES:
assert resolve_handler(source, registry) is not None, f"kit misses source={source}"
def test_real_build_product_handlers_covers_all_scheduled_sources() -> None:
"""Реальный (не stub!) build_product_handlers покрывает КАЖДЫЙ canonical source.
2026-07-04 deep-review #2397 Part C: старая версия этого теста строила registry ИЗ
`_PRODUCT_SOURCES` через recording-stub (`_build_recording_registry`) и проверяла, что
тот же набор резолвится тавтология, ничего не гоняющая через реальный код. Эта версия
зовёт настоящий `product_handlers.build_product_handlers(ctx)` если кто-то уронит
handler-entry в product_handlers.py (или source добавили в scrape_schedules seed, но
забыли завести handler), тест падает.
ctx=None безопасен: build_product_handlers его не замыкает при сборке dict (сами job'ы
получают ctx только во время dispatch) см. её докстринг + test_deals_freshness_monitor.
"""
from app.services.product_handlers import build_product_handlers
real_registry = build_registry(build_product_handlers(ctx=None)) # type: ignore[arg-type]
for source in _PRODUCT_SOURCES | _KIT_NATIVE_SOURCES:
assert (
resolve_handler(source, real_registry) is not None
), f"real build_product_handlers()/build_registry() misses source={source}"
def test_kit_native_handler_set() -> None: