Pinning settings per server left every other server free to run project code: rust-analyzer runs cargo check (build.rs, proc-macros) on each didSave despite the disabled init options, and jdtls, kotlin-language-server, elixir-ls, zls, clojure-lsp and haskell-language-server evaluate build files when they start. In an untrusted workspace only UNTRUSTED_SAFE_SERVERS now start (pyright, typescript, vue, svelte with their pinned settings; bash, yaml, dockerfile, intelephense, clangd without --query-driver). Every other built-in or user-declared server is skipped at the spawn chokepoint and in enabled_for, logged once per root and listed by hermes lsp status. The rust-analyzer init tweak is gone: it never starts untrusted now. On a local backend the npx tsc and rustfmt --check fallbacks are skipped the same way, since npx resolves the repo's node_modules/.bin/tsc (or its .npmrc registry) and rustup honours its rust-toolchain.toml.
72 lines
3.3 KiB
Python
72 lines
3.3 KiB
Python
"""Config-declared LSP servers (``lsp.servers.<id>`` with ``extensions``) route files and spawn like built-ins."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.lsp.manager import LSPService
|
|
from agent.lsp.servers import SERVERS, ServerContext, custom_servers, find_server_for_file, language_id_for
|
|
|
|
_MOCK = os.path.join(os.path.dirname(__file__), "_mock_lsp_server.py")
|
|
|
|
|
|
def test_custom_server_precedes_builtins_and_carries_language_id():
|
|
cfg = {
|
|
"pyright": {"disabled": True}, # override of a built-in id is NOT a custom server
|
|
"blade": {"command": [sys.executable, _MOCK], "extensions": [".PHP"], "language_id": "blade"},
|
|
"broken": {"command": "not-a-list", "extensions": [".zzz"]}, # malformed → skipped, not fatal
|
|
}
|
|
custom = custom_servers(cfg)
|
|
assert [s.server_id for s in custom] == ["blade"]
|
|
registry = [*custom, *SERVERS]
|
|
assert find_server_for_file("/w/index.php", registry).server_id == "blade" # custom wins over intelephense
|
|
assert find_server_for_file("/w/index.php").server_id == "intelephense" # built-in registry untouched
|
|
assert language_id_for("/w/index.php", custom[0]) == "blade"
|
|
assert language_id_for("/w/index.php") == "php"
|
|
spec = custom[0].build_spawn("/w", ServerContext("/w", install_strategy="manual"))
|
|
assert spec is not None and spec.command == [sys.executable, _MOCK]
|
|
|
|
|
|
@pytest.mark.timeout(60)
|
|
def test_service_gets_diagnostics_from_config_declared_server(tmp_path, monkeypatch):
|
|
"""End to end from config.yaml: the only production path from ``lsp.servers`` to a running server."""
|
|
from tools.environments.local import LocalEnvironment
|
|
from tools.file_operations import ShellFileOperations
|
|
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
(home / "config.yaml").write_text(json.dumps({"lsp": {
|
|
"install_strategy": "manual", "wait_timeout": 5.0, "idle_timeout": 0,
|
|
"trusted_workspaces": [str(tmp_path / "ws")],
|
|
"servers": {"panache": {"command": [sys.executable, _MOCK], "extensions": [".pnch"],
|
|
"env": {"MOCK_LSP_SCRIPT": "errors"}}},
|
|
}}), encoding="utf-8")
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
ws = tmp_path / "ws"
|
|
ws.mkdir()
|
|
subprocess.run(["git", "init", "-q", str(ws)], check=True)
|
|
target = ws / "doc.pnch"
|
|
target.write_text("hello\n", encoding="utf-8")
|
|
(ws / "x.rs").write_text("fn main() {}\n", encoding="utf-8")
|
|
svc = LSPService.create_from_config()
|
|
assert svc is not None
|
|
try:
|
|
assert svc.enabled_for(str(target))
|
|
diags = svc.get_diagnostics_sync(str(target))
|
|
assert diags and diags[0]["source"] == "mock-lsp"
|
|
# Control: a built-in extension still routes to the registry entry, not the custom server.
|
|
assert svc._server_for(str(ws / "x.rs")).server_id == "rust-analyzer"
|
|
# The write path captures pre-write content for the custom extension only via the service.
|
|
fops = ShellFileOperations(LocalEnvironment())
|
|
with patch.object(fops, "_lsp_service", return_value=svc):
|
|
assert fops._lsp_handles_extension(".pnch")
|
|
with patch.object(fops, "_lsp_service", return_value=None):
|
|
assert not fops._lsp_handles_extension(".pnch") and fops._lsp_handles_extension(".rs")
|
|
finally:
|
|
svc.shutdown()
|