Files
temp/features/steps/lsp_tool_adapter_coverage_steps.py
hamza.khyari c200fe0f86 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

220 lines
7.6 KiB
Python

"""Step definitions for lsp_tool_adapter_coverage.feature.
Covers uncovered lines in src/cleveragents/lsp/tool_adapter.py:
- Line 198: ValueError when config is None
- Lines 126-132: workspace-symbols handler (empty query / valid query)
- Line 137: missing file_path guard
- Line 141: diagnostics delegation
- Lines 143-147: completions delegation
- Lines 150-153: unimplemented capability fallback
"""
from __future__ import annotations
from unittest.mock import MagicMock
from behave import given, then, when
from cleveragents.lsp.errors import LspNotAvailableError
from cleveragents.lsp.models import LspCapability
from cleveragents.lsp.tool_adapter import (
LspToolAdapter,
_make_runtime_handler,
)
# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------
_SERVER_NAME = "test/mock-server"
@given("ltacov an LspToolAdapter with no runtime")
def step_ltacov_adapter_no_runtime(context):
context.ltacov_adapter = LspToolAdapter(runtime=None)
@given("ltacov a mock LspRuntime")
def step_ltacov_mock_runtime(context):
context.ltacov_runtime = MagicMock()
context.ltacov_runtime.get_diagnostics = MagicMock(return_value=[])
context.ltacov_runtime.get_completions = MagicMock(return_value=[])
@given("ltacov the runtime get_diagnostics returns sample items")
def step_ltacov_diagnostics_returns_items(context):
context.ltacov_sample_items = [{"message": "unused import", "severity": 2}]
context.ltacov_runtime.get_diagnostics.return_value = context.ltacov_sample_items
@given("ltacov the runtime get_completions returns sample items")
def step_ltacov_completions_returns_items(context):
context.ltacov_sample_items = [{"label": "my_func", "kind": 3}]
context.ltacov_runtime.get_completions.return_value = context.ltacov_sample_items
@given("ltacov a runtime handler for workspace_symbols capability")
def step_ltacov_handler_workspace_symbols(context):
context.ltacov_handler = _make_runtime_handler(
context.ltacov_runtime, _SERVER_NAME, LspCapability.WORKSPACE_SYMBOLS
)
@given("ltacov a runtime handler for diagnostics capability")
def step_ltacov_handler_diagnostics(context):
context.ltacov_handler = _make_runtime_handler(
context.ltacov_runtime, _SERVER_NAME, LspCapability.DIAGNOSTICS
)
@given("ltacov a runtime handler for completions capability")
def step_ltacov_handler_completions(context):
context.ltacov_handler = _make_runtime_handler(
context.ltacov_runtime, _SERVER_NAME, LspCapability.COMPLETIONS
)
@given("ltacov a runtime handler for hover capability")
def step_ltacov_handler_hover(context):
context.ltacov_handler = _make_runtime_handler(
context.ltacov_runtime, _SERVER_NAME, LspCapability.HOVER
)
@given("ltacov a runtime handler for references capability")
def step_ltacov_handler_references(context):
context.ltacov_handler = _make_runtime_handler(
context.ltacov_runtime, _SERVER_NAME, LspCapability.REFERENCES
)
# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------
@when("ltacov generate_tool_specs is called with None config")
def step_ltacov_generate_with_none(context):
context.ltacov_error = None
try:
context.ltacov_adapter.generate_tool_specs(None)
except ValueError as exc:
context.ltacov_error = exc
@when("ltacov the handler is called with no query argument")
def step_ltacov_handler_no_query(context):
context.ltacov_error = None
context.ltacov_result = None
try:
context.ltacov_result = context.ltacov_handler()
except Exception as exc:
context.ltacov_error = exc
@when('ltacov the handler is called with query "{query}"')
def step_ltacov_handler_with_query(context, query):
context.ltacov_error = None
context.ltacov_result = None
try:
context.ltacov_result = context.ltacov_handler(query=query)
except LspNotAvailableError as exc:
context.ltacov_error = exc
@when("ltacov the handler is called with no file_path argument")
def step_ltacov_handler_no_filepath(context):
context.ltacov_error = None
context.ltacov_result = None
try:
context.ltacov_result = context.ltacov_handler()
except Exception as exc:
context.ltacov_error = exc
@when('ltacov the handler is called with file_path "{file_path}"')
def step_ltacov_handler_with_filepath(context, file_path):
context.ltacov_error = None
context.ltacov_result = None
try:
context.ltacov_result = context.ltacov_handler(file_path=file_path)
except LspNotAvailableError as exc:
context.ltacov_error = exc
@when(
'ltacov the handler is called with file_path "{file_path}" line {line:d} column {column:d}'
)
def step_ltacov_handler_with_filepath_line_col(context, file_path, line, column):
context.ltacov_error = None
context.ltacov_result = None
try:
context.ltacov_result = context.ltacov_handler(
file_path=file_path, line=line, column=column
)
except LspNotAvailableError as exc:
context.ltacov_error = exc
# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------
@then('ltacov a ValueError with message "{msg}" is raised')
def step_ltacov_valueerror_raised(context, msg):
assert context.ltacov_error is not None, "Expected a ValueError but none was raised"
assert isinstance(context.ltacov_error, ValueError), (
f"Expected ValueError, got {type(context.ltacov_error).__name__}"
)
assert msg in str(context.ltacov_error), (
f"Expected message to contain '{msg}', got '{context.ltacov_error}'"
)
@then('ltacov the result is an error dict saying "{expected_msg}"')
def step_ltacov_error_dict(context, expected_msg):
assert context.ltacov_error is None, (
f"Expected no exception, but got {context.ltacov_error}"
)
assert isinstance(context.ltacov_result, dict), (
f"Expected dict, got {type(context.ltacov_result)}"
)
assert "error" in context.ltacov_result, (
f"Expected 'error' key in result: {context.ltacov_result}"
)
assert expected_msg in context.ltacov_result["error"], (
f"Expected error to contain '{expected_msg}', "
f"got '{context.ltacov_result['error']}'"
)
@then('ltacov an LspNotAvailableError is raised mentioning "{capability}"')
def step_ltacov_lsp_not_available(context, capability):
assert context.ltacov_error is not None, (
"Expected LspNotAvailableError but none was raised"
)
assert isinstance(context.ltacov_error, LspNotAvailableError), (
f"Expected LspNotAvailableError, got {type(context.ltacov_error).__name__}"
)
assert capability in str(context.ltacov_error), (
f"Expected error message to mention '{capability}', "
f"got '{context.ltacov_error}'"
)
@then('ltacov the result contains a "{key}" key with the sample items')
def step_ltacov_result_with_key(context, key):
assert context.ltacov_error is None, (
f"Expected no exception, but got {context.ltacov_error}"
)
assert isinstance(context.ltacov_result, dict), (
f"Expected dict, got {type(context.ltacov_result)}"
)
assert key in context.ltacov_result, (
f"Expected key '{key}' in result: {context.ltacov_result}"
)
assert context.ltacov_result[key] == context.ltacov_sample_items, (
f"Expected {context.ltacov_sample_items}, got {context.ltacov_result[key]}"
)