mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-14 14:31:00 +00:00
chore(merge): sync master into dev/4.11.x
This commit is contained in:
@@ -3,9 +3,26 @@ PipelineManager unit tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from importlib import import_module
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot.pkg.workspace.entities import WorkspaceExecutionBinding
|
||||
from langbot.pkg.workspace.errors import WorkspaceGenerationMismatchError, WorkspaceInvariantError
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
def _context(pipeline_uuid: str = 'test-uuid') -> ExecutionContext:
|
||||
return ExecutionContext(
|
||||
instance_uuid='test-instance',
|
||||
workspace_uuid='test-workspace',
|
||||
placement_generation=1,
|
||||
pipeline_uuid=pipeline_uuid,
|
||||
)
|
||||
|
||||
|
||||
def get_pipelinemgr_module():
|
||||
return import_module('langbot.pkg.pipeline.pipelinemgr')
|
||||
@@ -37,6 +54,95 @@ async def test_pipeline_manager_initialize(mock_app):
|
||||
assert len(manager.pipelines) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_startup_reuses_validated_pipeline_binding(mock_app):
|
||||
class TenantUow:
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
return False
|
||||
|
||||
binding = WorkspaceExecutionBinding(
|
||||
instance_uuid='test-instance',
|
||||
workspace_uuid='test-workspace',
|
||||
placement_generation=1,
|
||||
write_fenced=False,
|
||||
state='active',
|
||||
)
|
||||
pipeline_entity = Mock(
|
||||
uuid='test-uuid',
|
||||
workspace_uuid='test-workspace',
|
||||
stages=[],
|
||||
config={},
|
||||
extensions_preferences={},
|
||||
)
|
||||
mock_app.persistence_mgr.mode = SimpleNamespace(value='cloud_runtime')
|
||||
mock_app.persistence_mgr.tenant_uow = lambda _workspace_uuid: TenantUow()
|
||||
mock_app.persistence_mgr.execute_async = AsyncMock(return_value=Mock(all=Mock(return_value=[pipeline_entity])))
|
||||
mock_app.workspace_service.list_active_execution_bindings = AsyncMock(return_value=[binding])
|
||||
mock_app.workspace_service.get_execution_binding = AsyncMock(
|
||||
side_effect=AssertionError('startup pipeline loader repeated a validated binding lookup')
|
||||
)
|
||||
manager = get_pipelinemgr_module().PipelineManager(mock_app)
|
||||
manager.stage_dict = {}
|
||||
|
||||
await manager.load_pipelines_from_db()
|
||||
|
||||
assert len(manager.pipelines) == 1
|
||||
mock_app.workspace_service.get_execution_binding.assert_not_awaited()
|
||||
|
||||
|
||||
def test_generation_advance_prunes_superseded_workspace_pipelines(mock_app):
|
||||
class NoGlobalIterationDict(dict):
|
||||
def __iter__(self):
|
||||
raise AssertionError('generation advance scanned every pipeline')
|
||||
|
||||
def items(self):
|
||||
raise AssertionError('generation advance scanned every pipeline')
|
||||
|
||||
def values(self):
|
||||
raise AssertionError('generation advance scanned every pipeline')
|
||||
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
manager = pipelinemgr.PipelineManager(mock_app)
|
||||
old_context = _context()
|
||||
next_context = ExecutionContext(
|
||||
instance_uuid=old_context.instance_uuid,
|
||||
workspace_uuid=old_context.workspace_uuid,
|
||||
placement_generation=2,
|
||||
pipeline_uuid=old_context.pipeline_uuid,
|
||||
)
|
||||
old_pipeline = SimpleNamespace(
|
||||
execution_context=old_context,
|
||||
workspace_uuid=old_context.workspace_uuid,
|
||||
placement_generation=old_context.placement_generation,
|
||||
)
|
||||
other_pipelines = [
|
||||
SimpleNamespace(
|
||||
execution_context=ExecutionContext(
|
||||
instance_uuid='test-instance',
|
||||
workspace_uuid=f'workspace-{index}',
|
||||
placement_generation=1,
|
||||
pipeline_uuid=f'pipeline-{index}',
|
||||
),
|
||||
workspace_uuid=f'workspace-{index}',
|
||||
placement_generation=1,
|
||||
)
|
||||
for index in range(1_000)
|
||||
]
|
||||
manager.pipelines = [old_pipeline, *other_pipelines]
|
||||
|
||||
manager._observe_execution_context(old_context)
|
||||
manager._pipelines_by_key = NoGlobalIterationDict(manager._pipelines_by_key)
|
||||
manager._observe_execution_context(next_context)
|
||||
manager._pipelines_by_key = dict(manager._pipelines_by_key)
|
||||
|
||||
assert manager.pipelines == other_pipelines
|
||||
with pytest.raises(WorkspaceInvariantError, match='rolled back'):
|
||||
manager._observe_execution_context(old_context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_pipeline(mock_app):
|
||||
"""Test loading a single pipeline"""
|
||||
@@ -51,11 +157,12 @@ async def test_load_pipeline(mock_app):
|
||||
# Create test pipeline entity
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.stages = []
|
||||
pipeline_entity.config = {'test': 'config'}
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
|
||||
await manager.load_pipeline(pipeline_entity)
|
||||
await manager.load_pipeline(_context(), pipeline_entity)
|
||||
|
||||
assert len(manager.pipelines) == 1
|
||||
assert manager.pipelines[0].pipeline_entity.uuid == 'test-uuid'
|
||||
@@ -75,19 +182,20 @@ async def test_get_pipeline_by_uuid(mock_app):
|
||||
# Create and add test pipeline
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.stages = []
|
||||
pipeline_entity.config = {}
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
|
||||
await manager.load_pipeline(pipeline_entity)
|
||||
await manager.load_pipeline(_context(), pipeline_entity)
|
||||
|
||||
# Test retrieval
|
||||
result = await manager.get_pipeline_by_uuid('test-uuid')
|
||||
result = await manager.get_pipeline_by_uuid(_context(), 'test-uuid')
|
||||
assert result is not None
|
||||
assert result.pipeline_entity.uuid == 'test-uuid'
|
||||
|
||||
# Test non-existent UUID
|
||||
result = await manager.get_pipeline_by_uuid('non-existent')
|
||||
result = await manager.get_pipeline_by_uuid(_context('non-existent'), 'non-existent')
|
||||
assert result is None
|
||||
|
||||
|
||||
@@ -105,15 +213,16 @@ async def test_remove_pipeline(mock_app):
|
||||
# Create and add test pipeline
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.stages = []
|
||||
pipeline_entity.config = {}
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
|
||||
await manager.load_pipeline(pipeline_entity)
|
||||
await manager.load_pipeline(_context(), pipeline_entity)
|
||||
assert len(manager.pipelines) == 1
|
||||
|
||||
# Remove pipeline
|
||||
await manager.remove_pipeline('test-uuid')
|
||||
await manager.remove_pipeline(_context(), 'test-uuid')
|
||||
assert len(manager.pipelines) == 0
|
||||
|
||||
|
||||
@@ -143,128 +252,119 @@ async def test_runtime_pipeline_execute(mock_app, sample_query):
|
||||
|
||||
# Create pipeline entity
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-pipeline-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.config = sample_query.pipeline_config
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
|
||||
# Create runtime pipeline
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [stage_container])
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(
|
||||
mock_app,
|
||||
pipeline_entity,
|
||||
[stage_container],
|
||||
_context('test-pipeline-uuid'),
|
||||
)
|
||||
|
||||
# Mock plugin connector
|
||||
event_ctx = Mock()
|
||||
event_ctx.is_prevented_default = Mock(return_value=False)
|
||||
mock_app.plugin_connector.emit_event = AsyncMock(return_value=event_ctx)
|
||||
|
||||
# Add query to cached_queries to prevent KeyError in finally block
|
||||
mock_app.query_pool.cached_queries[sample_query.query_id] = sample_query
|
||||
|
||||
# Execute pipeline
|
||||
await runtime_pipeline.run(sample_query)
|
||||
|
||||
# Verify stage was called
|
||||
mock_stage.process.assert_called_once()
|
||||
mock_app.query_pool.remove_query.assert_awaited_once_with(sample_query)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_pipeline_delivers_latest_chunk_as_final(mock_app, sample_query):
|
||||
"""The terminal chunk, not the first chunk, controls final stream delivery."""
|
||||
async def test_runtime_pipeline_rejects_stale_generation_before_side_effects(
|
||||
mock_app,
|
||||
sample_query,
|
||||
):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
entities = get_entities_module()
|
||||
provider_message = import_module('langbot_plugin.api.entities.builtin.provider.message')
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-pipeline-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.config = sample_query.pipeline_config
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(
|
||||
mock_app,
|
||||
pipeline_entity,
|
||||
[],
|
||||
_context('test-pipeline-uuid'),
|
||||
)
|
||||
mock_app.workspace_service.get_execution_binding.side_effect = WorkspaceGenerationMismatchError('stale generation')
|
||||
|
||||
with pytest.raises(WorkspaceGenerationMismatchError):
|
||||
await runtime_pipeline.run(sample_query)
|
||||
|
||||
mock_app.plugin_connector.emit_event.assert_not_awaited()
|
||||
sample_query.adapter.reply_message.assert_not_awaited()
|
||||
sample_query.adapter.reply_message_chunk.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_pipeline_revalidates_after_awaited_stage(
|
||||
mock_app,
|
||||
sample_query,
|
||||
):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
stage = get_stage_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
entities = get_entities_module()
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-pipeline-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.config = sample_query.pipeline_config
|
||||
pipeline_entity.extensions_preferences = {'plugins': []}
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
first_chunk = provider_message.MessageChunk(role='assistant', content='Starting', is_final=False)
|
||||
final_chunk = provider_message.MessageChunk(role='assistant', content='Done', is_final=True)
|
||||
sample_query.resp_messages = [first_chunk, final_chunk]
|
||||
sample_query.adapter.is_stream_output_supported = AsyncMock(return_value=True)
|
||||
result = entities.StageProcessResult(
|
||||
result_type=entities.ResultType.CONTINUE,
|
||||
new_query=sample_query,
|
||||
user_notice='StartingDone',
|
||||
user_notice='must not be sent',
|
||||
console_notice='',
|
||||
debug_notice='',
|
||||
error_notice='',
|
||||
)
|
||||
|
||||
await runtime_pipeline._check_output(sample_query, result)
|
||||
async def stage_process(*_args):
|
||||
mock_app.workspace_service.get_execution_binding.side_effect = WorkspaceGenerationMismatchError(
|
||||
'generation changed during stage'
|
||||
)
|
||||
return result
|
||||
|
||||
sample_query.adapter.reply_message_chunk.assert_awaited_once()
|
||||
call = sample_query.adapter.reply_message_chunk.await_args.kwargs
|
||||
assert call['bot_message'] is final_chunk
|
||||
assert call['is_final'] is True
|
||||
mock_stage = Mock(spec=stage.PipelineStage)
|
||||
mock_stage.process = Mock(side_effect=stage_process)
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(
|
||||
mock_app,
|
||||
pipeline_entity,
|
||||
[pipelinemgr.StageInstContainer(inst_name='TestStage', inst=mock_stage)],
|
||||
_context('test-pipeline-uuid'),
|
||||
)
|
||||
|
||||
with pytest.raises(WorkspaceGenerationMismatchError):
|
||||
await runtime_pipeline._execute_from_stage(0, sample_query)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_back_stage_delivers_latest_chunk_as_final(mock_app, sample_query):
|
||||
respback = import_module('langbot.pkg.pipeline.respback.respback')
|
||||
provider_message = import_module('langbot_plugin.api.entities.builtin.provider.message')
|
||||
platform_message = import_module('langbot_plugin.api.entities.builtin.platform.message')
|
||||
|
||||
first_chunk = provider_message.MessageChunk(role='assistant', content='Starting', is_final=False)
|
||||
final_chunk = provider_message.MessageChunk(role='assistant', content='Done', is_final=True)
|
||||
sample_query.resp_messages = [first_chunk, final_chunk]
|
||||
sample_query.resp_message_chain = [platform_message.MessageChain([platform_message.Plain(text='StartingDone')])]
|
||||
sample_query.pipeline_config['output']['force-delay'] = {'min': 0, 'max': 0}
|
||||
sample_query.adapter.is_stream_output_supported = AsyncMock(return_value=True)
|
||||
|
||||
await respback.SendResponseBackStage(mock_app).process(sample_query, 'response-back')
|
||||
|
||||
sample_query.adapter.reply_message_chunk.assert_awaited_once()
|
||||
call = sample_query.adapter.reply_message_chunk.await_args.kwargs
|
||||
assert call['bot_message'] is final_chunk
|
||||
assert call['is_final'] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_back_stage_keeps_consuming_after_stream_delivery_failure(mock_app, sample_query):
|
||||
respback = import_module('langbot.pkg.pipeline.respback.respback')
|
||||
provider_message = import_module('langbot_plugin.api.entities.builtin.provider.message')
|
||||
platform_message = import_module('langbot_plugin.api.entities.builtin.platform.message')
|
||||
|
||||
chunk = provider_message.MessageChunk(role='assistant', content='Progress', is_final=False)
|
||||
sample_query.resp_messages = [chunk]
|
||||
sample_query.resp_message_chain = [platform_message.MessageChain([platform_message.Plain(text='Progress')])]
|
||||
sample_query.pipeline_config['output']['force-delay'] = {'min': 0, 'max': 0}
|
||||
sample_query.adapter.is_stream_output_supported = AsyncMock(return_value=True)
|
||||
sample_query.adapter.reply_message_chunk.side_effect = RuntimeError('stream update failed')
|
||||
|
||||
result = await respback.SendResponseBackStage(mock_app).process(sample_query, 'response-back')
|
||||
|
||||
assert result.result_type.name == 'CONTINUE'
|
||||
sample_query.adapter.reply_message.assert_not_awaited()
|
||||
sample_query.adapter.reply_message_chunk.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_back_stage_falls_back_to_plain_message_for_failed_final_chunk(mock_app, sample_query):
|
||||
respback = import_module('langbot.pkg.pipeline.respback.respback')
|
||||
provider_message = import_module('langbot_plugin.api.entities.builtin.provider.message')
|
||||
platform_message = import_module('langbot_plugin.api.entities.builtin.platform.message')
|
||||
|
||||
chunk = provider_message.MessageChunk(role='assistant', content='Final answer', is_final=True)
|
||||
sample_query.resp_messages = [chunk]
|
||||
sample_query.resp_message_chain = [platform_message.MessageChain([platform_message.Plain(text='Final answer')])]
|
||||
sample_query.pipeline_config['output']['force-delay'] = {'min': 0, 'max': 0}
|
||||
sample_query.adapter.is_stream_output_supported = AsyncMock(return_value=True)
|
||||
sample_query.adapter.reply_message_chunk.side_effect = RuntimeError('stream update failed')
|
||||
|
||||
result = await respback.SendResponseBackStage(mock_app).process(sample_query, 'response-back')
|
||||
|
||||
assert result.result_type.name == 'CONTINUE'
|
||||
sample_query.adapter.reply_message.assert_awaited_once()
|
||||
|
||||
|
||||
def test_runtime_pipeline_prefers_runner_mcp_resources(mock_app):
|
||||
"""Runner resource selection should override extension preferences."""
|
||||
def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||
"""AgentRunner resource selection should override legacy extension prefs."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner': {'id': RUNNER_ID},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
RUNNER_ID: {
|
||||
'mcp-resources': [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}],
|
||||
'mcp-resource-agent-read-enabled': False,
|
||||
},
|
||||
@@ -276,22 +376,24 @@ def test_runtime_pipeline_prefers_runner_mcp_resources(mock_app):
|
||||
'mcp_resource_agent_read_enabled': True,
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [], _context())
|
||||
|
||||
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}]
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
|
||||
def test_runtime_pipeline_falls_back_to_extension_mcp_resources(mock_app):
|
||||
"""Extension preferences apply when the current runner has no override."""
|
||||
"""Existing extension prefs remain compatible until a runner value exists."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.uuid = 'test-uuid'
|
||||
pipeline_entity.workspace_uuid = 'test-workspace'
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {'plugin:langbot-team/LocalAgent/default': {}},
|
||||
'runner': {'id': RUNNER_ID},
|
||||
'runner_config': {RUNNER_ID: {}},
|
||||
}
|
||||
}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
@@ -299,99 +401,7 @@ def test_runtime_pipeline_falls_back_to_extension_mcp_resources(mock_app):
|
||||
'mcp_resource_agent_read_enabled': False,
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [], _context())
|
||||
|
||||
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}]
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false', [], {}])
|
||||
def test_runtime_pipeline_mcp_resource_read_flag_fails_closed(mock_app, invalid_value):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:test/runner/default'},
|
||||
'runner_config': {
|
||||
'plugin:test/runner/default': {
|
||||
'mcp-resource-agent-read-enabled': invalid_value,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
pipeline_entity.extensions_preferences = {'mcp_resource_agent_read_enabled': True}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false', [], {}])
|
||||
def test_runtime_pipeline_extension_enable_all_flags_fail_closed(mock_app, invalid_value):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
'enable_all_plugins': invalid_value,
|
||||
'plugins': [{'author': 'allowed', 'name': 'plugin'}],
|
||||
'enable_all_mcp_servers': invalid_value,
|
||||
'mcp_servers': ['bound-mcp'],
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.enable_all_plugins is False
|
||||
assert runtime_pipeline.bound_plugins == ['allowed/plugin']
|
||||
assert runtime_pipeline.enable_all_mcp_servers is False
|
||||
assert runtime_pipeline.bound_mcp_servers == ['bound-mcp']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_preferences', [None, [], '', 0, False])
|
||||
def test_runtime_pipeline_malformed_extension_root_disables_all_extensions(
|
||||
mock_app,
|
||||
invalid_preferences,
|
||||
):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {}
|
||||
pipeline_entity.extensions_preferences = invalid_preferences
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.enable_all_plugins is False
|
||||
assert runtime_pipeline.bound_plugins == []
|
||||
assert runtime_pipeline.enable_all_mcp_servers is False
|
||||
assert runtime_pipeline.bound_mcp_servers == []
|
||||
assert runtime_pipeline.mcp_resource_attachments == []
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
|
||||
def test_runtime_pipeline_malformed_extension_lists_are_empty_allowlists(mock_app):
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
'enable_all_plugins': True,
|
||||
'plugins': 'allowed/plugin',
|
||||
'enable_all_mcp_servers': True,
|
||||
'mcp_servers': 'bound-mcp',
|
||||
'mcp_resources': 'file:///README.md',
|
||||
'mcp_resource_agent_read_enabled': True,
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.enable_all_plugins is False
|
||||
assert runtime_pipeline.bound_plugins == []
|
||||
assert runtime_pipeline.enable_all_mcp_servers is False
|
||||
assert runtime_pipeline.bound_mcp_servers == []
|
||||
assert runtime_pipeline.mcp_resource_attachments == []
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
Reference in New Issue
Block a user