fix(mcp-kb): add transactional batch insert and atomic document update

- Wrap store_embeddings_batch in transaction for all-or-nothing semantics
- Add replace_source_embeddings method for atomic document updates
- Update collection_manager to use transactional replace
- Prevents race conditions and data inconsistency (closes #77)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-04 01:07:40 +01:00
parent 953af52d0e
commit cd7a9ccbdf
4 changed files with 195 additions and 50 deletions

View File

@@ -61,6 +61,7 @@ def mock_database():
mock_db.delete_by_source = AsyncMock(return_value=1)
mock_db.delete_collection = AsyncMock(return_value=5)
mock_db.delete_by_ids = AsyncMock(return_value=2)
mock_db.replace_source_embeddings = AsyncMock(return_value=(1, ["new-id-1"]))
mock_db.list_collections = AsyncMock(return_value=[])
mock_db.get_collection_stats = AsyncMock()
mock_db.cleanup_expired = AsyncMock(return_value=0)

View File

@@ -192,7 +192,7 @@ class TestCollectionManager:
@pytest.mark.asyncio
async def test_update_document(self, collection_manager):
"""Test updating a document."""
"""Test updating a document with atomic replace."""
result = await collection_manager.update_document(
project_id="proj-123",
agent_id="agent-456",
@@ -201,9 +201,10 @@ class TestCollectionManager:
collection="default",
)
# Should delete first, then ingest
collection_manager._database.delete_by_source.assert_called_once()
# Should use atomic replace (delete + insert in transaction)
collection_manager._database.replace_source_embeddings.assert_called_once()
assert result.success is True
assert len(result.chunk_ids) == 1
@pytest.mark.asyncio
async def test_cleanup_expired(self, collection_manager):