ZOUIT_OVERLAP_SUB17 (единственный subcategory-blocker = охранная зона ЛЭП/газа/трубопровода) был ХАРД-блокером на ЛЮБОЕ пересечение → блокировал 434/574 (75.6%) участков как «нельзя строить МКД». Прод-замер: реальное покрытие участка этими зонами median=6%, p90=47%, только 1/17 >60% — тонкие инж-коридоры. По РФ охранная зона ограничивает застройку ВНУТРИ полосы, а не весь участок (МКД сажается на незанятой части). Хард-блок на 6%-покрытии неверен и (после фикса резолва зон #1891) прятал financial_estimate у ~90% участков. Фикс: area-gate. - quarter_dump_lookup._get_zouit_overlaps + _get_cad_zouit_overlaps: добавлен coverage_pct (доля площади участка под зоной, в geography м², NULLIF-guard). - gate_verdict.compute_gate_verdict: sub-17 (и cad network/blocker) АГРЕГИРУЮТСЯ (sum-capped, консервативно к блоку) → блокер ТОЛЬКО если покрытие > settings.gate_zouit_engineering_blocker_min_coverage (0.6, env-tunable), иначе WARNING ZOUIT_ENGINEERING_PARTIAL с % покрытия. Больше нет 4× дубль-блокеров. СЗЗ/OKN/прочие ЗОУИТ — без изменений (warnings). main_vri/резидентность — без изменений. - coverage_pct отсутствует (legacy/synthetic) → трактуем как 0 → warning (документировано: дамп всегда даёт coverage; ложный хард-блок дороже). Тесты: +помесь gate/aggregation/coverage (131 combined passed); правка _make_zouit_row (7-я колонка coverage_pct) в tests/test_quarter_dump_lookup.py. Полный бэкенд локально: 2 failed → fixed → перепроверка. ruff чисто. Схема не менялась (gate_verdict/overlaps — dict[str,Any], api-types regen не нужен). Refs #1881 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
6 KiB
Python
146 lines
6 KiB
Python
"""Tests for network-obremenenie классификатор (#1070) — pure function, no DB.
|
||
|
||
type_zone-строки взяты из реальных distinct-значений cad_zouit (PROD, 2026-06-14).
|
||
"""
|
||
|
||
from app.services.site_finder.gate_verdict import compute_gate_verdict
|
||
from app.services.site_finder.network_obremenenie import (
|
||
classify_network_zone,
|
||
is_network_obremenenie,
|
||
network_kind_label,
|
||
)
|
||
|
||
# ── classify_network_zone: реальные type_zone из cad_zouit ────────────────────
|
||
|
||
|
||
def test_thermal_zone():
|
||
assert classify_network_zone("Охранная зона тепловых сетей") == "thermal"
|
||
|
||
|
||
def test_electricity_zone():
|
||
tz = (
|
||
"Охранная зона объектов электроэнергетики (объектов электросетевого хозяйства "
|
||
"и объектов по производству электрической энергии)"
|
||
)
|
||
assert classify_network_zone(tz) == "electricity"
|
||
|
||
|
||
def test_pipeline_zone_gas_first():
|
||
# «трубопроводов (газопроводов, нефтепроводов …)» — газ ловится раньше pipeline.
|
||
tz = (
|
||
"Охранная зона трубопроводов (газопроводов, нефтепроводов "
|
||
"и нефтепродуктопроводов, аммиакопроводов)"
|
||
)
|
||
assert classify_network_zone(tz) == "gas"
|
||
|
||
|
||
def test_communication_zone():
|
||
assert classify_network_zone("Охранная зона линий и сооружений связи") == "communication"
|
||
|
||
|
||
def test_general_engineering_zone():
|
||
assert classify_network_zone("Охранная зона инженерных коммуникаций") == "general"
|
||
|
||
|
||
# ── негативные: НЕ сетевые ЗОУИТ ──────────────────────────────────────────────
|
||
|
||
|
||
def test_servitude_not_network():
|
||
assert classify_network_zone("Зона публичного сервитута") is None
|
||
|
||
|
||
def test_szz_not_network():
|
||
assert classify_network_zone("Санитарно-защитная зона") is None
|
||
|
||
|
||
def test_natural_zouit_not_network():
|
||
assert classify_network_zone("Зоны с особыми условиями использования территории") is None
|
||
|
||
|
||
def test_none_and_empty():
|
||
assert classify_network_zone(None) is None
|
||
assert classify_network_zone("") is None
|
||
|
||
|
||
def test_general_keyword_without_protection_marker_is_skipped():
|
||
# «инженерные коммуникации» без «охранная зона» → не ловим (избегаем ложных).
|
||
assert classify_network_zone("Размещение инженерных коммуникаций") is None
|
||
|
||
|
||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
|
||
def test_is_network_obremenenie():
|
||
assert is_network_obremenenie("Охранная зона тепловых сетей") is True
|
||
assert is_network_obremenenie("Зона публичного сервитута") is False
|
||
|
||
|
||
def test_network_kind_label():
|
||
assert network_kind_label("thermal") == "теплоснабжение"
|
||
assert network_kind_label("general") == "инженерные коммуникации"
|
||
assert network_kind_label(None) is None
|
||
|
||
|
||
# ── gate_verdict: сетевое обременение → выделенный blocker ─────────────────────
|
||
|
||
|
||
def _dump_available() -> dict:
|
||
return {"available": True, "stale": False}
|
||
|
||
|
||
def test_gate_verdict_thermal_network_blocker():
|
||
"""Теплосеть (#1070 ООО Брик) покрывающая участок → ZOUIT_NETWORK_OBREMENENIE blocker.
|
||
|
||
Area-gated: при полном покрытии (coverage > порога) сетевое обременение блокирует.
|
||
"""
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Охранная зона тепловых сетей",
|
||
"name": "ТС-123",
|
||
"is_network_zone": True,
|
||
"network_kind": "thermal",
|
||
"network_kind_label": "теплоснабжение",
|
||
"coverage_pct": 0.8,
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(None, overlaps, None, _dump_available())
|
||
codes = [b["code"] for b in verdict["blockers"]]
|
||
assert "ZOUIT_NETWORK_OBREMENENIE" in codes
|
||
assert verdict["can_build_mkd"] is False
|
||
detail = next(
|
||
b["detail"] for b in verdict["blockers"] if b["code"] == "ZOUIT_NETWORK_OBREMENENIE"
|
||
)
|
||
assert "теплоснабжение" in detail
|
||
|
||
|
||
def test_gate_verdict_servitude_not_network_blocker():
|
||
"""Сервитут (не сеть) → НЕ ZOUIT_NETWORK_OBREMENENIE."""
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Зона публичного сервитута",
|
||
"name": "",
|
||
"is_network_zone": False,
|
||
"network_kind": None,
|
||
"network_kind_label": None,
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(None, overlaps, None, _dump_available())
|
||
codes = [b["code"] for b in verdict["blockers"]]
|
||
assert "ZOUIT_NETWORK_OBREMENENIE" not in codes
|
||
|
||
|
||
def test_gate_verdict_network_classified_when_flag_absent():
|
||
"""Back-compat: overlap без флага → gate_verdict классифицирует сам (area-gated blocker)."""
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Охранная зона объектов электроэнергетики",
|
||
"name": "ВЛ-10кВ",
|
||
"coverage_pct": 0.75,
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(None, overlaps, None, _dump_available())
|
||
codes = [b["code"] for b in verdict["blockers"]]
|
||
assert "ZOUIT_NETWORK_OBREMENENIE" in codes
|