mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-21 03:39:54 +00:00
23344a9a3c
Ship LSP semantic diagnostics as a bundled plugin (plugins/lsp/) using existing hook system. Zero lines of core code modified. Plugin wiring: - pre_tool_call: capture LSP baseline before write_file/patch - transform_tool_result: inject diagnostics into tool result JSON - on_session_start/on_session_end + atexit: lifecycle management Key design: - Baselines keyed by (session_id, abs_path) for concurrent safety - Diagnostics added as 'lsp_diagnostics' JSON field (preserves shape) - Per-file workspace detection (no static session-start gate) - V4A multi-file patch skipped for MVP - Short timeout (3s) — cold start degrades gracefully - os.path.exists heuristic for Docker/SSH backend skip - First relevant write with no server → INFO log with install hint Tests: 77/77 pass including: - Protocol framing, reporter formatting, workspace resolution - Client E2E against mock LSP server (live_system_guard_bypass) - Eventlog steady-state silence contract - Backend-gate heuristic (local vs non-local paths) - Full hook flow integration (pre→write→transform with diagnostics) Source: PR #24168 by @teknium1, PR #24155 by @OutThisLife Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
150 lines
4.1 KiB
Python
150 lines
4.1 KiB
Python
"""Tests for the synchronous LSPService wrapper.
|
|
|
|
Drives the service through ``snapshot_baseline`` →
|
|
``get_diagnostics_sync`` against the mock LSP server, exercising the
|
|
delta filter that ``tools/file_operations._check_lint_delta`` relies
|
|
on.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from plugins.lsp.manager import LSPService
|
|
from plugins.lsp.servers import (
|
|
SERVERS,
|
|
ServerContext,
|
|
ServerDef,
|
|
SpawnSpec,
|
|
find_server_for_file,
|
|
)
|
|
|
|
|
|
MOCK_SERVER = str(Path(__file__).parent / "_mock_lsp_server.py")
|
|
|
|
|
|
def _install_mock_server(monkeypatch, script: str = "errors", server_id: str = "pyright"):
|
|
"""Replace one registered server with a wrapper that spawns the mock.
|
|
|
|
We reuse ``pyright`` so .py files route to it. This keeps the
|
|
test free of any LSP toolchain dependency.
|
|
"""
|
|
target_index = next(i for i, s in enumerate(SERVERS) if s.server_id == server_id)
|
|
original = SERVERS[target_index]
|
|
|
|
def _spawn(root: str, ctx: ServerContext) -> SpawnSpec:
|
|
env = {"MOCK_LSP_SCRIPT": script}
|
|
return SpawnSpec(
|
|
command=[sys.executable, MOCK_SERVER],
|
|
workspace_root=root,
|
|
cwd=root,
|
|
env=env,
|
|
initialization_options={},
|
|
)
|
|
|
|
replacement = ServerDef(
|
|
server_id=server_id,
|
|
extensions=original.extensions,
|
|
resolve_root=lambda fp, ws: ws, # always use workspace root
|
|
build_spawn=_spawn,
|
|
seed_first_push=False,
|
|
description="mock " + server_id,
|
|
)
|
|
# Patch the SERVERS list element directly + restore on teardown.
|
|
SERVERS[target_index] = replacement
|
|
|
|
yield
|
|
|
|
SERVERS[target_index] = original
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_pyright(monkeypatch, tmp_path):
|
|
"""Install the mock as ``pyright`` and create a fake git workspace."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / ".git").mkdir()
|
|
(repo / "pyproject.toml").write_text("") # so pyright's root resolver finds it
|
|
monkeypatch.chdir(str(repo))
|
|
gen = _install_mock_server(monkeypatch, "errors", "pyright")
|
|
next(gen)
|
|
yield repo
|
|
try:
|
|
next(gen)
|
|
except StopIteration:
|
|
pass
|
|
|
|
|
|
def test_service_returns_empty_when_disabled(tmp_path):
|
|
svc = LSPService(
|
|
enabled=False,
|
|
wait_mode="document",
|
|
wait_timeout=2.0,
|
|
install_strategy="auto",
|
|
)
|
|
assert not svc.is_active()
|
|
f = tmp_path / "x.py"
|
|
f.write_text("")
|
|
assert svc.get_diagnostics_sync(str(f)) == []
|
|
svc.shutdown()
|
|
|
|
|
|
def test_service_skips_files_outside_workspace(tmp_path):
|
|
"""Files outside any git worktree must not trigger LSP."""
|
|
svc = LSPService(
|
|
enabled=True,
|
|
wait_mode="document",
|
|
wait_timeout=2.0,
|
|
install_strategy="manual",
|
|
)
|
|
f = tmp_path / "x.py"
|
|
f.write_text("")
|
|
# No .git anywhere — service should report not enabled for this file.
|
|
assert not svc.enabled_for(str(f))
|
|
svc.shutdown()
|
|
|
|
|
|
def test_service_e2e_delta_filter(mock_pyright):
|
|
"""End-to-end: snapshot baseline → wait → delta returned."""
|
|
repo = mock_pyright
|
|
f = repo / "x.py"
|
|
f.write_text("print('hi')\n")
|
|
|
|
svc = LSPService(
|
|
enabled=True,
|
|
wait_mode="document",
|
|
wait_timeout=3.0,
|
|
install_strategy="manual",
|
|
)
|
|
try:
|
|
assert svc.enabled_for(str(f))
|
|
# Baseline first — server pushes 1 error.
|
|
svc.snapshot_baseline(str(f))
|
|
# Re-poll: same error is in baseline, so delta is empty.
|
|
new_diags = svc.get_diagnostics_sync(str(f))
|
|
assert new_diags == []
|
|
finally:
|
|
svc.shutdown()
|
|
|
|
|
|
def test_service_status_includes_clients(mock_pyright):
|
|
repo = mock_pyright
|
|
f = repo / "x.py"
|
|
f.write_text("")
|
|
svc = LSPService(
|
|
enabled=True,
|
|
wait_mode="document",
|
|
wait_timeout=3.0,
|
|
install_strategy="manual",
|
|
)
|
|
try:
|
|
svc.get_diagnostics_sync(str(f))
|
|
info = svc.get_status()
|
|
assert info["enabled"] is True
|
|
assert any(c["server_id"] == "pyright" for c in info["clients"])
|
|
finally:
|
|
svc.shutdown()
|