Files
cleveragents-core/features/steps/lsp_config_fields_steps.py
T
hamza.khyari c97ec273a9
CI / build (pull_request) Successful in 21s
CI / helm (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 26s
CI / quality (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 1m2s
CI / security (pull_request) Successful in 1m7s
CI / integration_tests (pull_request) Successful in 5m51s
CI / unit_tests (pull_request) Successful in 7m29s
CI / docker (pull_request) Successful in 2m13s
CI / coverage (pull_request) Successful in 8m50s
CI / benchmark-publish (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 15m34s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 59m14s
feat(lsp): add missing LspServerConfig model fields
Add specification-required fields to LspServerConfig:

- description: str (max_length=1000, default='')
- transport: LspTransport enum (stdio/tcp/pipe, default=stdio)
- initialization: dict[str, Any] (LSP initializationOptions, default={})
- workspace_settings: dict[str, Any] (workspace/didChangeConfiguration, default={})

All fields have defaults for backward compatibility with existing
configs. LspTransport enum exported from lsp package.

Tests: 17 Behave scenarios, 5 Robot integration tests.
Existing LSP tests: 250/250 pass unchanged.

ISSUES CLOSED: #835
2026-03-30 11:51:27 +00:00

286 lines
9.6 KiB
Python

from __future__ import annotations
import json
from typing import Any
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError as PydanticValidationError
from cleveragents.lsp.models import (
LspCapability,
LspServerConfig,
LspTransport,
)
from cleveragents.lsp.registry import LspRegistry
# ---------------------------------------------------------------------------
# LspTransport enum steps
# ---------------------------------------------------------------------------
@given("the LspTransport enum is defined")
def step_transport_enum_defined(context: Context) -> None:
context.lsp_transport_enum = LspTransport
@then('the LspTransport enum should have "{value}" value')
def step_transport_enum_value(context: Context, value: str) -> None:
assert value in [t.value for t in LspTransport], (
f"LspTransport does not have value '{value}'"
)
# ---------------------------------------------------------------------------
# Config creation helpers
# ---------------------------------------------------------------------------
def _minimal_config(**overrides: Any) -> LspServerConfig:
"""Create a minimal valid LspServerConfig with overrides."""
defaults: dict[str, Any] = {
"name": "local/test-server",
"command": "echo",
"languages": ["python"],
}
defaults.update(overrides)
return LspServerConfig(**defaults)
@given("I create a minimal LspServerConfig")
def step_create_minimal_config(context: Context) -> None:
context.lsp_cfg = _minimal_config()
@given('I create a config with description "{desc}"')
def step_create_config_with_desc(context: Context, desc: str) -> None:
context.lsp_cfg = _minimal_config(description=desc)
@given("I create a config with a description of exactly 1000 characters")
def step_create_config_1000_desc(context: Context) -> None:
context.lsp_cfg = _minimal_config(description="x" * 1000)
@given('I create a config with transport "{transport}"')
def step_create_config_with_transport(context: Context, transport: str) -> None:
context.lsp_cfg = _minimal_config(transport=transport)
@given("I create a config with initialization {init_json}")
def step_create_config_with_init(context: Context, init_json: str) -> None:
init = json.loads(init_json)
context.lsp_cfg = _minimal_config(initialization=init)
@given("I create a config with workspace_settings {ws_json}")
def step_create_config_with_ws(context: Context, ws_json: str) -> None:
ws = json.loads(ws_json)
context.lsp_cfg = _minimal_config(workspace_settings=ws)
@given("I create a config with all new fields populated")
def step_create_config_all_fields(context: Context) -> None:
context.lsp_cfg = LspServerConfig(
name="local/pyright",
description="Pyright for Python type checking",
command="pyright-langserver",
args=["--stdio"],
transport=LspTransport.STDIO,
languages=["python"],
capabilities=[LspCapability.DIAGNOSTICS, LspCapability.COMPLETIONS],
env={"PYRIGHT_CACHE": "/tmp/cache"},
initialization={"python": {"pythonPath": "/usr/bin/python3"}},
workspace_settings={"editor.formatOnSave": True},
)
@given("I create a LspServerConfig with only name, command, and languages")
def step_create_config_minimal_only(context: Context) -> None:
context.lsp_cfg = LspServerConfig(
name="local/minimal",
command="echo",
languages=["python"],
)
# ---------------------------------------------------------------------------
# Validation error steps
# ---------------------------------------------------------------------------
@when("I try to create a config with a description of 1001 characters")
def step_create_config_long_desc(context: Context) -> None:
try:
_minimal_config(description="x" * 1001)
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when('I try to create a config with transport "{transport}"')
def step_create_config_invalid_transport(context: Context, transport: str) -> None:
try:
_minimal_config(transport=transport)
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when("I try to create a config with initialization as a string")
def step_create_config_init_string(context: Context) -> None:
try:
_minimal_config(initialization="not a dict")
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@when("I try to create a config with workspace_settings as a string")
def step_create_config_ws_string(context: Context) -> None:
try:
_minimal_config(workspace_settings="not a dict")
context.cfg_error = None
except (ValueError, PydanticValidationError) as exc:
context.cfg_error = exc
@then("a ValidationError should be raised for config")
def step_config_validation_error(context: Context) -> None:
assert context.cfg_error is not None, "Expected a validation error"
assert isinstance(context.cfg_error, (ValueError, PydanticValidationError)), (
f"Expected ValidationError, got {type(context.cfg_error).__name__}"
)
# ---------------------------------------------------------------------------
# Assertion steps
# ---------------------------------------------------------------------------
@then('the config transport should be "{expected}"')
def step_check_transport(context: Context, expected: str) -> None:
assert context.lsp_cfg.transport.value == expected
@then('the config description should be "{expected}"')
def step_check_description(context: Context, expected: str) -> None:
assert context.lsp_cfg.description == expected
@then("the config description should be empty")
def step_check_description_empty(context: Context) -> None:
assert context.lsp_cfg.description == ""
@then("the config description should have length {length:d}")
def step_check_description_length(context: Context, length: int) -> None:
assert len(context.lsp_cfg.description) == length
@then('the config initialization should have key "{key}"')
def step_check_init_key(context: Context, key: str) -> None:
assert key in context.lsp_cfg.initialization
@then('the config initialization "{key}" value should equal {expected_json}')
def step_check_init_value(context: Context, key: str, expected_json: str) -> None:
expected = json.loads(expected_json)
actual = context.lsp_cfg.initialization[key]
assert actual == expected, f"Expected {expected}, got {actual}"
@then("the config initialization should be empty")
def step_check_init_empty(context: Context) -> None:
assert context.lsp_cfg.initialization == {}
@then('the config workspace_settings should have key "{key}"')
def step_check_ws_key(context: Context, key: str) -> None:
assert key in context.lsp_cfg.workspace_settings
@then("the config workspace_settings should be empty")
def step_check_ws_empty(context: Context) -> None:
assert context.lsp_cfg.workspace_settings == {}
# ---------------------------------------------------------------------------
# Serialization round-trip steps
# ---------------------------------------------------------------------------
@when("I serialize the config to dict and back")
def step_serialize_roundtrip(context: Context) -> None:
data = context.lsp_cfg.model_dump(mode="json")
context.deserialized_cfg = LspServerConfig.model_validate(data)
@then("the deserialized config should match the original exactly")
def step_check_roundtrip_exact(context: Context) -> None:
assert context.lsp_cfg == context.deserialized_cfg
@when("I call to_dict on the config")
def step_call_to_dict(context: Context) -> None:
context.cfg_dict = context.lsp_cfg.to_dict()
@then('the config dict should include "{key}"')
def step_cfg_dict_has_key(context: Context, key: str) -> None:
assert key in context.cfg_dict, (
f"Key '{key}' not in dict: {list(context.cfg_dict.keys())}"
)
# ---------------------------------------------------------------------------
# Backward compatibility steps
# ---------------------------------------------------------------------------
@then("the config should have default description")
def step_default_desc(context: Context) -> None:
assert context.lsp_cfg.description == ""
@then("the config should have default transport")
def step_default_transport(context: Context) -> None:
assert context.lsp_cfg.transport == LspTransport.STDIO
@then("the config should have default initialization")
def step_default_init(context: Context) -> None:
assert context.lsp_cfg.initialization == {}
@then("the config should have default workspace_settings")
def step_default_ws(context: Context) -> None:
assert context.lsp_cfg.workspace_settings == {}
# ---------------------------------------------------------------------------
# Registry steps
# ---------------------------------------------------------------------------
@given("I have a clean LspRegistry")
def step_clean_registry(context: Context) -> None:
context.lsp_reg = LspRegistry()
@when("I register the config in the registry")
def step_register_config(context: Context) -> None:
context.lsp_reg.register(context.lsp_cfg)
@then("the registry should contain the config")
def step_registry_contains(context: Context) -> None:
assert context.lsp_cfg.name in context.lsp_reg
@then("retrieving the config should preserve all new fields")
def step_registry_preserves_fields(context: Context) -> None:
retrieved = context.lsp_reg.get(context.lsp_cfg.name)
assert retrieved is not None
assert retrieved == context.lsp_cfg