gendesign/backend/tests/test_weight_profiles.py
bot-backend 1307d55da6
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m52s
Deploy / build-worker (push) Successful in 3m0s
Deploy / deploy (push) Successful in 1m30s
fix(site-finder): метка источника весов выводится из результата резолва, а не из входа (#2811) (#2817)
2026-08-10 10:34:39 +00:00

269 lines
10 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.

"""Unit tests для WeightProfile Pydantic models + resolve_weights.
Mock-based — без реальной БД. Проверяет:
- WeightProfileCreate: отклонение неизвестных категорий
- WeightProfileCreate: отклонение весов за пределами MIN/MAX
- WeightProfileCreate: корректные веса проходят валидацию
- WeightProfileUpdate: weights=None не вызывает ошибку
- resolve_weights: нет user_id и profile_id → системные дефолты
- resolve_weights: user_id задан, default-профиль есть → его веса
- resolve_weights: profile_id задан → его веса
- resolve_weights: профиль не найден → системные дефолты (fallback) + source != profile
"""
from __future__ import annotations
import logging
from unittest.mock import MagicMock
import pytest
from pydantic import ValidationError
from app.services.site_finder.weight_profiles import (
_SYSTEM_POI_WEIGHTS,
WeightProfile,
WeightProfileCreate,
WeightProfileUpdate,
resolve_weights,
)
# ── Validation tests ───────────────────────────────────────────────────────────
def test_validate_unknown_category() -> None:
"""Неизвестная POI-категория → ValidationError."""
with pytest.raises(ValidationError, match="Unknown POI categories"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"unknown": 1.0},
)
def test_validate_weight_bounds_too_high() -> None:
"""Вес > MAX_WEIGHT → ValidationError."""
with pytest.raises(ValidationError, match="out of bounds"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"school": 99.0},
)
def test_validate_weight_negative_too_low() -> None:
"""Вес < MIN_WEIGHT → ValidationError."""
with pytest.raises(ValidationError, match="out of bounds"):
WeightProfileCreate(
user_id="user-1",
profile_name="test",
weights={"tram_stop": -3.0},
)
def test_validate_valid_weights() -> None:
"""Корректные веса проходят валидацию без ошибок."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="my profile",
weights={"school": 1.5, "tram_stop": -0.5, "park": 2.9},
)
assert profile.weights["school"] == 1.5
assert profile.weights["tram_stop"] == -0.5
def test_validate_empty_weights() -> None:
"""Пустой dict weights допустим (используются системные дефолты)."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="empty",
weights={},
)
assert profile.weights == {}
def test_validate_boundary_values() -> None:
"""Граничные значения MIN_WEIGHT и MAX_WEIGHT включительно — допустимы."""
profile = WeightProfileCreate(
user_id="user-1",
profile_name="boundary",
weights={"school": -2.0, "park": 3.0},
)
assert profile.weights["school"] == -2.0
assert profile.weights["park"] == 3.0
def test_validate_update_weights_none() -> None:
"""WeightProfileUpdate с weights=None не вызывает ошибку валидации."""
upd = WeightProfileUpdate(weights=None)
assert upd.weights is None
def test_validate_update_unknown_category() -> None:
"""WeightProfileUpdate с неизвестной категорией → ValidationError."""
with pytest.raises(ValidationError, match="Unknown POI categories"):
WeightProfileUpdate(weights={"bogus_category": 1.0})
# ── resolve_weights tests ──────────────────────────────────────────────────────
def test_resolve_weights_system_default() -> None:
"""Оба аргумента None → возвращаются системные веса."""
db = MagicMock()
result = resolve_weights(db, user_id=None, profile_id=None)
assert result.weights == _SYSTEM_POI_WEIGHTS
assert result.source == "system"
# db не должен вызываться вообще
db.execute.assert_not_called()
def test_resolve_weights_system_default_returns_copy() -> None:
"""Возвращается копия словаря, не ссылка на _SYSTEM_POI_WEIGHTS."""
db = MagicMock()
result = resolve_weights(db, user_id=None, profile_id=None)
result.weights["school"] = 999.0
# Оригинал не изменён
assert _SYSTEM_POI_WEIGHTS["school"] == 1.5
def _make_profile_mock(weights: dict[str, float]) -> WeightProfile:
"""Вспомогательная функция: создать WeightProfile с заданными weights."""
from datetime import UTC, datetime
return WeightProfile(
id=1,
user_id="user-1",
profile_name="test",
weights=weights,
is_default=True,
description=None,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
def test_resolve_weights_uses_default_profile() -> None:
"""user_id задан, default-профиль найден → его веса."""
custom_weights = {"school": 2.0, "park": 0.5}
profile = _make_profile_mock(custom_weights)
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original = wp_module.get_default_profile
wp_module.get_default_profile = lambda _db, uid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=None)
finally:
wp_module.get_default_profile = original
assert result.weights == custom_weights
assert result.source == "user_default"
def test_resolve_weights_uses_specific_profile() -> None:
"""profile_id задан → веса конкретного профиля."""
custom_weights = {"kindergarten": 1.8, "metro_stop": 1.0}
profile = _make_profile_mock(custom_weights)
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original = wp_module.get_profile
wp_module.get_profile = lambda _db, uid, pid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=42)
finally:
wp_module.get_profile = original
assert result.weights == custom_weights
assert result.source == "profile"
def test_resolve_weights_profile_not_found_fallback() -> None:
"""Профиль не найден (get_profile → None) → системные дефолты."""
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original_get = wp_module.get_profile
original_default = wp_module.get_default_profile
wp_module.get_profile = lambda _db, uid, pid: None
wp_module.get_default_profile = lambda _db, uid: None
try:
result = resolve_weights(db, user_id="user-1", profile_id=999)
finally:
wp_module.get_profile = original_get
wp_module.get_default_profile = original_default
assert result.weights == _SYSTEM_POI_WEIGHTS
# #2811: главное — источник НЕ выдаёт себя за профиль, которого не нашли
assert result.source == "system"
def test_resolve_weights_empty_profile_weights_fallback() -> None:
"""Профиль найден но weights пустой → системные дефолты."""
profile = _make_profile_mock({}) # пустые weights
db = MagicMock()
import app.services.site_finder.weight_profiles as wp_module
original_default = wp_module.get_default_profile
wp_module.get_default_profile = lambda _db, uid: profile
try:
result = resolve_weights(db, user_id="user-1", profile_id=None)
finally:
wp_module.get_default_profile = original_default
assert result.weights == _SYSTEM_POI_WEIGHTS
assert result.source == "system"
def test_resolve_weights_profile_id_without_owner_is_not_profile(
caplog: pytest.LogCaptureFixture,
) -> None:
"""#2811 сценарий 1: profile_id есть, user_id нет → первая ветка не выполняется.
Ровно это жило на проде: ран analysis_runs #4000 от 2026-08-07 —
source='profile', profile_id=1, а tram_stop=-0.5 (системный, у профиля 1 он
-0.4). Метка обязана быть 'system', а промах — попасть в warning.
"""
db = MagicMock()
with caplog.at_level(logging.WARNING, logger="app.services.site_finder.weight_profiles"):
result = resolve_weights(db, user_id=None, profile_id=1)
assert result.source == "system"
assert result.weights == _SYSTEM_POI_WEIGHTS
assert "profile_id=1" in caplog.text
db.execute.assert_not_called() # профиль даже не искали
def test_resolve_weights_missing_profile_falls_to_user_default_not_profile(
caplog: pytest.LogCaptureFixture,
) -> None:
"""#2811 сценарий 3: profile_id не найден, но у юзера есть default-профиль.
Худший вариант: веса НЕ системные, поэтому по значениям подмена вообще не
видна. Метка должна сказать 'user_default', а не 'profile'.
"""
import app.services.site_finder.weight_profiles as wp_module
default_profile = _make_profile_mock({"school": 2.0})
db = MagicMock()
original_get = wp_module.get_profile
original_default = wp_module.get_default_profile
wp_module.get_profile = lambda _db, uid, pid: None
wp_module.get_default_profile = lambda _db, uid: default_profile
try:
with caplog.at_level(logging.WARNING, logger="app.services.site_finder.weight_profiles"):
result = resolve_weights(db, user_id="user-1", profile_id=999)
finally:
wp_module.get_profile = original_get
wp_module.get_default_profile = original_default
assert result.source == "user_default"
assert result.weights == {"school": 2.0}
assert "profile_id=999" in caplog.text