Files
cleveragents-core/features/steps/lsp_hover_definitions_steps.py
T
hamza.khyari c200fe0f86
CI / lint (push) Successful in 18s
CI / unit_tests (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / build (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / security (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / helm (push) Has been cancelled
CI / quality (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / docker (push) Has been cancelled
feat(lsp): implement get_hover and get_definitions for LSP runtime (#1240)
Add hover and definition support to LspClient and LspRuntime.

- LspClient.get_hover(): sends textDocument/hover, returns Hover dict or None
- LspClient.get_definitions(): sends textDocument/definition, handles
  Location, Location[], and LocationLink[] responses
- LspRuntime wrappers: input validation, file reading, language
  detection, 1-based to 0-based line/column conversion
- try/finally for did_close() safety on both new methods
- Tool adapter: HOVER and DEFINITIONS dispatch to runtime instead of
  raising LspNotAvailableError
- Updated lsp_tool_adapter_coverage test (HOVER -> REFERENCES)
- 21 Behave BDD scenarios with full path coverage

Closes #1243

Co-authored-by: Hamza Khyari <hamza.khyari@cleverthis.com>
Co-committed-by: Hamza Khyari <hamza.khyari@cleverthis.com>
2026-04-02 16:52:33 +00:00

183 lines
6.3 KiB
Python

"""Step definitions for lsp_hover_definitions.feature.
Comprehensive tests for get_hover and get_definitions on LspClient
and LspRuntime, covering all response paths and input validation.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
from behave import then, when
from cleveragents.lsp.client import LspClient
from cleveragents.lsp.runtime import LspRuntime
def _mock_client(**send_return: Any) -> LspClient:
transport = MagicMock()
transport.is_alive = True
client = LspClient(transport=transport)
client._send_request = MagicMock(**send_return)
return client
def _runtime_no_init() -> LspRuntime:
return LspRuntime.__new__(LspRuntime)
# ── Method existence ─────────────────────────────────────
@then('LspClient should have a "{method}" method for lsp_hd')
def step_client_has(context: object, method: str) -> None:
assert hasattr(LspClient, method) and callable(getattr(LspClient, method))
@then('LspRuntime should have a "{method}" method for lsp_hd')
def step_runtime_has(context: object, method: str) -> None:
assert hasattr(LspRuntime, method) and callable(getattr(LspRuntime, method))
# ── LspClient get_hover paths ────────────────────────────
@when("I call LspClient.get_hover with mock returning a dict for lsp_hd")
def step_hover_dict(context: object) -> None:
client = _mock_client(return_value={"contents": "**int**"})
context.lsp_hd_hover = client.get_hover("file:///t.py", 0, 0)
@when("I call LspClient.get_hover with mock returning null for lsp_hd")
def step_hover_null(context: object) -> None:
client = _mock_client(return_value=None)
context.lsp_hd_hover = client.get_hover("file:///t.py", 0, 0)
@when("I call LspClient.get_hover with mock returning an integer for lsp_hd")
def step_hover_int(context: object) -> None:
client = _mock_client(return_value=42)
context.lsp_hd_hover = client.get_hover("file:///t.py", 0, 0)
@then("the hover result should be a dict for lsp_hd")
def step_hover_is_dict(context: object) -> None:
assert isinstance(context.lsp_hd_hover, dict)
@then("the hover result should be None for lsp_hd")
def step_hover_is_none(context: object) -> None:
assert context.lsp_hd_hover is None
# ── LspClient get_definitions paths ──────────────────────
@when("I call LspClient.get_definitions with mock returning a list for lsp_hd")
def step_defs_list(context: object) -> None:
client = _mock_client(return_value=[{"uri": "f", "range": {}}])
context.lsp_hd_defs = client.get_definitions("f", 0, 0)
@when("I call LspClient.get_definitions with mock returning a single dict for lsp_hd")
def step_defs_single(context: object) -> None:
client = _mock_client(return_value={"uri": "f", "range": {}})
context.lsp_hd_defs = client.get_definitions("f", 0, 0)
@when("I call LspClient.get_definitions with mock returning null for lsp_hd")
def step_defs_null(context: object) -> None:
client = _mock_client(return_value=None)
context.lsp_hd_defs = client.get_definitions("f", 0, 0)
@when("I call LspClient.get_definitions with mock returning an integer for lsp_hd")
def step_defs_int(context: object) -> None:
client = _mock_client(return_value=42)
context.lsp_hd_defs = client.get_definitions("f", 0, 0)
@then("the definitions result should be a list with 1 item for lsp_hd")
def step_defs_one(context: object) -> None:
defs = context.lsp_hd_defs
assert isinstance(defs, list) and len(defs) == 1
@then("the definitions result should be an empty list for lsp_hd")
def step_defs_empty(context: object) -> None:
defs = context.lsp_hd_defs
assert isinstance(defs, list) and len(defs) == 0
# ── LspRuntime input validation ──────────────────────────
def _call_rt(method: str, **overrides: Any) -> Exception | None:
rt = _runtime_no_init()
kw = {"name": "x", "file_path": "/t", "line": 1, "column": 1}
kw.update(overrides)
try:
getattr(rt, method)(**kw)
except ValueError as exc:
return exc
return None
@when("I call LspRuntime.get_hover with empty name for lsp_hd")
def step_rt_hover_name(context: object) -> None:
context.lsp_hd_err = _call_rt("get_hover", name="")
@when("I call LspRuntime.get_hover with empty file_path for lsp_hd")
def step_rt_hover_path(context: object) -> None:
context.lsp_hd_err = _call_rt("get_hover", file_path="")
@when("I call LspRuntime.get_hover with line 0 for lsp_hd")
def step_rt_hover_line(context: object) -> None:
context.lsp_hd_err = _call_rt("get_hover", line=0)
@when("I call LspRuntime.get_hover with column 0 for lsp_hd")
def step_rt_hover_col(context: object) -> None:
context.lsp_hd_err = _call_rt("get_hover", column=0)
@when("I call LspRuntime.get_definitions with empty name for lsp_hd")
def step_rt_defs_name(context: object) -> None:
context.lsp_hd_err = _call_rt("get_definitions", name="")
@when("I call LspRuntime.get_definitions with empty file_path for lsp_hd")
def step_rt_defs_path(context: object) -> None:
context.lsp_hd_err = _call_rt("get_definitions", file_path="")
@when("I call LspRuntime.get_definitions with line 0 for lsp_hd")
def step_rt_defs_line(context: object) -> None:
context.lsp_hd_err = _call_rt("get_definitions", line=0)
@when("I call LspRuntime.get_definitions with column 0 for lsp_hd")
def step_rt_defs_col(context: object) -> None:
context.lsp_hd_err = _call_rt("get_definitions", column=0)
@then('a ValueError should be raised for lsp_hd with message "{msg}"')
def step_val_err(context: object, msg: str) -> None:
err = context.lsp_hd_err
assert err is not None, "Expected ValueError"
assert msg in str(err), f"'{msg}' not in: {err}"
# ── Tool adapter ─────────────────────────────────────────
@then('the tool adapter capability map should include "{cap}" for lsp_hd')
def step_adapter_cap(context: object, cap: str) -> None:
from cleveragents.lsp.tool_adapter import _CAPABILITY_TOOL_MAP
caps = [c.value for c in _CAPABILITY_TOOL_MAP]
assert cap in caps, f"'{cap}' not in: {caps}"