gendesign/tradein-mvp/backend/tests/test_2996_mislocated_houses_watchdog.py
bot-backend cdf493f345
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
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
2026-08-21 12:01:52 +00:00

112 lines
6.8 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.

"""Сторож на дома, уехавшие от собственных объявлений (#2996).
Прод 21.08.2026: 23 дома стоят вне Свердловской области, к ним привязано 591
объявление. Адреса екатеринбургские («Ул. 8 Марта», «Крауля», «Амундсена»),
координаты — Варшава, Белград, Таллин, Владивосток, Ижевск.
Причина найдена и она НЕ в живом пути: 22 из 23 несут в `raw_payload` след
разового бэкфилла `full_backfill_2026-05-27`, который звал Yandex-геокодер
напрямую, без резолва города. Живой путь чист — 0 записей вне региона в
`geocode_cache` из 10 448 и 2 из 88 544 у `listings`.
**17 из 22 помечены геокодером `precision: "exact"`.** Точность отвечает на
«нашёлся ли номер дома», а не «в том ли городе», и критерием приёмки быть не
может. Поэтому сторож проверяет ПРИНАДЛЕЖНОСТЬ, а не «точность».
Инвариант нарочно не географический — «дом рядом со своими объявлениями», а не
«дом внутри рамки области». Рамка сломалась бы при выходе в Москву; этот
инвариант переживёт расширение.
Порог 100 км не подобран на глаз. Замер по 9 052 домам с объявлениями:
ближе 1 км 8 914 (98.5 %)
15 км 64
525 км 40
25100 км 9 ← настоящие пригороды: Сарапулка, Кедровка,
Чусовское Озеро, Ревда, Первоуральск (макс. 46.7)
дальше 100 км 25 ← ближайший 119.2, максимум 5 079
Между 46.7 и 119.2 км нет НИ ОДНОГО дома. 100 лежит в середине пустого
промежутка, и ловит на два дома больше, чем рамка по области (оба проверены —
настоящие, «Ул. Белинского/Фурманова» за 207 км с 34 объявлениями).
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import inspect
import re
def test_threshold_sits_in_the_measured_gap() -> None:
"""Головной: порог задан и лежит между пригородами и патологией.
46.7 км — самый дальний настоящий пригород, 119.2 — ближайший дефект.
Порог вне этого промежутка либо ловит пригороды, либо пропускает дефекты.
"""
from app.api.v1 import admin
порог = getattr(admin, "_MISLOCATED_KM", None)
assert порог is not None, (
"порога нет вовсе — сторож не задан, дома в Варшаве и Таллине никем не считаются"
)
assert 46.7 < порог < 119.2, (
f"порог {порог} вне измеренного промежутка (46.7, 119.2): ниже — ловит "
"настоящие пригороды, выше — пропускает дефекты"
)
def test_endpoint_exists_and_reports_scale() -> None:
"""Ручка обязана отдавать не только счётчик, но и МАСШТАБ.
Одно число «25» не даёт принять решение об очистке: нужно, сколько
объявлений привязано к этим домам (на проде 591) и какие это дома.
"""
from app.api.v1 import admin
ручка = getattr(admin, "get_mislocated_houses", None)
модель = getattr(admin, "MislocatedHousesResponse", None)
assert ручка is not None and модель is not None, (
"ручки нет — дома в Варшаве и Таллине не перечисляет никто; "
f"есть ли обработчик: {ручка is not None}, есть ли модель: {модель is not None}"
)
поля = модель.model_fields
for нужное in ("threshold_km", "houses_count", "listings_count", "houses"):
assert нужное in поля, f"нет поля {нужное}; есть: {sorted(поля)}"
def test_query_compares_house_to_its_own_listings() -> None:
"""Контроль смысла: инвариант «рядом со своими», а не «внутри рамки».
Рамка по области сломалась бы при выходе в Москву — ровно то, ради чего
заведена #2996. Ловит «починку» через координатный BETWEEN.
"""
from app.api.v1 import admin
запрос = getattr(admin, "_MISLOCATED_SQL", None)
assert запрос is not None, "запроса нет — проверять принадлежность нечем"
src = str(запрос)
assert "l.house_id_fk = h.id" in src, "дом не сравнивается со СВОИМИ объявлениями"
assert "percentile_disc(0.5)" in src, "берётся не медиана координат объявлений"
assert not re.search(r"ST_Y\(.*BETWEEN", src), (
"появилась географическая рамка — она не переживёт расширение региона"
)
def test_check_stays_out_of_the_polled_endpoint() -> None:
"""Контроль цены: тяжёлая проверка не должна попасть в опрашиваемую ручку.
Замер прода: этот запрос ~445 мс, обе выборки /scraper/data-quality вместе —
27 мс, а фронт опрашивает её каждые 120 с. Первая редакция этой правки клала
проверку туда и давала 17-кратное удорожание; замер это и остановил.
"""
from app.api.v1 import admin
src = inspect.getsource(admin.get_data_quality)
assert "percentile_disc" not in src, (
"тяжёлый агрегат вернулся в опрашиваемую каждые 2 минуты ручку"
)
assert "mislocated" not in src.lower(), "проверка снова подмешана в data-quality"