fix(forecasting): нормализовать dict-district в build_site_finder_report (#1130 follow-up) #1199

Merged
bot-backend merged 1 commit from fix/orchestrator-district-dict-guard into main 2026-06-13 04:38:08 +00:00

1 commit

Author SHA1 Message Date
31c5974c72 fix(forecasting): нормализовать dict-district в build_site_finder_report (#1130 follow-up)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m26s
CI / backend-tests (pull_request) Successful in 6m22s
Deploy / build-backend (push) Successful in 1m45s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m12s
`analyze["district"]` в этой кодовой базе — dict вида
{"district_name": "Верх-Исетский", "dist_to_center": 0.0, "median_price_per_m2": ...}.
Штатный caller (`workers/tasks/forecast.py:123`) явно извлекает `district_name`:
`district = row.district or analyze["district"]["district_name"]`. Но новые callers
(тесты, расширения чата, ad-hoc эндпоинты) легко передают сырой dict без знания этой
конвенции — тогда внутри §9.x-слоёв compute_market_metrics(district=<dict>) падает
с TypeError: unhashable type: 'dict' в forecast_request_cache.wrapper,
`_safe_call` это проглатывает → секции future_market.forecasts_by_horizon=[] и
scenarios.by_scenario={} тихо остаются пустыми (silent degrade, не 500).

Добавлен `_normalize_district(district)` — pure-нормализация на входе оркестратора:
  - str → как есть;
  - None → None;
  - dict с district_name (непустая строка) → извлекаем;
  - dict без district_name / с пустым / неподдерживаемый тип → None + logger.warning.

7 unit-тестов в test_orchestrator.py::TestNormalizeDistrict (все варианты входов).
Не меняет поведение штатного caller'а (str → str), только защищает от случайных
dict-callers.

Discovered through: #1130 Phase A (мой первый тестовый скрипт со скормленным
сырым `analyze["district"]` dict выдал forecasts.n_horizons=0 + 15 TypeError'ов
в _safe_call). Закрывает чип task_4a4aa3bb.

Refs #1130
2026-06-13 09:24:55 +05:00