From ec6145961903f876ab70db4bfbafce6e98410acb Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <1051233107@qq.com> Date: Sat, 16 May 2026 11:31:59 +0800 Subject: [PATCH] fix(api): avoid mutating bot update payload (#2194) --- src/langbot/pkg/api/http/service/bot.py | 14 +++-- .../api/http/service/test_bot_service.py | 62 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 tests/unit_tests/api/http/service/test_bot_service.py diff --git a/src/langbot/pkg/api/http/service/bot.py b/src/langbot/pkg/api/http/service/bot.py index 8cdb701d..b8af0861 100644 --- a/src/langbot/pkg/api/http/service/bot.py +++ b/src/langbot/pkg/api/http/service/bot.py @@ -120,24 +120,26 @@ class BotService: async def update_bot(self, bot_uuid: str, bot_data: dict) -> None: """Update bot""" - if 'uuid' in bot_data: - del bot_data['uuid'] + update_data = bot_data.copy() + + if 'uuid' in update_data: + del update_data['uuid'] # set use_pipeline_name - if 'use_pipeline_uuid' in bot_data: + if 'use_pipeline_uuid' in update_data: result = await self.ap.persistence_mgr.execute_async( sqlalchemy.select(persistence_pipeline.LegacyPipeline).where( - persistence_pipeline.LegacyPipeline.uuid == bot_data['use_pipeline_uuid'] + persistence_pipeline.LegacyPipeline.uuid == update_data['use_pipeline_uuid'] ) ) pipeline = result.first() if pipeline is not None: - bot_data['use_pipeline_name'] = pipeline.name + update_data['use_pipeline_name'] = pipeline.name else: raise Exception('Pipeline not found') await self.ap.persistence_mgr.execute_async( - sqlalchemy.update(persistence_bot.Bot).values(bot_data).where(persistence_bot.Bot.uuid == bot_uuid) + sqlalchemy.update(persistence_bot.Bot).values(update_data).where(persistence_bot.Bot.uuid == bot_uuid) ) await self.ap.platform_mgr.remove_bot(bot_uuid) diff --git a/tests/unit_tests/api/http/service/test_bot_service.py b/tests/unit_tests/api/http/service/test_bot_service.py new file mode 100644 index 00000000..6fdc2342 --- /dev/null +++ b/tests/unit_tests/api/http/service/test_bot_service.py @@ -0,0 +1,62 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock + +from sqlalchemy.sql.dml import Update + +from langbot.pkg.api.http.service.bot import BotService + + +class _FakeResult: + def __init__(self, value): + self.value = value + + def first(self): + return self.value + + +class _PersistenceManager: + def __init__(self): + self.update_values = None + + async def execute_async(self, statement): + if isinstance(statement, Update): + self.update_values = { + key: value for key, value in statement.compile().params.items() if not key.startswith('uuid_') + } + return None + + return _FakeResult(SimpleNamespace(name='Updated Pipeline')) + + +async def test_update_bot_copies_input_before_filtering_and_setting_pipeline_name(): + persistence_mgr = _PersistenceManager() + runtime_bot = SimpleNamespace(enable=False) + platform_mgr = SimpleNamespace( + remove_bot=AsyncMock(), + load_bot=AsyncMock(return_value=runtime_bot), + ) + ap = SimpleNamespace( + persistence_mgr=persistence_mgr, + platform_mgr=platform_mgr, + sess_mgr=SimpleNamespace(session_list=[]), + ) + service = BotService(ap) + service.get_bot = AsyncMock(return_value={'uuid': 'bot-1'}) + payload = { + 'uuid': 'caller-owned-uuid', + 'name': 'Test Bot', + 'use_pipeline_uuid': 'pipeline-1', + } + + await service.update_bot('bot-1', payload) + + assert payload == { + 'uuid': 'caller-owned-uuid', + 'name': 'Test Bot', + 'use_pipeline_uuid': 'pipeline-1', + } + assert persistence_mgr.update_values == { + 'name': 'Test Bot', + 'use_pipeline_uuid': 'pipeline-1', + 'use_pipeline_name': 'Updated Pipeline', + }