@@ -2426,3 +2426,116 @@ async def test_fast_write_entity_null_user_id(entity_service: EntityService):
24262426 entity = await entity_service .fast_write_entity (schema , external_id = str (uuid .uuid4 ()))
24272427 assert entity .created_by is None
24282428 assert entity .last_updated_by is None
2429+
2430+
2431+ # --- Concurrent Delete Resilience ---
2432+
2433+
2434+ @pytest .mark .asyncio
2435+ async def test_delete_entity_by_id_already_deleted (entity_service : EntityService ):
2436+ """delete_entity returns True when entity was already deleted (concurrent delete)."""
2437+ entity_data = EntitySchema (
2438+ title = "ConcurrentDeleteTarget" ,
2439+ directory = "test" ,
2440+ note_type = "test" ,
2441+ )
2442+ created = await entity_service .create_entity (entity_data )
2443+ entity_id = created .id
2444+
2445+ # Delete once - should succeed
2446+ assert await entity_service .delete_entity (entity_id ) is True
2447+
2448+ # Delete again by ID - should return True (already deleted), not raise ValueError
2449+ assert await entity_service .delete_entity (entity_id ) is True
2450+
2451+
2452+ @pytest .mark .asyncio
2453+ async def test_delete_entity_by_permalink_already_deleted (entity_service : EntityService ):
2454+ """delete_entity returns True when entity was already deleted by permalink."""
2455+ entity_data = EntitySchema (
2456+ title = "ConcurrentDeleteByPermalink" ,
2457+ directory = "test" ,
2458+ note_type = "test" ,
2459+ )
2460+ created = await entity_service .create_entity (entity_data )
2461+
2462+ # Delete once
2463+ assert await entity_service .delete_entity (created .id ) is True
2464+
2465+ # Delete again by permalink - should return True (EntityNotFoundError caught)
2466+ assert await entity_service .delete_entity (entity_data .permalink ) is True
2467+
2468+
2469+ @pytest .mark .asyncio
2470+ async def test_delete_directory_concurrent_resilience (entity_service : EntityService ):
2471+ """delete_directory succeeds even when entities are concurrently deleted."""
2472+ # Create entities in a directory
2473+ entities = []
2474+ for i in range (5 ):
2475+ entity_data = EntitySchema (
2476+ title = f"DirEntity{ i } " ,
2477+ directory = "concurrent-test" ,
2478+ note_type = "test" ,
2479+ )
2480+ created = await entity_service .create_entity (entity_data )
2481+ entities .append (created )
2482+
2483+ # Delete some entities directly to simulate concurrent delete
2484+ await entity_service .delete_entity (entities [0 ].id )
2485+ await entity_service .delete_entity (entities [2 ].id )
2486+
2487+ # Now delete the directory - should handle already-deleted entities gracefully
2488+ result = await entity_service .delete_directory ("concurrent-test" )
2489+
2490+ # Only 3 remain in DB (2 were already deleted before the directory query)
2491+ assert result .total_files == 3
2492+ assert result .successful_deletes == 3
2493+ assert result .failed_deletes == 0
2494+ assert len (result .errors ) == 0
2495+
2496+
2497+ @pytest .mark .asyncio
2498+ async def test_delete_directory_all_already_deleted (entity_service : EntityService ):
2499+ """delete_directory handles case where all entities were concurrently deleted."""
2500+ # Create entities
2501+ created_entities = []
2502+ for i in range (3 ):
2503+ entity_data = EntitySchema (
2504+ title = f"AllGone{ i } " ,
2505+ directory = "all-deleted" ,
2506+ note_type = "test" ,
2507+ )
2508+ created = await entity_service .create_entity (entity_data )
2509+ created_entities .append (created )
2510+
2511+ # Delete all entities directly
2512+ for e in created_entities :
2513+ await entity_service .delete_entity (e .id )
2514+
2515+ # Directory delete should report empty (entities no longer found in prefix query)
2516+ result = await entity_service .delete_directory ("all-deleted" )
2517+ assert result .total_files == 0
2518+ assert result .successful_deletes == 0
2519+ assert result .failed_deletes == 0
2520+
2521+
2522+ @pytest .mark .asyncio
2523+ async def test_delete_directory_entity_deleted_between_query_and_delete (
2524+ entity_service : EntityService ,
2525+ ):
2526+ """Simulates the real race condition: entity exists in prefix query but is deleted
2527+ by a concurrent request before delete_entity is called."""
2528+ # Create entities
2529+ entity_data = EntitySchema (title = "RaceTarget" , directory = "race-dir" , note_type = "test" )
2530+ created = await entity_service .create_entity (entity_data )
2531+
2532+ # Get the entities via prefix query (as delete_directory does)
2533+ entities = await entity_service .repository .find_by_directory_prefix ("race-dir" )
2534+ assert len (entities ) == 1
2535+
2536+ # Now delete the entity behind the scenes (simulating a concurrent request)
2537+ await entity_service .delete_entity (created .id )
2538+
2539+ # Call delete_entity with the stale entity ID - should return True, not raise
2540+ result = await entity_service .delete_entity (entities [0 ].id )
2541+ assert result is True
0 commit comments