22c3cddf08
CI / lint (pull_request) Successful in 39s
CI / quality (pull_request) Successful in 51s
CI / typecheck (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 32s
CI / helm (pull_request) Successful in 37s
CI / security (pull_request) Successful in 1m34s
CI / push-validation (pull_request) Successful in 31s
CI / unit_tests (pull_request) Successful in 4m38s
CI / docker (pull_request) Successful in 1m54s
CI / integration_tests (pull_request) Successful in 8m35s
CI / coverage (pull_request) Successful in 9m1s
CI / status-check (pull_request) Successful in 3s
The DoS mitigation added in db389a730 wraps each message-body read in
``select()`` so a stalled client cannot pin the server forever. The
existing transport tests route through MockLspTransport, whose
``BytesIO`` raises ``UnsupportedOperation`` on ``fileno()`` -- so
``_read_body_with_timeout`` always falls through the BytesIO fast
path and the actual ``select()``-based mitigation code (the part
that runs in production) is never executed. That left ~19 new lines
uncovered, dragging total coverage below the 96.5% floor and
failing the coverage gate.
Two new scenarios drive the helper through an ``os.pipe()`` whose
read fd satisfies ``fileno()``, so ``use_select`` is True and the
real DoS-protection path runs:
* ``timeout=0.0`` makes the deadline already past on the first
iteration, exercising the ``if timeout <= 0:`` early-exit warning.
* ``timeout=0.05`` lets ``select.select()`` run and time out with no
ready descriptors, exercising the ``if not ready:`` warning.
Both paths log ``lsp.transport.read_timeout`` and return ``None``,
matching the production behaviour the helper was added to provide.
ISSUES CLOSED: #5566
498 lines
21 KiB
Gherkin
498 lines
21 KiB
Gherkin
@phase2 @domain @lsp @lsp_server_stub
|
|
Feature: LSP server stub JSON-RPC protocol
|
|
As a developer integrating LSP into CleverAgents
|
|
I want a minimal LSP server stub that handles core lifecycle methods
|
|
So that I can test the JSON-RPC transport and protocol handshake
|
|
|
|
Background:
|
|
Given a mock LSP transport
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Initialize handshake
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_initialize
|
|
Scenario: LSP initialize returns server info and capabilities
|
|
Given an initialize request with id 1
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
And the result for id 1 should contain serverInfo with version "0.1.0"
|
|
And the result for id 1 should contain capabilities key "textDocumentSync"
|
|
|
|
@lsp_initialize
|
|
Scenario: LSP initialize with client info is accepted
|
|
Given an initialize request with id 10 and clientInfo name "test-client"
|
|
When the LSP server processes all messages
|
|
Then the response for id 10 should have result
|
|
And the result for id 10 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_initialize
|
|
Scenario: Initialized notification is silently accepted
|
|
Given an initialize request with id 1
|
|
And a notification for method "initialized"
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And there should be no error responses
|
|
|
|
@lsp_initialize
|
|
Scenario: Initialized notification before initialize is silently ignored
|
|
Given a notification for method "initialized"
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then there should be no error responses
|
|
And the server exit code should be 1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shutdown handshake
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_shutdown
|
|
Scenario: LSP shutdown returns null result
|
|
Given an initialize request with id 1
|
|
And a shutdown request with id 2
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 2 should have null result
|
|
And the server exit code should be 0
|
|
|
|
@lsp_shutdown
|
|
Scenario: LSP exit without prior shutdown returns exit code 1
|
|
Given an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the server exit code should be 1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Unsupported methods
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_unsupported
|
|
Scenario: Unsupported method after initialize returns MethodNotFound error
|
|
Given an initialize request with id 1
|
|
And a request for method "textDocument/completion" with id 5
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 5 should have error code -32601
|
|
And the error message for id 5 should contain "Not implemented: textDocument/completion"
|
|
|
|
@lsp_unsupported
|
|
Scenario: Unsupported method textDocument/hover after initialize returns error
|
|
Given an initialize request with id 1
|
|
And a request for method "textDocument/hover" with id 6
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 6 should have error code -32601
|
|
And the error message for id 6 should contain "Not implemented: textDocument/hover"
|
|
|
|
@lsp_unsupported
|
|
Scenario: Unsupported method before initialize returns ServerNotInitialized
|
|
Given a request for method "textDocument/completion" with id 5
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 5 should have error code -32002
|
|
And the error message for id 5 should contain "Server not initialized"
|
|
|
|
@lsp_unsupported
|
|
Scenario: Unsupported notification is silently ignored
|
|
Given a notification for method "textDocument/didOpen"
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then there should be no error responses
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Invalid JSON-RPC messages
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_invalid
|
|
Scenario: Invalid JSON returns parse error response
|
|
Given raw bytes that are not valid JSON-RPC
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then a parse error response should have been sent
|
|
And the server should have stopped
|
|
|
|
@lsp_invalid
|
|
Scenario: Missing jsonrpc version field returns error
|
|
Given a message with missing jsonrpc version and id 7
|
|
When the LSP server processes all messages
|
|
Then the response for id 7 should have error code -32600
|
|
And the error message for id 7 should contain "jsonrpc version"
|
|
|
|
@lsp_invalid
|
|
Scenario: Missing method field returns error
|
|
Given a message with missing method and id 8
|
|
When the LSP server processes all messages
|
|
Then the response for id 8 should have error code -32600
|
|
And the error message for id 8 should contain "method"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_lifecycle
|
|
Scenario: Server startup reports PID
|
|
Given an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the server should have logged its PID
|
|
|
|
@lsp_lifecycle
|
|
Scenario: Server handles EOF gracefully
|
|
When the LSP server processes all messages with empty input
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# A2A facade wiring
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_a2a
|
|
Scenario: LSP server stores provided A2A facade
|
|
Given an LSP server with A2A facade
|
|
And an initialize request with id 1
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
And the server should hold the provided A2A facade
|
|
|
|
@lsp_a2a
|
|
Scenario: LSP server lazily creates A2A facade on property access
|
|
Given an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the server facade should be lazily created
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Full handshake sequence
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_full_handshake
|
|
Scenario: Complete LSP lifecycle (initialize, shutdown, exit)
|
|
Given an initialize request with id 1
|
|
And a shutdown request with id 2
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the response for id 2 should have null result
|
|
And the server exit code should be 0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server construction validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_construction
|
|
Scenario: LspServer rejects invalid input_stream
|
|
When I try to create an LspServer with invalid input stream
|
|
Then a TypeError should be raised for input stream
|
|
|
|
@lsp_construction
|
|
Scenario: LspServer rejects invalid output_stream
|
|
When I try to create an LspServer with invalid output stream
|
|
Then a TypeError should be raised for output stream
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Transport edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Server recovers from invalid message and handles subsequent request
|
|
Given a message with missing jsonrpc version and id 7
|
|
And an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 7 should have error code -32600
|
|
And the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Request with non-dict params uses empty params
|
|
Given an initialize request with id 1
|
|
And a request with non-dict params and id 20
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 20 should have error code -32601
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Negative content length is handled gracefully
|
|
Given a message with negative content length
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.negative_content_length"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Incomplete message body is handled gracefully
|
|
Given a message with incomplete body
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.incomplete_read"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Unknown method returns MethodNotFound without data field
|
|
Given an initialize request with id 1
|
|
And a request for method "custom/withData" with id 30
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 30 should have error code -32601
|
|
And the error response for id 30 should not contain a data field
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Missing content length header is handled
|
|
Given a message with missing content length header
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.missing_content_length"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Oversized content length is rejected
|
|
Given a message with content length exceeding the maximum
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.content_length_exceeded"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Too many header lines are rejected
|
|
Given a message with too many header lines
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.too_many_header_lines"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: JSON array body returns InvalidRequest error
|
|
Given a message with a JSON array body
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then an InvalidRequest error response should have been sent
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Non-integer content length is handled gracefully
|
|
Given a message with non-integer content length
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.invalid_content_length"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Zero content length returns parse error
|
|
Given a message with content length zero
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then a parse error response should have been sent
|
|
And the server should have stopped
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Duplicate content-length headers uses last value
|
|
Given a message with duplicate content-length headers and id 60
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 60 should have result
|
|
And the result for id 60 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Shutdown without prior initialize returns ServerNotInitialized
|
|
Given a shutdown request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have error code -32002
|
|
And the error message for id 1 should contain "Server not initialized"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Initialize without params key uses empty params
|
|
Given a message with initialize request without params key and id 50
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 50 should have result
|
|
And the result for id 50 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post-shutdown request rejection (LSP spec §3.16)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_shutdown
|
|
Scenario: Request after shutdown returns InvalidRequest error
|
|
Given an initialize request with id 1
|
|
And a shutdown request with id 2
|
|
And a request for method "textDocument/completion" with id 3
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 3 should have error code -32600
|
|
And the error message for id 3 should contain "shutting down"
|
|
|
|
@lsp_shutdown
|
|
Scenario: Notification after shutdown is silently ignored
|
|
Given an initialize request with id 1
|
|
And a shutdown request with id 2
|
|
And a notification for method "textDocument/didOpen"
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the response for id 2 should have null result
|
|
And there should be no error responses
|
|
And the server exit code should be 0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Double initialize rejection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_initialize
|
|
Scenario: Second initialize request returns InvalidRequest error
|
|
Given an initialize request with id 1
|
|
And an initialize request with id 2
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the response for id 2 should have error code -32600
|
|
And the error message for id 2 should contain "already initialized"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stream desync recovery (transport error followed by valid message)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Server recovers after oversized message with body and processes next request
|
|
Given an oversized message with real body followed by an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Server recovers after invalid content-length header and processes next request
|
|
Given a message with non-integer content length followed by an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Server recovers after too many header lines and processes next request
|
|
Given a message with too many header lines followed by an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: EOF during body discard of oversized message is handled gracefully
|
|
Given an oversized message with a short body that triggers EOF during discard
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.content_length_exceeded"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Oversized single header line is handled gracefully
|
|
Given a message with a header line exceeding the maximum line length
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.too_many_header_lines"
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Deeply nested JSON triggers parse error response
|
|
Given a message with deeply nested JSON
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then a parse error response should have been sent
|
|
And a transport warning should have been logged for "lsp.transport.json_recursion_error"
|
|
And the server should have stopped
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI serve command
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_cli_serve
|
|
Scenario: CLI serve command rejects invalid log level
|
|
When I run lsp CLI serve with log level "banana"
|
|
Then the lsp serve CLI should have failed
|
|
And the lsp serve CLI output should contain "Invalid log level"
|
|
|
|
@lsp_cli_serve
|
|
Scenario: CLI serve command accepts valid log level and runs
|
|
When I run lsp CLI serve with piped empty stdin
|
|
Then the lsp serve CLI should have exited with code 1
|
|
And the lsp serve CLI output should contain "CleverAgents LSP Server"
|
|
|
|
@lsp_cli_serve
|
|
Scenario: CLI serve command with debug log level
|
|
When I run lsp CLI serve with log level "debug" and piped empty stdin
|
|
Then the lsp serve CLI should have exited with code 1
|
|
And the lsp serve CLI output should contain "CleverAgents LSP Server"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Broken output stream (pipe failure)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_transport_edge
|
|
Scenario: Broken output stream stops the server gracefully
|
|
Given an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages with a broken output stream
|
|
Then the server should have stopped
|
|
And the server exit code should be 1
|
|
And a transport warning should have been logged for "lsp.transport.write_error"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# String-valued JSON-RPC request id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_initialize
|
|
Scenario: String request id is echoed correctly
|
|
Given an initialize request with string id "alpha-1"
|
|
And an exit notification
|
|
When the LSP server processes all messages
|
|
Then the response for string id "alpha-1" should have result
|
|
And the result for string id "alpha-1" should contain serverInfo with name "cleveragents-lsp-stub"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EOF after shutdown without exit notification
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_shutdown
|
|
Scenario: EOF after shutdown without exit returns exit code 0
|
|
Given an initialize request with id 1
|
|
And a shutdown request with id 2
|
|
When the LSP server processes all messages
|
|
Then the response for id 2 should have null result
|
|
And the server exit code should be 0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-message read timeout (DoS protection)
|
|
# ---------------------------------------------------------------------------
|
|
@lsp_transport_edge @lsp_dos_protection
|
|
Scenario: Read timeout is configurable via constructor parameter
|
|
Given an initialize request with id 1
|
|
And an exit notification
|
|
When the LSP server processes all messages with read timeout 60.0
|
|
Then the response for id 1 should have result
|
|
And the result for id 1 should contain serverInfo with name "cleveragents-lsp-stub"
|
|
And the server exit code should be 1
|
|
|
|
@lsp_transport_edge @lsp_dos_protection
|
|
Scenario: MESSAGE_READ_TIMEOUT constant is exported and has correct default value
|
|
|
|
When I check the MESSAGE_READ_TIMEOUT constant
|
|
Then the MESSAGE_READ_TIMEOUT should be 30
|
|
And MESSAGE_READ_TIMEOUT should be importable from lsp server module
|
|
|
|
@lsp_transport_edge @lsp_dos_protection
|
|
Scenario: Read body times out immediately with read_timeout zero on a real pipe
|
|
When I invoke read body with timeout 0.0 on an empty pipe expecting 10 bytes
|
|
Then the read body result should be None
|
|
And a transport read_timeout warning should be captured
|
|
|
|
@lsp_transport_edge @lsp_dos_protection
|
|
Scenario: Read body times out when select reports no data on a real pipe
|
|
When I invoke read body with timeout 0.05 on an empty pipe expecting 10 bytes
|
|
Then the read body result should be None
|
|
And a transport read_timeout warning should be captured
|