All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
147 lines
7.1 KiB
Python
147 lines
7.1 KiB
Python
"""#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
|