"""Достижимость тиров матчинга домов (#2674). Эпик #2674 — «код написан, отревьюен, смержен и ни разу не сработал». Два верхних тира `match_or_create_house` были именно такими: за всю историю 0 из 49 502 строк house_sources имеют matched_method 'cadastr_exact' или 'fias_exact'. Причина у fias-тира структурная: параметр `house_fias_id` существовал только в сигнатуре самой функции — ни Protocol `scraper_kit.contracts.HouseMatcher`, ни продуктовый адаптер `RealMatcherAdapter`, ни прямые вызывающие его не имели, т.е. передать значение было НЕКОМУ. Обычный тест этого не ловит: он вызывает функцию напрямую и передаёт параметр сам. Ловит — сверка сигнатуры с границей вызова. Здесь она и живёт: каждый keyword-параметр матчера обязан быть выразим у вызывающего, иначе ветка недостижима в проде. """ from __future__ import annotations import inspect import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from scraper_kit.contracts import HouseMatcher from app.services.matching.houses import match_house_readonly, match_or_create_house from app.services.scraper_adapters import RealMatcherAdapter def _params(func: object) -> set[str]: return set(inspect.signature(func).parameters) - {"self", "db"} def test_create_path_matcher_params_are_all_reachable_from_the_boundary() -> None: """Каждый параметр `match_or_create_house` должен быть передаваем через границу. Границы две, и обе обязаны выражать полный набор: * Protocol `HouseMatcher` — контракт, который видит scraper-kit; * `RealMatcherAdapter` — единственная боевая реализация этого контракта. Параметр, которого нет ни там ни там, — недостижимая ветка (случай `house_fias_id`, #2674). RED до фикса: {'house_fias_id'}. """ impl = _params(match_or_create_house) unreachable_via_protocol = impl - _params(HouseMatcher.match_or_create_house) unreachable_via_adapter = impl - _params(RealMatcherAdapter.match_or_create_house) assert not unreachable_via_protocol, ( f"параметры матчера, недостижимые через Protocol HouseMatcher: " f"{sorted(unreachable_via_protocol)} — ветка не сработает в проде" ) assert not unreachable_via_adapter, ( f"параметры матчера, недостижимые через RealMatcherAdapter: " f"{sorted(unreachable_via_adapter)} — ветка не сработает в проде" ) def test_fias_tier_is_gone_from_create_path_but_alive_in_readonly() -> None: """Fias-тир удалён там, где источника ФИАС нет, и сохранён там, где он есть. `match_house_readonly` зовёт estimator.resolve_target_house и передаёт ФИАС из payload.target_fias_id / DaData — источник реальный, тир оставляем. """ assert "house_fias_id" not in _params(match_or_create_house) assert "house_fias_id" in _params(match_house_readonly) # Ищем именно литерал method-значения ('"fias_exact"'), а не слово в комментарии: # комментарий-надгробие про удалённый тир остаться должен, ветка — нет. assert '"fias_exact"' not in inspect.getsource( match_or_create_house ), "мёртвая fias-ветка вернулась в путь создания домов" assert '"fias_exact"' in inspect.getsource(match_house_readonly) def test_readonly_fias_tier_is_actually_fed_by_its_caller() -> None: """Единственный живой ФИАС-тир должен ПОЛУЧАТЬ аргумент, а не только объявлять параметр. Проверки выше — структурные: они видят имя в сигнатуре. Этого мало. Уберут аргумент на настоящей границе вызова (`estimator.estimate_quality` → `match_house_readonly`) — сигнатура останется прежней, оба теста выше зелёные, а тир снова мёртв, и мы снова здесь. Доказательство, что структурной проверки недостаточно, лежит рядом: `building_cadastral_number` её проходит и при этом сработал 0 раз из 49 502. """ from app.services.estimator import estimate_quality src = inspect.getsource(estimate_quality) assert "match_house_readonly" in src, "estimate_quality больше не зовёт read-only матчер" assert "house_fias_id=" in src, ( "estimate_quality перестал передавать house_fias_id — единственный живой fias-тир " "снова недостижим (это и есть класс ошибки #2674)" ) def test_house_key_never_accepts_flat_cadastre() -> None: """Ключ ДОМА строится только на кадастре здания — не на кадастре квартиры (#2674). Ловушка «оставленного рабочего приёмника»: у квартиры свой кадастр, поэтому Tier 0 по нему не сматчит НИКОГДА, упадёт в New-house INSERT и проштампует номер квартиры в houses.cadastral_number; плюс `cad` снимает P1-страж «безномерный адрес без кадастра не создаём». Две квартиры одного дома → два дома. RED до фикса: параметр был в сигнатуре, а `cad = building_cadastral_number or cadastral_number`. """ assert "cadastral_number" not in _params(match_or_create_house) assert "building_cadastral_number" in _params(match_or_create_house) src = inspect.getsource(match_or_create_house) assert ( "cad = building_cadastral_number\n" in src ), "в ключ дома вернулся фолбэк на кадастр квартиры"