gendesign/backend/tests/test_gate_verdict.py
lekss361 76a88b2827
feat(site-finder): gate verdict aggregator — can-build-MKD signal (#32 G5 sub-PR 1/2) (#142)
Pure-function aggregator collapses nspd_zoning / nspd_zouit_overlaps /
nspd_engineering_nearby / nspd_dump в один GateVerdict TypedDict.

Logic:
- ПЗЗ не Ж-* → BLOCKER
- ЗОУИТ sub=17 (инжен. охранная) → BLOCKER
- Другие ЗОУИТ → WARNING
- Нет инжен. сетей в 200м → WARNING
- nspd_dump stale → source: nspd_dump_partial
- Нет dump → can_build_mkd: 'unknown'

Integration: новое поле gate_verdict в analyze_parcel response.

Tests: 15/15 pass (mock-based).

Vault: code/modules/Module_Gate_Verdict.md NEW.

Closes #140 (sub-PR 2 frontend закроет #32)

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 00:59:53 +03:00

117 lines
5.5 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