mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-10 13:10:57 +00:00
fix: ruff
This commit is contained in:
@@ -18,13 +18,12 @@ import shutil
|
|||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from langbot.pkg.box.backend import BaseSandboxBackend
|
from langbot.pkg.box.backend import BaseSandboxBackend
|
||||||
from langbot.pkg.box.client import ActionRPCBoxClient
|
from langbot.pkg.box.client import ActionRPCBoxClient
|
||||||
from langbot.pkg.box.errors import BoxBackendUnavailableError, BoxRuntimeUnavailableError
|
from langbot.pkg.box.errors import BoxBackendUnavailableError
|
||||||
from langbot.pkg.box.models import BoxExecutionStatus, BoxNetworkMode, BoxSpec
|
from langbot.pkg.box.models import BoxExecutionStatus, BoxNetworkMode, BoxSpec
|
||||||
from langbot.pkg.box.runtime import BoxRuntime
|
from langbot.pkg.box.runtime import BoxRuntime
|
||||||
from langbot.pkg.box.server import BoxServerHandler
|
from langbot.pkg.box.server import BoxServerHandler
|
||||||
@@ -166,20 +165,24 @@ async def test_session_persists_files(box_client: ActionRPCBoxClient):
|
|||||||
"""Write a file in one exec, read it back in a second exec on the same session."""
|
"""Write a file in one exec, read it back in a second exec on the same session."""
|
||||||
sid = 'int-persist'
|
sid = 'int-persist'
|
||||||
|
|
||||||
write_result = await box_client.execute(BoxSpec(
|
write_result = await box_client.execute(
|
||||||
cmd='echo "hello from file" > /tmp/testfile.txt',
|
BoxSpec(
|
||||||
session_id=sid,
|
cmd='echo "hello from file" > /tmp/testfile.txt',
|
||||||
workdir='/tmp',
|
session_id=sid,
|
||||||
image=_TEST_IMAGE,
|
workdir='/tmp',
|
||||||
))
|
image=_TEST_IMAGE,
|
||||||
|
)
|
||||||
|
)
|
||||||
assert write_result.exit_code == 0
|
assert write_result.exit_code == 0
|
||||||
|
|
||||||
read_result = await box_client.execute(BoxSpec(
|
read_result = await box_client.execute(
|
||||||
cmd='cat /tmp/testfile.txt',
|
BoxSpec(
|
||||||
session_id=sid,
|
cmd='cat /tmp/testfile.txt',
|
||||||
workdir='/tmp',
|
session_id=sid,
|
||||||
image=_TEST_IMAGE,
|
workdir='/tmp',
|
||||||
))
|
image=_TEST_IMAGE,
|
||||||
|
)
|
||||||
|
)
|
||||||
assert read_result.exit_code == 0
|
assert read_result.exit_code == 0
|
||||||
assert 'hello from file' in read_result.stdout
|
assert 'hello from file' in read_result.stdout
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import subprocess
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
from aiohttp import web
|
|
||||||
from aiohttp.test_utils import TestServer
|
from aiohttp.test_utils import TestServer
|
||||||
|
|
||||||
from langbot.pkg.box.client import ActionRPCBoxClient
|
from langbot.pkg.box.client import ActionRPCBoxClient
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from langbot_plugin.box.client import ActionRPCBoxClient
|
from langbot_plugin.box.client import ActionRPCBoxClient
|
||||||
from langbot.pkg.box.connector import BoxRuntimeConnector
|
from langbot.pkg.box.connector import BoxRuntimeConnector
|
||||||
from langbot_plugin.box.errors import BoxRuntimeUnavailableError
|
|
||||||
|
|
||||||
|
|
||||||
def make_app(logger: Mock, runtime_url: str = ''):
|
def make_app(logger: Mock, runtime_url: str = ''):
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|||||||
|
|
||||||
from langbot_plugin.box.backend import BaseSandboxBackend
|
from langbot_plugin.box.backend import BaseSandboxBackend
|
||||||
from langbot_plugin.box.client import BoxRuntimeClient, ActionRPCBoxClient
|
from langbot_plugin.box.client import BoxRuntimeClient, ActionRPCBoxClient
|
||||||
from langbot_plugin.box.errors import BoxBackendUnavailableError, BoxSessionConflictError, BoxSessionNotFoundError, BoxValidationError
|
from langbot_plugin.box.errors import (
|
||||||
|
BoxBackendUnavailableError,
|
||||||
|
BoxSessionConflictError,
|
||||||
|
BoxSessionNotFoundError,
|
||||||
|
BoxValidationError,
|
||||||
|
)
|
||||||
from langbot_plugin.box.models import (
|
from langbot_plugin.box.models import (
|
||||||
BUILTIN_PROFILES,
|
BUILTIN_PROFILES,
|
||||||
BoxExecutionResult,
|
BoxExecutionResult,
|
||||||
@@ -20,7 +25,6 @@ from langbot_plugin.box.models import (
|
|||||||
BoxHostMountMode,
|
BoxHostMountMode,
|
||||||
BoxManagedProcessSpec,
|
BoxManagedProcessSpec,
|
||||||
BoxNetworkMode,
|
BoxNetworkMode,
|
||||||
BoxProfile,
|
|
||||||
BoxSessionInfo,
|
BoxSessionInfo,
|
||||||
BoxSpec,
|
BoxSpec,
|
||||||
)
|
)
|
||||||
@@ -70,7 +74,6 @@ class _InProcessBoxRuntimeClient(BoxRuntimeClient):
|
|||||||
return self._runtime.get_session(session_id)
|
return self._runtime.get_session(session_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FakeBackend(BaseSandboxBackend):
|
class FakeBackend(BaseSandboxBackend):
|
||||||
def __init__(self, logger: Mock, available: bool = True):
|
def __init__(self, logger: Mock, available: bool = True):
|
||||||
super().__init__(logger)
|
super().__init__(logger)
|
||||||
@@ -520,7 +523,9 @@ async def test_profile_locked_field_cannot_be_overridden():
|
|||||||
logger = Mock()
|
logger = Mock()
|
||||||
backend = FakeBackend(logger)
|
backend = FakeBackend(logger)
|
||||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||||
service = BoxService(make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime))
|
service = BoxService(
|
||||||
|
make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime)
|
||||||
|
)
|
||||||
await service.initialize()
|
await service.initialize()
|
||||||
|
|
||||||
result = await service.execute_sandbox_tool(
|
result = await service.execute_sandbox_tool(
|
||||||
@@ -631,7 +636,9 @@ async def test_profile_offline_readonly_locks_read_only_rootfs():
|
|||||||
logger = Mock()
|
logger = Mock()
|
||||||
backend = FakeBackend(logger)
|
backend = FakeBackend(logger)
|
||||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||||
service = BoxService(make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime))
|
service = BoxService(
|
||||||
|
make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime)
|
||||||
|
)
|
||||||
await service.initialize()
|
await service.initialize()
|
||||||
|
|
||||||
await service.execute_sandbox_tool(
|
await service.execute_sandbox_tool(
|
||||||
@@ -649,7 +656,9 @@ async def test_profile_network_extended_has_relaxed_limits():
|
|||||||
logger = Mock()
|
logger = Mock()
|
||||||
backend = FakeBackend(logger)
|
backend = FakeBackend(logger)
|
||||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||||
service = BoxService(make_app(logger, profile='network_extended'), client=_InProcessBoxRuntimeClient(logger, runtime))
|
service = BoxService(
|
||||||
|
make_app(logger, profile='network_extended'), client=_InProcessBoxRuntimeClient(logger, runtime)
|
||||||
|
)
|
||||||
await service.initialize()
|
await service.initialize()
|
||||||
|
|
||||||
await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(42))
|
await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(42))
|
||||||
@@ -1028,4 +1037,3 @@ class TestBoxHostMountModeNone:
|
|||||||
host_path_mode=BoxHostMountMode.READ_ONLY,
|
host_path_mode=BoxHostMountMode.READ_ONLY,
|
||||||
workdir='/opt/custom',
|
workdir='/opt/custom',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -43,14 +43,7 @@ class RecordingProvider:
|
|||||||
function=provider_message.FunctionCall(
|
function=provider_message.FunctionCall(
|
||||||
name='sandbox_exec',
|
name='sandbox_exec',
|
||||||
arguments=json.dumps(
|
arguments=json.dumps(
|
||||||
{
|
{'cmd': ("python - <<'PY'\nnums = [1, 2, 3, 4]\nprint(sum(nums) / len(nums))\nPY")}
|
||||||
'cmd': (
|
|
||||||
"python - <<'PY'\n"
|
|
||||||
"nums = [1, 2, 3, 4]\n"
|
|
||||||
'print(sum(nums) / len(nums))\n'
|
|
||||||
'PY'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -60,7 +53,7 @@ class RecordingProvider:
|
|||||||
tool_result = json.loads(messages[-1].content)
|
tool_result = json.loads(messages[-1].content)
|
||||||
return provider_message.Message(
|
return provider_message.Message(
|
||||||
role='assistant',
|
role='assistant',
|
||||||
content=f"The average is {tool_result['stdout']}.",
|
content=f'The average is {tool_result["stdout"]}.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,7 +185,7 @@ async def test_localagent_uses_sandbox_exec_for_exact_calculation():
|
|||||||
tool_manager.execute_func_call.assert_awaited_once()
|
tool_manager.execute_func_call.assert_awaited_once()
|
||||||
tool_name, tool_parameters = tool_manager.execute_func_call.await_args.args[:2]
|
tool_name, tool_parameters = tool_manager.execute_func_call.await_args.args[:2]
|
||||||
assert tool_name == 'sandbox_exec'
|
assert tool_name == 'sandbox_exec'
|
||||||
assert "print(sum(nums) / len(nums))" in tool_parameters['cmd']
|
assert 'print(sum(nums) / len(nums))' in tool_parameters['cmd']
|
||||||
|
|
||||||
first_request = provider.requests[0]
|
first_request = provider.requests[0]
|
||||||
assert any(
|
assert any(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Uses importlib.util.spec_from_file_location to load mcp.py directly without
|
Uses importlib.util.spec_from_file_location to load mcp.py directly without
|
||||||
triggering the circular import chain through the app module.
|
triggering the circular import chain through the app module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
@@ -20,6 +21,7 @@ import pytest
|
|||||||
# Load mcp.py directly from file path, with stub dependencies
|
# Load mcp.py directly from file path, with stub dependencies
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _stub_module(fqn: str, attrs: dict | None = None, is_package: bool = False):
|
def _stub_module(fqn: str, attrs: dict | None = None, is_package: bool = False):
|
||||||
"""Create or return a stub module and register it in sys.modules."""
|
"""Create or return a stub module and register it in sys.modules."""
|
||||||
if fqn in sys.modules:
|
if fqn in sys.modules:
|
||||||
@@ -59,9 +61,12 @@ def mcp_module():
|
|||||||
_save_and_stub('langbot_plugin.api.entities.events.pipeline_query', {})
|
_save_and_stub('langbot_plugin.api.entities.events.pipeline_query', {})
|
||||||
_save_and_stub('langbot_plugin.api.entities.builtin', is_package=True)
|
_save_and_stub('langbot_plugin.api.entities.builtin', is_package=True)
|
||||||
_save_and_stub('langbot_plugin.api.entities.builtin.resource', is_package=True)
|
_save_and_stub('langbot_plugin.api.entities.builtin.resource', is_package=True)
|
||||||
_save_and_stub('langbot_plugin.api.entities.builtin.resource.tool', {
|
_save_and_stub(
|
||||||
'LLMTool': type('LLMTool', (), {}),
|
'langbot_plugin.api.entities.builtin.resource.tool',
|
||||||
})
|
{
|
||||||
|
'LLMTool': type('LLMTool', (), {}),
|
||||||
|
},
|
||||||
|
)
|
||||||
_save_and_stub('langbot_plugin.api.entities.builtin.provider', is_package=True)
|
_save_and_stub('langbot_plugin.api.entities.builtin.provider', is_package=True)
|
||||||
_save_and_stub('langbot_plugin.api.entities.builtin.provider.message', {})
|
_save_and_stub('langbot_plugin.api.entities.builtin.provider.message', {})
|
||||||
_save_and_stub('sqlalchemy', {'select': Mock()})
|
_save_and_stub('sqlalchemy', {'select': Mock()})
|
||||||
@@ -78,9 +83,12 @@ def mcp_module():
|
|||||||
_save_and_stub('langbot.pkg', is_package=True)
|
_save_and_stub('langbot.pkg', is_package=True)
|
||||||
_save_and_stub('langbot.pkg.provider', is_package=True)
|
_save_and_stub('langbot.pkg.provider', is_package=True)
|
||||||
_save_and_stub('langbot.pkg.provider.tools', is_package=True)
|
_save_and_stub('langbot.pkg.provider.tools', is_package=True)
|
||||||
_save_and_stub('langbot.pkg.provider.tools.loader', {
|
_save_and_stub(
|
||||||
'ToolLoader': type('ToolLoader', (), {'__init__': lambda self, ap: None}),
|
'langbot.pkg.provider.tools.loader',
|
||||||
})
|
{
|
||||||
|
'ToolLoader': type('ToolLoader', (), {'__init__': lambda self, ap: None}),
|
||||||
|
},
|
||||||
|
)
|
||||||
_save_and_stub('langbot.pkg.provider.tools.loaders', is_package=True)
|
_save_and_stub('langbot.pkg.provider.tools.loaders', is_package=True)
|
||||||
_save_and_stub('langbot.pkg.core', is_package=True)
|
_save_and_stub('langbot.pkg.core', is_package=True)
|
||||||
_save_and_stub('langbot.pkg.core.app', {'Application': type('Application', (), {})})
|
_save_and_stub('langbot.pkg.core.app', {'Application': type('Application', (), {})})
|
||||||
@@ -90,9 +98,11 @@ def mcp_module():
|
|||||||
|
|
||||||
# box models
|
# box models
|
||||||
import enum as _enum
|
import enum as _enum
|
||||||
|
|
||||||
class _BPS(str, _enum.Enum):
|
class _BPS(str, _enum.Enum):
|
||||||
RUNNING = 'running'
|
RUNNING = 'running'
|
||||||
EXITED = 'exited'
|
EXITED = 'exited'
|
||||||
|
|
||||||
_save_and_stub('langbot_plugin.box', is_package=True)
|
_save_and_stub('langbot_plugin.box', is_package=True)
|
||||||
_save_and_stub('langbot_plugin.box.models', {'BoxManagedProcessStatus': _BPS})
|
_save_and_stub('langbot_plugin.box.models', {'BoxManagedProcessStatus': _BPS})
|
||||||
|
|
||||||
@@ -100,8 +110,17 @@ def mcp_module():
|
|||||||
mod_fqn = 'langbot.pkg.provider.tools.loaders.mcp'
|
mod_fqn = 'langbot.pkg.provider.tools.loaders.mcp'
|
||||||
sys.modules.pop(mod_fqn, None)
|
sys.modules.pop(mod_fqn, None)
|
||||||
mcp_path = os.path.join(
|
mcp_path = os.path.join(
|
||||||
os.path.dirname(__file__), '..', '..', '..',
|
os.path.dirname(__file__),
|
||||||
'src', 'langbot', 'pkg', 'provider', 'tools', 'loaders', 'mcp.py',
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'src',
|
||||||
|
'langbot',
|
||||||
|
'pkg',
|
||||||
|
'provider',
|
||||||
|
'tools',
|
||||||
|
'loaders',
|
||||||
|
'mcp.py',
|
||||||
)
|
)
|
||||||
mcp_path = os.path.normpath(mcp_path)
|
mcp_path = os.path.normpath(mcp_path)
|
||||||
spec = importlib.util.spec_from_file_location(mod_fqn, mcp_path)
|
spec = importlib.util.spec_from_file_location(mod_fqn, mcp_path)
|
||||||
@@ -124,6 +143,7 @@ def mcp_module():
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _make_ap():
|
def _make_ap():
|
||||||
ap = Mock()
|
ap = Mock()
|
||||||
ap.logger = Mock()
|
ap.logger = Mock()
|
||||||
@@ -160,28 +180,32 @@ class TestMCPServerBoxConfig:
|
|||||||
assert cfg.read_only_rootfs is None
|
assert cfg.read_only_rootfs is None
|
||||||
|
|
||||||
def test_custom_values(self, mcp_module):
|
def test_custom_values(self, mcp_module):
|
||||||
cfg = mcp_module.MCPServerBoxConfig.model_validate({
|
cfg = mcp_module.MCPServerBoxConfig.model_validate(
|
||||||
'image': 'node:20',
|
{
|
||||||
'network': 'on',
|
'image': 'node:20',
|
||||||
'host_path': '/home/user/mcp',
|
'network': 'on',
|
||||||
'host_path_mode': 'rw',
|
'host_path': '/home/user/mcp',
|
||||||
'env': {'FOO': 'bar'},
|
'host_path_mode': 'rw',
|
||||||
'startup_timeout_sec': 60,
|
'env': {'FOO': 'bar'},
|
||||||
'cpus': 2.0,
|
'startup_timeout_sec': 60,
|
||||||
'memory_mb': 1024,
|
'cpus': 2.0,
|
||||||
'pids_limit': 256,
|
'memory_mb': 1024,
|
||||||
'read_only_rootfs': False,
|
'pids_limit': 256,
|
||||||
})
|
'read_only_rootfs': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
assert cfg.image == 'node:20'
|
assert cfg.image == 'node:20'
|
||||||
assert cfg.network == 'on'
|
assert cfg.network == 'on'
|
||||||
assert cfg.cpus == 2.0
|
assert cfg.cpus == 2.0
|
||||||
assert cfg.memory_mb == 1024
|
assert cfg.memory_mb == 1024
|
||||||
|
|
||||||
def test_extra_fields_ignored(self, mcp_module):
|
def test_extra_fields_ignored(self, mcp_module):
|
||||||
cfg = mcp_module.MCPServerBoxConfig.model_validate({
|
cfg = mcp_module.MCPServerBoxConfig.model_validate(
|
||||||
'image': 'node:20',
|
{
|
||||||
'unknown_field': 'whatever',
|
'image': 'node:20',
|
||||||
})
|
'unknown_field': 'whatever',
|
||||||
|
}
|
||||||
|
)
|
||||||
assert cfg.image == 'node:20'
|
assert cfg.image == 'node:20'
|
||||||
assert not hasattr(cfg, 'unknown_field')
|
assert not hasattr(cfg, 'unknown_field')
|
||||||
|
|
||||||
@@ -191,56 +215,98 @@ class TestMCPServerBoxConfig:
|
|||||||
|
|
||||||
class TestRewritePath:
|
class TestRewritePath:
|
||||||
def test_no_host_path_returns_unchanged(self, mcp_module):
|
def test_no_host_path_returns_unchanged(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
assert s._rewrite_path('/some/path', None) == '/some/path'
|
assert s._rewrite_path('/some/path', None) == '/some/path'
|
||||||
|
|
||||||
def test_empty_path_returns_empty(self, mcp_module):
|
def test_empty_path_returns_empty(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
assert s._rewrite_path('', '/home/user/mcp') == ''
|
assert s._rewrite_path('', '/home/user/mcp') == ''
|
||||||
|
|
||||||
def test_prefix_match_rewrites(self, mcp_module):
|
def test_prefix_match_rewrites(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._rewrite_path('/home/user/mcp/server.py', '/home/user/mcp')
|
result = s._rewrite_path('/home/user/mcp/server.py', '/home/user/mcp')
|
||||||
assert result == '/workspace/server.py'
|
assert result == '/workspace/server.py'
|
||||||
|
|
||||||
def test_exact_match_rewrites_to_workspace(self, mcp_module):
|
def test_exact_match_rewrites_to_workspace(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._rewrite_path('/home/user/mcp', '/home/user/mcp')
|
result = s._rewrite_path('/home/user/mcp', '/home/user/mcp')
|
||||||
assert result == '/workspace'
|
assert result == '/workspace'
|
||||||
|
|
||||||
def test_non_matching_path_unchanged(self, mcp_module):
|
def test_non_matching_path_unchanged(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._rewrite_path('/opt/other/server.py', '/home/user/mcp')
|
result = s._rewrite_path('/opt/other/server.py', '/home/user/mcp')
|
||||||
assert result == '/opt/other/server.py'
|
assert result == '/opt/other/server.py'
|
||||||
|
|
||||||
def test_similar_prefix_not_rewritten(self, mcp_module):
|
def test_similar_prefix_not_rewritten(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._rewrite_path('/home/user/mcp-other/file.py', '/home/user/mcp')
|
result = s._rewrite_path('/home/user/mcp-other/file.py', '/home/user/mcp')
|
||||||
assert result == '/home/user/mcp-other/file.py'
|
assert result == '/home/user/mcp-other/file.py'
|
||||||
|
|
||||||
def test_nested_subpath_rewrites(self, mcp_module):
|
def test_nested_subpath_rewrites(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._rewrite_path('/home/user/mcp/src/lib/main.py', '/home/user/mcp')
|
result = s._rewrite_path('/home/user/mcp/src/lib/main.py', '/home/user/mcp')
|
||||||
assert result == '/workspace/src/lib/main.py'
|
assert result == '/workspace/src/lib/main.py'
|
||||||
|
|
||||||
@@ -250,25 +316,43 @@ class TestRewritePath:
|
|||||||
|
|
||||||
class TestInferHostPath:
|
class TestInferHostPath:
|
||||||
def test_no_absolute_paths_returns_none(self, mcp_module):
|
def test_no_absolute_paths_returns_none(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': ['server.py'],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': ['server.py'],
|
||||||
|
},
|
||||||
|
)
|
||||||
assert s._infer_host_path() is None
|
assert s._infer_host_path() is None
|
||||||
|
|
||||||
def test_nonexistent_path_returns_none(self, mcp_module):
|
def test_nonexistent_path_returns_none(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': '/nonexistent/path/to/python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': '/nonexistent/path/to/python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
assert s._infer_host_path() is None
|
assert s._infer_host_path() is None
|
||||||
|
|
||||||
def test_existing_absolute_path_infers_directory(self, mcp_module):
|
def test_existing_absolute_path_infers_directory(self, mcp_module):
|
||||||
with tempfile.NamedTemporaryFile(suffix='.py') as f:
|
with tempfile.NamedTemporaryFile(suffix='.py') as f:
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [f.name],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [f.name],
|
||||||
|
},
|
||||||
|
)
|
||||||
result = s._infer_host_path()
|
result = s._infer_host_path()
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result == os.path.dirname(os.path.realpath(f.name))
|
assert result == os.path.dirname(os.path.realpath(f.name))
|
||||||
@@ -279,10 +363,16 @@ class TestInferHostPath:
|
|||||||
|
|
||||||
class TestBuildBoxSessionPayload:
|
class TestBuildBoxSessionPayload:
|
||||||
def test_minimal_config(self, mcp_module):
|
def test_minimal_config(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_session_payload('session-123')
|
payload = s._build_box_session_payload('session-123')
|
||||||
assert payload['session_id'] == 'session-123'
|
assert payload['session_id'] == 'session-123'
|
||||||
assert payload['workdir'] == '/workspace'
|
assert payload['workdir'] == '/workspace'
|
||||||
@@ -290,21 +380,33 @@ class TestBuildBoxSessionPayload:
|
|||||||
assert 'host_path' not in payload
|
assert 'host_path' not in payload
|
||||||
|
|
||||||
def test_with_host_path(self, mcp_module):
|
def test_with_host_path(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
'box': {'host_path': '/home/user/mcp', 'host_path_mode': 'ro'},
|
'name': 'test',
|
||||||
})
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
'box': {'host_path': '/home/user/mcp', 'host_path_mode': 'ro'},
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_session_payload('session-123')
|
payload = s._build_box_session_payload('session-123')
|
||||||
assert payload['host_path'] == '/home/user/mcp'
|
assert payload['host_path'] == '/home/user/mcp'
|
||||||
assert payload['host_path_mode'] == 'ro'
|
assert payload['host_path_mode'] == 'ro'
|
||||||
|
|
||||||
def test_optional_fields_included_when_set(self, mcp_module):
|
def test_optional_fields_included_when_set(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
'box': {'image': 'node:20', 'cpus': 2.0, 'memory_mb': 1024, 'pids_limit': 256},
|
'name': 'test',
|
||||||
})
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
'box': {'image': 'node:20', 'cpus': 2.0, 'memory_mb': 1024, 'pids_limit': 256},
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_session_payload('session-123')
|
payload = s._build_box_session_payload('session-123')
|
||||||
assert payload['image'] == 'node:20'
|
assert payload['image'] == 'node:20'
|
||||||
assert payload['cpus'] == 2.0
|
assert payload['cpus'] == 2.0
|
||||||
@@ -312,10 +414,16 @@ class TestBuildBoxSessionPayload:
|
|||||||
assert payload['pids_limit'] == 256
|
assert payload['pids_limit'] == 256
|
||||||
|
|
||||||
def test_none_fields_excluded(self, mcp_module):
|
def test_none_fields_excluded(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_session_payload('session-123')
|
payload = s._build_box_session_payload('session-123')
|
||||||
assert 'image' not in payload
|
assert 'image' not in payload
|
||||||
assert 'cpus' not in payload
|
assert 'cpus' not in payload
|
||||||
@@ -326,10 +434,17 @@ class TestBuildBoxSessionPayload:
|
|||||||
|
|
||||||
class TestBuildBoxProcessPayload:
|
class TestBuildBoxProcessPayload:
|
||||||
def test_basic_payload(self, mcp_module):
|
def test_basic_payload(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': ['server.py'], 'env': {'KEY': 'val'},
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': ['server.py'],
|
||||||
|
'env': {'KEY': 'val'},
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_process_payload()
|
payload = s._build_box_process_payload()
|
||||||
assert payload['command'] == 'python'
|
assert payload['command'] == 'python'
|
||||||
assert payload['args'] == ['server.py']
|
assert payload['args'] == ['server.py']
|
||||||
@@ -337,26 +452,36 @@ class TestBuildBoxProcessPayload:
|
|||||||
assert payload['cwd'] == '/workspace'
|
assert payload['cwd'] == '/workspace'
|
||||||
|
|
||||||
def test_path_rewriting_applied(self, mcp_module):
|
def test_path_rewriting_applied(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': '/home/user/mcp/venv/bin/python',
|
{
|
||||||
'args': ['/home/user/mcp/server.py', '--config', '/home/user/mcp/config.json'],
|
'name': 'test',
|
||||||
'env': {},
|
'uuid': 'u1',
|
||||||
'box': {'host_path': '/home/user/mcp'},
|
'mode': 'sse',
|
||||||
})
|
'command': '/home/user/mcp/venv/bin/python',
|
||||||
|
'args': ['/home/user/mcp/server.py', '--config', '/home/user/mcp/config.json'],
|
||||||
|
'env': {},
|
||||||
|
'box': {'host_path': '/home/user/mcp'},
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_process_payload()
|
payload = s._build_box_process_payload()
|
||||||
# venv python is replaced with plain 'python' (deps installed in-container)
|
# venv python is replaced with plain 'python' (deps installed in-container)
|
||||||
assert payload['command'] == 'python'
|
assert payload['command'] == 'python'
|
||||||
assert payload['args'] == ['/workspace/server.py', '--config', '/workspace/config.json']
|
assert payload['args'] == ['/workspace/server.py', '--config', '/workspace/config.json']
|
||||||
|
|
||||||
def test_non_matching_args_not_rewritten(self, mcp_module):
|
def test_non_matching_args_not_rewritten(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python',
|
{
|
||||||
'args': ['/opt/other/server.py', '--flag'],
|
'name': 'test',
|
||||||
'env': {},
|
'uuid': 'u1',
|
||||||
'box': {'host_path': '/home/user/mcp'},
|
'mode': 'sse',
|
||||||
})
|
'command': 'python',
|
||||||
|
'args': ['/opt/other/server.py', '--flag'],
|
||||||
|
'env': {},
|
||||||
|
'box': {'host_path': '/home/user/mcp'},
|
||||||
|
},
|
||||||
|
)
|
||||||
payload = s._build_box_process_payload()
|
payload = s._build_box_process_payload()
|
||||||
assert payload['command'] == 'python'
|
assert payload['command'] == 'python'
|
||||||
assert payload['args'] == ['/opt/other/server.py', '--flag']
|
assert payload['args'] == ['/opt/other/server.py', '--flag']
|
||||||
@@ -367,10 +492,16 @@ class TestBuildBoxProcessPayload:
|
|||||||
|
|
||||||
class TestGetRuntimeInfoDict:
|
class TestGetRuntimeInfoDict:
|
||||||
def test_non_stdio_session(self, mcp_module):
|
def test_non_stdio_session(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'test-uuid', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'test-uuid',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
info = s.get_runtime_info_dict()
|
info = s.get_runtime_info_dict()
|
||||||
assert info['status'] == 'connecting'
|
assert info['status'] == 'connecting'
|
||||||
assert 'box_session_id' not in info
|
assert 'box_session_id' not in info
|
||||||
@@ -378,10 +509,17 @@ class TestGetRuntimeInfoDict:
|
|||||||
def test_stdio_session_includes_box_info(self, mcp_module):
|
def test_stdio_session_includes_box_info(self, mcp_module):
|
||||||
ap = _make_ap()
|
ap = _make_ap()
|
||||||
ap.box_service.available = True
|
ap.box_service.available = True
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'test-uuid', 'mode': 'stdio',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
}, ap=ap)
|
'name': 'test',
|
||||||
|
'uuid': 'test-uuid',
|
||||||
|
'mode': 'stdio',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
ap=ap,
|
||||||
|
)
|
||||||
info = s.get_runtime_info_dict()
|
info = s.get_runtime_info_dict()
|
||||||
assert info['box_session_id'] == 'mcp-test-uuid'
|
assert info['box_session_id'] == 'mcp-test-uuid'
|
||||||
assert info['box_enabled'] is True
|
assert info['box_enabled'] is True
|
||||||
@@ -389,10 +527,17 @@ class TestGetRuntimeInfoDict:
|
|||||||
def test_stdio_session_without_box_runtime(self, mcp_module):
|
def test_stdio_session_without_box_runtime(self, mcp_module):
|
||||||
ap = _make_ap()
|
ap = _make_ap()
|
||||||
ap.box_service.available = False
|
ap.box_service.available = False
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'test-uuid', 'mode': 'stdio',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
}, ap=ap)
|
'name': 'test',
|
||||||
|
'uuid': 'test-uuid',
|
||||||
|
'mode': 'stdio',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
ap=ap,
|
||||||
|
)
|
||||||
info = s.get_runtime_info_dict()
|
info = s.get_runtime_info_dict()
|
||||||
assert 'box_session_id' not in info
|
assert 'box_session_id' not in info
|
||||||
|
|
||||||
@@ -402,20 +547,32 @@ class TestGetRuntimeInfoDict:
|
|||||||
|
|
||||||
class TestBoxConfigParsing:
|
class TestBoxConfigParsing:
|
||||||
def test_box_config_parsed_from_server_config(self, mcp_module):
|
def test_box_config_parsed_from_server_config(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
'box': {'image': 'node:20', 'host_path': '/home/user/mcp'},
|
'name': 'test',
|
||||||
})
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
'box': {'image': 'node:20', 'host_path': '/home/user/mcp'},
|
||||||
|
},
|
||||||
|
)
|
||||||
assert isinstance(s.box_config, mcp_module.MCPServerBoxConfig)
|
assert isinstance(s.box_config, mcp_module.MCPServerBoxConfig)
|
||||||
assert s.box_config.image == 'node:20'
|
assert s.box_config.image == 'node:20'
|
||||||
assert s.box_config.host_path == '/home/user/mcp'
|
assert s.box_config.host_path == '/home/user/mcp'
|
||||||
|
|
||||||
def test_missing_box_key_uses_defaults(self, mcp_module):
|
def test_missing_box_key_uses_defaults(self, mcp_module):
|
||||||
s = _make_session(mcp_module, {
|
s = _make_session(
|
||||||
'name': 'test', 'uuid': 'u1', 'mode': 'sse',
|
mcp_module,
|
||||||
'command': 'python', 'args': [],
|
{
|
||||||
})
|
'name': 'test',
|
||||||
|
'uuid': 'u1',
|
||||||
|
'mode': 'sse',
|
||||||
|
'command': 'python',
|
||||||
|
'args': [],
|
||||||
|
},
|
||||||
|
)
|
||||||
assert isinstance(s.box_config, mcp_module.MCPServerBoxConfig)
|
assert isinstance(s.box_config, mcp_module.MCPServerBoxConfig)
|
||||||
assert s.box_config.image is None
|
assert s.box_config.image is None
|
||||||
assert s.box_config.host_path_mode == 'ro'
|
assert s.box_config.host_path_mode == 'ro'
|
||||||
|
|||||||
Reference in New Issue
Block a user