"""Parity-proof: admin.py Yandex debug-роуты (#2305, Group A) — legacy vs scraper_kit. `app/api/v1/admin.py::scrape_yandex_detail` / `scrape_yandex_valuation` теперь зовут `scraper_kit.providers.yandex.{detail,valuation}` вместо `app.services.scrapers.yandex_{detail,valuation}` (тот же `/scrape` root-роут — `scraper_kit.providers.yandex.serp.YandexRealtyScraper`). Каждая функция ниже — чистое, offline-тестируемое parsing-ядро (см. `tests/support/README.md`: "Live network/DB в parity-тестах избегайте"). - `_entity_to_lot` — `/scrape` root-роут → `YandexRealtyScraper.fetch_around` конвертирует gate-API entity → ScrapedLot этой функцией (также доказывает ScrapedLot-контракт, общий с `save_listings`/`scripts/ingest_domclick_jsonl.py`). Легаси `app.services.scrapers.yandex_detail` / `yandex_valuation` удалены (#2277 финальный шаг scraper_kit-миграции, 0 runtime importers) — `_parse_card_fields` / `_parse_house_meta` parity-проверки убраны отсюда вместе с ними; kit-side coverage для обеих функций сохранена standalone в `tests/test_yandex_detail_structural.py` / `tests/test_yandex_valuation.py`. `yandex_realty` остаётся легаси (не мигрирован) — его `_entity_to_lot` parity-проверка не тронута. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") from scraper_kit.providers.yandex.serp import _entity_to_lot as kit_entity_to_lot from app.services.scrapers.yandex_realty import _entity_to_lot as legacy_entity_to_lot from tests.support.parity import assert_parity # gate-API entity (realty.yandex.ru), без creationDate/author — исключает # time-dependent поля (days_on_market) из фикстуры (deterministic offline). _YANDEX_ENTITY = { "offerId": "12345", "url": "//realty.yandex.ru/offer/12345", "price": {"value": 8500000, "valuePerPart": 157000}, "area": {"value": 54.3}, "livingSpace": {"value": 32.1}, "kitchenSpace": {"value": 9.5}, "roomsTotal": 2, "floorsOffered": [5], "floorsTotal": 9, "ceilingHeight": 2.7, "building": {"builtYear": 2015, "buildingType": "MONOLIT"}, "location": { "point": {"latitude": 56.83, "longitude": 60.6}, "geocoderAddress": "Екатеринбург, ул. Тестовая, 1", }, "mainImages": ["//avatars.mds.yandex.net/img1.jpg"], } def test_yandex_entity_to_lot_are_class_distinct() -> None: """Sanity: legacy/kit ScrapedLot — разные классы (pydantic), обычный `==` даёт False.""" legacy_result = legacy_entity_to_lot(_YANDEX_ENTITY) kit_result = kit_entity_to_lot(_YANDEX_ENTITY) assert legacy_result is not None assert kit_result is not None assert type(legacy_result) is not type(kit_result) assert legacy_result.model_dump() == kit_result.model_dump() def test_parity_yandex_serp_entity_to_lot() -> None: """`/scrape` root-роут (admin.py) → YandexRealtyScraper.fetch_around → _entity_to_lot. Также доказывает ScrapedLot-контракт, общий с `save_listings` (используется и `/scrape`, и `scripts/ingest_domclick_jsonl.py`). """ assert_parity( legacy_fn=legacy_entity_to_lot, kit_fn=kit_entity_to_lot, fixtures=[(_YANDEX_ENTITY,)], ) def test_parity_harness_catches_yandex_entity_divergence() -> None: """Harness реально ЛОВИТ расхождение — мутируем price_rub в kit-результате.""" import pytest from tests.support.parity import ParityMismatchError legacy_result = legacy_entity_to_lot(_YANDEX_ENTITY) kit_result = kit_entity_to_lot(_YANDEX_ENTITY) assert legacy_result is not None assert kit_result is not None # ScrapedLot — pydantic BaseModel (НЕ dataclass) → model_copy, не dataclasses.replace. mutated_kit_result = kit_result.model_copy(update={"price_rub": kit_result.price_rub + 1}) with pytest.raises(ParityMismatchError) as exc_info: assert_parity( legacy_fn=lambda: legacy_result, kit_fn=lambda: mutated_kit_result, fixtures=[()], ) assert "price_rub" in str(exc_info.value)