mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-11 13:20:58 +00:00
chore(merge): sync master into dev/4.11.x
This commit is contained in:
@@ -21,10 +21,13 @@ import json
|
||||
|
||||
from langbot.pkg.api.http.service.pipeline import PipelineService, default_stage_order
|
||||
from langbot.pkg.entity.persistence.pipeline import LegacyPipeline
|
||||
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
||||
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
WORKSPACE_UUID = 'workspace-a'
|
||||
|
||||
|
||||
def _create_mock_pipeline(
|
||||
pipeline_uuid: str = None,
|
||||
@@ -77,7 +80,9 @@ class TestPipelineServiceGetPipelineMetadata:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
result = await service.get_pipeline_metadata()
|
||||
result = await service.get_pipeline_metadata(
|
||||
WORKSPACE_UUID,
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert len(result) == 4
|
||||
@@ -107,7 +112,9 @@ class TestPipelineServiceGetPipelines:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
result = await service.get_pipelines()
|
||||
result = await service.get_pipelines(
|
||||
WORKSPACE_UUID,
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert result == []
|
||||
@@ -133,7 +140,9 @@ class TestPipelineServiceGetPipelines:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
result = await service.get_pipelines()
|
||||
result = await service.get_pipelines(
|
||||
WORKSPACE_UUID,
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert len(result) == 2
|
||||
@@ -152,7 +161,7 @@ class TestPipelineServiceGetPipelines:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
await service.get_pipelines(sort_by='updated_at', sort_order='ASC')
|
||||
await service.get_pipelines(WORKSPACE_UUID, sort_by='updated_at', sort_order='ASC')
|
||||
|
||||
# Verify - execute was called with sort parameters
|
||||
ap.persistence_mgr.execute_async.assert_called_once()
|
||||
@@ -181,7 +190,7 @@ class TestPipelineServiceGetPipeline:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
result = await service.get_pipeline('test-uuid')
|
||||
result = await service.get_pipeline(WORKSPACE_UUID, 'test-uuid')
|
||||
|
||||
# Verify
|
||||
assert result is not None
|
||||
@@ -200,7 +209,7 @@ class TestPipelineServiceGetPipeline:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
result = await service.get_pipeline('nonexistent-uuid')
|
||||
result = await service.get_pipeline(WORKSPACE_UUID, 'nonexistent-uuid')
|
||||
|
||||
# Verify
|
||||
assert result is None
|
||||
@@ -229,64 +238,7 @@ class TestPipelineServiceCreatePipeline:
|
||||
|
||||
# Execute & Verify
|
||||
with pytest.raises(ValueError, match='Maximum number of pipelines'):
|
||||
await service.create_pipeline({'name': 'New Pipeline'})
|
||||
|
||||
@pytest.mark.parametrize('invalid_preferences', [None, [], 'all', 0, False])
|
||||
async def test_create_pipeline_rejects_non_object_extension_preferences(
|
||||
self,
|
||||
invalid_preferences,
|
||||
):
|
||||
service = PipelineService(SimpleNamespace())
|
||||
|
||||
with pytest.raises(ValueError, match='extensions_preferences must be an object'):
|
||||
await service.create_pipeline(
|
||||
{
|
||||
'name': 'Invalid Pipeline',
|
||||
'extensions_preferences': invalid_preferences,
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_create_pipeline_rejects_non_boolean_runner_security_field(self, invalid_value):
|
||||
service = PipelineService(SimpleNamespace())
|
||||
runner_id = 'plugin:test/runner/default'
|
||||
|
||||
with pytest.raises(ValueError, match='enable-all-tools.*boolean'):
|
||||
await service.create_pipeline(
|
||||
{
|
||||
'name': 'Invalid Pipeline',
|
||||
'config': {
|
||||
'ai': {
|
||||
'runner': {'id': runner_id},
|
||||
'runner_config': {runner_id: {'enable-all-tools': invalid_value}},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_create_pipeline_rejects_non_boolean_mcp_resource_enabled(self, invalid_value):
|
||||
service = PipelineService(SimpleNamespace())
|
||||
runner_id = 'plugin:test/runner/default'
|
||||
|
||||
with pytest.raises(ValueError, match=r'mcp-resources\[0\]\.enabled.*boolean'):
|
||||
await service.create_pipeline(
|
||||
{
|
||||
'name': 'Invalid Pipeline',
|
||||
'config': {
|
||||
'ai': {
|
||||
'runner': {'id': runner_id},
|
||||
'runner_config': {
|
||||
runner_id: {
|
||||
'mcp-resources': [
|
||||
{'uri': 'file:///README.md', 'enabled': invalid_value},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline'})
|
||||
|
||||
async def test_create_pipeline_no_limit(self):
|
||||
"""Creates pipeline without limit when max_pipelines=-1."""
|
||||
@@ -315,7 +267,7 @@ class TestPipelineServiceCreatePipeline:
|
||||
with patch(
|
||||
'langbot.pkg.utils.paths.get_resource_path', return_value='templates/default-pipeline-config.json'
|
||||
):
|
||||
bot_uuid = await service.create_pipeline({'name': 'New Pipeline'})
|
||||
bot_uuid = await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline'})
|
||||
|
||||
# Verify
|
||||
assert bot_uuid is not None
|
||||
@@ -350,7 +302,7 @@ class TestPipelineServiceCreatePipeline:
|
||||
with patch(
|
||||
'langbot.pkg.utils.paths.get_resource_path', return_value='templates/default-pipeline-config.json'
|
||||
):
|
||||
await service.create_pipeline({'name': 'Default Pipeline'}, default=True)
|
||||
await service.create_pipeline(WORKSPACE_UUID, {'name': 'Default Pipeline'}, default=True)
|
||||
|
||||
# Verify - execute was called
|
||||
ap.persistence_mgr.execute_async.assert_called()
|
||||
@@ -397,7 +349,7 @@ class TestPipelineServiceCreatePipeline:
|
||||
with patch(
|
||||
'langbot.pkg.utils.paths.get_resource_path', return_value='templates/default-pipeline-config.json'
|
||||
):
|
||||
await service.create_pipeline({'name': 'New Pipeline'})
|
||||
await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline'})
|
||||
|
||||
assert len(insert_params) == 1
|
||||
assert insert_params[0]['extensions_preferences'] == {
|
||||
@@ -410,6 +362,19 @@ class TestPipelineServiceCreatePipeline:
|
||||
}
|
||||
|
||||
|
||||
class _MockResultWithBots:
|
||||
"""Helper class to mock SQLAlchemy result with iterable .all() method."""
|
||||
|
||||
def __init__(self, bots_list):
|
||||
self._bots_list = bots_list
|
||||
|
||||
def all(self):
|
||||
return self._bots_list
|
||||
|
||||
def first(self):
|
||||
return self._bots_list[0] if self._bots_list else None
|
||||
|
||||
|
||||
class TestPipelineServiceUpdatePipeline:
|
||||
"""Tests for update_pipeline method."""
|
||||
|
||||
@@ -423,20 +388,22 @@ class TestPipelineServiceUpdatePipeline:
|
||||
ap.pipeline_mgr.load_pipeline = AsyncMock()
|
||||
ap.sess_mgr = SimpleNamespace()
|
||||
ap.sess_mgr.session_list = []
|
||||
ap.bot_service = None # No bot_service when not updating name
|
||||
|
||||
ap.persistence_mgr.execute_async = AsyncMock()
|
||||
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid', 'name': 'Updated'})
|
||||
|
||||
# Execute with protected fields.
|
||||
# Execute with protected fields - no name change, so no bot sync
|
||||
pipeline_data = {
|
||||
'uuid': 'should-be-removed',
|
||||
'for_version': 'should-be-removed',
|
||||
'stages': ['should-be-removed'],
|
||||
'is_default': True,
|
||||
'description': 'New description',
|
||||
'description': 'New description', # Not name change, so no bot_service needed
|
||||
}
|
||||
await service.update_pipeline('test-uuid', pipeline_data)
|
||||
await service.update_pipeline(WORKSPACE_UUID, 'test-uuid', pipeline_data)
|
||||
|
||||
update_params = ap.persistence_mgr.execute_async.await_args_list[0].args[0].compile().params
|
||||
assert update_params['description'] == 'New description'
|
||||
@@ -444,45 +411,8 @@ class TestPipelineServiceUpdatePipeline:
|
||||
assert ['should-be-removed'] not in update_params.values()
|
||||
assert not any(value is True for value in update_params.values())
|
||||
|
||||
@pytest.mark.parametrize('invalid_preferences', [None, [], 'all', 0, False])
|
||||
async def test_update_pipeline_rejects_non_object_extension_preferences_before_write(
|
||||
self,
|
||||
invalid_preferences,
|
||||
):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
|
||||
with pytest.raises(ValueError, match='extensions_preferences must be an object'):
|
||||
await service.update_pipeline(
|
||||
'test-uuid',
|
||||
{'extensions_preferences': invalid_preferences},
|
||||
)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_update_pipeline_rejects_non_boolean_runner_security_field_before_write(self, invalid_value):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
runner_id = 'plugin:test/runner/default'
|
||||
|
||||
with pytest.raises(ValueError, match='mcp-resource-agent-read-enabled.*boolean'):
|
||||
await service.update_pipeline(
|
||||
'test-uuid',
|
||||
{
|
||||
'config': {
|
||||
'ai': {
|
||||
'runner': {'id': runner_id},
|
||||
'runner_config': {runner_id: {'mcp-resource-agent-read-enabled': invalid_value}},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
|
||||
async def test_update_pipeline_name_does_not_rewrite_bot_routes(self):
|
||||
"""Bot event bindings remain independent from pipeline display names."""
|
||||
async def test_update_pipeline_syncs_bot_names(self):
|
||||
"""Updates bot use_pipeline_name when pipeline name changes."""
|
||||
# Setup
|
||||
ap = SimpleNamespace()
|
||||
ap.persistence_mgr = SimpleNamespace()
|
||||
@@ -494,13 +424,44 @@ class TestPipelineServiceUpdatePipeline:
|
||||
ap.bot_service = SimpleNamespace()
|
||||
ap.bot_service.update_bot = AsyncMock()
|
||||
|
||||
ap.persistence_mgr.execute_async = AsyncMock(return_value=Mock())
|
||||
# Create proper mock Bot entities with uuid attribute
|
||||
mock_bot1 = Mock()
|
||||
mock_bot1.uuid = 'bot-uuid-1'
|
||||
mock_bot2 = Mock()
|
||||
mock_bot2.uuid = 'bot-uuid-2'
|
||||
|
||||
# Create bot list
|
||||
bot_list = [mock_bot1, mock_bot2]
|
||||
|
||||
# Create mock result using helper class
|
||||
bot_result = _MockResultWithBots(bot_list)
|
||||
|
||||
# The order of calls in update_pipeline:
|
||||
# 1. UPDATE (line 125) - returns Mock (no result needed)
|
||||
# 2. SELECT bots (line 136) - returns bot_result with .all()
|
||||
call_count = 0
|
||||
|
||||
async def mock_execute(query):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
# First call is the UPDATE - just return a Mock
|
||||
return Mock()
|
||||
elif call_count == 2:
|
||||
# Second call is the SELECT bots - return proper result
|
||||
return bot_result
|
||||
return Mock() # Any additional calls
|
||||
|
||||
ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
|
||||
ap.persistence_mgr.serialize_model = Mock(return_value={})
|
||||
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid', 'name': 'New Name'})
|
||||
|
||||
await service.update_pipeline('test-uuid', {'name': 'New Name'})
|
||||
# Execute with name change
|
||||
await service.update_pipeline(WORKSPACE_UUID, 'test-uuid', {'name': 'New Name'})
|
||||
|
||||
# Bots no longer denormalize Pipeline names in the EventBindings model.
|
||||
ap.bot_service.update_bot.assert_not_awaited()
|
||||
|
||||
async def test_update_pipeline_clears_conversations(self):
|
||||
@@ -526,7 +487,7 @@ class TestPipelineServiceUpdatePipeline:
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid'})
|
||||
|
||||
# Execute
|
||||
await service.update_pipeline('test-uuid', {'description': 'Updated'})
|
||||
await service.update_pipeline(WORKSPACE_UUID, 'test-uuid', {'description': 'Updated'})
|
||||
|
||||
# Verify - conversation was cleared
|
||||
assert session.using_conversation is None
|
||||
@@ -547,10 +508,10 @@ class TestPipelineServiceDeletePipeline:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute
|
||||
await service.delete_pipeline('test-uuid')
|
||||
await service.delete_pipeline(WORKSPACE_UUID, 'test-uuid')
|
||||
|
||||
# Verify
|
||||
ap.pipeline_mgr.remove_pipeline.assert_called_once_with('test-uuid')
|
||||
ap.pipeline_mgr.remove_pipeline.assert_called_once_with(WORKSPACE_UUID, 'test-uuid')
|
||||
ap.persistence_mgr.execute_async.assert_called_once()
|
||||
|
||||
async def test_delete_pipeline_nonexistent_uuid(self):
|
||||
@@ -565,7 +526,7 @@ class TestPipelineServiceDeletePipeline:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute - should not raise
|
||||
await service.delete_pipeline('nonexistent-uuid')
|
||||
await service.delete_pipeline(WORKSPACE_UUID, 'nonexistent-uuid')
|
||||
|
||||
# Verify
|
||||
ap.pipeline_mgr.remove_pipeline.assert_called_once()
|
||||
@@ -597,7 +558,7 @@ class TestPipelineServiceCopyPipeline:
|
||||
|
||||
# Execute & Verify
|
||||
with pytest.raises(ValueError, match='Maximum number of pipelines'):
|
||||
await service.copy_pipeline('original-uuid')
|
||||
await service.copy_pipeline(WORKSPACE_UUID, 'original-uuid')
|
||||
|
||||
async def test_copy_pipeline_not_found_raises(self):
|
||||
"""Raises ValueError when original pipeline not found."""
|
||||
@@ -618,8 +579,8 @@ class TestPipelineServiceCopyPipeline:
|
||||
ap.persistence_mgr.serialize_model = Mock(return_value={})
|
||||
|
||||
# Execute & Verify
|
||||
with pytest.raises(ValueError, match='Pipeline original-uuid not found'):
|
||||
await service.copy_pipeline('original-uuid')
|
||||
with pytest.raises(WorkspaceNotFoundError, match='Pipeline original-uuid not found'):
|
||||
await service.copy_pipeline(WORKSPACE_UUID, 'original-uuid')
|
||||
|
||||
async def test_copy_pipeline_creates_copy(self):
|
||||
"""Creates a copy with (Copy) suffix."""
|
||||
@@ -662,7 +623,7 @@ class TestPipelineServiceCopyPipeline:
|
||||
)
|
||||
|
||||
# Execute
|
||||
new_uuid = await service.copy_pipeline('original-uuid')
|
||||
new_uuid = await service.copy_pipeline(WORKSPACE_UUID, 'original-uuid')
|
||||
|
||||
# Verify
|
||||
assert new_uuid is not None
|
||||
@@ -695,7 +656,7 @@ class TestPipelineServiceCopyPipeline:
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'copy-uuid', 'is_default': False})
|
||||
|
||||
# Execute
|
||||
await service.copy_pipeline('original-uuid')
|
||||
await service.copy_pipeline(WORKSPACE_UUID, 'original-uuid')
|
||||
|
||||
# Verify - pipeline_mgr.load_pipeline called (copy created)
|
||||
ap.pipeline_mgr.load_pipeline.assert_called_once()
|
||||
@@ -715,100 +676,8 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
service = PipelineService(ap)
|
||||
|
||||
# Execute & Verify
|
||||
with pytest.raises(ValueError, match='Pipeline nonexistent-uuid not found'):
|
||||
await service.update_pipeline_extensions('nonexistent-uuid', [])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('field', 'invalid_value'),
|
||||
[
|
||||
('bound_plugins', 'author/plugin'),
|
||||
('bound_plugins', [{'author': 'author'}]),
|
||||
('bound_mcp_servers', 'server-1'),
|
||||
('bound_mcp_servers', ['server-1', 2]),
|
||||
('bound_skills', 'skill-1'),
|
||||
('bound_skills', ['skill-1', None]),
|
||||
('bound_mcp_resources', {'uri': 'file:///README.md'}),
|
||||
('bound_mcp_resources', [{'uri': 'file:///README.md'}, 'bad']),
|
||||
],
|
||||
)
|
||||
async def test_update_extensions_rejects_malformed_binding_lists_before_query(
|
||||
self,
|
||||
field,
|
||||
invalid_value,
|
||||
):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
kwargs = {field: invalid_value}
|
||||
if field != 'bound_plugins':
|
||||
kwargs['bound_plugins'] = []
|
||||
|
||||
with pytest.raises(ValueError, match=field):
|
||||
await service.update_pipeline_extensions('test-uuid', **kwargs)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, 'false'])
|
||||
async def test_update_extensions_rejects_non_boolean_resource_read_before_query(self, invalid_value):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
|
||||
with pytest.raises(ValueError, match='mcp_resource_agent_read_enabled.*boolean'):
|
||||
await service.update_pipeline_extensions(
|
||||
'test-uuid',
|
||||
[],
|
||||
mcp_resource_agent_read_enabled=invalid_value,
|
||||
)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'field',
|
||||
[
|
||||
'enable_all_plugins',
|
||||
'enable_all_mcp_servers',
|
||||
'enable_all_skills',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_update_extensions_rejects_non_boolean_enable_all_flags_before_query(
|
||||
self,
|
||||
field,
|
||||
invalid_value,
|
||||
):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
|
||||
with pytest.raises(ValueError, match=rf'{field}.*boolean'):
|
||||
await service.update_pipeline_extensions(
|
||||
'test-uuid',
|
||||
[],
|
||||
**{field: invalid_value},
|
||||
)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_update_extensions_rejects_non_boolean_attachment_enabled_before_query(
|
||||
self,
|
||||
invalid_value,
|
||||
):
|
||||
ap = SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=AsyncMock()))
|
||||
service = PipelineService(ap)
|
||||
|
||||
with pytest.raises(ValueError, match=r'bound_mcp_resources.*enabled.*boolean'):
|
||||
await service.update_pipeline_extensions(
|
||||
'test-uuid',
|
||||
[],
|
||||
bound_mcp_resources=[
|
||||
{
|
||||
'server_uuid': 'server-1',
|
||||
'uri': 'file:///README.md',
|
||||
'enabled': invalid_value,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
ap.persistence_mgr.execute_async.assert_not_awaited()
|
||||
with pytest.raises(WorkspaceNotFoundError, match='Pipeline nonexistent-uuid not found'):
|
||||
await service.update_pipeline_extensions(WORKSPACE_UUID, 'nonexistent-uuid', [])
|
||||
|
||||
async def test_update_extensions_sets_plugins(self):
|
||||
"""Updates plugins in extensions_preferences."""
|
||||
@@ -836,7 +705,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
'uuid': 'test-uuid',
|
||||
'extensions_preferences': {
|
||||
'enable_all_plugins': False,
|
||||
'plugins': [{'plugin_uuid': 'plugin-1'}],
|
||||
'plugins': [{'author': 'test', 'name': 'plugin-1'}],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -847,7 +716,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
'uuid': 'test-uuid',
|
||||
'extensions_preferences': {
|
||||
'enable_all_plugins': False,
|
||||
'plugins': [{'plugin_uuid': 'plugin-1'}],
|
||||
'plugins': [{'author': 'test', 'name': 'plugin-1'}],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -855,6 +724,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
# Execute
|
||||
bound_plugins = [{'author': 'test', 'name': 'plugin-1'}]
|
||||
await service.update_pipeline_extensions(
|
||||
WORKSPACE_UUID,
|
||||
'test-uuid',
|
||||
bound_plugins=bound_plugins,
|
||||
enable_all_plugins=False,
|
||||
@@ -904,6 +774,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
|
||||
# Execute
|
||||
await service.update_pipeline_extensions(
|
||||
WORKSPACE_UUID,
|
||||
'test-uuid',
|
||||
bound_plugins=[],
|
||||
bound_mcp_servers=['mcp-server-1'],
|
||||
@@ -951,7 +822,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
)
|
||||
|
||||
# Execute - bound_mcp_servers is None (not provided)
|
||||
await service.update_pipeline_extensions('test-uuid', bound_plugins=[])
|
||||
await service.update_pipeline_extensions(WORKSPACE_UUID, 'test-uuid', bound_plugins=[])
|
||||
|
||||
# Verify - persistence was called
|
||||
ap.persistence_mgr.execute_async.assert_called()
|
||||
@@ -990,7 +861,7 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid'})
|
||||
|
||||
await service.update_pipeline_extensions('test-uuid', bound_plugins=[])
|
||||
await service.update_pipeline_extensions(WORKSPACE_UUID, 'test-uuid', bound_plugins=[])
|
||||
|
||||
assert original_pipeline.extensions_preferences['mcp_resource_agent_read_enabled'] is False
|
||||
assert original_pipeline.extensions_preferences['mcp_resources'] == [
|
||||
@@ -998,6 +869,82 @@ class TestPipelineServiceUpdatePipelineExtensions:
|
||||
]
|
||||
|
||||
|
||||
class TestPipelineSecretRoundtrip:
|
||||
async def test_resource_view_redacts_runner_secrets_without_mutating_serialized_data(self):
|
||||
raw = {
|
||||
'uuid': 'pipeline-secret',
|
||||
'config': {
|
||||
'ai': {
|
||||
'n8n': {
|
||||
'webhook-url': 'https://hook.invalid/bearer-secret',
|
||||
'headers': {'Authorization': 'Bearer secret'},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
pipeline = _create_mock_pipeline(pipeline_uuid='pipeline-secret')
|
||||
ap = SimpleNamespace(
|
||||
persistence_mgr=SimpleNamespace(
|
||||
execute_async=AsyncMock(return_value=_create_mock_result([pipeline])),
|
||||
serialize_model=Mock(return_value=raw),
|
||||
)
|
||||
)
|
||||
|
||||
redacted = await PipelineService(ap).get_pipelines(WORKSPACE_UUID)
|
||||
|
||||
assert redacted[0]['config']['ai']['n8n']['webhook-url'] == '***'
|
||||
assert redacted[0]['config']['ai']['n8n']['headers']['Authorization'] == '***'
|
||||
assert raw['config']['ai']['n8n']['webhook-url'] == 'https://hook.invalid/bearer-secret'
|
||||
|
||||
async def test_masked_runner_config_update_restores_existing_secret(self):
|
||||
raw_config = {
|
||||
'ai': {
|
||||
'n8n': {
|
||||
'webhook-url': 'https://hook.invalid/bearer-secret',
|
||||
'headers': {'Authorization': 'Bearer secret'},
|
||||
'timeout': 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
current_pipeline = {'uuid': 'pipeline-secret', 'config': raw_config}
|
||||
write_result = Mock(rowcount=1)
|
||||
ap = SimpleNamespace(
|
||||
persistence_mgr=SimpleNamespace(execute_async=AsyncMock(return_value=write_result)),
|
||||
pipeline_mgr=SimpleNamespace(remove_pipeline=AsyncMock(), load_pipeline=AsyncMock()),
|
||||
sess_mgr=SimpleNamespace(session_list=[]),
|
||||
)
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(side_effect=[current_pipeline, current_pipeline])
|
||||
|
||||
await service.update_pipeline(
|
||||
WORKSPACE_UUID,
|
||||
'pipeline-secret',
|
||||
{
|
||||
'config': {
|
||||
'ai': {
|
||||
'n8n': {
|
||||
'webhook-url': '***',
|
||||
'headers': {'Authorization': '***'},
|
||||
'timeout': 60,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
statement = ap.persistence_mgr.execute_async.await_args.args[0]
|
||||
stored_config = next(value.value for column, value in statement._values.items() if column.key == 'config')
|
||||
assert stored_config == {
|
||||
'ai': {
|
||||
'n8n': {
|
||||
'webhook-url': 'https://hook.invalid/bearer-secret',
|
||||
'headers': {'Authorization': 'Bearer secret'},
|
||||
'timeout': 60,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestDefaultStageOrder:
|
||||
"""Tests for default_stage_order constant."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user