Московская область считается по своему ряду Сбериндекса, а не по общероссийскому
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m30s

Загрузчик научился тянуть REF_AREA=50 раньше, и 12.09.2026 ряд приехал на прод:
real_estate_deals, сегмент «Вторичный», 116 месячных точек с 2017-01 по 2026-08 —
та же глубина, что у Москвы и Свердловской области. До этой правки код 50 в карте
_SBER_REGION_SERIES отсутствовал, поэтому поправка по времени для области считалась
по ряду «Россия», то есть ошибалась на всё расхождение области со страной. Теперь
ошибка остаётся только внутрирегиональной.

Область автоматически попадает в SBER_REQUIRED_REGIONS, то есть пропажа её ряда
станет видимой монитору свежести — это и есть желаемое, данные под наблюдением.

Фикстуры монитора, которые перечисляли ряды литералами, перевязаны на карту
эстиматора: они краснели от самого факта появления третьего обязательного региона,
хотя проверяют не его. Тест загрузчика, утверждавший, что ряд загружен, но
эстиматором НЕ используется, заменён на сцепку имён: имя у загрузчика и у
эстиматора обязано совпадать буква в букву, иначе один читает пустоту, а второй
требует ряд, которого никто не пишет.

Комментарий в реестре регионов о том, что кода 50 в карте ещё нет, поправлен.
Набор тиров обогащения у области остаётся пустым: у Москвы он тоже пуст, а свой
ряд она читает — поправка по времени идёт мимо enrichment_tiers.
This commit is contained in:
bot-backend 2026-09-12 17:42:36 +03:00
parent c265e1f769
commit 6106ee1be8
4 changed files with 59 additions and 20 deletions

View file

@ -328,6 +328,13 @@ SBER_TIME_ADJUST_REGION = "Свердловская область" # ряд р
_SBER_REGION_SERIES: dict[int, str] = {
66: SBER_TIME_ADJUST_REGION,
77: "Москва",
# Московская область — свой ряд, а не фолбэк на «Россию». Загрузчик научился
# тянуть REF_AREA=50 (services/sber_index), и 12.09.2026 ряд приехал на прод:
# real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08 — та же глубина,
# что у Москвы и обл.66. До этой строки область считалась по общероссийскому
# ряду, то есть коридор ошибался на расхождение области со страной; теперь
# ошибка только внутрирегиональная.
50: "Московская область",
}
# Ряд для региона ВНЕ карты. Выбран общероссийский, а не отказ от поправки (1.0):

View file

@ -233,9 +233,11 @@ REGIONS: dict[int, Region] = {
),
# Тиров обогащения у области пока НЕТ ни одного: IMV/квартальный
# индекс/кадастр/POI не заведены (проверено — frozenset() пуст
# намеренно, не заглушка). Ряд Сбериндекса по области загружен в
# источник (#3498), но в карте _SBER_REGION_SERIES эстиматора кода 50
# ЕЩЁ НЕТ — тир пока недоступен, добавляется отдельным решением.
# намеренно, не заглушка). Ряд Сбериндекса по области с 12.09.2026 в
# карте _SBER_REGION_SERIES эстиматора есть, но тиром он от этого не
# становится: у Москвы набор тиров тоже пуст, а свой ряд она читает —
# поправка по времени идёт мимо enrichment_tiers (TIER_SBER_INDEX нигде
# за пределами этого реестра не спрашивают).
enrichment_tiers=frozenset(),
# Источники по области несут настоящий city (Химки, Балашиха — не
# муниципальный округ/поселение, в отличие от Москвы) — перезаписывать

View file

