feat(box): add --standalone-box flag and 3-way transport decision for Box runtime

Align Box runtime connection logic with Plugin runtime's pattern:
- Docker: WebSocket to langbot_box container (ws://langbot_box:5411)
- --standalone-box: WebSocket to external Box process (ws://localhost:5411)
- Windows: subprocess + WebSocket (workaround for async stdio limitation)
- Unix/macOS: subprocess + stdio pipe (unchanged)

BoxRuntimeConnector now inherits ManagedRuntimeConnector for subprocess
lifecycle reuse. Add langbot_box service to docker-compose.yaml.
This commit is contained in:
Junyan Qin
2026-04-17 22:38:27 +08:00
committed by WangCham
parent 1b4107a90a
commit f4406cd972
6 changed files with 234 additions and 89 deletions

View File

@@ -25,41 +25,64 @@ def make_app(logger: Mock, runtime_url: str = ''):
)
def test_box_runtime_connector_manages_local_when_no_url():
def test_box_runtime_connector_stdio_when_no_url(monkeypatch: pytest.MonkeyPatch):
"""Without runtime_url, on a non-Docker Unix platform, use stdio."""
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
connector = BoxRuntimeConnector(make_app(Mock()))
assert connector.manages_local_runtime is True
assert connector._uses_websocket() is False
assert isinstance(connector.client, ActionRPCBoxClient)
def test_box_runtime_connector_remote_when_url_configured():
def test_box_runtime_connector_ws_when_url_configured(monkeypatch: pytest.MonkeyPatch):
"""With an explicit runtime_url, always use WebSocket."""
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
logger = Mock()
connector = BoxRuntimeConnector(make_app(logger, runtime_url='http://box-runtime:5410'))
assert connector.manages_local_runtime is False
assert connector._uses_websocket() is True
assert isinstance(connector.client, ActionRPCBoxClient)
def test_box_runtime_connector_manages_local_in_docker(monkeypatch: pytest.MonkeyPatch):
def test_box_runtime_connector_ws_in_docker(monkeypatch: pytest.MonkeyPatch):
"""Inside Docker (no explicit URL), use WebSocket to reach a sibling container."""
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'docker')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
connector = BoxRuntimeConnector(make_app(Mock()))
assert connector.manages_local_runtime is True
assert connector.ws_relay_base_url == 'http://127.0.0.1:5410'
assert connector._uses_websocket() is True
assert connector.ws_relay_base_url == 'http://langbot_box:5410'
def test_box_runtime_connector_ws_relay_url_default():
def test_box_runtime_connector_ws_with_standalone_flag(monkeypatch: pytest.MonkeyPatch):
"""With --standalone-box flag, use WebSocket even on a local Unix platform."""
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', True)
connector = BoxRuntimeConnector(make_app(Mock()))
assert connector._uses_websocket() is True
def test_box_runtime_connector_ws_relay_url_default(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
connector = BoxRuntimeConnector(make_app(Mock()))
assert connector.ws_relay_base_url == 'http://127.0.0.1:5410'
def test_box_runtime_connector_ws_relay_url_explicit():
def test_box_runtime_connector_ws_relay_url_explicit(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
connector = BoxRuntimeConnector(make_app(Mock(), runtime_url='http://box-runtime:5410'))
assert connector.ws_relay_base_url == 'http://box-runtime:5410'
def test_box_runtime_connector_dispose_terminates_subprocess():
def test_box_runtime_connector_dispose_terminates_subprocess(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
logger = Mock()
connector = BoxRuntimeConnector(make_app(logger))
subprocess = Mock()