"""#2464: достоверность должна видеть зону ПЗЗ из ОБОИХ источников. Зона приходит двумя путями: * `zoning` — старый per-parcel слой из таблицы `pzz_zones_ekb`. На проде она **пуста** (0 строк, замер 19.08.2026), поэтому `data_available` там всегда False; * `nspd_zoning` — территориальные зоны дампа НСПД, а для участков в зазорах между зонами зона синтезируется резолвером геопортала. Пока в `_compute_confidence` передавали только первый, подскор «zoning» был 0.2 у КАЖДОГО участка, а оговорка утверждала неправду. Прогон analyze на проде, участок 66:41:0402029:25 (вызов функции в живом контейнере): nspd_zoning.zone_code = 'Ж-5' <- зона известна zoning.data_available = False confidence = 0.61 medium подскор zoning = 0.2 оговорка: «ПЗЗ zone_code не известен — нельзя оценить разрешённое использование» То есть один и тот же ответ показывал зону Ж-5 и заявлял, что зона неизвестна. Подскоров семь, цена ошибки в композите — (1.0 − 0.2) / 7 = 0.114. """ from __future__ import annotations from typing import Any from app.api.v1.parcels import _compute_confidence _CAVEAT = "ПЗЗ zone_code не известен" def _confidence(**over: Any) -> dict[str, Any]: """Вызов с общими аргументами. `nspd_zoning` НЕ передаётся, если его явно не попросили: тесты-контроли обязаны исполняться и на origin/main, где такого параметра ещё нет. Иначе они падали бы там с TypeError и контролями не были бы — красное «нет такой возможности» неотличимо от красного «значение неверно». """ base: dict[str, Any] = { "source": "cad_quarter", "poi_rows": [], "district_row": None, "competitor_rows": [], "noise_map_rows_nearby": 0, "air_q": None, "weather": None, "market_trend": None, "zoning": {"data_available": False}, } base.update(over) return _compute_confidence(**base) def test_nspd_zone_counts_as_known() -> None: """Зона есть только в nspd_zoning → подскор полный, ложной оговорки нет.""" res = _confidence(nspd_zoning={"zone_code": "Ж-5"}) assert res["breakdown"]["zoning"] == 1.0 assert not any(_CAVEAT in c for c in res["caveats"]), ( "оговорка «зона неизвестна» при известной зоне Ж-5 — ровно то, что видел прод" ) def test_regulation_zone_index_also_counts() -> None: """Синтезированная геопорталом зона несёт regulation_zone_index, а не zone_code.""" res = _confidence(nspd_zoning={"regulation_zone_index": "Ж-2"}) assert res["breakdown"]["zoning"] == 1.0 def test_no_zone_anywhere_keeps_the_caveat() -> None: """Контроль: когда зоны нет НИ В ОДНОМ источнике, оговорка остаётся. Зелёный по обе стороны правки — иначе правка просто глушила бы предупреждение. """ res = _confidence() # без nspd_zoning — исполняется и на origin/main assert res["breakdown"]["zoning"] == 0.2 assert any(_CAVEAT in c for c in res["caveats"]) def test_legacy_source_still_honoured() -> None: """Контроль: если старый слой когда-нибудь наполнится, он по-прежнему считается.""" res = _confidence(zoning={"data_available": True}) # без nspd_zoning assert res["breakdown"]["zoning"] == 1.0 def test_composite_gain_matches_the_measured_cost() -> None: """Цена дефекта в композите — ровно 0.114, как посчитано по проду.""" without = _confidence() with_zone = _confidence(nspd_zoning={"zone_code": "Ж-5"}) assert len(without["breakdown"]) == 7, "число подскоров изменилось — пересчитать цену" assert round(with_zone["value"] - without["value"], 2) == 0.11 # ── поведенческая проверка на уровне ручки + проводка ─────────────────────────── # # Проверки выше падают на origin/main с TypeError — это «возможности нет», а не # «значение неверно». Тест ниже красный ПО ПОВЕДЕНИЮ: на origin/main ручка вернёт # ложную оговорку при известной зоне. Он же сторожит проводку: параметр можно # добавить в функцию и забыть передать на вызове, и все проверки выше останутся # зелёными. def test_analyze_does_not_claim_unknown_zone_when_nspd_resolved_it() -> None: from unittest.mock import patch from fastapi.testclient import TestClient from app.core.db import get_db from app.main import app from tests.api.v1.test_analyze_market_price import ( _make_db_for_analyze, _override_db, _start_patches, _stop_patches, ) db = _make_db_for_analyze() app.dependency_overrides[get_db] = _override_db(db) _start_patches() try: with patch( "app.api.v1.parcels.get_quarter_dump_data", return_value={ "nspd_zoning": {"zone_code": "Ж-5", "zone_name": "Многоэтажная жилая"}, "nspd_zouit_overlaps": [], "nspd_engineering_nearby": [], "nspd_dump": {"available": True, "stale": False, "harvest_triggered": False}, }, ): resp = TestClient(app).post("/api/v1/parcels/66:41:0204016:10/analyze") assert resp.status_code == 200, resp.text body = resp.json() finally: app.dependency_overrides.clear() _stop_patches() assert (body.get("nspd_zoning") or {}).get("zone_code") == "Ж-5", ( "предусловие теста не выполнено: зона не доехала до ответа" ) caveats = " ".join(body.get("confidence_caveats") or []) assert _CAVEAT not in caveats, ( "ответ показывает зону Ж-5 и одновременно заявляет, что зона неизвестна" ) assert (body.get("confidence_breakdown") or {}).get("zoning") == 1.0