mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-19 00:40:59 +00:00
chore: merge master into dev/4.11.x
This commit is contained in:
@@ -93,14 +93,10 @@ class TestRunAgent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_revalidates_trusted_execution_context(self):
|
||||
connector = create_mock_connector()
|
||||
connector._current_execution_context = AsyncMock(
|
||||
return_value=TEST_EXECUTION_CONTEXT
|
||||
)
|
||||
connector._current_execution_context = AsyncMock(return_value=TEST_EXECUTION_CONTEXT)
|
||||
|
||||
class RuntimeHandler:
|
||||
installation_scope = Mock(
|
||||
side_effect=lambda _binding: nullcontext()
|
||||
)
|
||||
installation_scope = Mock(side_effect=lambda _binding: nullcontext())
|
||||
|
||||
async def run_agent(self, *_args):
|
||||
yield {'type': 'run.completed'}
|
||||
@@ -113,16 +109,12 @@ class TestRunAgent:
|
||||
)
|
||||
|
||||
assert results == [{'type': 'run.completed'}]
|
||||
connector.require_workspace_context.assert_awaited_once_with(
|
||||
TEST_EXECUTION_CONTEXT
|
||||
)
|
||||
connector.require_workspace_context.assert_awaited_once_with(TEST_EXECUTION_CONTEXT)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_payload_workspace_mismatch(self):
|
||||
connector = create_mock_connector()
|
||||
connector._current_execution_context = AsyncMock(
|
||||
return_value=TEST_EXECUTION_CONTEXT
|
||||
)
|
||||
connector._current_execution_context = AsyncMock(return_value=TEST_EXECUTION_CONTEXT)
|
||||
configure_handler(connector, AsyncMock())
|
||||
|
||||
with pytest.raises(WorkspaceNotFoundError, match='Plugin resource not found'):
|
||||
@@ -670,8 +662,13 @@ class TestDisabledPluginEarlyReturns:
|
||||
mock_app.instance_config.data = {'plugin': {'enable': False}}
|
||||
|
||||
connector = connector_module.PluginRuntimeConnector(mock_app, mock_disconnect)
|
||||
execution_context = connector_module.ExecutionContext(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=1,
|
||||
)
|
||||
|
||||
result = await connector.get_debug_info()
|
||||
result = await connector.get_debug_info(execution_context)
|
||||
|
||||
assert result == {}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from langbot_plugin.runtime.security import (
|
||||
)
|
||||
|
||||
|
||||
def make_connector() -> PluginRuntimeConnector:
|
||||
def make_connector(*, cloud: bool = False) -> PluginRuntimeConnector:
|
||||
app = SimpleNamespace(
|
||||
logger=Mock(),
|
||||
instance_config=SimpleNamespace(
|
||||
@@ -34,6 +34,7 @@ def make_connector() -> PluginRuntimeConnector:
|
||||
'space': {'url': ''},
|
||||
}
|
||||
),
|
||||
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||
)
|
||||
return PluginRuntimeConnector(app, AsyncMock())
|
||||
|
||||
@@ -142,6 +143,49 @@ async def test_stdio_runtime_connection_does_not_capture_unconsumed_stderr(
|
||||
await connector.aclose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_connect_timeout_is_rejected_before_transport_startup(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
connector = make_connector()
|
||||
connector.ap.instance_config.data['plugin']['connect_timeout_seconds'] = 0
|
||||
stdio_controller = Mock()
|
||||
websocket_controller = Mock()
|
||||
create_task = Mock()
|
||||
get_platform = Mock(return_value='linux')
|
||||
use_websocket = Mock(return_value=False)
|
||||
connector._start_runtime_subprocess = AsyncMock()
|
||||
monkeypatch.setattr(connector_module.constants, 'instance_id', 'instance-a')
|
||||
monkeypatch.setattr(connector_module.asyncio, 'create_task', create_task)
|
||||
monkeypatch.setattr(connector_module.platform, 'get_platform', get_platform)
|
||||
monkeypatch.setattr(
|
||||
connector_module.platform,
|
||||
'use_websocket_to_connect_plugin_runtime',
|
||||
use_websocket,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.stdio_client_controller,
|
||||
'StdioClientController',
|
||||
stdio_controller,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.ws_client_controller,
|
||||
'WebSocketClientController',
|
||||
websocket_controller,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match='plugin.connect_timeout_seconds'):
|
||||
await connector.initialize()
|
||||
|
||||
get_platform.assert_not_called()
|
||||
use_websocket.assert_not_called()
|
||||
stdio_controller.assert_not_called()
|
||||
websocket_controller.assert_not_called()
|
||||
connector._start_runtime_subprocess.assert_not_awaited()
|
||||
create_task.assert_not_called()
|
||||
assert connector._transport_task is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_disconnect_notifies_once_and_clears_handler(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@@ -292,10 +336,17 @@ def test_closed_deployment_selects_instance_scoped_shared_profile():
|
||||
assert connector.runtime_profile == 'shared'
|
||||
|
||||
|
||||
def test_external_runtime_control_headers_require_strong_secret(monkeypatch):
|
||||
def test_external_runtime_control_headers_are_empty_when_secret_is_unset(monkeypatch):
|
||||
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||
connector = make_connector()
|
||||
|
||||
assert connector._control_headers(allow_generate=False) == {}
|
||||
|
||||
|
||||
def test_cloud_runtime_rejects_missing_control_secret(monkeypatch):
|
||||
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||
connector = make_connector(cloud=True)
|
||||
|
||||
with pytest.raises(PluginRuntimeNotConnectedError, match=PLUGIN_RUNTIME_CONTROL_TOKEN_ENV):
|
||||
connector._control_headers(allow_generate=False)
|
||||
|
||||
|
||||
@@ -153,6 +153,31 @@ async def test_empty_projected_workspaces_do_not_retain_installation_sets():
|
||||
connector.handler.reconcile_plugin_installations.assert_awaited_once_with(())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shared_reconcile_logs_workspace_installation_counts_and_elapsed_time():
|
||||
binding_a = execution_binding('workspace-a')
|
||||
binding_b = execution_binding('workspace-b')
|
||||
setting_a = plugin_setting('01', 'a' * 64)
|
||||
setting_b = plugin_setting('02', 'b' * 64)
|
||||
connector = shared_connector(
|
||||
[[binding_a, binding_b]],
|
||||
{'workspace-a': [setting_a], 'workspace-b': [setting_b]},
|
||||
)
|
||||
connector.handler = runtime_handler()
|
||||
await connector._prepare_connected_runtime()
|
||||
|
||||
matching_calls = [
|
||||
call
|
||||
for call in connector.ap.logger.info.call_args_list
|
||||
if call.args
|
||||
and call.args[0]
|
||||
== 'Shared plugin runtime reconcile completed: workspaces=%d desired_installations=%d elapsed_seconds=%.3f'
|
||||
]
|
||||
assert len(matching_calls) == 1
|
||||
assert matching_calls[0].args[1:3] == (2, 2)
|
||||
assert matching_calls[0].args[3] >= 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fresh_shared_runtime_cache_replays_persisted_local_package():
|
||||
package = b'local-lbpkg-bytes'
|
||||
|
||||
@@ -6,9 +6,10 @@ Tests cover:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from importlib import import_module
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def get_connector_module():
|
||||
"""Lazy import to avoid circular import issues."""
|
||||
@@ -60,3 +61,28 @@ def test_runtime_id_is_stable_across_core_restarts(monkeypatch):
|
||||
monkeypatch.setattr(connector.constants, 'instance_id', 'instance-a')
|
||||
|
||||
assert connector.PluginRuntimeConnector._build_runtime_id() == 'instance-a:plugin-runtime'
|
||||
|
||||
|
||||
def test_runtime_connect_timeout_defaults_to_three_minutes():
|
||||
connector = get_connector_module()
|
||||
assert connector.PluginRuntimeConnector._runtime_connect_timeout({}) == 180.0
|
||||
|
||||
|
||||
def test_runtime_connect_timeout_reads_typed_plugin_config():
|
||||
connector = get_connector_module()
|
||||
assert connector.PluginRuntimeConnector._runtime_connect_timeout({'connect_timeout_seconds': 45.5}) == 45.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [True, False, None, 0, -1, float('nan'), float('inf'), '180', object()])
|
||||
def test_runtime_connect_timeout_rejects_invalid_values(value):
|
||||
connector = get_connector_module()
|
||||
with pytest.raises(ValueError, match='plugin.connect_timeout_seconds'):
|
||||
connector.PluginRuntimeConnector._runtime_connect_timeout({'connect_timeout_seconds': value})
|
||||
|
||||
|
||||
def test_runtime_connect_timeout_error_displays_actual_seconds():
|
||||
connector = get_connector_module()
|
||||
|
||||
assert connector.PluginRuntimeConnector._runtime_connect_timeout_error(45.5) == (
|
||||
'Plugin runtime did not become ready within 45.5 seconds'
|
||||
)
|
||||
|
||||
@@ -9,8 +9,8 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding
|
||||
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||
|
||||
|
||||
def make_handler(app):
|
||||
@@ -67,6 +67,20 @@ def make_handler(app):
|
||||
return runtime_handler
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_plugin_installations_allows_cloud_cold_start_to_finish():
|
||||
app = SimpleNamespace()
|
||||
runtime_handler = make_handler(app)
|
||||
runtime_handler.call_action = AsyncMock(return_value={})
|
||||
binding = next(iter(runtime_handler._installation_bindings.values()))[0]
|
||||
desired = PluginInstallationDesiredState(binding=binding, enabled=True)
|
||||
|
||||
await runtime_handler.reconcile_plugin_installations((desired,))
|
||||
|
||||
assert runtime_handler.call_action.await_args.args[0] == LangBotToRuntimeAction.RECONCILE_PLUGIN_INSTALLATIONS
|
||||
assert runtime_handler.call_action.await_args.kwargs['timeout'] == 300
|
||||
|
||||
|
||||
class TestHandlerQueryVariables:
|
||||
"""Tests for handler query variable logic."""
|
||||
|
||||
|
||||
@@ -396,9 +396,7 @@ async def test_legacy_oss_knowledge_file_reply_uses_complete_installation_bindin
|
||||
get_file_stream=AsyncMock(return_value=b'knowledge-file'),
|
||||
)
|
||||
runtime_handler.send_file = AsyncMock(return_value='knowledge-file-key')
|
||||
legacy_context = workspace_context().for_installation(
|
||||
installation_context.installation_uuid
|
||||
)
|
||||
legacy_context = workspace_context().for_installation(installation_context.installation_uuid)
|
||||
|
||||
response = await invoke_with_context(
|
||||
runtime_handler,
|
||||
@@ -505,3 +503,23 @@ async def test_host_to_runtime_action_carries_trusted_connector_context():
|
||||
'runtime_id': 'runtime-a',
|
||||
}
|
||||
assert request.get('context') is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_debug_info_converts_execution_context_to_sdk_action_context():
|
||||
runtime_handler, _app, _installation_context = make_handler()
|
||||
runtime_handler.call_action = AsyncMock(return_value={'plugin_debug_key': 'debug-key'})
|
||||
execution_context = ExecutionContext(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=7,
|
||||
)
|
||||
|
||||
result = await runtime_handler.get_debug_info(execution_context)
|
||||
|
||||
assert result == {'plugin_debug_key': 'debug-key'}
|
||||
assert runtime_handler.call_action.await_args.kwargs['action_context'] == ActionContext(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=7,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user