Files
cleveragents-core/robot/helper_lsp_registry.py
hamza.khyari 4ff075e0da
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 18s
CI / quality (pull_request) Successful in 32s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 57s
CI / lint (pull_request) Successful in 3m19s
CI / integration_tests (pull_request) Successful in 3m44s
CI / unit_tests (pull_request) Successful in 6m39s
CI / docker (pull_request) Successful in 1m9s
CI / e2e_tests (pull_request) Successful in 9m38s
CI / coverage (pull_request) Successful in 11m19s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Failing after 1h13m38s
feat(lsp): implement functional LSP runtime
Replace local-mode stubs with real LSP protocol support:

- StdioTransport: subprocess management with JSON-RPC framing
- LspClient: LSP protocol (initialize/shutdown/diagnostics/completions)
- LspLifecycleManager: reference-counted instances, health checks, crash restart
- LspRuntime: registry-based server lookup, auto-restart on crash
- LspToolAdapter: runtime-delegating handlers with local-mode fallback
- LanguageDiscovery: 4-layer detection (extension, shebang, UKO, project)
- activate_bindings/deactivate_bindings: actor compiler LSP binding wiring

Tests: 27 Behave scenarios, 6 Robot integration tests, 250 existing pass.

ISSUES CLOSED: #826
2026-03-24 13:01:12 +00:00

152 lines
4.5 KiB
Python

"""Helper script for Robot Framework LSP registry smoke tests.
Usage:
python helper_lsp_registry.py <command>
Commands:
register-and-get Register an LSP server and retrieve it
list-filter Register servers and list with namespace filter
remove-server Register and remove an LSP server
runtime-stub Verify runtime stub raises in local mode
tool-adapter Verify tool adapter generates specs
"""
from __future__ import annotations
import sys
from pathlib import Path
# Ensure src is on the path
src_dir = str(Path(__file__).resolve().parents[1] / "src")
if src_dir not in sys.path:
sys.path.insert(0, src_dir)
from cleveragents.lsp.errors import ( # noqa: E402
LspError,
LspNotAvailableError,
LspServerNotFoundError,
)
from cleveragents.lsp.models import ( # noqa: E402
LspCapability,
LspServerConfig,
)
from cleveragents.lsp.registry import LspRegistry # noqa: E402
from cleveragents.lsp.runtime import LspRuntime # noqa: E402
from cleveragents.lsp.tool_adapter import LspToolAdapter # noqa: E402
def _make_config(
name: str,
command: str = "stub-cmd",
languages: list[str] | None = None,
capabilities: list[LspCapability] | None = None,
) -> LspServerConfig:
return LspServerConfig(
name=name,
command=command,
languages=languages or [],
capabilities=capabilities or [],
)
def cmd_register_and_get() -> None:
registry = LspRegistry()
config = _make_config("local/pyright", "pyright-langserver", ["python"])
registry.register(config)
result = registry.get("local/pyright")
assert result is not None, "LSP server not found"
assert result.name == "local/pyright"
assert "python" in result.languages
print("lsp-registry-ok")
def cmd_list_filter() -> None:
registry = LspRegistry()
registry.register(_make_config("local/pyright", languages=["python"]))
registry.register(_make_config("local/tsserver", languages=["typescript"]))
registry.register(_make_config("devops/clangd", languages=["c"]))
all_servers = registry.list_servers()
assert len(all_servers) == 3
local_servers = registry.list_servers(namespace="local")
assert len(local_servers) == 2
python_servers = registry.list_servers(language="python")
assert len(python_servers) == 1
print("lsp-list-ok")
def cmd_remove_server() -> None:
registry = LspRegistry()
registry.register(_make_config("local/pyright", languages=["python"]))
assert registry.remove("local/pyright") is True
assert registry.get("local/pyright") is None
assert registry.remove("local/pyright") is False
print("lsp-remove-ok")
def cmd_runtime_stub() -> None:
runtime = LspRuntime()
# The functional runtime raises LspServerNotFoundError for
# unregistered servers (not LspNotAvailableError like the old stub).
for fn_name, args in [
("start_server", ("local/pyright", "/tmp")),
("stop_server", ("local/pyright",)),
("get_diagnostics", ("local/pyright", "/tmp/test.py")),
("get_completions", ("local/pyright", "/tmp/test.py", 1, 1)),
]:
try:
getattr(runtime, fn_name)(*args)
print(f"FAIL: {fn_name} should have raised an error")
sys.exit(1)
except (LspNotAvailableError, LspServerNotFoundError, LspError):
pass
print("lsp-runtime-stub-ok")
def cmd_tool_adapter() -> None:
config = _make_config(
"local/pyright",
languages=["python"],
capabilities=[LspCapability.DIAGNOSTICS, LspCapability.COMPLETIONS],
)
adapter = LspToolAdapter()
specs = adapter.generate_tool_specs(config)
assert len(specs) == 2, f"Expected 2 specs, got {len(specs)}"
names = {s["name"] for s in specs}
assert "local/pyright/diagnostics" in names
assert "local/pyright/completions" in names
# Verify handler raises in local mode
try:
specs[0]["handler"]()
print("FAIL: handler should have raised LspNotAvailableError")
sys.exit(1)
except LspNotAvailableError:
pass
print("lsp-tool-adapter-ok")
COMMANDS = {
"register-and-get": cmd_register_and_get,
"list-filter": cmd_list_filter,
"remove-server": cmd_remove_server,
"runtime-stub": cmd_runtime_stub,
"tool-adapter": cmd_tool_adapter,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(COMMANDS)}>", file=sys.stderr)
sys.exit(1)
COMMANDS[sys.argv[1]]()