"""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(): """NO_ENGINEERING_NEARBY — информационное предупреждение, не блокер и не даунгрейдит вердикт. Чистый жилой участок (Ж-зона, нет ЗОУИТ) без сетей в 200м должен оставаться «Можно» — отсутствие сетей не является юридическим запретом на МКД (#1617). """ nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"} verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True}) assert verdict["can_build_mkd"] is True assert verdict["verdict_label"] == "Можно" # не «С ограничениями» (#1617) assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"]) assert verdict["blockers"] == [] def test_no_engineering_plus_real_warning_still_downgrades(): """NO_ENGINEERING_NEARBY совместно с реальным предупреждением (ЗОУИТ) → «С ограничениями». Реальный ЗОУИТ-warning должен продолжать даунгрейдить вердикт; NO_ENGINEERING_NEARBY при этом тоже присутствует в warnings[] (#1617). """ nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"} overlaps = [{"subcategory": 5, "name": "Придорожная полоса", "layer": "12345"}] # нет инженерных сетей рядом — пустой список verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True}) assert verdict["can_build_mkd"] is True assert verdict["verdict_label"] == "С ограничениями" assert any(w["code"] == "ZOUIT_SUB5" for w in verdict["warnings"]) assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"]) assert verdict["blockers"] == [] def test_no_engineering_real_blocker_still_rejects(): """Реальный блокер (охранная зона ЛЭП) при отсутствии сетей → «Нельзя» (#1617).""" 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 verdict["verdict_label"] == "Нельзя" assert any(b["code"] == "ZOUIT_OVERLAP_SUB17" for b in 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"])