@@ -1155,202 +1155,70 @@ async def test_prepare_tables_idempotent_after_creation():
11551155 await service .close ()
11561156
11571157
1158- @pytest .mark .asyncio
1159- async def test_append_event_skips_for_update_when_no_app_or_user_delta ():
1160- """FOR UPDATE locks on app_states/user_states should be skipped when the
1161- event carries no delta for those scopes."""
1162- service = DatabaseSessionService ('sqlite+aiosqlite:///:memory:' )
1163- try :
1164- session = await service .create_session (
1165- app_name = 'my_app' , user_id = 'user' , session_id = 's1'
1166- )
1167-
1168- # Patch _supports_row_level_locking to simulate PostgreSQL behavior
1169- with mock .patch .object (
1170- service , '_supports_row_level_locking' , return_value = True
1171- ):
1172- original_fn = database_session_service ._select_required_state
1173- calls = []
1174-
1175- async def spy (** kwargs ):
1176- calls .append (kwargs .get ('use_row_level_locking' ))
1177- return await original_fn (** kwargs )
1178-
1179- with mock .patch .object (
1180- database_session_service ,
1181- '_select_required_state' ,
1182- side_effect = spy ,
1183- ):
1184- # Event with only session-scoped state (no app: or user: prefix)
1185- event_no_scoped_delta = Event (
1186- invocation_id = 'inv1' ,
1187- author = 'user' ,
1188- actions = EventActions (state_delta = {'session_key' : 'v1' }),
1189- )
1190- await service .append_event (session , event_no_scoped_delta )
1191-
1192- # Two calls: one for app_states, one for user_states
1193- assert len (calls ) == 2
1194- # Both should be False since there's no app: or user: delta
1195- assert calls [0 ] is False
1196- assert calls [1 ] is False
1197- finally :
1198- await service .close ()
1199-
1200-
1201- @pytest .mark .asyncio
1202- async def test_append_event_skips_for_update_when_no_state_delta ():
1203- """FOR UPDATE locks should be skipped when event has no state_delta at all."""
1204- service = DatabaseSessionService ('sqlite+aiosqlite:///:memory:' )
1205- try :
1206- session = await service .create_session (
1207- app_name = 'my_app' , user_id = 'user' , session_id = 's1'
1208- )
1209-
1210- with mock .patch .object (
1211- service , '_supports_row_level_locking' , return_value = True
1212- ):
1213- original_fn = database_session_service ._select_required_state
1214- calls = []
1215-
1216- async def spy (** kwargs ):
1217- calls .append (kwargs .get ('use_row_level_locking' ))
1218- return await original_fn (** kwargs )
1219-
1220- with mock .patch .object (
1221- database_session_service ,
1222- '_select_required_state' ,
1223- side_effect = spy ,
1224- ):
1225- event_no_delta = Event (
1226- invocation_id = 'inv2' ,
1227- author = 'user' ,
1228- )
1229- await service .append_event (session , event_no_delta )
1230-
1231- assert len (calls ) == 2
1232- assert calls [0 ] is False
1233- assert calls [1 ] is False
1234- finally :
1235- await service .close ()
1236-
1237-
1238- @pytest .mark .asyncio
1239- async def test_append_event_acquires_for_update_when_app_delta_present ():
1240- """FOR UPDATE lock on app_states should be acquired when event has app:
1241- prefixed state delta."""
1158+ @pytest .fixture
1159+ async def lock_spy_harness ():
1160+ """Sets up a DatabaseSessionService with a spy on _select_required_state."""
12421161 service = DatabaseSessionService ('sqlite+aiosqlite:///:memory:' )
12431162 try :
12441163 session = await service .create_session (
12451164 app_name = 'my_app' , user_id = 'user' , session_id = 's1'
12461165 )
12471166
1248- with mock .patch .object (
1249- service , '_supports_row_level_locking' , return_value = True
1167+ calls = []
1168+ original_fn = database_session_service ._select_required_state
1169+
1170+ async def spy (** kwargs ):
1171+ calls .append (kwargs .get ('use_row_level_locking' ))
1172+ return await original_fn (** kwargs )
1173+
1174+ with (
1175+ mock .patch .object (
1176+ service , '_supports_row_level_locking' , return_value = True
1177+ ),
1178+ mock .patch .object (
1179+ database_session_service ,
1180+ '_select_required_state' ,
1181+ side_effect = spy ,
1182+ ),
12501183 ):
1251- original_fn = database_session_service ._select_required_state
1252- calls = []
1253-
1254- async def spy (** kwargs ):
1255- calls .append (kwargs .get ('use_row_level_locking' ))
1256- return await original_fn (** kwargs )
1257-
1258- with mock .patch .object (
1259- database_session_service ,
1260- '_select_required_state' ,
1261- side_effect = spy ,
1262- ):
1263- event_app_delta = Event (
1264- invocation_id = 'inv3' ,
1265- author = 'user' ,
1266- actions = EventActions (state_delta = {'app:key1' : 'v1' }),
1267- )
1268- await service .append_event (session , event_app_delta )
1269-
1270- assert len (calls ) == 2
1271- # app_states lock: True (has app: delta)
1272- assert calls [0 ] is True
1273- # user_states lock: False (no user: delta)
1274- assert calls [1 ] is False
1184+ yield service , session , calls
12751185 finally :
12761186 await service .close ()
12771187
12781188
12791189@pytest .mark .asyncio
1280- async def test_append_event_acquires_for_update_when_user_delta_present ():
1281- """FOR UPDATE lock on user_states should be acquired when event has user:
1282- prefixed state delta."""
1283- service = DatabaseSessionService ('sqlite+aiosqlite:///:memory:' )
1284- try :
1285- session = await service .create_session (
1286- app_name = 'my_app' , user_id = 'user' , session_id = 's1'
1287- )
1288-
1289- with mock .patch .object (
1290- service , '_supports_row_level_locking' , return_value = True
1291- ):
1292- original_fn = database_session_service ._select_required_state
1293- calls = []
1294-
1295- async def spy (** kwargs ):
1296- calls .append (kwargs .get ('use_row_level_locking' ))
1297- return await original_fn (** kwargs )
1298-
1299- with mock .patch .object (
1300- database_session_service ,
1301- '_select_required_state' ,
1302- side_effect = spy ,
1303- ):
1304- event_user_delta = Event (
1305- invocation_id = 'inv4' ,
1306- author = 'user' ,
1307- actions = EventActions (state_delta = {'user:key1' : 'v1' }),
1308- )
1309- await service .append_event (session , event_user_delta )
1310-
1311- assert len (calls ) == 2
1312- # app_states lock: False (no app: delta)
1313- assert calls [0 ] is False
1314- # user_states lock: True (has user: delta)
1315- assert calls [1 ] is True
1316- finally :
1317- await service .close ()
1318-
1319-
1320- @pytest .mark .asyncio
1321- async def test_append_event_acquires_for_update_when_both_deltas_present ():
1322- """FOR UPDATE locks on both app_states and user_states should be acquired
1323- when the event has both app: and user: prefixed state deltas."""
1324- service = DatabaseSessionService ('sqlite+aiosqlite:///:memory:' )
1325- try :
1326- session = await service .create_session (
1327- app_name = 'my_app' , user_id = 'user' , session_id = 's1'
1328- )
1329-
1330- with mock .patch .object (
1331- service , '_supports_row_level_locking' , return_value = True
1332- ):
1333- original_fn = database_session_service ._select_required_state
1334- calls = []
1335-
1336- async def spy (** kwargs ):
1337- calls .append (kwargs .get ('use_row_level_locking' ))
1338- return await original_fn (** kwargs )
1339-
1340- with mock .patch .object (
1341- database_session_service ,
1342- '_select_required_state' ,
1343- side_effect = spy ,
1344- ):
1345- event_both = Event (
1346- invocation_id = 'inv5' ,
1347- author = 'user' ,
1348- actions = EventActions (state_delta = {'app:ak' : 'av' , 'user:uk' : 'uv' }),
1349- )
1350- await service .append_event (session , event_both )
1351-
1352- assert len (calls ) == 2
1353- assert calls [0 ] is True
1354- assert calls [1 ] is True
1355- finally :
1356- await service .close ()
1190+ @pytest .mark .parametrize (
1191+ 'state_delta, expected_app_lock, expected_user_lock' ,
1192+ [
1193+ pytest .param (
1194+ {'session_key' : 'v1' },
1195+ False ,
1196+ False ,
1197+ id = 'session_only_delta' ,
1198+ ),
1199+ pytest .param (None , False , False , id = 'no_state_delta' ),
1200+ pytest .param ({'app:key1' : 'v1' }, True , False , id = 'app_delta_only' ),
1201+ pytest .param ({'user:key1' : 'v1' }, False , True , id = 'user_delta_only' ),
1202+ pytest .param (
1203+ {'app:ak' : 'av' , 'user:uk' : 'uv' },
1204+ True ,
1205+ True ,
1206+ id = 'both_app_and_user_delta' ,
1207+ ),
1208+ ],
1209+ )
1210+ async def test_append_event_conditional_for_update_locking (
1211+ lock_spy_harness , state_delta , expected_app_lock , expected_user_lock
1212+ ):
1213+ """FOR UPDATE locks should only be acquired for scopes that have deltas."""
1214+ service , session , calls = lock_spy_harness
1215+
1216+ kwargs = {'invocation_id' : 'inv' , 'author' : 'user' }
1217+ if state_delta is not None :
1218+ kwargs ['actions' ] = EventActions (state_delta = state_delta )
1219+ event = Event (** kwargs )
1220+ await service .append_event (session , event )
1221+
1222+ assert len (calls ) == 2
1223+ assert calls [0 ] is expected_app_lock
1224+ assert calls [1 ] is expected_user_lock
0 commit comments