diff --git a/src/langbot/pkg/api/http/service/pipeline.py b/src/langbot/pkg/api/http/service/pipeline.py index ad75ffe7..9175aba5 100644 --- a/src/langbot/pkg/api/http/service/pipeline.py +++ b/src/langbot/pkg/api/http/service/pipeline.py @@ -113,14 +113,9 @@ class PipelineService: return pipeline_data['uuid'] async def update_pipeline(self, pipeline_uuid: str, pipeline_data: dict) -> None: - if 'uuid' in pipeline_data: - del pipeline_data['uuid'] - if 'for_version' in pipeline_data: - del pipeline_data['for_version'] - if 'stages' in pipeline_data: - del pipeline_data['stages'] - if 'is_default' in pipeline_data: - del pipeline_data['is_default'] + pipeline_data = pipeline_data.copy() + for protected_field in ('uuid', 'for_version', 'stages', 'is_default'): + pipeline_data.pop(protected_field, None) await self.ap.persistence_mgr.execute_async( sqlalchemy.update(persistence_pipeline.LegacyPipeline) diff --git a/tests/unit_tests/pipeline/test_pipeline_service.py b/tests/unit_tests/pipeline/test_pipeline_service.py new file mode 100644 index 00000000..b862c3ff --- /dev/null +++ b/tests/unit_tests/pipeline/test_pipeline_service.py @@ -0,0 +1,43 @@ +from unittest.mock import AsyncMock, Mock + +import pytest + +from langbot.pkg.api.http.service.pipeline import PipelineService + + +@pytest.mark.asyncio +async def test_update_pipeline_filters_protected_fields_without_mutating_input(mock_app): + service = PipelineService(mock_app) + loaded_pipeline = Mock() + service.get_pipeline = AsyncMock(return_value=loaded_pipeline) + + bot = Mock(uuid='bot-uuid') + bot_result = Mock(all=Mock(return_value=[bot])) + mock_app.persistence_mgr.execute_async = AsyncMock(side_effect=[None, bot_result]) + mock_app.bot_service = Mock(update_bot=AsyncMock()) + mock_app.pipeline_mgr = Mock(remove_pipeline=AsyncMock(), load_pipeline=AsyncMock()) + mock_app.sess_mgr.session_list = [] + + pipeline_data = { + 'uuid': 'caller-uuid', + 'for_version': '1.0.0', + 'stages': ['CallerStage'], + 'is_default': True, + 'name': 'Updated pipeline', + } + original_pipeline_data = pipeline_data.copy() + + await service.update_pipeline('pipeline-uuid', pipeline_data) + + assert pipeline_data == original_pipeline_data + + update_stmt = mock_app.persistence_mgr.execute_async.await_args_list[0].args[0] + updated_fields = {getattr(field, 'key', str(field)) for field in update_stmt._values} + assert updated_fields == {'name'} + + mock_app.bot_service.update_bot.assert_awaited_once_with( + 'bot-uuid', + {'use_pipeline_name': 'Updated pipeline'}, + ) + mock_app.pipeline_mgr.remove_pipeline.assert_awaited_once_with('pipeline-uuid') + mock_app.pipeline_mgr.load_pipeline.assert_awaited_once_with(loaded_pipeline)