"""Сбой в connection-capacity не должен травить общую сессию (#2464). `_query_power_points` и `_query_water_latest` делали `db.execute` без SAVEPOINT, тогда как четыре соседних `_query_*` в том же файле его имеют. Сессия здесь ОБЩАЯ с вызывающим, и цепочка выходила такая: сбой в _query_power_points → исключение всплывает (в get_connection_capacity try/except нет) → его ГЛОТАЕТ _get_connection_capacity в full_report_pdf.py ради деградации §3 → транзакция остаётся aborted → падает уже СЛЕДУЮЩИЙ запрос — рыночная цена в _generate_concept_result → концепция молча уходит в class_norm-фолбэк То есть отказ проявляется не там, где случился, и выглядит как «нет рыночных данных». Проверяется на `PostgresLikeSession` (tests/support/pg_session.py) — двойнике с настоящей семантикой aborted-транзакции. На `MagicMock` такой тест был бы зелёным по построению: у него нет aborted-состояния, и любой следующий execute «успешен». """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import pytest from tests.support.pg_session import AbortedTransactionError, PostgresLikeSession _QUERIES = ("_query_power_points", "_query_water_latest") def _call(name: str, db: PostgresLikeSession): from app.services.site_finder import connection_capacity_lookup as mod fn = getattr(mod, name) if name == "_query_power_points": return fn(db, "POLYGON((0 0,0 1,1 1,1 0,0 0))", 5000) return fn(db) @pytest.mark.parametrize("name", _QUERIES) def test_failure_leaves_session_usable(name: str) -> None: """После проглоченного вызывающим сбоя следующий запрос обязан пройти. На origin/main второй execute падает с AbortedTransactionError — ровно та отложенная поломка, что уводит концепцию в class_norm. """ db = PostgresLikeSession(fail_on=(1,)) with pytest.raises(RuntimeError): # сам сбой пробрасывается, это ожидаемо _call(name, db) # Вызывающий (full_report_pdf._get_connection_capacity) исключение глотает, # и дальше по коду идёт СЛЕДУЮЩИЙ запрос на той же сессии. db.execute("SELECT 1") # на origin/main здесь AbortedTransactionError @pytest.mark.parametrize("name", _QUERIES) def test_savepoint_is_actually_entered(name: str) -> None: """Запрос действительно идёт внутри SAVEPOINT, а не просто «не падает». Отдельно от предыдущего: тест выше прошёл бы и если бы кто-то «починил» функцию, убрав из неё запрос целиком. """ db = PostgresLikeSession(fail_on=(), rows=[]) # здоровый путь _call(name, db) assert db.savepoints_entered >= 1, ( f"{name}: запрос выполнен вне SAVEPOINT — при сбое сессия останется aborted" ) assert db.calls >= 1, f"{name}: запрос вообще не выполнялся" def test_double_reproduces_poisoning_without_savepoint() -> None: """Контроль на сам двойник: без SAVEPOINT он ДОЛЖЕН травить сессию. Без этой проверки зелёные тесты выше нельзя отличить от «двойник ничего не моделирует» — та же ловушка, что с MagicMock. """ db = PostgresLikeSession(fail_on=(1,)) with pytest.raises(RuntimeError): db.execute("BOOM") with pytest.raises(AbortedTransactionError): db.execute("SELECT 1")