gendesign/backend/tests/test_gate_verdict.py
Light1YT 3f245026ee
All checks were successful
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 2m12s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 9m0s
CI / openapi-codegen-check (pull_request) Successful in 1m50s
CI / backend-tests (pull_request) Successful in 8m49s
feat(site-finder): выделить сетевые обременения из ЗОУИТ (#1070, Refs #103)
cad_zouit содержит ~1930 сетевых охранных зон (тепло 548/электро 69/газ 23/связь 9/
инж.коммуникации 1281), но analyze отдавал их общими ЗОУИТ-overlaps без сетевого среза.
Pure-function classify_network_zone (type_zone→NetworkKind), сверен с prod distinct.
Overlap-строки cad_zouit несут is_network_zone/network_kind/network_kind_label; gate_verdict
даёт выделенный blocker ZOUIT_NETWORK_OBREMENENIE с видом сети (back-compat: классификация
на месте если флага нет; блокирующее поведение МКД сохранено). ЗСО санохраны источника
исключена из water (не сеть). 18 тестов.

#1070-предложение «сетевые обременения как analyze-слой» — реализовано через текущий cad_zouit.
ТУ-данные ООО Брик = needs-human (нет публичного API). engineering_networks loader = honest
blocked (источник публично пуст — QGIS-темплейт Минстроя 0 строк).

Closes #1070
2026-06-14 19:17:33 +05:00

212 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for gate_verdict aggregator — pure function, no DB."""
from app.services.site_finder.gate_verdict import (
compute_gate_verdict,
is_residential_zone,
)
# ── is_residential_zone unit tests ────────────────────────────────────────────
def test_residential_zone_code_zh2():
assert is_residential_zone("Ж-2", None) is True
def test_residential_zone_code_zh5():
assert is_residential_zone("Ж5", None) is True
def test_residential_zone_name_fallback():
assert is_residential_zone(None, "Жилая зона смешанной застройки") is True
def test_non_residential_zone_code():
assert is_residential_zone("П-1", "Производственная П-1") is False
def test_non_residential_zone_recreation():
assert is_residential_zone("Р-2", "Рекреационная Р-2") is False
# ── compute_gate_verdict ──────────────────────────────────────────────────────
def test_no_dump_returns_unknown():
"""Если nspd_dump отсутствует или available=False → unknown + no_data."""
verdict = compute_gate_verdict(None, None, None, None)
assert verdict["can_build_mkd"] == "unknown"
assert verdict["source"] == "no_data"
assert verdict["verdict_label"] == "Нужна проверка"
assert any(w["code"] == "NO_NSPD_DUMP" for w in verdict["warnings"])
assert verdict["checks_performed"] == []
def test_no_dump_available_false():
verdict = compute_gate_verdict(None, None, None, {"available": False})
assert verdict["can_build_mkd"] == "unknown"
assert verdict["source"] == "no_data"
def test_residential_zone_no_overlaps_can_build():
"""Жилая зона + нет ЗОУИТ + есть сети → Можно."""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая зона Ж-2"}
verdict = compute_gate_verdict(nspd_zoning, [], [{"name": "ТП-5"}], {"available": True})
assert verdict["can_build_mkd"] is True
assert verdict["verdict_label"] == "Можно"
assert verdict["blockers"] == []
assert verdict["source"] == "nspd_dump"
def test_production_zone_blocks():
"""Производственная зона → blocker PZZ_NOT_RESIDENTIAL → Нельзя."""
nspd_zoning = {"zone_code": "П-1", "zone_name": "Производственная П-1"}
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
assert verdict["can_build_mkd"] is False
assert verdict["verdict_label"] == "Нельзя"
assert any(b["code"] == "PZZ_NOT_RESIDENTIAL" for b in verdict["blockers"])
def test_zouit_subcategory_17_blocks():
"""ЗОУИТ subcategory=17 (охранная зона ЛЭП) блокирует даже жилую зону."""
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
overlaps = [{"subcategory": 17, "name": "ЛЭП 110кВ", "layer": "37578"}]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is False
assert any("ZOUIT_OVERLAP_SUB17" == b["code"] for b in verdict["blockers"])
assert "ЛЭП 110кВ" in verdict["blockers"][0]["detail"]
def test_zouit_non_blocker_subcategory_is_warning():
"""ЗОУИТ с subcategory != 17 → warning, не blocker."""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
overlaps = [{"subcategory": 5, "name": "Придорожная полоса", "layer": "12345"}]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
assert verdict["can_build_mkd"] is True
assert verdict["verdict_label"] == "С ограничениями"
assert any("ZOUIT_SUB5" == w["code"] for w in verdict["warnings"])
assert verdict["blockers"] == []
def test_no_engineering_warning_not_blocker():
"""Отсутствие инженерных сетей в 200м — предупреждение, не блокер."""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
assert verdict["can_build_mkd"] is True # warnings не блокируют
assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"])
assert verdict["blockers"] == []
def test_stale_dump_marks_source_partial():
"""stale=True → source='nspd_dump_partial'."""
nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}
dump = {"available": True, "stale": True}
verdict = compute_gate_verdict(nspd_zoning, [], [{"name": "ТП-3"}], dump)
assert verdict["source"] == "nspd_dump_partial"
def test_checks_performed_list():
"""checks_performed содержит все 3 проверки при наличии данных."""
nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
assert len(verdict["checks_performed"]) == 3
assert "ПЗЗ зональность" in verdict["checks_performed"]
assert "ЗОУИТ пересечения" in verdict["checks_performed"]
def test_pzz_unknown_warning_when_no_zoning():
"""Если nspd_zoning=None при наличии dump — PZZ_UNKNOWN warning."""
verdict = compute_gate_verdict(None, [], [{"name": "ТП-1"}], {"available": True})
assert any(w["code"] == "PZZ_UNKNOWN" for w in verdict["warnings"])
assert verdict["can_build_mkd"] is True
# ── cad_zouit fallback path (#232) ────────────────────────────────────────────
def test_cad_zouit_ohranaya_zona_truboprovodov_blocks():
"""cad_zouit 'охранная зона трубопроводов' → сетевое обременение (газ) → Нельзя (#1070).
После #1070 трубопроводы/газ классифицируются как ZOUIT_NETWORK_OBREMENENIE
(более специфичный код, чем прежний ZOUIT_CAD_BLOCKER) — поведение «блокирует МКД»
сохранено.
"""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
overlaps = [
{
"source": "cad_zouit",
"type_zone": "Охранная зона трубопроводов (газопроводов, нефтепроводов)",
"layer": "Охранная зона трубопроводов",
"name": "Газопровод высокого давления",
}
]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is False
assert verdict["verdict_label"] == "Нельзя"
assert any(b["code"] == "ZOUIT_NETWORK_OBREMENENIE" for b in verdict["blockers"])
def test_cad_zouit_elektr_blocks():
"""cad_zouit электросетевая охранная зона → сетевое обременение (электро) → blocker (#1070)."""
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
overlaps = [
{
"source": "cad_zouit",
"type_zone": "Охранная зона электросетевого объекта",
"layer": "Охранная зона электросетевого объекта",
"name": "ЛЭП 110 кВ",
}
]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is False
assert any(b["code"] == "ZOUIT_NETWORK_OBREMENENIE" for b in verdict["blockers"])
def test_cad_zouit_szz_is_warning_not_blocker():
"""cad_zouit overlap с 'СЗЗ' → ZOUIT_CAD_SZZ warning, не blocker."""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
overlaps = [
{
"source": "cad_zouit",
"type_zone": "Санитарно-защитная зона предприятия",
"layer": "Санитарно-защитная зона предприятия",
"name": "СЗЗ завода",
}
]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
assert verdict["can_build_mkd"] is True
assert verdict["verdict_label"] == "С ограничениями"
assert any(w["code"] == "ZOUIT_CAD_SZZ" for w in verdict["warnings"])
assert verdict["blockers"] == []
def test_cad_zouit_other_is_warning():
"""cad_zouit overlap без blocker/СЗЗ keyword → ZOUIT_CAD_OTHER warning."""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
overlaps = [
{
"source": "cad_zouit",
"type_zone": "Зона публичного сервитута",
"layer": "Зона публичного сервитута",
"name": None,
}
]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
assert verdict["can_build_mkd"] is True
assert any(w["code"] == "ZOUIT_CAD_OTHER" for w in verdict["warnings"])
assert verdict["blockers"] == []
def test_nspd_subcategory_path_backward_compat():
"""NSPD dump path (source != 'cad_zouit') продолжает работать через subcategory."""
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
overlaps = [
{
"source": "nspd-quarter-dump",
"subcategory": 17,
"name": "ЛЭП 110кВ",
"layer": "37578",
}
]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is False
assert any(b["code"] == "ZOUIT_OVERLAP_SUB17" for b in verdict["blockers"])