"""Cian valuation time-budget — slow Cian must degrade gracefully, not block /estimate. Prod latency incident: the Cian Valuation call was the ONLY ungated enrichment in the estimate pipeline — it times out at ~25s on every cache-miss, blocking the whole request past the gateway/patience window. The Yandex valuation right above it was already wrapped in `_with_budget`; this mirrors that guard for Cian. Contracts locked here: 1. `estimate_cian_valuation_timeout_s` exists and defaults to 8.0s. 2. A Cian-valuation timeout (TimeoutError surfaced by the _with_budget wrapper) degrades to an AggregatedEstimate WITHOUT 'cian_valuation' in sources_used and no 5xx — the same graceful None path as a network error. Style mirrors test_estimator_null_floor_timeout.py. """ import os # Settings requires DATABASE_URL at init time. Set dummy DSN before any app import. os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db") from unittest.mock import AsyncMock, MagicMock, patch import anyio def _make_fake_geo(): from app.services.geocoder import GeocodeResult return GeocodeResult( lat=56.838, lon=60.595, full_address="Свердловская обл., Екатеринбург, ул. Учителей, 18", provider="nominatim", ) def _make_payload_full(): """Floor present so the floor-gated Cian source is actually awaited.""" from app.schemas.trade_in import TradeInEstimateInput return TradeInEstimateInput( address="ЕКБ, ул. Учителей, 18", area_m2=38.8, rooms=1, floor=4, total_floors=16, ) def test_cian_valuation_timeout_setting_default() -> None: """The new budget setting exists and defaults to 8.0s.""" from app.core.config import settings assert hasattr(settings, "estimate_cian_valuation_timeout_s") assert settings.estimate_cian_valuation_timeout_s == 8.0 def test_estimate_cian_timeout_degrades_no_5xx() -> None: """Cian valuation raising TimeoutError → estimate without it, no exception. The _with_budget() guard wraps the Cian call; a TimeoutError must map to the same graceful None path as a network error. Estimator returns an AggregatedEstimate; 'cian_valuation' absent from sources_used. """ from app.services.estimator import estimate_quality db = MagicMock() payload = _make_payload_full() # Cian call hangs/raises a timeout — simulate the slow-upstream case. cian_mock = AsyncMock(side_effect=TimeoutError("cian slow")) async def _run() -> None: with ( patch("app.services.estimator.geocode", new=AsyncMock(return_value=_make_fake_geo())), patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)), patch("app.services.estimator._fetch_analogs", return_value=([], False, "W")), patch("app.services.estimator._fetch_deals", return_value=[]), patch( "app.services.estimator._get_or_fetch_imv_cached", new=AsyncMock(return_value=None) ), patch( "app.services.estimator._get_or_fetch_yandex_valuation_cached", new=AsyncMock(return_value=None), ), patch("app.services.estimator.estimate_via_cian_valuation", new=cian_mock), patch("app.services.estimator._get_asking_sold_ratio", return_value=(None, None)), ): result = await estimate_quality(payload, db) assert result.estimate_id is not None # no 5xx — degraded gracefully assert "cian_valuation" not in result.sources_used anyio.run(_run)