"""Тесты изолированной write-сессии в get_or_fetch_zone_regulation (#1850 item 4). На cache-MISS резолвер пишет регламент в кэш в СОБСТВЕННОЙ короткой сессии (SessionLocal()), а НЕ коммитит SHARED-сессию вызывающего (/analyze коммитит её ровно один раз в конце через persist_analysis_run). Проверяем: - happy path: shared db.commit НИКОГДА не вызывается, fresh write-session коммитит РОВНО раз, сессия закрыта; read-back идёт по SHARED db (форма результата сохранена для callers); - graceful: upsert_zone_regulation вернул None (таблица недоступна) → fresh session rollback, shared db не тронута, функция возвращает результат read-back без падения. Сеть/БД не дёргаются: фейковый клиент + monkeypatch SessionLocal (recording-фейк) + отдельная фейковая SHARED Session, считающая commit/rollback. """ from __future__ import annotations from typing import Any import pytest from app.services.scrapers.ekb_geoportal_client import ZoneRegulation from app.services.site_finder import zone_regulation as zr def _reg(idx: str) -> ZoneRegulation: return ZoneRegulation( zone_index=idx, zone_full_name=f"{idx} зона", main_vri=[], conditional_vri=[], auxiliary_vri=[], limit_params=[], ) class _MissThenResolveClient: """Фейковый WFS-клиент: индекс зоны есть, но кэш промахивается → live-резолв регламента.""" def __init__(self, zone_index: str) -> None: self._zone_index = zone_index self.index_calls = 0 self.regulation_calls = 0 def zone_index_at(self, lon: float, lat: float) -> str: self.index_calls += 1 return self._zone_index def zone_regulation_at(self, lon: float, lat: float) -> ZoneRegulation: self.regulation_calls += 1 return _reg(self._zone_index) class _SharedDB: """Фейковая SHARED Session вызывающего: считает commit/rollback, ничего не пишет. commit() должен остаться НЕТРОНУТЫМ — фикс #1850 item 4 запрещает преждевременный коммит in-flight analyze-транзакции из резолвера. """ def __init__(self) -> None: self.commits = 0 self.rollbacks = 0 def commit(self) -> None: self.commits += 1 def rollback(self) -> None: self.rollbacks += 1 class _WriteDB: """Фейковая короткоживущая write-сессия (то, что отдаёт monkeypatched SessionLocal).""" def __init__(self) -> None: self.commits = 0 self.rollbacks = 0 self.closed = False def commit(self) -> None: self.commits += 1 def rollback(self) -> None: self.rollbacks += 1 def close(self) -> None: self.closed = True @pytest.fixture(autouse=True) def _reset_memo() -> Any: """Чистый memo до/после теста (процесс-локальный кэш течёт между тестами).""" zr._reset_zone_index_memo() yield zr._reset_zone_index_memo() def test_cache_miss_writes_in_isolated_session(monkeypatch: Any) -> None: """Cache-miss: shared db.commit НЕ вызывается, fresh write-session коммитит РОВНО раз.""" write_db = _WriteDB() created: list[_WriteDB] = [] def _session_local() -> _WriteDB: created.append(write_db) return write_db monkeypatch.setattr(zr, "SessionLocal", _session_local) upserts: list[ZoneRegulation] = [] # upsert вернул params_numeric (не None) → write-сессия должна закоммитить. monkeypatch.setattr( zr, "upsert_zone_regulation", lambda db, reg, **kw: upserts.append(reg) or {"x": [1]} ) # get_cached дёргается на SHARED db ДВАЖДЫ: 1) до fetch (cache MISS → None, форсит резолв), # 2) read-back после commit write-сессии (READ COMMITTED видит строку → dict для callers). cache_reads: list[Any] = [] def _cached(db: Any, zone_index: str | None, **kw: Any) -> dict[str, Any] | None: cache_reads.append(db) assert db is shared # резолвер читает кэш ИМЕННО по SHARED db, не по write-сессии return None if len(cache_reads) == 1 else {"zone_index": zone_index} monkeypatch.setattr(zr, "get_cached_zone_regulation", _cached) shared = _SharedDB() client = _MissThenResolveClient("Ц-1") result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client) # type: ignore[arg-type] # SHARED транзакция НЕ тронута резолвером. assert shared.commits == 0 assert shared.rollbacks == 0 # Свежая write-сессия закоммитила РОВНО один раз и закрыта. assert len(created) == 1 assert write_db.commits == 1 assert write_db.rollbacks == 0 assert write_db.closed is True # Апсёрт ушёл в write-сессию, не в shared. assert upserts == [_reg("Ц-1")] # get_cached на SHARED db: cache-check (miss) + read-back (hit) = 2 чтения. assert len(cache_reads) == 2 # Read-back вернул кэшированную форму (dict) для callers parcels.py / ird_analyze.py. assert result == {"zone_index": "Ц-1"} def test_cache_miss_table_missing_rolls_back_gracefully(monkeypatch: Any) -> None: """upsert вернул None (таблица недоступна) → fresh session rollback, shared не тронута.""" write_db = _WriteDB() monkeypatch.setattr(zr, "SessionLocal", lambda: write_db) # upsert_zone_regulation graceful-возврат None (OperationalError/ProgrammingError внутри). monkeypatch.setattr(zr, "upsert_zone_regulation", lambda db, reg, **kw: None) # Кэш недоступен и для read-back → None (форма как при недоступной таблице у callers). monkeypatch.setattr(zr, "get_cached_zone_regulation", lambda *a, **kw: None) shared = _SharedDB() client = _MissThenResolveClient("Ж-2") result = zr.get_or_fetch_zone_regulation(shared, 60.6, 56.8, client=client) # type: ignore[arg-type] # Write-сессия откатилась (нечего персистить), не коммитила, закрыта. assert write_db.commits == 0 assert write_db.rollbacks == 1 assert write_db.closed is True # SHARED транзакция вообще не тронута. assert shared.commits == 0 assert shared.rollbacks == 0 # Функция отработала gracefully (read-back None при недоступном кэше). assert result is None