fix(lsp): defer LSP client start until bridge handshake complete
Sublime's LSP package reads .sublime-project rows directly at boot, so managed LSP-pyright / LSP-ruff entries left ``enabled: true`` from the previous Sublime PID immediately spawn ``local_bridge lsp-stdio`` against a broker socket whose path encodes the dead PID (``sessions-local-bridge-<host>-<pid>.sock``). The helper exits 1, the LSP package retries 5x in 180s, then disables pyright/ruff for the rest of the session — observable as a crash storm in the console before the user does anything. Add a ``disable_stale_managed_lsp_rows_on_disk`` helper that flips ``enabled: false`` on every Sessions-managed LSP row whose ``--bridge-socket`` is missing or stale, preserving live rows and any user-managed (``sessions_remote_stdio_managed: false``) rows untouched. Wire it into ``register_sessions_transport_hooks`` so plugin_loaded runs the disable across every open Sessions workspace before the LSP package gets a chance to spawn the helper. Once ``_on_persistent_bridge_handshake_ready`` fires, the existing refresh path rewrites the same rows with ``enabled: true`` plus the live broker socket and triggers ``lsp_restart_server`` so pyright/ruff attach cleanly. Also threads a ``managed_lsp_enabled`` keyword through ``build_managed_lsp_settings_block`` / ``merge_sessions_lsp_into_project_data`` / ``refresh_project_file_lsp_block`` so future callers can write the same disabled row without going through the disk-only helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,7 @@ from .jupyter_hosting import (
|
||||
)
|
||||
from .lsp_project_wiring import (
|
||||
collect_lsp_diagnostics_snapshot,
|
||||
disable_stale_managed_lsp_rows_on_disk,
|
||||
existing_managed_broker_sockets,
|
||||
explain_lsp_attach_blockers,
|
||||
format_lsp_diagnostics_panel_text,
|
||||
@@ -4304,6 +4305,85 @@ def _workspace_runtime_connected(
|
||||
def register_sessions_transport_hooks() -> None:
|
||||
"""Wire transport observers (bridge handshake → project LSP refresh)."""
|
||||
register_bridge_handshake_listener(_on_persistent_bridge_handshake_ready)
|
||||
_disable_pre_handshake_managed_lsp_rows_on_open_windows()
|
||||
|
||||
|
||||
def _disable_pre_handshake_managed_lsp_rows_on_open_windows() -> None:
|
||||
"""Force ``enabled: false`` on every Sessions ``.sublime-project`` at boot.
|
||||
|
||||
Sublime's LSP package reads the project file directly when the window
|
||||
activates, so any managed LSP row left ``enabled: true`` from the
|
||||
previous Sublime PID will spawn ``local_bridge lsp-stdio`` against a
|
||||
broker socket whose path encodes the dead PID
|
||||
(``sessions-local-bridge-<host>-<pid>.sock``). The helper exits 1
|
||||
immediately, the LSP package retries 5x in 180s, and pyright/ruff get
|
||||
permanently disabled for the session — observable as the LSP crash
|
||||
storm at boot. Disabling rows on disk before any view activation gives
|
||||
the bridge handshake time to complete; once
|
||||
:func:`_on_persistent_bridge_handshake_ready` fires it rewrites the
|
||||
same rows with ``enabled: true`` and the live broker socket, then
|
||||
triggers ``lsp_restart_server`` so pyright/ruff attach cleanly.
|
||||
|
||||
Best-effort: any I/O / JSON error is swallowed (traced) so a malformed
|
||||
user project file cannot block plugin load.
|
||||
"""
|
||||
windows_fn = getattr(sublime, "windows", None)
|
||||
if not callable(windows_fn):
|
||||
return
|
||||
try:
|
||||
windows_iter = list(windows_fn())
|
||||
except Exception as exc: # noqa: BLE001 — Sublime API edge cases at boot
|
||||
_trace_event(
|
||||
"lsp.pre_handshake_disable_failed",
|
||||
stage="enumerate_windows",
|
||||
error=repr(exc),
|
||||
)
|
||||
return
|
||||
for window in windows_iter:
|
||||
try:
|
||||
_disable_pre_handshake_managed_lsp_rows_for_window(window)
|
||||
except Exception as exc: # noqa: BLE001 — best-effort per window
|
||||
_trace_event(
|
||||
"lsp.pre_handshake_disable_failed",
|
||||
stage="per_window",
|
||||
error=repr(exc),
|
||||
)
|
||||
|
||||
|
||||
def _disable_pre_handshake_managed_lsp_rows_for_window(window: object) -> None:
|
||||
"""Run :func:`disable_stale_managed_lsp_rows_on_disk` for one window."""
|
||||
project_file_name_fn = getattr(window, "project_file_name", None)
|
||||
project_path_str = (
|
||||
project_file_name_fn() if callable(project_file_name_fn) else None
|
||||
)
|
||||
if not isinstance(project_path_str, str) or not project_path_str.strip():
|
||||
return
|
||||
project_path = Path(project_path_str)
|
||||
if not project_path.is_file():
|
||||
return
|
||||
settings = SessionsSettings()
|
||||
context = _workspace_context(window, settings, missing_detail_message=False)
|
||||
if context is None:
|
||||
return
|
||||
host_alias = getattr(context.recent_entry, "host_alias", "") or ""
|
||||
handshake = bridge_handshake_info(host_alias) if host_alias else None
|
||||
live_socket: Optional[str] = None
|
||||
if isinstance(handshake, dict):
|
||||
candidate = handshake.get("broker_socket")
|
||||
if isinstance(candidate, str) and candidate.strip():
|
||||
live_socket = candidate.strip()
|
||||
flipped = disable_stale_managed_lsp_rows_on_disk(
|
||||
project_path,
|
||||
live_broker_socket=live_socket,
|
||||
)
|
||||
if flipped:
|
||||
_trace_event(
|
||||
"lsp.pre_handshake_disable_applied",
|
||||
host_alias=host_alias,
|
||||
cache_key=context.cache_key,
|
||||
disabled_clients=flipped,
|
||||
live_broker_socket=live_socket,
|
||||
)
|
||||
|
||||
|
||||
def _on_persistent_bridge_handshake_ready(host_alias: str) -> None:
|
||||
|
||||
@@ -190,12 +190,23 @@ def build_managed_lsp_settings_block(
|
||||
host_alias: str,
|
||||
local_cache_root: str,
|
||||
active_python_path: Optional[str] = None,
|
||||
managed_lsp_enabled: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return an ``LSP`` settings subtree for managed remote stdio clients.
|
||||
|
||||
When ``active_python_path`` is supplied, the pyright client row also gets
|
||||
``settings.python.pythonPath`` pointing at that remote interpreter so
|
||||
LSP-pyright uses the chosen environment.
|
||||
|
||||
``managed_lsp_enabled`` controls the per-row ``"enabled"`` flag. The
|
||||
bridge handshake must have completed (broker socket present + listening)
|
||||
before LSP clients can attach; spawning ``local_bridge lsp-stdio``
|
||||
against a stale or missing broker_socket exits 1 immediately, and the
|
||||
Sublime LSP package retries five times in 180s before disabling the
|
||||
client for the session. To avoid that crash storm at Sublime boot the
|
||||
refresh path passes ``managed_lsp_enabled=False`` until the broker
|
||||
socket is observed live, then flips back to ``True`` once
|
||||
``_on_persistent_bridge_handshake_ready`` fires.
|
||||
"""
|
||||
local_uri, remote_uri = lsp_uri_prefix_pair(
|
||||
local_cache_root=local_cache_root,
|
||||
@@ -229,7 +240,7 @@ def build_managed_lsp_settings_block(
|
||||
settings_block["python"] = {"pythonPath": active_python_path}
|
||||
lsp_root[entry.project_client_key] = {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": True,
|
||||
"enabled": bool(managed_lsp_enabled),
|
||||
"selector": entry.sublime_selector,
|
||||
"command": command,
|
||||
"settings": settings_block,
|
||||
@@ -247,8 +258,13 @@ def merge_sessions_lsp_into_project_data(
|
||||
host_alias: str,
|
||||
local_cache_root: str,
|
||||
active_python_path: Optional[str] = None,
|
||||
managed_lsp_enabled: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a copy of ``project_data`` with managed ``settings.LSP`` rows merged."""
|
||||
"""Return a copy of ``project_data`` with managed ``settings.LSP`` rows merged.
|
||||
|
||||
See :func:`build_managed_lsp_settings_block` for the meaning of
|
||||
``managed_lsp_enabled``.
|
||||
"""
|
||||
base = dict(project_data)
|
||||
settings = _as_str_dict(base.get("settings"))
|
||||
existing_lsp = _as_str_dict(settings.get("LSP"))
|
||||
@@ -260,6 +276,7 @@ def merge_sessions_lsp_into_project_data(
|
||||
host_alias=host_alias,
|
||||
local_cache_root=local_cache_root,
|
||||
active_python_path=active_python_path,
|
||||
managed_lsp_enabled=managed_lsp_enabled,
|
||||
)
|
||||
merged_lsp: Dict[str, Any] = dict(existing_lsp)
|
||||
_normalize_managed_lsp_client_aliases(merged_lsp)
|
||||
@@ -393,6 +410,7 @@ def refresh_project_file_lsp_block(
|
||||
host_alias: str,
|
||||
local_cache_root: str,
|
||||
active_python_path: Optional[str] = None,
|
||||
managed_lsp_enabled: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Read project JSON from disk, merge managed LSP, write back, return merged.
|
||||
|
||||
@@ -403,6 +421,9 @@ def refresh_project_file_lsp_block(
|
||||
Cargo.lock / .sublime-project touch, and we got user feedback that the
|
||||
noise was excessive. The short-circuit preserves the merged return value
|
||||
for callers that depend on it.
|
||||
|
||||
See :func:`build_managed_lsp_settings_block` for the meaning of
|
||||
``managed_lsp_enabled``.
|
||||
"""
|
||||
raw = project_file_path.read_text(encoding="utf-8")
|
||||
existing = _parse_sublime_project_json(raw)
|
||||
@@ -417,6 +438,7 @@ def refresh_project_file_lsp_block(
|
||||
host_alias=host_alias,
|
||||
local_cache_root=local_cache_root,
|
||||
active_python_path=active_python_path,
|
||||
managed_lsp_enabled=managed_lsp_enabled,
|
||||
)
|
||||
rendered = json.dumps(merged, indent=2, sort_keys=True) + "\n"
|
||||
if rendered != raw:
|
||||
@@ -424,6 +446,81 @@ def refresh_project_file_lsp_block(
|
||||
return merged
|
||||
|
||||
|
||||
def disable_stale_managed_lsp_rows_on_disk(
|
||||
project_file_path: Path,
|
||||
*,
|
||||
live_broker_socket: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""Set ``enabled: false`` on managed LSP rows whose broker socket is dead.
|
||||
|
||||
Called at Sublime startup before the bridge handshake completes (and
|
||||
before LSP-pyright / LSP-ruff get a chance to spawn the Sessions
|
||||
``local_bridge lsp-stdio`` helper against a stale ``--bridge-socket``
|
||||
path left over from the previous Sublime PID). Without this gate the
|
||||
helper exits 1 immediately, the LSP package retries 5 times in 180s,
|
||||
then disables both clients for the entire session — observable as a
|
||||
crash storm in the console at boot.
|
||||
|
||||
Returns the list of client keys whose ``enabled`` flag flipped to
|
||||
``False`` so the caller can emit a single trace summarising the
|
||||
pre-handshake disable. Writes to disk only when at least one row
|
||||
changed; preserves user-managed (``sessions_remote_stdio_managed:
|
||||
False``) rows untouched.
|
||||
|
||||
``live_broker_socket`` is the broker socket reported by the current
|
||||
handshake (if any). When provided, rows whose ``--bridge-socket``
|
||||
already matches the live path are left enabled.
|
||||
"""
|
||||
try:
|
||||
raw = project_file_path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return []
|
||||
try:
|
||||
existing = _parse_sublime_project_json(raw)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if not isinstance(existing, dict):
|
||||
return []
|
||||
settings = existing.get("settings")
|
||||
if not isinstance(settings, dict):
|
||||
return []
|
||||
lsp = settings.get("LSP")
|
||||
if not isinstance(lsp, dict):
|
||||
return []
|
||||
live = (live_broker_socket or "").strip()
|
||||
flipped: List[str] = []
|
||||
for client_key, row in lsp.items():
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if not row.get(SESSIONS_REMOTE_LSP_MANAGED_KEY):
|
||||
continue
|
||||
if row.get("enabled") is False:
|
||||
continue
|
||||
command = row.get("command")
|
||||
row_socket = ""
|
||||
if isinstance(command, list):
|
||||
for i, arg in enumerate(command):
|
||||
if arg == "--bridge-socket" and i + 1 < len(command):
|
||||
next_arg = command[i + 1]
|
||||
row_socket = str(next_arg) if isinstance(next_arg, str) else ""
|
||||
break
|
||||
# Row's broker socket already matches a live one — leave enabled.
|
||||
if live and row_socket == live and Path(row_socket).exists():
|
||||
continue
|
||||
# Anything else (empty, stale path, missing file) is unsafe to keep
|
||||
# enabled until the handshake refresh re-validates the socket.
|
||||
row["enabled"] = False
|
||||
flipped.append(str(client_key))
|
||||
if not flipped:
|
||||
return []
|
||||
rendered = json.dumps(existing, indent=2, sort_keys=True) + "\n"
|
||||
if rendered == raw:
|
||||
# Spelling difference only (e.g. key order); no semantic change.
|
||||
return []
|
||||
project_file_path.write_text(rendered, encoding="utf-8")
|
||||
return sorted(flipped)
|
||||
|
||||
|
||||
def trace_lsp_workspace_activation(
|
||||
*,
|
||||
host_alias: str,
|
||||
@@ -504,6 +601,7 @@ __all__ = (
|
||||
"SESSIONS_REMOTE_LSP_MANAGED_KEY",
|
||||
"build_managed_lsp_settings_block",
|
||||
"collect_lsp_diagnostics_snapshot",
|
||||
"disable_stale_managed_lsp_rows_on_disk",
|
||||
"existing_managed_broker_sockets",
|
||||
"explain_lsp_attach_blockers",
|
||||
"format_lsp_diagnostics_panel_text",
|
||||
|
||||
@@ -275,6 +275,160 @@ def test_register_sessions_transport_hooks_idempotent(
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_register_sessions_transport_hooks_disables_stale_lsp_rows_on_open_windows(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""``plugin_loaded`` flips ``enabled: false`` on every workspace's stale rows.
|
||||
|
||||
This is the boot-time gate that prevents the LSP-pyright / LSP-ruff crash
|
||||
storm: the previous Sublime PID's broker socket path
|
||||
(``sessions-local-bridge-<host>-<pid>.sock``) is dead, so leaving the row
|
||||
enabled would let the LSP package spawn ``local_bridge lsp-stdio`` which
|
||||
exits 1 immediately and gets disabled after 5 retries.
|
||||
"""
|
||||
host = "devhost"
|
||||
proj = tmp_path / "p.sublime-project"
|
||||
stale_sock = tmp_path / "stale.sock" # never created → stale
|
||||
proj.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"folders": [],
|
||||
"settings": {
|
||||
"LSP": {
|
||||
"LSP-pyright": {
|
||||
"sessions_remote_stdio_managed": True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/fake/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(stale_sock),
|
||||
],
|
||||
},
|
||||
"LSP-ruff": {
|
||||
"sessions_remote_stdio_managed": True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/fake/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(stale_sock),
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
window = FakeWindow()
|
||||
window.project_file_name = lambda: str(proj)
|
||||
ctx = _make_workspace_context(tmp_path, host_alias=host)
|
||||
|
||||
monkeypatch.setattr(
|
||||
commands, "register_bridge_handshake_listener", lambda _cb: None
|
||||
)
|
||||
monkeypatch.setattr(commands.sublime, "windows", lambda: [window])
|
||||
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **k: ctx)
|
||||
monkeypatch.setattr(commands, "bridge_handshake_info", lambda _h: None)
|
||||
monkeypatch.setattr(commands, "_trace_event", lambda *a, **k: None)
|
||||
|
||||
commands.register_sessions_transport_hooks()
|
||||
|
||||
rows = json.loads(proj.read_text(encoding="utf-8"))["settings"]["LSP"]
|
||||
assert rows["LSP-pyright"]["enabled"] is False
|
||||
assert rows["LSP-ruff"]["enabled"] is False
|
||||
|
||||
|
||||
def test_register_sessions_transport_hooks_keeps_live_socket_enabled(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""If a row's broker socket is already live, ``enabled`` stays ``True``."""
|
||||
host = "devhost"
|
||||
live_sock = tmp_path / "live.sock"
|
||||
live_sock.write_text("", encoding="utf-8")
|
||||
proj = tmp_path / "p.sublime-project"
|
||||
proj.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"settings": {
|
||||
"LSP": {
|
||||
"LSP-pyright": {
|
||||
"sessions_remote_stdio_managed": True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/fake/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(live_sock),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
window = FakeWindow()
|
||||
window.project_file_name = lambda: str(proj)
|
||||
ctx = _make_workspace_context(tmp_path, host_alias=host)
|
||||
|
||||
monkeypatch.setattr(
|
||||
commands, "register_bridge_handshake_listener", lambda _cb: None
|
||||
)
|
||||
monkeypatch.setattr(commands.sublime, "windows", lambda: [window])
|
||||
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **k: ctx)
|
||||
monkeypatch.setattr(
|
||||
commands,
|
||||
"bridge_handshake_info",
|
||||
lambda _h: {"broker_socket": str(live_sock)},
|
||||
)
|
||||
monkeypatch.setattr(commands, "_trace_event", lambda *a, **k: None)
|
||||
|
||||
commands.register_sessions_transport_hooks()
|
||||
|
||||
rows = json.loads(proj.read_text(encoding="utf-8"))["settings"]["LSP"]
|
||||
assert rows["LSP-pyright"]["enabled"] is True
|
||||
|
||||
|
||||
def test_register_sessions_transport_hooks_skips_non_sessions_window(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Non-Sessions windows must not have their ``.sublime-project`` rewritten."""
|
||||
proj = tmp_path / "external.sublime-project"
|
||||
original = {
|
||||
"settings": {
|
||||
"LSP": {
|
||||
"LSP-pyright": {
|
||||
"enabled": True,
|
||||
"command": ["custom-pyright"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proj.write_text(json.dumps(original), encoding="utf-8")
|
||||
raw_before = proj.read_text(encoding="utf-8")
|
||||
|
||||
window = FakeWindow()
|
||||
window.project_file_name = lambda: str(proj)
|
||||
|
||||
monkeypatch.setattr(
|
||||
commands, "register_bridge_handshake_listener", lambda _cb: None
|
||||
)
|
||||
monkeypatch.setattr(commands.sublime, "windows", lambda: [window])
|
||||
monkeypatch.setattr(commands, "_workspace_context", lambda *a, **k: None)
|
||||
monkeypatch.setattr(commands, "_trace_event", lambda *a, **k: None)
|
||||
|
||||
commands.register_sessions_transport_hooks()
|
||||
|
||||
assert proj.read_text(encoding="utf-8") == raw_before
|
||||
|
||||
|
||||
def test_sessions_diagnose_lsp_workspace_shows_panel(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -12,7 +12,9 @@ from sessions.lsp_project_wiring import (
|
||||
SESSIONS_LSP_RUFF_CLIENT_KEY,
|
||||
SESSIONS_LSP_RUST_ANALYZER_CLIENT_KEY,
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY,
|
||||
build_managed_lsp_settings_block,
|
||||
collect_lsp_diagnostics_snapshot,
|
||||
disable_stale_managed_lsp_rows_on_disk,
|
||||
existing_managed_broker_sockets,
|
||||
explain_lsp_attach_blockers,
|
||||
format_lsp_diagnostics_panel_text,
|
||||
@@ -533,3 +535,219 @@ def test_existing_managed_broker_sockets_missing_command_arg(
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert existing_managed_broker_sockets(project_path) == [("LSP-pyright", "")]
|
||||
|
||||
|
||||
def test_build_managed_lsp_settings_block_disabled_when_flag_false(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""``managed_lsp_enabled=False`` writes ``enabled: false`` per row."""
|
||||
block = build_managed_lsp_settings_block(
|
||||
bridge_path="/bridge",
|
||||
broker_socket="/sock",
|
||||
workspace_id="ws1",
|
||||
remote_workspace_root="/r",
|
||||
host_alias="dev",
|
||||
local_cache_root=str(tmp_path / "c"),
|
||||
managed_lsp_enabled=False,
|
||||
)
|
||||
for client_key, row in block.items():
|
||||
assert row["enabled"] is False, client_key
|
||||
assert row[SESSIONS_REMOTE_LSP_MANAGED_KEY] is True, client_key
|
||||
|
||||
|
||||
def test_merge_propagates_managed_lsp_disabled(tmp_path: Path) -> None:
|
||||
"""``merge_sessions_lsp_into_project_data`` forwards the disable flag."""
|
||||
merged = merge_sessions_lsp_into_project_data(
|
||||
{"settings": {}},
|
||||
bridge_path="/b",
|
||||
broker_socket="",
|
||||
workspace_id="w",
|
||||
remote_workspace_root="/r",
|
||||
host_alias="h",
|
||||
local_cache_root=str(tmp_path / "c"),
|
||||
managed_lsp_enabled=False,
|
||||
)
|
||||
pyright = merged["settings"]["LSP"][SESSIONS_LSP_PYRIGHT_CLIENT_KEY]
|
||||
assert pyright["enabled"] is False
|
||||
ruff = merged["settings"]["LSP"][SESSIONS_LSP_RUFF_CLIENT_KEY]
|
||||
assert ruff["enabled"] is False
|
||||
|
||||
|
||||
def test_refresh_project_file_lsp_block_propagates_disabled_flag(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
(tmp_path / "lc").mkdir()
|
||||
proj.write_text(json.dumps({"settings": {}}), encoding="utf-8")
|
||||
merged = refresh_project_file_lsp_block(
|
||||
proj,
|
||||
bridge_path="/bridge",
|
||||
broker_socket="",
|
||||
workspace_id="w",
|
||||
remote_workspace_root="/r",
|
||||
host_alias="h",
|
||||
local_cache_root=str(tmp_path / "lc"),
|
||||
managed_lsp_enabled=False,
|
||||
)
|
||||
pyright = merged["settings"]["LSP"][SESSIONS_LSP_PYRIGHT_CLIENT_KEY]
|
||||
assert pyright["enabled"] is False
|
||||
on_disk = json.loads(proj.read_text(encoding="utf-8"))
|
||||
assert (
|
||||
on_disk["settings"]["LSP"][SESSIONS_LSP_PYRIGHT_CLIENT_KEY]["enabled"] is False
|
||||
)
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_flips_enabled_when_socket_missing(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
proj.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"settings": {
|
||||
"LSP": {
|
||||
SESSIONS_LSP_PYRIGHT_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(tmp_path / "stale.sock"),
|
||||
],
|
||||
},
|
||||
SESSIONS_LSP_RUFF_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(tmp_path / "stale.sock"),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
flipped = disable_stale_managed_lsp_rows_on_disk(proj)
|
||||
assert flipped == [SESSIONS_LSP_PYRIGHT_CLIENT_KEY, SESSIONS_LSP_RUFF_CLIENT_KEY]
|
||||
after = json.loads(proj.read_text(encoding="utf-8"))
|
||||
rows = after["settings"]["LSP"]
|
||||
assert rows[SESSIONS_LSP_PYRIGHT_CLIENT_KEY]["enabled"] is False
|
||||
assert rows[SESSIONS_LSP_RUFF_CLIENT_KEY]["enabled"] is False
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_keeps_live_socket_enabled(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
live_sock = tmp_path / "live.sock"
|
||||
live_sock.write_text("", encoding="utf-8")
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
proj.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"settings": {
|
||||
"LSP": {
|
||||
SESSIONS_LSP_PYRIGHT_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(live_sock),
|
||||
],
|
||||
},
|
||||
SESSIONS_LSP_RUFF_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": True,
|
||||
"command": [
|
||||
"/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
str(tmp_path / "stale.sock"),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
flipped = disable_stale_managed_lsp_rows_on_disk(
|
||||
proj, live_broker_socket=str(live_sock)
|
||||
)
|
||||
assert flipped == [SESSIONS_LSP_RUFF_CLIENT_KEY]
|
||||
rows = json.loads(proj.read_text(encoding="utf-8"))["settings"]["LSP"]
|
||||
assert rows[SESSIONS_LSP_PYRIGHT_CLIENT_KEY]["enabled"] is True
|
||||
assert rows[SESSIONS_LSP_RUFF_CLIENT_KEY]["enabled"] is False
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_skips_user_managed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
proj.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"settings": {
|
||||
"LSP": {
|
||||
SESSIONS_LSP_PYRIGHT_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: False,
|
||||
"enabled": True,
|
||||
"command": ["custom-pyright"],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert disable_stale_managed_lsp_rows_on_disk(proj) == []
|
||||
rows = json.loads(proj.read_text(encoding="utf-8"))["settings"]["LSP"]
|
||||
assert rows[SESSIONS_LSP_PYRIGHT_CLIENT_KEY]["enabled"] is True
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_handles_missing_file(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
missing = tmp_path / "does-not-exist.sublime-project"
|
||||
assert disable_stale_managed_lsp_rows_on_disk(missing) == []
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_handles_malformed_json(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
proj.write_text("{not-json", encoding="utf-8")
|
||||
assert disable_stale_managed_lsp_rows_on_disk(proj) == []
|
||||
|
||||
|
||||
def test_disable_stale_managed_lsp_rows_no_op_when_already_disabled(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
proj = tmp_path / "ws.sublime-project"
|
||||
original = {
|
||||
"settings": {
|
||||
"LSP": {
|
||||
SESSIONS_LSP_PYRIGHT_CLIENT_KEY: {
|
||||
SESSIONS_REMOTE_LSP_MANAGED_KEY: True,
|
||||
"enabled": False,
|
||||
"command": [
|
||||
"/bridge",
|
||||
"lsp-stdio",
|
||||
"--bridge-socket",
|
||||
"/already/missing.sock",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proj.write_text(json.dumps(original), encoding="utf-8")
|
||||
raw_before = proj.read_text(encoding="utf-8")
|
||||
assert disable_stale_managed_lsp_rows_on_disk(proj) == []
|
||||
# File contents unchanged when no row needed flipping.
|
||||
assert proj.read_text(encoding="utf-8") == raw_before
|
||||
|
||||
Reference in New Issue
Block a user