— это ветка `_extract_krt_section`
+# через `find_parent`, которой одиночные блоки выше не касаются вовсе.
+#
+# ЧЕГО ЭТА ЗАМЕНА НЕ ПРОВЕРЯЕТ (осознанная потеря, зафиксирована в #2778):
+# «на живой странице ЕКБ ≥14 площадок». Это утверждение о полноте разбора
+# реального документа, синтетической страницей оно не заменяется. На проде
+# ekb_krt_sites = 15 строк, то есть по факту сейчас всё разбирается, но
+# автоматической проверки этого больше нет — она вернётся, когда в репозитории
+# появится образец страницы.
+_MULTISITE_PAGE = (
+ "
"
+ + _BRUSNIKA_BLOCK
+ + _BABUSHKINA_BLOCK
+ + textwrap.dedent(
+ """\
+
2. КРТ нежилой застройки в районе ул. Краснофлотцев:
+
Площадь территории: 3,65 га;
\
+Градостроительный потенциал: 117 640 кв. м;
\
+Всего многоквартирных домов: 13.
+ """
+ )
+ + "
"
+)
+
+
+class TestMultiSitePage:
+ """Разбор страницы из нескольких блоков — без локального дампа."""
@pytest.fixture(scope="class")
- def dump_html(self):
- """Загружает локальный дамп страницы.
+ def sites(self):
+ return parse_krt_page(_MULTISITE_PAGE)
- Тест пропускается если файл отсутствует (CI без дампа).
- """
- import os
+ def test_all_three_sites_parsed(self, sites):
+ """Три блока в двух годовых секциях → три площадки."""
+ assert len(sites) == 3, f"Ожидалось 3 площадки, получено {len(sites)}: {sites}"
- dump_path = os.path.join(
- os.path.dirname(__file__),
- "../../../../.playwright-mcp/ekb-docs/krt_page_raw.html",
- )
- if not os.path.exists(dump_path):
- pytest.skip("krt_page_raw.html dump not found — skip real-dump test")
- with open(dump_path, encoding="utf-8") as fh:
- return fh.read()
-
- def test_site_count_from_dump(self, dump_html):
- """Из дампа должно распарситься ≥ 14 площадок (реальных на странице 15)."""
- sites = parse_krt_page(dump_html)
- assert len(sites) >= 14, f"Expected ≥ 14 sites, got {len(sites)}"
-
- def test_no_duplicate_site_names(self, dump_html):
- sites = parse_krt_page(dump_html)
+ def test_no_duplicate_site_names(self, sites):
+ """Номер «2.» встречается дважды (2022 и 2024) — имена всё равно уникальны."""
names = [s.site_name for s in sites]
- assert len(names) == len(set(names)), "Дубликаты site_name в результате"
+ assert sum(n.startswith("2.") for n in names) == 2, names
+ assert len(names) == len(set(names)), f"Дубликаты site_name: {names}"
+
+ def test_blocks_do_not_leak_into_each_other(self, sites):
+ """Границы блоков соблюдены: договор и застройщик остались у своей площадки.
+
+ Регресс, который этим ловится: если block_end съезжает, «Брусника» и
+ договор 1-2/21.7-14 из первого блока прилипают ко второму и третьему.
+ """
+ with_developer = [s for s in sites if s.developer_name]
+ assert len(with_developer) == 1, [s.developer_name for s in sites]
+ assert "Брусника" in with_developer[0].developer_name
+ assert [s.contract_number for s in sites].count("1-2/21.7-14") == 1
+
+ def test_kinds_are_read_per_block(self, sites):
+ """Третий блок — «нежилой застройки», не должен унаследовать вид соседа."""
+ kinds = sorted(s.krt_kind for s in sites if s.krt_kind)
+ assert kinds == ["жилой застройки", "жилой застройки", "нежилой застройки"], kinds
diff --git a/backend/tests/skip_allowlist.txt b/backend/tests/skip_allowlist.txt
index 6fda8d13..02ca21bc 100644
--- a/backend/tests/skip_allowlist.txt
+++ b/backend/tests/skip_allowlist.txt
@@ -73,14 +73,22 @@ tests/scrapers/test_nspd_bulk_client.py::test_search_by_quarter_real_ekb
tests/scrapers/test_nspd_bulk_client.py::test_wms_feature_info_real
tests/scrapers/test_nspd_grid_walk.py::test_live_nspd_grid_walk_skipped
-# ── Образцы, которых нет в git ────────────────────────────────────────────────
-# ДОЛГ, а не норма: парсеры проверяются на реальных дампах, которые лежат только
-# в untracked `.playwright-mcp/`. Пока файлы не в репозитории, эти 8 проверок не
-# исполняются нигде. Правильная починка — закоммитить образцы в tests/fixtures/
-# (как сделано в tradein-mvp/backend/tests/fixtures/*.html), после чего строки
-# отсюда убрать.
-tests/services/scrapers/test_ekb_krt_registry.py::TestMultiSiteCountFromDump::test_no_duplicate_site_names
-tests/services/scrapers/test_ekb_krt_registry.py::TestMultiSiteCountFromDump::test_site_count_from_dump
+# ── Образец, которого нет в git ───────────────────────────────────────────────
+# ДОЛГ, а не норма. Было 8 записей; 2 из них (test_ekb_krt_registry.py::
+# TestMultiSiteCountFromDump) сняты в #2778: дампа `krt_page_raw.html` не
+# оказалось НИГДЕ — ни в репозитории, ни на дисках, ни на проде (в БД лежит
+# `raw_block` = уже очищенный текст, вход парсера из него не восстановить), —
+# зато свойство, которое они проверяли (несколько блоков на странице, уникальные
+# site_name), выразимо без дампа и теперь проверяется на многоблочной странице,
+# собранной из блоков, уже лежащих в том же тест-файле.
+#
+# Оставшиеся 6 так не лечатся: они проверяют извлечение таблиц из НАСТОЯЩЕГО PDF
+# через pdfplumber, соседний класс TestParsePptTep уже мокает pdfplumber целиком.
+# Нужен файл `ppt2018_22823_poyasnit_PPT.pdf` (или рабочий URL на ГИСОГД ЕКБ) —
+# запрошен у Anton в #2778, там же список того, что оживёт. Причин не коммитить
+# нет: официальный градостроительный документ, без персональных данных.
+# После получения — положить в tests/fixtures/, поправить `_REAL_PDF_PATH`,
+# убрать эти шесть строк.
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_area
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_phasing_has_rows
tests/services/scrapers/test_ekb_ppt_tep_parser.py::TestParsePptTepRealPdf::test_raw_tables_not_empty