11"""Tests for database module — async SQLAlchemy session management."""
22
3+ import asyncio
34from collections .abc import AsyncGenerator
45from pathlib import Path
56
67import pytest
78from sqlalchemy .ext .asyncio import AsyncSession
89
910from aignostics_foundry_core .database import (
11+ cli_run_with_db ,
12+ cli_run_with_engine ,
1013 dispose_engine ,
1114 execute_with_session ,
1215 get_db_session ,
1316 init_engine ,
1417 with_engine ,
1518)
1619
20+ NON_SQLITE_DB_URL = "postgresql+asyncpg://u:p@localhost/db"
21+
1722SESSION_KWARG = "session"
1823
1924
@@ -62,6 +67,12 @@ async def noop(**_kwargs: object) -> None:
6267
6368 await execute_with_session (noop ) # Session maker still functional
6469
70+ @pytest .mark .unit
71+ async def test_init_engine_non_sqlite_url_accepted (self ) -> None :
72+ """init_engine accepts a non-SQLite URL without raising (engine creation is lazy)."""
73+ init_engine (NON_SQLITE_DB_URL )
74+ await dispose_engine () # Must not raise even though no connection was attempted
75+
6576
6677class TestExecuteWithSession :
6778 """Tests for execute_with_session behaviour."""
@@ -80,6 +91,82 @@ async def capture_session(**kwargs: object) -> None: # noqa: RUF029
8091 assert len (received ) == 1
8192 assert isinstance (received [0 ], AsyncSession )
8293
94+ @pytest .mark .unit
95+ async def test_execute_with_session_raises_before_init (self ) -> None :
96+ """RuntimeError raised when execute_with_session is called before init_engine."""
97+
98+ async def noop (** _ : object ) -> None :
99+ pass
100+
101+ with pytest .raises (RuntimeError , match = "not initialized" ):
102+ await execute_with_session (noop )
103+
104+
105+ class TestCliRunWithDb :
106+ """Tests for cli_run_with_db synchronous CLI helper."""
107+
108+ @pytest .mark .unit
109+ async def test_cli_run_with_db_returns_function_result (self , sqlite_url : str ) -> None :
110+ """cli_run_with_db returns the value produced by the async function."""
111+
112+ async def return_42 (** _ : object ) -> int : # noqa: RUF029
113+ return 42
114+
115+ result = await asyncio .to_thread (cli_run_with_db , return_42 , db_url = sqlite_url )
116+
117+ assert result == 42
118+
119+ @pytest .mark .unit
120+ async def test_cli_run_with_db_disposes_engine_on_error (self , sqlite_url : str ) -> None :
121+ """cli_run_with_db disposes the engine via finally even when the function raises."""
122+ err_msg = "boom"
123+
124+ async def raise_error (** _ : object ) -> None : # noqa: RUF029
125+ raise ValueError (err_msg )
126+
127+ with pytest .raises (ValueError , match = err_msg ):
128+ await asyncio .to_thread (cli_run_with_db , raise_error , db_url = sqlite_url )
129+
130+ # Engine was cleaned up; init_engine followed by execute_with_session must work.
131+ async def noop (** _ : object ) -> None :
132+ pass
133+
134+ init_engine (sqlite_url )
135+ await execute_with_session (noop )
136+
137+
138+ class TestCliRunWithEngine :
139+ """Tests for cli_run_with_engine synchronous CLI helper."""
140+
141+ @pytest .mark .unit
142+ async def test_cli_run_with_engine_executes_function (self , sqlite_url : str ) -> None :
143+ """cli_run_with_engine returns the value produced by the async function."""
144+
145+ async def return_hello () -> str : # noqa: RUF029
146+ return "hello"
147+
148+ result = await asyncio .to_thread (cli_run_with_engine , return_hello , db_url = sqlite_url )
149+
150+ assert result == "hello"
151+
152+ @pytest .mark .unit
153+ async def test_cli_run_with_engine_disposes_engine_on_error (self , sqlite_url : str ) -> None :
154+ """cli_run_with_engine disposes the engine via finally even when the function raises."""
155+ err_msg = "boom"
156+
157+ async def raise_error () -> None : # noqa: RUF029
158+ raise ValueError (err_msg )
159+
160+ with pytest .raises (ValueError , match = err_msg ):
161+ await asyncio .to_thread (cli_run_with_engine , raise_error , db_url = sqlite_url )
162+
163+ # Engine was cleaned up; init_engine followed by execute_with_session must work.
164+ async def noop (** _ : object ) -> None :
165+ pass
166+
167+ init_engine (sqlite_url )
168+ await execute_with_session (noop )
169+
83170
84171class TestWithEngine :
85172 """Tests for the with_engine decorator factory."""
@@ -96,3 +183,15 @@ async def my_job() -> None: # noqa: RUF029
96183 await my_job ()
97184
98185 assert calls == [True ]
186+
187+ @pytest .mark .unit
188+ async def test_with_engine_propagates_exception (self , sqlite_url : str ) -> None :
189+ """Exceptions raised inside a @with_engine-decorated function are re-raised."""
190+ err_msg = "job failed"
191+
192+ @with_engine (db_url = sqlite_url )
193+ async def failing_job () -> None : # noqa: RUF029
194+ raise RuntimeError (err_msg )
195+
196+ with pytest .raises (RuntimeError , match = err_msg ):
197+ await failing_job ()
0 commit comments