fix(runtime): preserve explicit replies and report bot configuration errors

This commit is contained in:
RockChinQ
2026-09-10 17:21:22 +08:00
parent b7a04f7a24
commit 8903a40c41
41 changed files with 1108 additions and 155 deletions
@@ -0,0 +1,138 @@
from __future__ import annotations
import sys
import types
from importlib import import_module
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
import quart
core_app_module = types.ModuleType('langbot.pkg.core.app')
core_app_module.Application = object
sys.modules.setdefault('langbot.pkg.core.app', core_app_module)
pytestmark = pytest.mark.asyncio
async def _create_test_client(bot_service: SimpleNamespace):
app = quart.Quart(__name__)
account = SimpleNamespace(
uuid='account-test',
user='test@example.com',
)
user_service = SimpleNamespace(
get_authenticated_account=AsyncMock(return_value=account),
)
access = SimpleNamespace(
workspace=SimpleNamespace(uuid='workspace-test'),
membership=SimpleNamespace(
uuid='membership-test',
role='developer',
projection_revision=1,
),
execution=SimpleNamespace(
instance_uuid='instance-test',
placement_generation=1,
),
)
ap = SimpleNamespace(
bot_service=bot_service,
user_service=user_service,
apikey_service=SimpleNamespace(authenticate_api_key=AsyncMock(return_value=None)),
workspace_collaboration_service=SimpleNamespace(resolve_account_workspace=AsyncMock(return_value=access)),
)
BotsRouterGroup = import_module('langbot.pkg.api.http.controller.groups.platform.bots').BotsRouterGroup
group = BotsRouterGroup(ap, app)
await group.initialize()
return app.test_client()
@pytest.mark.parametrize('method,path', [('post', '/api/v1/platform/bots'), ('put', '/api/v1/platform/bots/bot-1')])
async def test_bot_config_error_preserves_details(method, path):
error = ValueError('Lark missing required config: app_id, app_secret, bot_name')
service = SimpleNamespace(create_bot=AsyncMock(side_effect=error), update_bot=AsyncMock(side_effect=error))
client = await _create_test_client(service)
response = await getattr(client, method)(
path, json={'adapter_config': {}}, headers={'Authorization': 'Bearer token'}
)
assert response.status_code == 400
body = await response.get_json()
assert body['code'] == 'invalid_bot_config'
assert body['msg'] == str(error)
async def test_bot_apply_failure_identifies_persisted_record():
from langbot.pkg.api.http.service.bot_errors import BotApplyError
service = SimpleNamespace(create_bot=AsyncMock(side_effect=BotApplyError('Missing app_id', 'saved-bot')))
client = await _create_test_client(service)
response = await client.post('/api/v1/platform/bots', json={}, headers={'Authorization': 'Bearer token'})
assert response.status_code == 400
body = await response.get_json()
assert body.pop('request_id')
assert body == {
'code': 'bot_apply_failed',
'msg': 'Missing app_id',
'data': {'uuid': 'saved-bot'},
}
async def test_unexpected_failure_keeps_request_reference_without_exception_details():
client = await _create_test_client(
SimpleNamespace(update_bot=AsyncMock(side_effect=RuntimeError('database password')))
)
response = await client.put('/api/v1/platform/bots/bot-1', json={}, headers={'Authorization': 'Bearer token'})
body = await response.get_json()
assert response.status_code == 500
assert body['request_id']
assert 'database password' not in str(body)
async def test_invalid_request_body_is_actionable():
service = SimpleNamespace(update_bot=AsyncMock())
client = await _create_test_client(service)
response = await client.put('/api/v1/platform/bots/bot-1', json=[], headers={'Authorization': 'Bearer token'})
assert response.status_code == 400
service.update_bot.assert_not_awaited()
async def test_runtime_error_redacts_persisted_secrets_on_partial_update():
from langbot.pkg.api.http.service.bot import BotService
from langbot.pkg.api.http.service.bot_errors import BotApplyError
ap = SimpleNamespace(
persistence_mgr=SimpleNamespace(execute_async=AsyncMock(return_value=SimpleNamespace(rowcount=1))),
platform_mgr=SimpleNamespace(
remove_bot=AsyncMock(),
load_bot=AsyncMock(side_effect=ValueError('Invalid app_secret: persisted-secret-value')),
),
)
service = BotService(ap)
service.get_bot = AsyncMock(
return_value={'uuid': 'bot-1', 'adapter_config': {'app_secret': 'persisted-secret-value'}}
)
with pytest.raises(BotApplyError) as captured:
await service.update_bot('workspace-test', 'bot-1', {'enable': True})
assert str(captured.value) == 'Invalid app_secret: ***'
assert captured.value.bot_uuid == 'bot-1'
ap.persistence_mgr.execute_async.assert_awaited_once()
async def test_validation_error_does_not_include_input_values():
import pydantic
from langbot.pkg.api.http.service.bot_errors import bot_error_message
class Config(pydantic.BaseModel):
app_secret: int
try:
Config(app_secret='private-value')
except pydantic.ValidationError as error:
result = bot_error_message(error, {'app_secret': 'private-value'})
assert 'app_secret' in result
assert 'private-value' not in result
assert 'input_value' not in result