gendesign/backend/tests/test_gate_verdict.py
lekss361 f70be684da feat(#29,#232): wire cad_parcels.permitted_use in analyze + cad_zouit fallback (G2+G3)
TASK A (#29 G2): add parcel_meta to analyze response
- New ParcelMeta Pydantic schema in app/schemas/parcel.py
- SELECT from cad_parcels WHERE cad_num=:c in analyze_parcel() (step 9f)
- Returns permitted_use_established_by_document, land_record_category_type,
  land_record_subtype, cost_value; None when row absent
- Tests: test_analyze_parcel_meta.py (found + not-found cases)

TASK B (#232 G3): cad_zouit fallback in _get_zouit_overlaps
- When nspd_quarter_dumps has zouit_count==0, fall back to ST_Intersects
  query on cad_zouit (3483 rows, GIST indexed)
- Overlaps tagged with source='cad_zouit'; format compatible with NSPD path
- gate_verdict.py: BLOCKER_TYPE_ZONE_KEYWORDS tuple for keyword-based
  classification (охранная зона / трубопровод / электр / газ -> blocker;
  СЗЗ -> warning); NSPD subcategory path preserved backward-compat
- Tests: 6 new test cases in test_gate_verdict.py covering cad_zouit path
  and backward-compat for NSPD subcategory path

Updated db.execute call sequence in test_analyze_*.py (index shift +1 at pos 10).
2026-05-17 08:17:22 +03:00

207 lines
9.3 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 overlap с 'охранная зона трубопроводов' → 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_CAD_BLOCKER" for b in verdict["blockers"])
def test_cad_zouit_elektr_blocks():
"""cad_zouit overlap с 'электр' substring в type_zone → blocker."""
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_CAD_BLOCKER" 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"])