mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 01:15:24 +08:00
* fix(pipeline): decouple runner from extension allowlist Co-authored-by: multica-agent <[email protected]> * fix(pipeline): complete runner diagnostics and real runtime coverage Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
125 lines
6.3 KiB
Python
125 lines
6.3 KiB
Python
"""Runner selection must survive restricted extension preferences on disk."""
|
|
|
|
from types import SimpleNamespace as NS
|
|
import json
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
import sqlalchemy as sa
|
|
|
|
from langbot.pkg.api.http.service.pipeline import PipelineService
|
|
from langbot.pkg.entity.persistence.pipeline import LegacyPipeline
|
|
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
|
from tests.unit_tests.api.service.test_pipeline_migration import env, context, RID # noqa: F401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_and_mcp_share_real_persistence_without_expanding_extensions(env, monkeypatch): # noqa: F811
|
|
from quart import Quart
|
|
from langbot.pkg.api.http.controller.groups.pipelines.pipelines import PipelinesRouterGroup
|
|
from langbot.pkg.api.mcp.server import LangBotMCPServer
|
|
|
|
monkeypatch.setattr('langbot.pkg.api.http.service.workspace_limits.workspace_limits', AsyncMock(return_value={}))
|
|
monkeypatch.setattr('langbot.pkg.api.mcp.server.get_request_context', context)
|
|
monkeypatch.setattr('langbot.pkg.api.mcp.server.get_workspace_language', AsyncMock(return_value='en_US'))
|
|
monkeypatch.setattr('langbot.pkg.api.http.controller.group.get_workspace_language', AsyncMock(return_value='en_US'))
|
|
env.ap.ver_mgr = NS(get_current_version=Mock(return_value='4.10'))
|
|
env.ap.pipeline_mgr.load_pipeline = AsyncMock()
|
|
env.ap.pipeline_mgr.remove_pipeline = AsyncMock()
|
|
env.ap.pipeline_service = PipelineService(env.ap)
|
|
app = Quart(__name__)
|
|
routes = PipelinesRouterGroup(env.ap, app)
|
|
monkeypatch.setattr(routes, '_authenticate_api_key', AsyncMock(return_value=context()))
|
|
monkeypatch.setattr(routes, '_record_operation', AsyncMock())
|
|
await routes.initialize()
|
|
http = app.test_client()
|
|
mcp = LangBotMCPServer(env.ap)
|
|
headers = {'X-API-Key': 'isolated-test-key'}
|
|
config = {
|
|
'ai': {
|
|
'runner': {'id': RID, 'expire-time': 0},
|
|
'runner_config': {RID: {'unknown': {'keep': [None, False, ' raw ']}}},
|
|
}
|
|
}
|
|
prefs = {'enable_all_plugins': False, 'plugins': [], 'enable_all_mcp_servers': False, 'mcp_servers': []}
|
|
async with env.engine.begin() as conn:
|
|
await conn.execute(
|
|
sa.update(LegacyPipeline)
|
|
.where(LegacyPipeline.uuid == 'one')
|
|
.values(config=config, extensions_preferences=prefs)
|
|
)
|
|
response = await http.put('/api/v1/pipelines/one', headers=headers, json={'config': config})
|
|
assert response.status_code == 200, await response.get_data(as_text=True)
|
|
assert (await response.get_json())['code'] == 0
|
|
result = await mcp.mcp.call_tool('get_pipeline', {'pipeline_uuid': 'one'})
|
|
saved = json.loads(result[0][0].text)
|
|
assert saved['config'] == config
|
|
assert saved['extensions_preferences'] == prefs
|
|
config['ai']['runner_config'][RID]['edited'] = 'via MCP'
|
|
result = await mcp.mcp.call_tool('update_pipeline', {'pipeline_uuid': 'one', 'pipeline_data': {'config': config}})
|
|
assert json.loads(result[0][0].text) == {'ok': True}
|
|
copied_response = await http.post('/api/v1/pipelines/one/copy', headers=headers)
|
|
assert copied_response.status_code == 200, await copied_response.get_data(as_text=True)
|
|
copied = (await copied_response.get_json())['data']['uuid']
|
|
for pipeline_id in ('one', copied):
|
|
response = await http.get(f'/api/v1/pipelines/{pipeline_id}', headers=headers)
|
|
saved = (await response.get_json())['data']['pipeline']
|
|
assert saved['config'] == config
|
|
assert saved['extensions_preferences']['plugins'] == []
|
|
assert saved['extensions_preferences']['enable_all_plugins'] is False
|
|
async with env.engine.connect() as conn:
|
|
row = (
|
|
await conn.execute(
|
|
sa.select(LegacyPipeline.config, LegacyPipeline.extensions_preferences).where(
|
|
LegacyPipeline.uuid == pipeline_id
|
|
)
|
|
)
|
|
).one()
|
|
assert row.config == config
|
|
assert row.extensions_preferences['plugins'] == []
|
|
assert row.extensions_preferences['enable_all_plugins'] is False
|
|
body = {'name': 'HTTP creation', 'description': '', 'config': config, 'extensions_preferences': prefs}
|
|
response = await http.post('/api/v1/pipelines', headers=headers, json=body)
|
|
assert response.status_code == 200, await response.get_data(as_text=True)
|
|
http_id = (await response.get_json())['data']['uuid']
|
|
body['name'] = 'MCP creation'
|
|
result = await mcp.mcp.call_tool('create_pipeline', {'pipeline_data': body})
|
|
mcp_id = json.loads(result[0][0].text)['uuid']
|
|
for pipeline_id in (http_id, mcp_id):
|
|
saved = await env.ap.pipeline_service.get_pipeline(context(), pipeline_id, include_secret=True)
|
|
assert saved['config'] == config
|
|
assert saved['extensions_preferences'] == prefs
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_edit_and_copy_do_not_expand_extension_permissions(env, monkeypatch): # noqa: F811
|
|
monkeypatch.setattr(
|
|
'langbot.pkg.api.http.service.workspace_limits.workspace_limits',
|
|
AsyncMock(return_value={}),
|
|
)
|
|
env.ap.ver_mgr = NS(get_current_version=Mock(return_value='4.10'))
|
|
env.ap.pipeline_mgr.load_pipeline = AsyncMock()
|
|
env.ap.pipeline_mgr.remove_pipeline = AsyncMock()
|
|
service = PipelineService(env.ap)
|
|
config = {'ai': {'runner': {'id': RID}, 'runner_config': {RID: {}}}}
|
|
prefs = {'enable_all_plugins': False, 'plugins': [], 'enable_all_mcp_servers': False, 'mcp_servers': []}
|
|
# Existing, pre-fix configuration: selected Runner with no extensions.
|
|
async with env.engine.begin() as conn:
|
|
await conn.execute(
|
|
sa.update(LegacyPipeline)
|
|
.where(LegacyPipeline.uuid == 'one')
|
|
.values(
|
|
config=config,
|
|
extensions_preferences=prefs,
|
|
)
|
|
)
|
|
await service.update_pipeline(context(), 'one', {'config': config, 'name': 'Edited'})
|
|
copied = await service.copy_pipeline(context(), 'one')
|
|
for pid in ('one', copied):
|
|
saved = await service.get_pipeline(context(), pid, include_secret=True)
|
|
assert saved['config'] == config
|
|
assert saved['extensions_preferences']['plugins'] == []
|
|
assert saved['extensions_preferences']['enable_all_plugins'] is False
|
|
with pytest.raises(WorkspaceNotFoundError):
|
|
await service.copy_pipeline(context(), 'foreign')
|