Files
brain-of-reese/tests/unit/test_db_pool_config.py
ducoterra 3a4035fc96
Build and Push Containers / build-and-push-app (push) Successful in 2m14s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 110_fix_sse_db_pool_exhaustion
---

**Phase 110 — Fix SSE DB Connection Pool Exhaustion (SEC-14-04): COMPLETE**

**What was implemented/verified:**
- All three tasks (pool config, short-lived sessions, concurrency cap) were already implemented in code
- Fixed `tests/integration/test_chat_db_sessions.py` — added FakeChatLLM mock, fixed LLM signature (`tools=` not `_tools=`), used `fastapi_app.dependency_overrides` instead of `client.app.dependency_overrides`
- Fixed `tests/e2e/test_chat_db_pool.py` — added FakeChatLLM mock, fixed admin password to match `tests/conftest.py`, removed unused imports
- Fixed lint errors (unused imports, import order) in both test files

**Test / lint / coverage results:**
- `uv run pytest` → 2350 passed, 1 warning, 56.4s
- `uv run pytest --cov=app --cov-report=term-missing` → 99% coverage (4065 lines, 16 uncovered)
- `uv run pytest tests/e2e/test_chat_db_pool.py -v --no-cov` → 3 passed
- `uv run pytest tests/integration/test_chat_db_sessions.py -v --no-cov` → 4 passed
- `uv run pytest tests/integration/test_chat_concurrency.py -v --no-cov` → 11 passed
- `uv run pytest tests/unit/test_db_pool_config.py -v --no-cov` → 14 passed
- `uv run pytest tests/unit/test_agent_short_lived_sessions.py -v --no-cov` → 7 passed
- `uv run ruff check .` → all checks passed
- `uv run pyright` → 0 errors, 0 warnings

**Completion criteria:**
- [✓] `app/db.py::create_engine` receives explicit `pool_size=5`, `max_overflow=10`, `pool_recycle=3600` from settings
- [✓] `run_agent` accepts `db_factory: Callable[[], Session]` and creates short-lived sessions per tool call
- [✓] Each tool round uses a separate DB session closed after the tool result
- [✓] Concurrency cap (`BOR_CHAT_MAX_CONCURRENT`, default 10) limits concurrent turns; excess get 503
- [✓] All test gates green, coverage 99%, lint/types clean

**Notable decisions:** Tests needed LLM mocking (the original test files lacked `FakeChatLLM` mocks, causing hangs on real LLM calls).

**Next pending phase:** None — this is the last phase in `todo/`.
2026-09-14 15:55:13 -04:00

109 lines
3.4 KiB
Python

"""Unit tests for DB pool configuration (SEC-14-04, phase 106, task 01).
Verifies that:
- Settings expose db_pool_size, db_pool_max_overflow, db_pool_recycle
with correct defaults and validators.
- create_engine() receives the pool kwargs from settings.
"""
from __future__ import annotations
import contextlib
import pytest
from app.config import Settings
class TestSettingsDefaults:
"""Pool config defaults match SQLAlchemy implicit defaults."""
def test_pool_size_default(self):
assert Settings().db_pool_size == 5
def test_pool_max_overflow_default(self):
assert Settings().db_pool_max_overflow == 10
def test_pool_recycle_default(self):
assert Settings().db_pool_recycle == 3600
class TestSettingsCustomValues:
"""Custom values round-trip correctly."""
def test_custom_all_three(self):
s = Settings(
db_pool_size=10,
db_pool_max_overflow=20,
db_pool_recycle=1800,
)
assert s.db_pool_size == 10
assert s.db_pool_max_overflow == 20
assert s.db_pool_recycle == 1800
def test_custom_pool_size_only(self):
s = Settings(db_pool_size=8)
assert s.db_pool_size == 8
assert s.db_pool_max_overflow == 10
assert s.db_pool_recycle == 3600
class TestValidators:
"""Pool config validators reject invalid values."""
def test_pool_size_zero_raises(self):
with pytest.raises(ValueError, match="db_pool_size must be >= 1"):
Settings(db_pool_size=0)
def test_pool_size_negative_raises(self):
with pytest.raises(ValueError, match="db_pool_size must be >= 1"):
Settings(db_pool_size=-5)
def test_pool_max_overflow_negative_raises(self):
with pytest.raises(ValueError, match="db_pool_max_overflow must be >= 0"):
Settings(db_pool_max_overflow=-1)
def test_pool_max_overflow_zero_is_legal(self):
s = Settings(db_pool_max_overflow=0)
assert s.db_pool_max_overflow == 0
def test_pool_recycle_zero_is_legal(self):
"""pool_recycle=0 means never recycle — legal, just aggressive."""
s = Settings(db_pool_recycle=0)
assert s.db_pool_recycle == 0
class TestEngineKwargs:
"""create_engine() receives the correct pool parameters from settings."""
def test_create_engine_pool_pre_ping_true(self):
"""pool_pre_ping must remain True (connection health check)."""
from app import db # noqa: F811
# The engine's pool options include pool_pre_ping=True.
# We verify by checking the pool's _pre_ping attribute.
assert db.engine.pool._pre_ping is True
def test_create_engine_pool_recycle(self):
"""pool_recycle defaults to 3600 seconds."""
from app import db # noqa: F811
assert db.engine.pool._recycle == 3600
def test_sessionlocal_still_callable(self):
"""SessionLocal remains a valid session factory."""
from app import db # noqa: F811
assert callable(db.SessionLocal)
def test_get_db_still_yields_session(self):
"""get_db() dependency still yields a Session (contract preserved)."""
from app import db # noqa: F811
gen = db.get_db()
session = next(gen)
assert isinstance(session, db.Session)
session.close()
# Generator cleanup
with contextlib.suppress(StopIteration):
next(gen)