6a8f724299
CI / build (pull_request) Successful in 18s
CI / security (pull_request) Successful in 58s
CI / lint (pull_request) Successful in 3m19s
CI / typecheck (pull_request) Successful in 3m54s
CI / quality (pull_request) Successful in 3m55s
CI / integration_tests (pull_request) Successful in 6m49s
CI / unit_tests (pull_request) Successful in 7m3s
CI / docker (pull_request) Successful in 1m6s
CI / e2e_tests (pull_request) Successful in 11m45s
CI / benchmark-publish (pull_request) Has been skipped
CI / coverage (pull_request) Successful in 12m52s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 58m46s
Align LspCapability enum with the spec's 11-capability set
(docs/specification.md lines 20705-20717):
Renamed: TYPE_INFO -> HOVER, SYMBOLS -> DOCUMENT_SYMBOLS,
FORMAT -> FORMATTING
Added: DEFINITIONS, SIGNATURE_HELP, WORKSPACE_SYMBOLS
Updated LspToolAdapter._CAPABILITY_TOOL_MAP (11 entries):
- code_actions suffix -> code-actions (hyphen per spec)
- RENAME gets dedicated schema with new_name parameter
- workspace_symbols gets query-based schema
Updated _input_schema_for() with 4 schema categories extracted
to module-level constants: file-only, position-based, rename
(with new_name), and query-based. Added defensive ValueError for
unmapped capabilities. Added additionalProperties: false.
Extended LspClient.initialize() to advertise all 11 capabilities
in the initialize request (hover, definition, references, rename,
codeAction, formatting, signatureHelp, documentSymbol, workspace
symbol).
Fixed _make_runtime_handler() to handle workspace_symbols as
query-only input (no file_path required), resolving the
schema/handler contract mismatch.
Fixed stale references: type_info -> hover, symbols ->
document_symbols in test fixtures, feature files, CLI docstring.
Updated docs/reference/lsp.md and CHANGELOG.md.
Behave tests (38 scenarios): enum completeness, tool spec generation
with structural validation, input schema per category, all 11
stubbed provider keys, negative tests for invalid capability and
defensive ValueError branch.
ISSUES CLOSED: #834
185 lines
7.5 KiB
Python
185 lines
7.5 KiB
Python
"""Step definitions for lsp_capability_enum.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.lsp.models import LspCapability, LspServerConfig
|
|
from cleveragents.lsp.tool_adapter import (
|
|
_CAPABILITY_TOOL_MAP,
|
|
LspToolAdapter,
|
|
_input_schema_for,
|
|
)
|
|
|
|
# ── Given steps ──────────────────────────────────────────────
|
|
|
|
|
|
@given("the LspCapability enum for lsp_cap")
|
|
def step_load_enum_lsp_cap(context: object) -> None:
|
|
"""Load the LspCapability enum."""
|
|
context.lsp_cap_enum = LspCapability # type: ignore[attr-defined]
|
|
|
|
|
|
@given("the lsp_cap capability tool map")
|
|
def step_load_tool_map_lsp_cap(context: object) -> None:
|
|
"""Load the capability tool map."""
|
|
context.lsp_cap_tool_map = _CAPABILITY_TOOL_MAP # type: ignore[attr-defined]
|
|
|
|
|
|
@given('an LspServerConfig with capability "{capability}" for lsp_cap')
|
|
def step_config_with_capability_lsp_cap(
|
|
context: object,
|
|
capability: str,
|
|
) -> None:
|
|
"""Create an LspServerConfig with a single capability."""
|
|
cap = LspCapability(capability)
|
|
context.lsp_cap_config = LspServerConfig( # type: ignore[attr-defined]
|
|
name="test/server",
|
|
languages=["python"],
|
|
command="test-server",
|
|
capabilities=[cap],
|
|
)
|
|
|
|
|
|
@given('the lsp_cap input schema for "{capability}"')
|
|
def step_input_schema_lsp_cap(context: object, capability: str) -> None:
|
|
"""Get the input schema for a capability."""
|
|
cap = LspCapability(capability)
|
|
context.lsp_cap_schema = _input_schema_for(cap) # type: ignore[attr-defined]
|
|
|
|
|
|
@given("the lsp_cap stubbed server capabilities")
|
|
def step_stubbed_caps_lsp_cap(context: object) -> None:
|
|
"""Load the stubbed server capabilities."""
|
|
from cleveragents.lsp.server import _STUBBED_CAPABILITIES
|
|
|
|
context.lsp_cap_stubbed = _STUBBED_CAPABILITIES # type: ignore[attr-defined]
|
|
|
|
|
|
# ── When steps ───────────────────────────────────────────────
|
|
|
|
|
|
@when("I generate tool specs for lsp_cap")
|
|
def step_generate_specs_lsp_cap(context: object) -> None:
|
|
"""Generate tool specs from the config."""
|
|
config = context.lsp_cap_config # type: ignore[attr-defined]
|
|
adapter = LspToolAdapter()
|
|
context.lsp_cap_specs = adapter.generate_tool_specs(config) # type: ignore[attr-defined]
|
|
|
|
|
|
# ── Then steps ───────────────────────────────────────────────
|
|
|
|
|
|
@then("the lsp_cap enum should have {count:d} members")
|
|
def step_enum_count_lsp_cap(context: object, count: int) -> None:
|
|
"""Assert the enum has the expected number of members."""
|
|
members = list(context.lsp_cap_enum) # type: ignore[attr-defined]
|
|
assert len(members) == count, (
|
|
f"Expected {count} members, got {len(members)}: {[m.value for m in members]}"
|
|
)
|
|
|
|
|
|
@then('the lsp_cap enum should include "{member}"')
|
|
def step_enum_includes_lsp_cap(context: object, member: str) -> None:
|
|
"""Assert the enum includes the given member value."""
|
|
values = [m.value for m in context.lsp_cap_enum] # type: ignore[attr-defined]
|
|
assert member in values, f"'{member}' not in enum values: {values}"
|
|
|
|
|
|
@then("the lsp_cap tool map should have {count:d} entries")
|
|
def step_tool_map_count_lsp_cap(context: object, count: int) -> None:
|
|
"""Assert the tool map has the expected number of entries."""
|
|
tool_map = context.lsp_cap_tool_map # type: ignore[attr-defined]
|
|
assert len(tool_map) == count, f"Expected {count} entries, got {len(tool_map)}"
|
|
|
|
|
|
@then("the lsp_cap generated specs should not be empty")
|
|
def step_specs_not_empty_lsp_cap(context: object) -> None:
|
|
"""Assert generated specs are not empty."""
|
|
specs = context.lsp_cap_specs # type: ignore[attr-defined]
|
|
assert len(specs) > 0, "Expected non-empty tool specs"
|
|
|
|
|
|
@then('the lsp_cap tool spec should have keys "name" and "input_schema"')
|
|
def step_spec_has_keys_lsp_cap(context: object) -> None:
|
|
"""Assert generated specs have required keys."""
|
|
specs = context.lsp_cap_specs # type: ignore[attr-defined]
|
|
for spec in specs:
|
|
assert "name" in spec, f"Spec missing 'name': {spec}"
|
|
assert "input_schema" in spec, f"Spec missing 'input_schema': {spec}"
|
|
|
|
|
|
@then('the lsp_cap tool name should end with "{suffix}"')
|
|
def step_tool_name_ends_with_lsp_cap(context: object, suffix: str) -> None:
|
|
"""Assert at least one tool name ends with the suffix."""
|
|
specs = context.lsp_cap_specs # type: ignore[attr-defined]
|
|
names = [s["name"] for s in specs]
|
|
assert any(n.endswith(f"/{suffix}") for n in names), (
|
|
f"No tool name ends with '/{suffix}': {names}"
|
|
)
|
|
|
|
|
|
@then('the lsp_cap schema should require "{field}"')
|
|
def step_schema_requires_lsp_cap(context: object, field: str) -> None:
|
|
"""Assert the schema requires the given field."""
|
|
schema = context.lsp_cap_schema # type: ignore[attr-defined]
|
|
required = schema.get("required", [])
|
|
assert field in required, f"'{field}' not in required: {required}"
|
|
|
|
|
|
@then('the lsp_cap schema should not require "{field}"')
|
|
def step_schema_not_requires_lsp_cap(context: object, field: str) -> None:
|
|
"""Assert the schema does not require the given field."""
|
|
schema = context.lsp_cap_schema # type: ignore[attr-defined]
|
|
required = schema.get("required", [])
|
|
assert field not in required, f"'{field}' should not be in required: {required}"
|
|
|
|
|
|
@then('the lsp_cap stubbed caps should include "{key}"')
|
|
def step_stubbed_includes_lsp_cap(context: object, key: str) -> None:
|
|
"""Assert the stubbed capabilities dict includes the key."""
|
|
caps = context.lsp_cap_stubbed # type: ignore[attr-defined]
|
|
assert key in caps, f"'{key}' not in stubbed capabilities: {list(caps)}"
|
|
|
|
|
|
# ── When/Then steps: negative tests ─────────────────────────
|
|
|
|
|
|
@when('I create an LspCapability with value "{value}" for lsp_cap')
|
|
def step_create_invalid_capability_lsp_cap(context: object, value: str) -> None:
|
|
"""Attempt to create an LspCapability with an invalid value."""
|
|
context.lsp_cap_error = None # type: ignore[attr-defined]
|
|
try:
|
|
LspCapability(value)
|
|
except ValueError as exc:
|
|
context.lsp_cap_error = exc # type: ignore[attr-defined]
|
|
|
|
|
|
@then("the lsp_cap creation should raise a ValueError")
|
|
def step_should_raise_value_error_lsp_cap(context: object) -> None:
|
|
"""Assert that a ValueError was raised."""
|
|
err = context.lsp_cap_error # type: ignore[attr-defined]
|
|
assert err is not None, "Expected ValueError but none was raised"
|
|
|
|
|
|
@when("I call _input_schema_for with a fake capability for lsp_cap")
|
|
def step_call_schema_fake_cap_lsp_cap(context: object) -> None:
|
|
"""Call _input_schema_for with a value not in any category tuple."""
|
|
from enum import StrEnum
|
|
|
|
class _FakeCap(StrEnum):
|
|
FAKE = "fake_capability"
|
|
|
|
context.lsp_cap_schema_error = None # type: ignore[attr-defined]
|
|
try:
|
|
_input_schema_for(_FakeCap.FAKE) # type: ignore[arg-type]
|
|
except ValueError as exc:
|
|
context.lsp_cap_schema_error = exc # type: ignore[attr-defined]
|
|
|
|
|
|
@then("the lsp_cap schema call should raise a ValueError")
|
|
def step_schema_should_raise_lsp_cap(context: object) -> None:
|
|
"""Assert _input_schema_for raised ValueError."""
|
|
err = context.lsp_cap_schema_error # type: ignore[attr-defined]
|
|
assert err is not None, "Expected ValueError from _input_schema_for but none raised"
|