feat(lsp): add missing LspServerConfig model fields
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
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
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
This commit is contained in:
@@ -2,6 +2,13 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added missing `LspServerConfig` model fields per specification:
|
||||
`description` (max 1000 chars), `transport` (`LspTransport` enum with
|
||||
`stdio`/`tcp`, default `stdio`), `initialization` (dict for LSP
|
||||
`initializationOptions`), and `workspace_settings` (dict for
|
||||
`workspace/didChangeConfiguration`). Updated `agents lsp show` Rich
|
||||
output to display new fields. All fields have defaults for backward
|
||||
compatibility. Includes 20 Behave scenarios and 5 Robot tests. (#835)
|
||||
- Added E2E test for Workflow Example 18: Container with Remote Repo Clone
|
||||
(trusted profile). Introduces the new `--clone-into` CLI flag on
|
||||
`resource add` for container-instance and devcontainer-instance resources
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
Feature: LSP Server Config Fields
|
||||
As a developer
|
||||
I need LspServerConfig to support description, transport, initialization, and workspace_settings
|
||||
So that I can fully configure LSP servers per the specification
|
||||
|
||||
# ── LspTransport enum ────────────────────────────────────────────
|
||||
|
||||
Scenario: LspTransport enum has stdio and tcp values
|
||||
Given the LspTransport enum is defined
|
||||
Then the LspTransport enum should have "stdio" value
|
||||
And the LspTransport enum should have "tcp" value
|
||||
|
||||
Scenario: LspTransport default is stdio
|
||||
Given I create a minimal LspServerConfig
|
||||
Then the config transport should be "stdio"
|
||||
|
||||
# ── description field ─────────────────────────────────────────────
|
||||
|
||||
Scenario: LspServerConfig accepts description
|
||||
Given I create a config with description "Pyright for Python type checking"
|
||||
Then the config description should be "Pyright for Python type checking"
|
||||
|
||||
Scenario: LspServerConfig description defaults to empty string
|
||||
Given I create a minimal LspServerConfig
|
||||
Then the config description should be empty
|
||||
|
||||
Scenario: LspServerConfig description at exactly 1000 characters is valid
|
||||
Given I create a config with a description of exactly 1000 characters
|
||||
Then the config description should have length 1000
|
||||
|
||||
Scenario: LspServerConfig description at 1001 characters is rejected
|
||||
When I try to create a config with a description of 1001 characters
|
||||
Then a ValidationError should be raised for config
|
||||
|
||||
Scenario: LspServerConfig strips whitespace from description
|
||||
Given I create a config with description " padded "
|
||||
Then the config description should be "padded"
|
||||
|
||||
# ── transport field ───────────────────────────────────────────────
|
||||
|
||||
Scenario: LspServerConfig accepts tcp transport
|
||||
Given I create a config with transport "tcp"
|
||||
Then the config transport should be "tcp"
|
||||
|
||||
Scenario: LspServerConfig rejects invalid transport value
|
||||
When I try to create a config with transport "websocket"
|
||||
Then a ValidationError should be raised for config
|
||||
|
||||
Scenario: LspServerConfig rejects pipe transport (not in spec)
|
||||
When I try to create a config with transport "pipe"
|
||||
Then a ValidationError should be raised for config
|
||||
|
||||
# ── initialization field ──────────────────────────────────────────
|
||||
|
||||
Scenario: LspServerConfig accepts initialization options
|
||||
Given I create a config with initialization {"python": {"pythonPath": "/usr/bin/python3"}}
|
||||
Then the config initialization should have key "python"
|
||||
And the config initialization "python" value should equal {"pythonPath": "/usr/bin/python3"}
|
||||
|
||||
Scenario: LspServerConfig initialization defaults to empty dict
|
||||
Given I create a minimal LspServerConfig
|
||||
Then the config initialization should be empty
|
||||
|
||||
Scenario: LspServerConfig rejects non-dict initialization
|
||||
When I try to create a config with initialization as a string
|
||||
Then a ValidationError should be raised for config
|
||||
|
||||
# ── workspace_settings field ──────────────────────────────────────
|
||||
|
||||
Scenario: LspServerConfig accepts workspace settings
|
||||
Given I create a config with workspace_settings {"editor.formatOnSave": true}
|
||||
Then the config workspace_settings should have key "editor.formatOnSave"
|
||||
|
||||
Scenario: LspServerConfig workspace_settings defaults to empty dict
|
||||
Given I create a minimal LspServerConfig
|
||||
Then the config workspace_settings should be empty
|
||||
|
||||
Scenario: LspServerConfig rejects non-dict workspace_settings
|
||||
When I try to create a config with workspace_settings as a string
|
||||
Then a ValidationError should be raised for config
|
||||
|
||||
# ── Serialization round-trip ──────────────────────────────────────
|
||||
|
||||
Scenario: LspServerConfig with all new fields serializes and deserializes
|
||||
Given I create a config with all new fields populated
|
||||
When I serialize the config to dict and back
|
||||
Then the deserialized config should match the original exactly
|
||||
|
||||
Scenario: LspServerConfig to_dict includes new fields
|
||||
Given I create a config with all new fields populated
|
||||
When I call to_dict on the config
|
||||
Then the config dict should include "description"
|
||||
And the config dict should include "transport"
|
||||
And the config dict should include "initialization"
|
||||
And the config dict should include "workspace_settings"
|
||||
|
||||
# ── Backward compatibility ────────────────────────────────────────
|
||||
|
||||
Scenario: Existing configs without new fields still work
|
||||
Given I create a LspServerConfig with only name, command, and languages
|
||||
Then the config should have default description
|
||||
And the config should have default transport
|
||||
And the config should have default initialization
|
||||
And the config should have default workspace_settings
|
||||
|
||||
# ── Registry accepts new fields ───────────────────────────────────
|
||||
|
||||
Scenario: LspRegistry accepts config with new fields
|
||||
Given I have a clean LspRegistry
|
||||
And I create a config with all new fields populated
|
||||
When I register the config in the registry
|
||||
Then the registry should contain the config
|
||||
And retrieving the config should preserve all new fields
|
||||
@@ -0,0 +1,285 @@
|
||||
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
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Robot Framework helper for LSP config fields integration tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from cleveragents.lsp.models import ( # noqa: E402
|
||||
LspCapability,
|
||||
LspServerConfig,
|
||||
LspTransport,
|
||||
)
|
||||
from cleveragents.lsp.registry import LspRegistry # noqa: E402
|
||||
|
||||
|
||||
def _run_transport_enum() -> None:
|
||||
"""Verify LspTransport enum values."""
|
||||
assert LspTransport.STDIO.value == "stdio"
|
||||
assert LspTransport.TCP.value == "tcp"
|
||||
# PIPE is not in the spec -- only stdio and tcp
|
||||
assert len(LspTransport) == 2, f"Expected 2 members, got {len(LspTransport)}"
|
||||
print("transport-enum-ok")
|
||||
|
||||
|
||||
def _run_config_defaults() -> None:
|
||||
"""Verify new field defaults."""
|
||||
cfg = LspServerConfig(name="local/test", command="echo", languages=["python"])
|
||||
assert cfg.description == ""
|
||||
assert cfg.transport == LspTransport.STDIO
|
||||
assert cfg.initialization == {}
|
||||
assert cfg.workspace_settings == {}
|
||||
print("config-defaults-ok")
|
||||
|
||||
|
||||
def _run_config_all_fields() -> None:
|
||||
"""Verify config with all fields populated."""
|
||||
cfg = LspServerConfig(
|
||||
name="local/pyright",
|
||||
description="Pyright type checker",
|
||||
command="pyright-langserver",
|
||||
args=["--stdio"],
|
||||
transport=LspTransport.STDIO,
|
||||
languages=["python"],
|
||||
capabilities=[LspCapability.DIAGNOSTICS],
|
||||
initialization={"python": {"pythonPath": "/usr/bin/python3"}},
|
||||
workspace_settings={"editor.formatOnSave": True},
|
||||
)
|
||||
assert cfg.description == "Pyright type checker"
|
||||
assert cfg.transport == LspTransport.STDIO
|
||||
assert "python" in cfg.initialization
|
||||
assert "editor.formatOnSave" in cfg.workspace_settings
|
||||
print("config-all-fields-ok")
|
||||
|
||||
|
||||
def _run_config_roundtrip() -> None:
|
||||
"""Verify serialization round-trip."""
|
||||
cfg = LspServerConfig(
|
||||
name="local/test",
|
||||
description="Test server",
|
||||
command="echo",
|
||||
transport=LspTransport.TCP,
|
||||
languages=["python"],
|
||||
initialization={"key": "value"},
|
||||
workspace_settings={"ws": True},
|
||||
)
|
||||
data = cfg.model_dump(mode="json")
|
||||
cfg2 = LspServerConfig.model_validate(data)
|
||||
assert cfg.description == cfg2.description
|
||||
assert cfg.transport == cfg2.transport
|
||||
assert cfg.initialization == cfg2.initialization
|
||||
assert cfg.workspace_settings == cfg2.workspace_settings
|
||||
print("config-roundtrip-ok")
|
||||
|
||||
|
||||
def _run_registry_preserves() -> None:
|
||||
"""Verify registry preserves new fields."""
|
||||
reg = LspRegistry()
|
||||
cfg = LspServerConfig(
|
||||
name="local/test",
|
||||
description="Test",
|
||||
command="echo",
|
||||
transport=LspTransport.TCP,
|
||||
languages=["python"],
|
||||
initialization={"k": "v"},
|
||||
workspace_settings={"w": True},
|
||||
)
|
||||
reg.register(cfg)
|
||||
retrieved = reg.get("local/test")
|
||||
assert retrieved is not None
|
||||
assert retrieved.description == "Test"
|
||||
assert retrieved.transport == LspTransport.TCP
|
||||
assert retrieved.initialization == {"k": "v"}
|
||||
assert retrieved.workspace_settings == {"w": True}
|
||||
print("registry-preserves-ok")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "all"
|
||||
dispatch = {
|
||||
"transport-enum": _run_transport_enum,
|
||||
"config-defaults": _run_config_defaults,
|
||||
"config-all-fields": _run_config_all_fields,
|
||||
"config-roundtrip": _run_config_roundtrip,
|
||||
"registry-preserves": _run_registry_preserves,
|
||||
}
|
||||
if cmd == "all":
|
||||
for fn in dispatch.values():
|
||||
fn()
|
||||
elif cmd in dispatch:
|
||||
dispatch[cmd]()
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,39 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for LSP config fields
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_lsp_config_fields.py
|
||||
|
||||
*** Test Cases ***
|
||||
LspTransport Enum Values
|
||||
[Documentation] LspTransport has stdio, tcp, pipe
|
||||
${result}= Run Process ${PYTHON} ${HELPER} transport-enum cwd=${WORKSPACE} on_timeout=kill timeout=30s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} transport-enum-ok
|
||||
|
||||
Config Default Values
|
||||
[Documentation] New fields default correctly
|
||||
${result}= Run Process ${PYTHON} ${HELPER} config-defaults cwd=${WORKSPACE} on_timeout=kill timeout=30s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} config-defaults-ok
|
||||
|
||||
Config All Fields Populated
|
||||
[Documentation] Config accepts all new fields
|
||||
${result}= Run Process ${PYTHON} ${HELPER} config-all-fields cwd=${WORKSPACE} on_timeout=kill timeout=30s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} config-all-fields-ok
|
||||
|
||||
Config Serialization Round Trip
|
||||
[Documentation] New fields survive serialization
|
||||
${result}= Run Process ${PYTHON} ${HELPER} config-roundtrip cwd=${WORKSPACE} on_timeout=kill timeout=30s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} config-roundtrip-ok
|
||||
|
||||
Registry Preserves New Fields
|
||||
[Documentation] Registry stores and retrieves new fields
|
||||
${result}= Run Process ${PYTHON} ${HELPER} registry-preserves cwd=${WORKSPACE} on_timeout=kill timeout=30s
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} registry-preserves-ok
|
||||
@@ -45,6 +45,7 @@ task M2.7.lsp-stubs.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -319,14 +320,24 @@ def show(
|
||||
details = (
|
||||
f"[bold]Name:[/bold] {config.name}\n"
|
||||
f"[bold]Namespace:[/bold] {config.namespace}\n"
|
||||
f"[bold]Description:[/bold] {config.description or '(none)'}\n"
|
||||
f"[bold]Command:[/bold] {config.command}\n"
|
||||
f"[bold]Args:[/bold] {' '.join(config.args) or '(none)'}\n"
|
||||
f"[bold]Transport:[/bold] {config.transport.value}\n"
|
||||
f"[bold]Languages:[/bold] {', '.join(config.languages) or '(none)'}\n"
|
||||
f"[bold]Capabilities:[/bold] "
|
||||
f"{', '.join(c.value for c in config.capabilities) or '(none)'}"
|
||||
)
|
||||
console.print(Panel(details, title="LSP Server Details", expand=False))
|
||||
|
||||
if config.initialization:
|
||||
init_str = json.dumps(config.initialization, indent=2)
|
||||
console.print(Panel(init_str, title="Initialization Options", expand=False))
|
||||
|
||||
if config.workspace_settings:
|
||||
ws_str = json.dumps(config.workspace_settings, indent=2)
|
||||
console.print(Panel(ws_str, title="Workspace Settings", expand=False))
|
||||
|
||||
if config.env:
|
||||
env_lines: list[str] = []
|
||||
for key, val in sorted(config.env.items()):
|
||||
|
||||
@@ -20,6 +20,7 @@ Usage::
|
||||
LspCapability,
|
||||
LspClient,
|
||||
LspLifecycleManager,
|
||||
LspTransport,
|
||||
StdioTransport,
|
||||
)
|
||||
"""
|
||||
@@ -36,6 +37,7 @@ from cleveragents.lsp.models import (
|
||||
LspBinding,
|
||||
LspCapability,
|
||||
LspServerConfig,
|
||||
LspTransport,
|
||||
)
|
||||
from cleveragents.lsp.registry import LspRegistry
|
||||
from cleveragents.lsp.runtime import LspRuntime
|
||||
@@ -57,5 +59,6 @@ __all__ = [
|
||||
"LspServerConfig",
|
||||
"LspServerNotFoundError",
|
||||
"LspToolAdapter",
|
||||
"LspTransport",
|
||||
"StdioTransport",
|
||||
]
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
Provides the data models used by the LSP Registry, Runtime, and
|
||||
Tool Adapter:
|
||||
|
||||
- :class:`LspTransport` -- enum of LSP transport mechanisms.
|
||||
- :class:`LspCapability` -- enum of LSP capabilities exposed as tools.
|
||||
- :class:`LspServerConfig` -- registration record for a language server.
|
||||
- :class:`LspBinding` -- binds an LSP server to an actor graph node.
|
||||
|
||||
Namespaced names follow the project-wide pattern
|
||||
``[[server:]namespace/]name``.
|
||||
|
||||
Based on ``docs/specification.md`` LSP Registry (lines 20499-20555)
|
||||
and issue #835.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,6 +23,24 @@ from typing import Any
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class LspTransport(StrEnum):
|
||||
"""Transport mechanism for LSP server communication.
|
||||
|
||||
Determines how the LSP Runtime connects to the language server
|
||||
process. Per specification (line 20551): ``stdio`` (default)
|
||||
or ``tcp``.
|
||||
|
||||
Members:
|
||||
STDIO: Standard input/output (default). The server is spawned
|
||||
as a subprocess and communicates via stdin/stdout JSON-RPC.
|
||||
TCP: TCP socket connection. The server listens on a port and
|
||||
the runtime connects as a client.
|
||||
"""
|
||||
|
||||
STDIO = "stdio"
|
||||
TCP = "tcp"
|
||||
|
||||
|
||||
class LspCapability(StrEnum):
|
||||
"""LSP capabilities that can be exposed as callable tools.
|
||||
|
||||
@@ -44,11 +66,17 @@ class LspServerConfig(BaseModel):
|
||||
|
||||
Attributes:
|
||||
name: Namespaced server name (e.g. ``local/pyright``).
|
||||
description: Human-readable description of the server's purpose.
|
||||
languages: Programming languages served (e.g. ``["python"]``).
|
||||
command: Executable command to start the server.
|
||||
args: Additional CLI arguments for the server process.
|
||||
transport: Communication transport (stdio or tcp).
|
||||
env: Extra environment variables for the server process.
|
||||
capabilities: LSP capabilities this server exposes.
|
||||
initialization: Custom ``initializationOptions`` sent during
|
||||
the LSP ``initialize`` handshake.
|
||||
workspace_settings: Workspace-specific settings sent via
|
||||
``workspace/didChangeConfiguration`` after initialization.
|
||||
"""
|
||||
|
||||
name: str = Field(
|
||||
@@ -56,6 +84,11 @@ class LspServerConfig(BaseModel):
|
||||
min_length=1,
|
||||
description="Namespaced server name (e.g. local/pyright)",
|
||||
)
|
||||
description: str = Field(
|
||||
default="",
|
||||
max_length=1000,
|
||||
description="Human-readable description of the language server",
|
||||
)
|
||||
languages: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Programming languages served (e.g. ['python'])",
|
||||
@@ -69,6 +102,10 @@ class LspServerConfig(BaseModel):
|
||||
default_factory=list,
|
||||
description="Additional CLI arguments for the server process",
|
||||
)
|
||||
transport: LspTransport = Field(
|
||||
default=LspTransport.STDIO,
|
||||
description="Communication transport: stdio (default) or tcp",
|
||||
)
|
||||
env: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description="Extra environment variables for the server process",
|
||||
@@ -77,6 +114,17 @@ class LspServerConfig(BaseModel):
|
||||
default_factory=list,
|
||||
description="LSP capabilities this server exposes",
|
||||
)
|
||||
initialization: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description=("LSP initializationOptions sent during the initialize handshake"),
|
||||
)
|
||||
workspace_settings: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"Workspace configuration sent via "
|
||||
"workspace/didChangeConfiguration after initialization"
|
||||
),
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
str_strip_whitespace=True,
|
||||
@@ -172,4 +220,5 @@ __all__ = [
|
||||
"LspBinding",
|
||||
"LspCapability",
|
||||
"LspServerConfig",
|
||||
"LspTransport",
|
||||
]
|
||||
|
||||
@@ -1193,3 +1193,6 @@ detect_directory_languages # noqa: B018, F821
|
||||
get_servers_for_language # noqa: B018, F821
|
||||
restart_server # noqa: B018, F821
|
||||
_make_runtime_handler # noqa: B018, F821
|
||||
|
||||
# LSP config fields — public API (#835)
|
||||
LspTransport # noqa: B018, F821
|
||||
|
||||
Reference in New Issue
Block a user