"""Unit-тесты гуманизации сетевых охранных зон §3 (#2119 C2/C3). Pure / без БД. Покрывает: - refine_net_kind: type_zone-база + дочистка general по name_by_doc (тепло/газ/электро). - human_label: regex-кейсы (ВЛ 0,4 кВ, КЛ 10 кВ, теплосеть с кад.номером, газопровод, нераспознанное), last-mile-подача ≤1 кВ, извлечение ТП. - _shape_network_zones: сортировка по distance + лимит _NETWORK_ZONE_PER_KIND per-kind. Строки name_by_doc — реальные образцы с прода (cad_zouit, 2026-07-03). """ from __future__ import annotations from app.services.site_finder import connection_capacity_lookup as ccl from app.services.site_finder.network_zone_humanize import human_label, refine_net_kind # type_zone-константы (как в проде / SQL-белом списке). _TZ_THERMAL = "Охранная зона тепловых сетей" _TZ_GENERAL = "Охранная зона инженерных коммуникаций" _TZ_ELECTRIC = ( "Охранная зона объектов электроэнергетики (объектов электросетевого хозяйства " "и объектов по производству электрической энергии)" ) _TZ_PIPELINE = ( "Охранная зона трубопроводов (газопроводов, нефтепроводов и " "нефтепродуктопроводов, аммиакопроводов)" ) _TZ_COMMS = "Охранная зона линий и сооружений связи" # ── refine_net_kind: база по type_zone ──────────────────────────────────────── def test_refine_thermal_type_zone_stays_thermal() -> None: """Специфичный type_zone «тепловых сетей» → thermal независимо от name.""" assert refine_net_kind(_TZ_THERMAL, "Тепловая сеть от ЦТП Баумана") == "thermal" def test_refine_electric_type_zone_stays_electricity() -> None: assert refine_net_kind(_TZ_ELECTRIC, "Охранная зона ВЛ 0,4кВ ТП 3164") == "electricity" def test_refine_pipeline_type_zone_stays_pipeline_or_gas() -> None: """type_zone «трубопроводов (газопроводов…)» c газом в имени → gas.""" assert refine_net_kind(_TZ_PIPELINE, "Охранная зона газопровода высокого давления") == "gas" def test_refine_comms_type_zone_stays_communication() -> None: assert refine_net_kind(_TZ_COMMS, "Охранная зона сооружения связи ООО «ВЕРТИКАЛЬ-УРАЛ»") == ( "communication" ) def test_refine_non_network_returns_none() -> None: assert refine_net_kind("Санитарно-защитная зона", "что угодно") is None assert refine_net_kind(None, None) is None # ── refine_net_kind: дочистка general по name_by_doc ─────────────────────────── def test_refine_general_thermal_by_name() -> None: """«Охранная зона инженерных коммуникаций» с теплосетью в name → thermal (прод-кейс).""" name = ( "Охранная зона объекта теплоснабжения «Трубопровод (тепловые сети) " "с кадастровым номером 66:41:0108079:76" ) assert refine_net_kind(_TZ_GENERAL, name) == "thermal" def test_refine_general_gas_by_name() -> None: """general с газопроводом в name → gas (прод-кейс догазификации).""" name = ( "Охранная зона газораспределительной сети в отношении объекта: Газопровод низкого давления" ) assert refine_net_kind(_TZ_GENERAL, name) == "gas" def test_refine_general_electricity_by_vl_regex() -> None: """general с «ВЛ 0,4 кВ» в name → electricity (даже без слова электросеть).""" assert refine_net_kind(_TZ_GENERAL, "Охранная зона ВЛ 0,4 кВ от ТП 71796") == "electricity" def test_refine_general_electricity_by_transformer() -> None: """general «Трансформаторная подстанция» без «ВЛ N кВ» → electricity (прод-кейс).""" name = ( 'Охранная зона объекта "Трансформаторная подстанция", кадастровый номер 66:41:0404019:333' ) assert refine_net_kind(_TZ_GENERAL, name) == "electricity" def test_refine_general_gas_distribution_not_electricity() -> None: """«газораспределительн» → gas раньше, «распределительн»-электро не перехватывает.""" assert refine_net_kind(_TZ_GENERAL, "Охранная зона газораспределительной сети") == "gas" def test_refine_general_unrecognized_stays_general() -> None: """general без распознанного вида → остаётся general («вид не уточнён»).""" assert refine_net_kind(_TZ_GENERAL, "Охранная зона неизвестного объекта") == "general" assert refine_net_kind(_TZ_GENERAL, None) == "general" # ── human_label: thermal ────────────────────────────────────────────────────── def test_label_thermal_with_cad_num() -> None: """thermal с кад.номером трубопровода в name → подпись включает кад.№.""" name = ( "Охранная зона объекта теплоснабжения с кадастровым номером " "66:41:0106021:815 (Тепловая сеть)" ) assert human_label("thermal", name) == ( "Тепловая сеть (по охранной зоне), кад. № 66:41:0106021:815" ) def test_label_thermal_without_cad_num() -> None: name = "Охранная зона объекта теплоснабжения «Тепловая сеть от ЦТП Баумана, 7а»" assert human_label("thermal", name) == "Тепловая сеть (по охранной зоне)" # ── human_label: electricity (ВЛ/КЛ N кВ + ТП) ───────────────────────────────── def test_label_electricity_vl_04kv_last_mile() -> None: """ВЛ 0,4 кВ (≤1 кВ) — last-mile: напряжение как второстепенный факт, ТП — якорь.""" name = "Охранная зона ВЛ 0,4кВ ТП 3164 руб.№16 от оп. 3 до уч. 66:41:0106128:17" assert human_label("electricity", name) == ( "Ввод от ТП 3164 (по охранной зоне, 0,4 кВ — last-mile)" ) def test_label_electricity_kl_10kv_magistral() -> None: """КЛ 10 кВ (>1 кВ) — магистральный ввод: напряжение в основной подписи.""" name = "Охранная зона КЛ 10 кВ от ТП 5521 до РП 12" assert human_label("electricity", name) == "Ввод 10 кВ от ТП 5521 (по охранной зоне)" def test_label_electricity_vl_6kv_with_ktpn() -> None: """ВЛ 6 кВ + КТПН — берём первый якорный ТП/КТПН, напряжение магистральное.""" name = ( "Охранная зона Линейное ответвление от ВЛ 6 кВ ТП 3736-3747 " "с отпайками, ТП 32505 (КТПН п/№ 589)" ) assert human_label("electricity", name) == "Ввод 6 кВ от ТП 3736-3747 (по охранной зоне)" def test_label_electricity_no_voltage_but_tp() -> None: """Нет напряжения, но есть ТП → «Электросеть от ТП M».""" assert human_label("electricity", "Охранная зона объекта от ТП 999") == ( "Электросеть от ТП 999 (по охранной зоне)" ) def test_label_electricity_bare() -> None: """Нет ни напряжения, ни ТП → generic электро-подпись.""" assert human_label("electricity", "Охранная зона объекта электросетевого хозяйства") == ( "Объект электросетевого хозяйства (по охранной зоне)" ) # ── human_label: gas / pipeline / general / прочие ───────────────────────────── def test_label_gas_with_cad_num() -> None: name = "Охранная зона газопровода высокого давления, кадастровый номер 66:41:0110003:7983" assert human_label("gas", name) == ( "Трубопровод (газо-/нефтепровод) по охранной зоне, кад. № 66:41:0110003:7983" ) def test_label_pipeline_without_cad_num() -> None: assert human_label("pipeline", "Охранная зона нефтепровода") == ( "Трубопровод (газо-/нефтепровод) по охранной зоне" ) def test_label_general_unrecognized() -> None: assert human_label("general", "Охранная зона инженерных коммуникаций") == ( "Инженерные коммуникации (вид не уточнён)" ) def test_label_communication_and_water() -> None: assert human_label("communication", "Охранная зона сооружения связи") == ( "Линия связи (по охранной зоне)" ) assert human_label("water", "Охранная зона водопровода") == ( "Сеть водоснабжения/водоотведения (по охранной зоне)" ) def test_label_none_fallback() -> None: assert human_label(None, "что угодно") == "Охранная зона" # ── _shape_network_zones: сортировка + лимит per-kind ────────────────────────── def _row(type_zone: str, name: str, dist: int, reg: str = "reg") -> dict: """Имитация .mappings()-строки cad_zouit (dict доступ по ключам).""" return { "type_zone": type_zone, "name_by_doc": name, "reg_numb_border": reg, "distance_m": dist, } def test_shape_limits_two_per_kind_sorted() -> None: """До 2 ближайших НА net_kind; в пределах вида — по distance ASC.""" rows = [ _row(_TZ_THERMAL, "Тепловая сеть A", 200, "r1"), _row(_TZ_THERMAL, "Тепловая сеть B", 50, "r2"), _row(_TZ_THERMAL, "Тепловая сеть C", 120, "r3"), # третья thermal — отбрасывается _row(_TZ_ELECTRIC, "Охранная зона ВЛ 10 кВ ТП 5", 80, "r4"), ] out = ccl._shape_network_zones(rows) thermal = [z for z in out if z["net_kind"] == "thermal"] electric = [z for z in out if z["net_kind"] == "electricity"] assert len(thermal) == 2 # лимит 2 из 3 assert [z["distance_m"] for z in thermal] == [50, 120] # ближайшие две, отсортированы assert len(electric) == 1 def test_shape_intersecting_zone_distance_zero() -> None: """distance_m=0 (зона пересекает участок) сохраняется и идёт первой.""" rows = [ _row(_TZ_THERMAL, "Тепловая сеть далеко", 250, "r1"), _row(_TZ_GENERAL, "объект теплоснабжения кад. 66:41:0108079:76", 0, "r2"), ] out = ccl._shape_network_zones(rows) assert out[0]["distance_m"] == 0 # general с теплом в name дочистился до thermal. assert out[0]["net_kind"] == "thermal" def test_shape_outer_sort_by_distance_then_kind() -> None: """Внешний порядок: distance ASC, затем net_kind (стабильно).""" rows = [ _row(_TZ_ELECTRIC, "ВЛ 10 кВ ТП 1", 100, "r1"), _row(_TZ_THERMAL, "Тепловая сеть", 100, "r2"), _row(_TZ_GENERAL, "Газопровод низкого давления", 30, "r3"), ] out = ccl._shape_network_zones(rows) assert out[0]["distance_m"] == 30 # gas ближе всех assert out[0]["net_kind"] == "gas" # при равном distance=100 — electricity раньше thermal (алфавит). assert [z["net_kind"] for z in out[1:]] == ["electricity", "thermal"] def test_shape_empty() -> None: assert ccl._shape_network_zones([]) == []