fix(sublime/sessions): gate auto-spawn paths behind explicit reconnect

After a Sublime restart, restoring a Sessions project window would
silently spawn the Rust bridge (SSH + ``session_helper`` push +
handshake) before the user invoked any reconnect command. Two
listeners triggered the spawn through ``execute_remote_exec_once``,
which calls ``_persistent_bridge_for_host`` with
``allow_spawn=True`` by default:

- ``SessionsPythonInterpreterStatusListener.on_activated_async``: on
  every Python view focus, if the cached interpreter version was
  missing, it scheduled ``_probe_active_python_version_task`` ->
  ``probe_interpreter_version`` -> ``execute_remote_exec_once``.
  Restored project windows always start with a cold version cache,
  so this fired for every restored ``.py`` view.
- ``SessionsRemotePythonPipelineListener.on_activated_async``: gated
  on ``sessions_remote_python_auto_diagnostics_on_open`` (default
  false) but had the same shape — when opted-in it would auto-spawn
  on focus.

Add the ``_workspace_runtime_connected`` gate that the other
on-load / on-activated callbacks already use (sidebar placeholder
hydrate, LSP workspace activation tracer, active-remote-view
revalidate). The status bar still paints the cached interpreter
label or the ``(…)`` ellipsis from project metadata, so the slot
isn't blank — only the bridge probe is deferred until the user
runs ``Sessions: Reconnect Current Workspace``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 14:03:22 +09:00
parent c066cb9962
commit 7ca1dbcb7c
2 changed files with 58 additions and 1 deletions

View File

@@ -571,6 +571,14 @@ class SessionsRemotePythonPipelineListener(sublime_plugin.EventListener):
window = window_fn() if callable(window_fn) else None
if window is None:
return
# Same rationale as the version-probe listener: don't spawn the
# bridge on a restored project window before the user has
# explicitly reconnected.
context = _root._workspace_context(
window, SessionsSettings(), missing_detail_message=False
)
if context is None or not _root._workspace_runtime_connected(window, context):
return
view_id_fn = getattr(view, "id", None)
view_id = view_id_fn() if callable(view_id_fn) else -1
if view_id < 0:
@@ -848,7 +856,11 @@ class SessionsPythonInterpreterStatusListener(sublime_plugin.EventListener):
host_alias = context.recent_entry.host_alias
cached = get_cached_version(host_alias, active_path)
_set_active_python_status(view, format_status_label(active_path, cached))
if cached is None:
# Defer the version probe until the user has explicitly reconnected.
# Restored project windows would otherwise spawn the bridge (SSH +
# session_helper push) on focus, which the user reported as
# unexpected auto-reconnect after Sublime restart.
if cached is None and _root._workspace_runtime_connected(window, context):
_root._run_in_background(
_probe_active_python_version_task,
view,

View File

@@ -324,6 +324,7 @@ def test_python_interpreter_status_listener_schedules_probe_when_uncached(
)
view = _StatusView(window)
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **kw: _ctx())
monkeypatch.setattr(commands, "_workspace_runtime_connected", lambda *a, **kw: True)
scheduled: List[Dict[str, Any]] = []
@@ -344,6 +345,49 @@ def test_python_interpreter_status_listener_schedules_probe_when_uncached(
)
def test_python_interpreter_status_listener_skips_probe_when_disconnected(
monkeypatch,
) -> None:
"""Restored project window must not auto-spawn the bridge for the version probe.
The listener should still paint the cached/ellipsis status so the user
sees the previously-known interpreter, but ``_run_in_background`` for
the probe is gated on ``_workspace_runtime_connected``. Without this
gate, opening Sublime + a Sessions project file silently spawns SSH +
pushes ``session_helper`` before the user has explicitly reconnected.
"""
from sessions.python_interpreter_registry import invalidate_version_cache
invalidate_version_cache()
listener = commands.SessionsPythonInterpreterStatusListener()
window = FakeWindow(
project_data={
"settings": {
"sessions_active_python_interpreter": "/srv/ws/.venv/bin/python"
}
}
)
view = _StatusView(window)
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **kw: _ctx())
monkeypatch.setattr(
commands, "_workspace_runtime_connected", lambda *a, **kw: False
)
scheduled: List[Dict[str, Any]] = []
monkeypatch.setattr(
commands,
"_run_in_background",
lambda *a, **kw: scheduled.append((a, kw)),
)
listener.on_activated_async(view)
# Status still paints with ellipsis so the slot is not blank.
assert view.statuses["sessions_active_python"] == "Python: ws (…)"
# But no probe is scheduled — bridge stays untouched until the user
# runs ``Sessions: Reconnect Current Workspace``.
assert scheduled == []
def test_apply_active_python_change_invalidates_version_cache(monkeypatch) -> None:
"""Selection-change path drops cached versions for the affected host."""
from sessions.python_interpreter_registry import (
@@ -611,6 +655,7 @@ def test_status_listener_handles_short_timeout_ms_zero_passthrough(monkeypatch)
)
view = _StatusView(window)
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **kw: _ctx())
monkeypatch.setattr(commands, "_workspace_runtime_connected", lambda *a, **kw: True)
# The probe (with timeout_ms=0) returns nothing → no repaint.
monkeypatch.setattr(