Files
hermes-agent/tests/agent/lsp/test_custom_servers.py
John Paul Soliva 59ec281140 fix(lsp): deny by default in an untrusted workspace and skip the toolchain lint fallbacks there
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.
2026-09-29 03:28:46 +05:30

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()