Files
hermes-agent/tests/agent/lsp/test_versionless_push_during_send.py
T
Teknium bed4abd106 fix(lsp): look up nested single-root clients and version docs before didChange send
Two LSP freshness bugs reported by @tobific (#108882, #108881):

- `_current_diags_async()` keyed the client lookup by the enclosing
  workspace root while `_get_or_spawn()` stores single-root servers under
  `srv.resolve_root(...)` (a nested package.json project). The lookup
  returned [] for a live client with diagnostics, so the delta baseline was
  refreshed from nothing. Use the same resolved-root key.

- `open_or_change()` published `_DocState.version` only after awaiting the
  didChange write. A versionless publishDiagnostics read during that await
  was credited with the OLD version and judged stale once the send resumed.
  Bump the version before the send; a failed send (swallowed by
  `_send_notification`) leaves a version nothing satisfies, i.e. "no
  verdict", which is the existing contract.

The mock server gains a push-only `versionless` script so the race is
reproducible without a real language server.
2026-09-12 05:09:11 -07:00

51 lines
2.0 KiB
Python

"""A versionless publishDiagnostics read while didChange is still being written must count as fresh.
``open_or_change`` used to bump ``_DocState.version`` only after awaiting the send. Servers that omit
``version`` are credited with ``doc.version`` at receipt, so a reply that landed during that await was
tagged with the OLD version and then rejected as stale once the send resumed. The mock replies
versionless; the paused send wrapper holds the await open until its reply has been read.
"""
from __future__ import annotations
import asyncio
import os
import sys
from pathlib import Path
import pytest
from agent.lsp.client import LSPClient
MOCK_SERVER = str(Path(__file__).parent / "_mock_lsp_server.py")
@pytest.mark.asyncio
async def test_versionless_push_read_during_didchange_send_is_fresh(tmp_path, monkeypatch):
src = tmp_path / "x.py"
src.write_text("bad\n", encoding="utf-8")
client = LSPClient(
server_id="mock-versionless", workspace_root=str(tmp_path),
command=[sys.executable, MOCK_SERVER], cwd=str(tmp_path),
env={"MOCK_LSP_SCRIPT": "versionless", "PYTHONPATH": os.environ.get("PYTHONPATH", "")},
)
await client.start()
try:
first = await client.open_file(str(src), language_id="python")
assert await client.wait_for_diagnostics(str(src), first, timeout=5)
real_send = client._send_notification
async def send_and_let_reply_land(method, params):
seen = client._push_counter
await real_send(method, params)
if method == "textDocument/didChange":
while client._push_counter == seen:
await asyncio.sleep(0.001)
monkeypatch.setattr(client, "_send_notification", send_and_let_reply_land)
src.write_text("clean\n", encoding="utf-8")
version = await client.open_file(str(src), language_id="python")
assert await client.wait_for_diagnostics(str(src), version, timeout=1)
assert client.diagnostics_for(str(src), fresh_only=True) == []
finally:
await client.shutdown()