Files
cleveragents-core/features/lsp_server_stub.feature
HAL9000 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
test(lsp): cover the select-based read-body timeout branch via os.pipe()
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
2026-06-05 17:25:35 -04:00

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