@ -539,16 +539,24 @@ async def test_pull_sber_indices_asking_benchmark_logged(caplog: pytest.LogCaptu
)
def test_moscow_oblast_ref_area_is_loaded_but_not_yet_used_by_estimator() -> None:
"""Область грузится заранее: ряд наполняется месяцами, задним числом не берётся.
def test_moscow_oblast_ref_area_feeds_the_estimator_under_the_same_name() -> None:
"""Имя ряда у загрузчика и у эстиматора обязано совпадать буква в букву.
Одновременно проверяем, что заведение ряда НЕ включает область в оценку и НЕ
поднимает тревогу монитора свежести: обе величины считаются от карты
эстиматора (_SBER_REGION_SERIES), а не от карты загрузчика.
Загрузчик завёл REF_AREA=50 раньше (#3498), когда карта эстиматора кода 50
ещё не знала и область считалась по «России». 12.09.2026 ряд приехал на прод
(real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08), и код 50 попал
в `_SBER_REGION_SERIES`. С этого момента расхождение имён перестаёт быть
безобидным: эстиматор молча читал бы пустой ряд, а монитор свежести требовал
бы ряд, которого загрузчик не пишет. Тест сцепляет обе стороны по имени.
"""
from app.services.estimator import SBER_MONITORED_REGIONS, SBER_REQUIRED_REGIONS
from app.services.estimator import (
SBER_MONITORED_REGIONS,
SBER_REQUIRED_REGIONS,
sber_region_series_name,
)
from app.services.sber_index import SBER_REF_AREAS
assert SBER_REF_AREAS["50"] == "Московская область"
assert "Московская область" not in SBER_REQUIRED_REGIONS
assert "Московская область" not in SBER_MONITORED_REGIONS
assert sber_region_series_name(50) == SBER_REF_AREAS["50"]
assert "Московская область" in SBER_REQUIRED_REGIONS
assert "Московская область" in SBER_MONITORED_REGIONS

View file

@ -88,6 +88,21 @@ def test_series_names_agree_with_region_registry() -> None:
assert regions_mod.REGIONS[code].name == name
def test_resolver_maps_50_to_moscow_oblast_not_russia() -> None:
"""Область получает СВОЙ ряд, а не общероссийский фолбэк.
Загрузчик научился тянуть REF_AREA=50 (#3498), и 12.09.2026 ряд приехал на
прод: real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08 та же
глубина, что у Москвы и обл.66. Пока кода 50 в карте не было, коридор
области считался по «России», то есть ошибался на всё расхождение области
со страной. Тест красный на прежней карте ПО ЗНАЧЕНИЮ: там вернулась бы
«Россия».
"""
assert est.sber_region_series_name(50) == "Московская область"
assert est.sber_region_series_name(50) != est.SBER_TIME_ADJUST_FALLBACK_REGION
assert "Московская область" in est.SBER_REQUIRED_REGIONS
def test_monitored_regions_cover_every_series_the_estimator_can_read() -> None:
assert set(est.SBER_MONITORED_REGIONS) == set(est._SBER_REGION_SERIES.values()) | {
est.SBER_TIME_ADJUST_FALLBACK_REGION
@ -275,8 +290,15 @@ def test_monitor_verdict_for_sverdlovsk_unchanged(monkeypatch: pytest.MonkeyPatc
def _without_moscow() -> _MonDB:
"""Источник переименовал «Москва» → ряда с прежним именем больше нет."""
return _MonDB({"Свердловская область": _JUN_2026, "Россия": _JUN_2026})
"""Источник переименовал «Москва» → ряда с прежним именем больше нет.
Остальные обязательные ряды на месте иначе тесты ниже считали бы пропажу
Москвы вместе с чужими пропажами. Строятся от карты эстиматора, чтобы новый
регион в ней не ронял эти тесты по счётчику (ровно так они и покраснели,
когда в карту приехала область).
"""
present = {r: _JUN_2026 for r in est.SBER_MONITORED_REGIONS if r != "Москва"}
return _MonDB(present)
def test_missing_moscow_does_not_suppress_the_sverdlovsk_verdict(
@ -352,7 +374,7 @@ def test_missing_fallback_series_does_not_fail_the_monitor(
) -> None:
"""«Россия» — фолбэк: сегодня по нему не считается ни одна сделка → не сбой."""
calls = _patch_runs(monkeypatch)
db = _MonDB({"Свердловская область": _JUN_2026, "Москва": _JUN_2026})
db = _MonDB({r: _JUN_2026 for r in est.SBER_REQUIRED_REGIONS})
out = mon.check_sber_freshness(db, run_id=4, params={}, now=_NOW) # type: ignore[arg-type]
assert calls["failed"] is None
assert out["regions_missing"] == 1
@ -457,15 +479,17 @@ def test_duplicate_series_name_does_not_pin_regions_missing(
def test_early_exit_fills_the_new_counters(monkeypatch: pytest.MonkeyPatch) -> None:
"""КРАСНЫЙ ДО ПРАВКИ: в раннем выходе новые ключи отдавали нули.
Пропажа ДВУХ обязательных рядов была неотличима от пропажи одного свердловского,
а найденный ряд не попадал в age_days_max вовсе.
Пропажа ВСЕХ обязательных рядов была неотличима от пропажи одного
свердловского, а найденный ряд не попадал в age_days_max вовсе. Счётчик
сверяем с длиной карты, а не с литералом: новый регион в ней не повод
этому тесту краснеть.
"""
calls = _patch_runs(monkeypatch)
db = _MonDB({est.SBER_TIME_ADJUST_FALLBACK_REGION: _JUN_2026})
out = mon.check_sber_freshness(db, run_id=11, params={}, now=_NOW) # type: ignore[arg-type]
assert calls["done"] is None
assert calls["failed"] is not None
assert out["regions_missing"] == 2
assert out["regions_missing"] == len(est.SBER_REQUIRED_REGIONS)
assert out["alert_regions_missing"] == 1 # ДО ПРАВКИ: 0
assert out["age_days_max"] == 72 # ДО ПРАВКИ: 0 — найденный ряд был не виден
# прежняя семантика раннего выхода не тронута
@ -492,9 +516,7 @@ class _MonDBDriverFault(_MonDB):
def execute(self, stmt: Any, params: dict[str, Any] | None = None) -> _Result:
if self._deactivated:
raise PendingRollbackError(
"Can't reconnect until invalid transaction is rolled back"
)
raise PendingRollbackError("Can't reconnect until invalid transaction is rolled back")
params = params or {}
if params.get("city") == self._broken:
self._deactivated = True