@@ -258,6 +258,41 @@ async def test_delete_entity_without_permalink(search_service, sample_entity):
258258 await search_service .handle_delete (sample_entity )
259259
260260
261+ @pytest .mark .asyncio
262+ async def test_handle_delete_clears_entity_vectors (
263+ search_service , sample_entity , monkeypatch
264+ ):
265+ """Regression guard for #764: handle_delete must drive vector-row cleanup
266+ so deleting an entity doesn't leave orphaned rows in `search_vector_chunks`
267+ or `search_vector_embeddings`.
268+
269+ Verified by spying on the repository's `delete_entity_vector_rows`. The
270+ short-circuit path inside `_clear_entity_vectors` (semantic disabled) is
271+ bypassed by forcing `_semantic_enabled=True` so we exercise the real
272+ delegation, not the no-op branch.
273+ """
274+ calls : list [int ] = []
275+
276+ async def spy_delete_entity_vector_rows (entity_id : int ) -> None :
277+ calls .append (entity_id )
278+
279+ # Force the cleanup path even if the test repo is configured without
280+ # semantic enabled — we're asserting the wiring, not embedding behavior.
281+ monkeypatch .setattr (search_service .repository , "_semantic_enabled" , True )
282+ monkeypatch .setattr (
283+ search_service .repository ,
284+ "delete_entity_vector_rows" ,
285+ spy_delete_entity_vector_rows ,
286+ )
287+
288+ await search_service .handle_delete (sample_entity )
289+
290+ assert calls == [sample_entity .id ], (
291+ f"handle_delete must call delete_entity_vector_rows({ sample_entity .id } ); "
292+ f"got calls={ calls } "
293+ )
294+
295+
261296@pytest .mark .asyncio
262297async def test_no_criteria (search_service , test_graph ):
263298 """Test search with no criteria returns empty list."""
0 commit comments