fix(plugin): honor display_plugin_debug_url port for managed Runtime (#2782)

Stdio and Windows launches always bound the SDK default debug port 5401,
so changing plugin.display_plugin_debug_url only updated the console URL
and a second Core instance hit Address already in use. Pass the configured
debug (and Windows control) ports into the Runtime CLI, and document the
behavior.

Fixes #2713

Co-authored-by: Li-john1021 <[email protected]>
This commit is contained in:
Li-john1021
2026-10-27 20:56:26 +08:00
committed by GitHub
co-authored by Li-john1021
parent 12debe2747
commit bd4e408d7e
5 changed files with 222 additions and 9 deletions
@@ -460,3 +460,171 @@ async def test_explicit_cloud_binding_is_revalidated_against_projection():
expected_generation=7,
)
connector._synchronize_workspace.assert_awaited_once_with(configured)
def test_runtime_debug_port_from_url_defaults_and_parses():
assert PluginRuntimeConnector._runtime_debug_port_from_url('') == 5401
assert PluginRuntimeConnector._runtime_debug_port_from_url('ws://localhost:5411/plugin/debug/ws') == 5411
assert PluginRuntimeConnector._runtime_debug_port_from_url('localhost:5412/plugin/debug/ws') == 5412
assert PluginRuntimeConnector._runtime_control_port_from_url('ws://langbot_plugin_runtime:5500/control/ws') == 5500
assert PluginRuntimeConnector._runtime_control_port_from_url('') == 5400
def test_managed_runtime_ports_follow_plugin_config_urls():
connector = make_connector()
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = 'ws://127.0.0.1:5500/control/ws'
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://127.0.0.1:5411/plugin/debug/ws'
assert connector._managed_runtime_ports() == (5500, 5411)
def test_managed_runtime_ports_default_when_urls_empty():
connector = make_connector()
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = ''
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = ''
assert connector._managed_runtime_ports() == (5400, 5401)
@pytest.mark.asyncio
async def test_stdio_runtime_passes_debug_port_from_display_url(
monkeypatch: pytest.MonkeyPatch,
):
connector = make_connector()
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://localhost:5411/plugin/debug/ws'
connector._prepare_connected_runtime = AsyncMock()
created = {}
monkeypatch.setattr(connector_module.constants, 'instance_id', 'instance-a')
class FakeRuntimeHandler:
def __init__(self, connection, disconnect_callback, ap):
self.release = asyncio.Event()
async def ping(self):
return None
async def set_runtime_config(self, **kwargs):
return None
async def run(self):
await self.release.wait()
async def close(self):
self.release.set()
class FakeController:
def __init__(self, **kwargs):
created.update(kwargs)
async def run(self, callback):
await callback(object())
async def close(self):
return None
monkeypatch.setattr(connector_module.platform, 'get_platform', lambda: 'linux')
monkeypatch.setattr(
connector_module.platform,
'use_websocket_to_connect_plugin_runtime',
lambda: False,
)
monkeypatch.setattr(
connector_module.handler,
'RuntimeConnectionHandler',
FakeRuntimeHandler,
)
monkeypatch.setattr(
connector_module.stdio_client_controller,
'StdioClientController',
FakeController,
)
await connector.initialize()
assert created['args'] == [
'-m',
'langbot_plugin.cli.__init__',
'rt',
'-s',
'--ws-debug-port',
'5411',
]
assert connector._connected.is_set()
await connector.aclose()
@pytest.mark.asyncio
async def test_windows_runtime_passes_control_and_debug_ports(
monkeypatch: pytest.MonkeyPatch,
):
connector = make_connector()
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = 'ws://localhost:5500/control/ws'
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://localhost:5411/plugin/debug/ws'
connector._prepare_connected_runtime = AsyncMock()
start_args: list[object] = []
created = {}
monkeypatch.setattr(connector_module.constants, 'instance_id', 'instance-a')
class FakeRuntimeHandler:
def __init__(self, connection, disconnect_callback, ap):
self.release = asyncio.Event()
async def ping(self):
return None
async def set_runtime_config(self, **kwargs):
return None
async def run(self):
await self.release.wait()
async def close(self):
self.release.set()
class FakeController:
def __init__(self, **kwargs):
created.update(kwargs)
async def run(self, callback):
await callback(object())
async def close(self):
return None
async def fake_start(*args, env_overrides=None):
start_args.extend(args)
start_args.append(env_overrides)
monkeypatch.setattr(connector_module.platform, 'get_platform', lambda: 'win32')
monkeypatch.setattr(
connector_module.platform,
'use_websocket_to_connect_plugin_runtime',
lambda: False,
)
monkeypatch.setattr(
connector_module.handler,
'RuntimeConnectionHandler',
FakeRuntimeHandler,
)
monkeypatch.setattr(
connector_module.ws_client_controller,
'WebSocketClientController',
FakeController,
)
monkeypatch.setattr(connector, '_start_runtime_subprocess', fake_start)
await connector.initialize()
assert start_args[:-1] == [
'-m',
'langbot_plugin.cli.__init__',
'rt',
'--ws-control-port',
'5500',
'--ws-debug-port',
'5411',
]
assert start_args[-1] == {PLUGIN_RUNTIME_CONTROL_TOKEN_ENV: connector._control_token}
assert created['ws_url'] == 'ws://localhost:5500/control/ws'
assert connector._connected.is_set()
await connector.aclose()