fix: None-поля в case_data ломали /analyze (float * NoneType)
Регресс B14: SearchInput.to_case_data() отдавал ключи с None
(elapsed_hours/age/...), а сервисы читают case_data.get(key, DEFAULT) —
ключ с None обманывает дефолт → TypeError в distance_service
('не удаётся запустить анализ' при запуске поиска со степ 1).
Фикс: to_case_data возвращает старую семантику _as_case_data —
None и пустые контейнеры выбрасываются. +2 регресс-теста.
192 passed.
This commit is contained in:
@@ -147,11 +147,38 @@ async def test_engine_does_not_touch_db_or_http():
|
|||||||
|
|
||||||
|
|
||||||
async def test_case_id_merge_loses_payload_empty_containers():
|
async def test_case_id_merge_loses_payload_empty_containers():
|
||||||
"""Пустые контейнеры payload не затирают карточку (старое поведение
|
"""Пустые контейнеры выбрасываются из case_data (старое поведение
|
||||||
_as_case_data сохранено на уровне роутера — здесь фиксируем семантику
|
_as_case_data): пустой diagnosis_type не доходит до сервисов."""
|
||||||
SearchInput: явно переданные пустые списки допустимы)."""
|
|
||||||
si = SearchInput(diagnosis_type=[])
|
si = SearchInput(diagnosis_type=[])
|
||||||
assert si.to_case_data()['diagnosis_type'] == []
|
assert 'diagnosis_type' not in si.to_case_data()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_none_fields_dropped_from_case_data():
|
||||||
|
"""Регресс 2026-09-09: None-поля не должны попадать в case_data.
|
||||||
|
|
||||||
|
Сервисы читают case_data.get(key, DEFAULT); ключ с None обманывает
|
||||||
|
дефолт → elapsed_hours=None ломал calculate_max_distance
|
||||||
|
(TypeError: float * NoneType) — «не удаётся запустить анализ».
|
||||||
|
"""
|
||||||
|
si = SearchInput(age=None, elapsed_hours=None, weather=None, gender=None)
|
||||||
|
d = si.to_case_data()
|
||||||
|
assert 'elapsed_hours' not in d
|
||||||
|
assert 'age' not in d
|
||||||
|
assert 'weather' not in d
|
||||||
|
assert 'diagnosis_type' not in d # пустой список тоже выбрасывается
|
||||||
|
|
||||||
|
# И полный прогон не падает:
|
||||||
|
model = await build_search_model(si, analyzer=_fake_analyzer())
|
||||||
|
assert model.max_distance_km > 0 # elapsed None → дефолт 1.0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_case_card_none_does_not_override_payload():
|
||||||
|
"""Слияние карточки (case.to_detail) с payload: None из карточки
|
||||||
|
не должен ломать расчёт при пустом payload."""
|
||||||
|
si = SearchInput(age=8, elapsed_hours=2.0, terrain=['лес'],
|
||||||
|
has_transport='bike', time_of_day='день')
|
||||||
|
d = si.to_case_data()
|
||||||
|
assert d['elapsed_hours'] == 2.0
|
||||||
|
|
||||||
|
|
||||||
async def test_fallback_fields_flow_through():
|
async def test_fallback_fields_flow_through():
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ class SearchInput(BaseModel):
|
|||||||
|
|
||||||
def to_case_data(self) -> dict[str, Any]:
|
def to_case_data(self) -> dict[str, Any]:
|
||||||
"""Плоский dict для сервисов (анализатор/скоринг) — тот же состав полей,
|
"""Плоский dict для сервисов (анализатор/скоринг) — тот же состав полей,
|
||||||
что раньше уходил из analyze_case."""
|
что раньше уходил из analyze_case.
|
||||||
|
|
||||||
|
None и пустые контейнеры выкидываются: сервисы читают через
|
||||||
|
case_data.get(key, DEFAULT), и ключ с None обманывает дефолт
|
||||||
|
(регресс 2026-09-09: elapsed_hours=None → TypeError в distance_service).
|
||||||
|
"""
|
||||||
data: dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
'age': self.age,
|
'age': self.age,
|
||||||
'gender': self.gender,
|
'gender': self.gender,
|
||||||
@@ -114,7 +119,11 @@ class SearchInput(BaseModel):
|
|||||||
data['terrain_primary'] = _first_terrain(self.terrain)
|
data['terrain_primary'] = _first_terrain(self.terrain)
|
||||||
if self.time_of_day:
|
if self.time_of_day:
|
||||||
data['time_of_day'] = self.time_of_day
|
data['time_of_day'] = self.time_of_day
|
||||||
return data
|
# Семантика старого _as_case_data: None и пустые ([]/{}) не проходят
|
||||||
|
return {
|
||||||
|
k: v for k, v in data.items()
|
||||||
|
if v is not None and not (isinstance(v, (list, dict)) and len(v) == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchModel(BaseModel):
|
class SearchModel(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user