23803f14ec
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 4m0s
CI / docker (pull_request) Successful in 38s
CI / integration_tests (pull_request) Successful in 4m48s
CI / coverage (pull_request) Successful in 4m31s
CI / lint (push) Successful in 12s
CI / quality (push) Successful in 16s
CI / build (push) Successful in 15s
CI / security (push) Successful in 32s
CI / typecheck (push) Successful in 35s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m27s
CI / integration_tests (push) Successful in 3m3s
CI / docker (push) Successful in 38s
CI / coverage (push) Successful in 4m25s
CI / benchmark-publish (push) Successful in 16m28s
CI / benchmark-regression (pull_request) Successful in 28m52s
Added minimal LSP server entrypoint supporting initialize/shutdown/exit handshake over JSON-RPC stdin/stdout transport with Content-Length framing. Unsupported methods return MethodNotFound error with descriptive message. Wired LSP requests through ACP facade in local mode. Added agents lsp serve CLI command with --log-level flag, PID output, and startup banner. Created reference documentation for the stub server. Includes Behave BDD tests for protocol handshake, Robot smoke test, and ASV startup latency benchmark. ISSUES CLOSED: #203
467 lines
20 KiB
Gherkin
467 lines
20 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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ACP facade wiring
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@lsp_acp
|
|
Scenario: LSP server stores provided ACP facade
|
|
Given an LSP server with ACP 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 ACP facade
|
|
|
|
@lsp_acp
|
|
Scenario: LSP server lazily creates ACP 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
|