Skip to main content

mcpls_core/lsp/
client.rs

1//! LSP client implementation with async request/response handling.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicI64, Ordering};
6
7use lsp_types::LspErrorCodes;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11use tokio::sync::{Mutex, mpsc, oneshot};
12use tokio::task::JoinHandle;
13use tokio::time::{Duration, timeout};
14use tracing::{debug, error, trace, warn};
15
16use crate::config::LspServerConfig;
17use crate::error::{Error, Result};
18use crate::lsp::transport::{LspTransport, LspTransportReader};
19use crate::lsp::types::{
20    InboundMessage, JsonRpcError, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
21    LspNotification, RequestId,
22};
23
24/// JSON-RPC protocol version.
25const JSONRPC_VERSION: &str = "2.0";
26
27/// LSP error code returned when the server cancels a request and wants the client to retry.
28const SERVER_CANCELLED_CODE: i32 = -32802;
29
30/// Maximum number of retry attempts for server-cancelled requests.
31const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
32
33/// Initial backoff delay for server-cancelled retries (milliseconds).
34const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
35
36/// Bounded capacity for the channel carrying fully-decoded inbound LSP
37/// messages from [`spawn_reader_task`]'s background task to
38/// [`LspClient::message_loop_inner`].
39///
40/// Backpressured (`send().await`, not `try_send`) unlike the best-effort
41/// notification/lifecycle lanes: dropping a frame here would desync
42/// request/response correlation or silently swallow a server-initiated
43/// request. Matches the command channel's capacity -- both lanes carry
44/// protocol-critical traffic at a similar cadence.
45const READER_CHANNEL_CAPACITY: usize = 100;
46
47/// How long [`LspClient::message_loop`] waits, after aborting the reader
48/// task it no longer drains, for that task to actually finish dropping its
49/// [`LspTransportReader`] (and the `ChildStdout` it owns).
50///
51/// `abort()` only requests cancellation -- the task's locals are dropped
52/// once the runtime next polls it, not synchronously at the call site.
53/// Mirrors `await_lsp_init_handle`'s reasoning in `crate::lib` for the same
54/// pattern. Short: the task is either already parked in a cancel-safe
55/// `.await` (aborts promptly) or has nothing left to do.
56const READER_TASK_ABORT_GRACE: Duration = Duration::from_secs(1);
57
58/// LSP request methods for which a `-32801` (`ContentModified`) error
59/// response is safe to retry automatically -- also declared to servers via
60/// `general.staleRequestSupport.retryOnContentModified` during initialize
61/// (see [`crate::lsp::LspServer`]'s handshake).
62///
63/// Per the LSP spec, `ContentModified` means the server noticed the document
64/// changed while it was computing a response; the (possibly stale) result may
65/// still be useful, or the client may choose to cancel the request instead.
66/// mcpls chooses to retry, which is safe for a read-only/idempotent request
67/// (hover, references, diagnostics, ...): a stale response is simply
68/// discarded and superseded by a fresh one.
69///
70/// Deliberately excludes every method in
71/// `crate::bridge::translator::edits` (`textDocument/rename`,
72/// `textDocument/formatting`, `textDocument/codeAction`): their result is an
73/// edit the MCP caller applies, and `-32801` means the document changed since
74/// the request was issued, so a retry at the original position could return
75/// an edit for content the caller no longer expects (e.g. renaming a
76/// different symbol than the one originally at that position).
77///
78/// This list only gates `-32801`. `-32802` (`ServerCancelled`) retry is
79/// unaffected and keeps retrying unconditionally for every method, as before.
80pub const CONTENT_MODIFIED_RETRY_METHODS: &[&str] = &[
81    "textDocument/signatureHelp",
82    "textDocument/inlayHint",
83    "textDocument/completion",
84    "textDocument/prepareCallHierarchy",
85    "callHierarchy/incomingCalls",
86    "callHierarchy/outgoingCalls",
87    "textDocument/diagnostic",
88    "textDocument/hover",
89    "textDocument/definition",
90    "textDocument/references",
91    "textDocument/implementation",
92    "textDocument/typeDefinition",
93    "textDocument/documentSymbol",
94    "workspace/symbol",
95];
96
97/// Byte-length threshold for the LSP error message forwarded to the MCP
98/// caller in [`Error::LspServerError`] (#313).
99///
100/// Deliberately much larger than [`crate::util::MAX_LOG_STRING_BYTES`]
101/// (used for this same error message in [`LspClient::request`]'s own log
102/// line): a legitimate LSP error (e.g. a verbose rust-analyzer
103/// type-mismatch diagnostic reported through an error response) can run
104/// into the low kilobytes, and that detail is useful to the calling model --
105/// a log line should stay terse, but a truncated-to-200-bytes error handed
106/// to the model would cut off real content on every longer-but-honest
107/// error. Still far below #311's 256 KiB cache-entry cap: this string is
108/// echoed directly into the MCP tool result / model context, not merely
109/// cached.
110const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
111
112/// Upper bound on the effective timeout for completion requests, regardless
113/// of `request_timeout_seconds`.
114///
115/// Completions are latency-sensitive: a completion list that takes longer
116/// than this is no longer useful to the caller. This is a deliberate MVP
117/// ceiling, not an oversight — completions cannot be configured above this
118/// value today. See [`LspClient::completion_timeout`].
119const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
120
121/// Upper bound on the effective timeout for a single `codeAction/resolve`
122/// request, regardless of `request_timeout_seconds`.
123///
124/// `handle_code_actions` (`bridge::translator::edits`) resolves up to
125/// `MAX_CODE_ACTION_RESOLVES` deferred actions concurrently after the
126/// initial `textDocument/codeAction` response; an uncapped per-resolve
127/// timeout would let a large `request_timeout_seconds` configuration make
128/// one tool call wait far longer than a caller expects for what is meant to
129/// be a best-effort follow-up. Mirrors [`COMPLETION_TIMEOUT_CAP`]'s
130/// reasoning. See [`LspClient::code_action_resolve_timeout`].
131const CODE_ACTION_RESOLVE_TIMEOUT_CAP: Duration = Duration::from_secs(10);
132
133/// Type alias for pending request tracking map.
134type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
135
136/// Spawns the dedicated background task that owns `reader` exclusively and
137/// decodes inbound LSP frames in a loop, handing each one to
138/// [`LspClient::message_loop_inner`] over the returned channel.
139///
140/// This is the fix for #451: [`LspTransportReader::receive`] is not
141/// cancel-safe, so it must never run as a branch of the `select!` in
142/// `message_loop_inner`, which also waits on `command_rx`. Running it here
143/// instead, on a task driven only by its own `.await`s, means it can never
144/// be cancelled mid-frame.
145///
146/// The task exits after sending one `Err` (I/O failure or EOF), once
147/// `message_loop_inner` drops its end of the channel (e.g. on shutdown), or
148/// when the returned [`JoinHandle`] is aborted. Callers must abort it once
149/// they stop draining the channel -- see [`LspClient::message_loop`], the
150/// only caller -- otherwise it stays parked in a blocking read holding the
151/// underlying `ChildStdout` open indefinitely: not itself a correctness bug
152/// (`has_exited()`, lifecycle.rs, checks the child process directly via
153/// `try_wait()` and doesn't care whether we still hold its stdout open), but
154/// a leaked task and file descriptor for the lifetime of that connection.
155fn spawn_reader_task(
156    mut reader: LspTransportReader,
157) -> (JoinHandle<()>, mpsc::Receiver<Result<InboundMessage>>) {
158    let (tx, rx) = mpsc::channel(READER_CHANNEL_CAPACITY);
159    let handle = tokio::spawn(async move {
160        loop {
161            let message = reader.receive().await;
162            let is_err = message.is_err();
163            if tx.send(message).await.is_err() || is_err {
164                break;
165            }
166        }
167    });
168    (handle, rx)
169}
170
171/// LSP client with async request/response handling.
172///
173/// This client manages communication with an LSP server, handling:
174/// - Concurrent requests with unique ID tracking
175/// - Background message loop for receiving responses
176/// - Timeout support for all requests
177/// - Graceful shutdown
178#[derive(Debug)]
179pub struct LspClient {
180    /// Configuration for this LSP server.
181    config: LspServerConfig,
182
183    /// Current server state.
184    state: Arc<Mutex<super::ServerState>>,
185
186    /// Atomic counter for request IDs.
187    request_counter: Arc<AtomicI64>,
188
189    /// Command sender for outbound messages.
190    command_tx: mpsc::Sender<ClientCommand>,
191
192    /// Requests awaiting a response, shared with the background message loop.
193    ///
194    /// Exposed here (not just captured by the loop) so [`Self::request`] can
195    /// remove its own entry on timeout instead of leaking it, and so a
196    /// connection known to be dead can fail its stragglers immediately via
197    /// [`Self::fail_pending_requests`] rather than leaving each to discover
198    /// that only when its own timeout elapses.
199    pending_requests: Arc<Mutex<PendingRequests>>,
200
201    /// Background receiver task handle.
202    receiver_task: Option<JoinHandle<Result<()>>>,
203}
204
205impl Clone for LspClient {
206    /// Creates a clone that shares the underlying connection.
207    ///
208    /// The clone does not own the receiver task and cannot perform shutdown.
209    /// All clones share the same command channel for sending requests.
210    fn clone(&self) -> Self {
211        Self {
212            config: self.config.clone(),
213            state: Arc::clone(&self.state),
214            request_counter: Arc::clone(&self.request_counter),
215            command_tx: self.command_tx.clone(),
216            pending_requests: Arc::clone(&self.pending_requests),
217            receiver_task: None,
218        }
219    }
220}
221
222/// Commands for client control.
223enum ClientCommand {
224    /// Send a request and wait for response.
225    SendRequest { request: JsonRpcRequest },
226    /// Send a notification (no response expected).
227    SendNotification {
228        method: String,
229        params: Option<Value>,
230    },
231    /// Shutdown the client.
232    Shutdown,
233}
234
235impl LspClient {
236    /// Create a new LSP client with the given configuration.
237    ///
238    /// The client starts in an uninitialized state. Call `initialize()` to
239    /// start the server and complete the initialization handshake.
240    #[must_use]
241    pub fn new(config: LspServerConfig) -> Self {
242        // Placeholder channel - the receiver is intentionally dropped since
243        // the client starts uninitialized. A real channel is created when
244        // `from_transport` or `from_transport_with_notifications` is called.
245        let (command_tx, _command_rx) = mpsc::channel(1); // Minimal capacity for placeholder
246
247        Self {
248            config,
249            state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
250            request_counter: Arc::new(AtomicI64::new(1)),
251            command_tx,
252            pending_requests: Arc::new(Mutex::new(HashMap::new())),
253            receiver_task: None,
254        }
255    }
256
257    /// Create client from transport (for testing or custom spawning).
258    ///
259    /// This method initializes the background message loop with the provided transport.
260    #[cfg(test)]
261    pub(crate) fn from_transport(
262        config: LspServerConfig,
263        transport: (LspTransport, LspTransportReader),
264    ) -> Self {
265        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
266        let request_counter = Arc::new(AtomicI64::new(1));
267        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
268
269        let (command_tx, command_rx) = mpsc::channel(100);
270
271        let receiver_task = tokio::spawn(Self::message_loop(
272            transport,
273            command_rx,
274            Arc::clone(&pending_requests),
275            None,
276            None,
277        ));
278
279        Self {
280            config,
281            state,
282            request_counter,
283            command_tx,
284            pending_requests,
285            receiver_task: Some(receiver_task),
286        }
287    }
288
289    /// Create client from transport with notification forwarding.
290    ///
291    /// Notifications are parsed and split across two lanes (P3): diagnostics/
292    /// log/showMessage go through `notification_tx`; `$/progress` `begin`/
293    /// `end` frames and unrecognized notifications (`LspNotification::Other`,
294    /// which carries rust-analyzer's `experimental/serverStatus`) go through
295    /// `lifecycle_tx` instead. A `$/progress` `report` frame is never
296    /// enqueued on either lane -- see [`Self::message_loop_inner`].
297    pub(crate) fn from_transport_with_notifications(
298        config: LspServerConfig,
299        transport: (LspTransport, LspTransportReader),
300        notification_tx: mpsc::Sender<LspNotification>,
301        lifecycle_tx: mpsc::Sender<LspNotification>,
302    ) -> Self {
303        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
304        let request_counter = Arc::new(AtomicI64::new(1));
305        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
306
307        let (command_tx, command_rx) = mpsc::channel(100);
308
309        let receiver_task = tokio::spawn(Self::message_loop(
310            transport,
311            command_rx,
312            Arc::clone(&pending_requests),
313            Some(notification_tx),
314            Some(lifecycle_tx),
315        ));
316
317        Self {
318            config,
319            state,
320            request_counter,
321            command_tx,
322            pending_requests,
323            receiver_task: Some(receiver_task),
324        }
325    }
326
327    /// Get the language ID for this client.
328    #[must_use]
329    pub fn language_id(&self) -> &str {
330        &self.config.language_id
331    }
332
333    /// Get the current server state.
334    pub async fn state(&self) -> super::ServerState {
335        *self.state.lock().await
336    }
337
338    /// The timeout applied to a single LSP request attempt, derived from
339    /// [`LspServerConfig::request_timeout_seconds`].
340    ///
341    /// This bounds one attempt, not a whole tool call: [`Self::request`]
342    /// retries up to `SERVER_CANCELLED_MAX_RETRIES` (3) additional times on a
343    /// `-32802` (`ServerCancelled`) or `-32801` (`ContentModified`) response,
344    /// sharing one attempt budget between the two codes, so the worst-case
345    /// latency for a single tool call is `4 * request_timeout() + 3.5s` (the
346    /// sum of the retry backoff delays).
347    ///
348    /// Exception: `get_code_actions` (`bridge::translator::edits`) can add a
349    /// second, concurrent round of requests on top of this bound -- up to
350    /// `MAX_CODE_ACTION_RESOLVES` `codeAction/resolve` calls, each retried
351    /// under the same rules but bounded by [`Self::code_action_resolve_timeout`]
352    /// rather than this timeout. Since those run concurrently with each
353    /// other (not with the initial `textDocument/codeAction` request), the
354    /// worst case for that one tool call is
355    /// `(4 * request_timeout() + 3.5s) + (4 * code_action_resolve_timeout() + 3.5s)`,
356    /// not a multiple scaling with the number of resolved actions.
357    ///
358    /// The configured value is clamped to the range from 1 second to
359    /// [`MAX_TIMEOUT_SECONDS`]. [`crate::serve`]/[`crate::serve_with`] now
360    /// validate the top-level `ServerConfig` (via [`ServerConfig::validate`],
361    /// which rejects `request_timeout_seconds` that is `0` or greater than
362    /// [`MAX_TIMEOUT_SECONDS`]) regardless of whether it came from
363    /// [`ServerConfig::load_from`] or was built programmatically by the
364    /// caller. But `Self::new`, [`super::LspServer::spawn`], and
365    /// [`super::LspServer::spawn_batch`] are all `pub` and take an
366    /// [`LspServerConfig`] (or [`super::ServerInitConfig`] wrapping one)
367    /// directly, bypassing that top-level validation entirely — it operates
368    /// on the top-level `ServerConfig`, not the per-server one. This clamp is
369    /// the last line of defense against a zero-duration timeout that would
370    /// fail every request instantly, or an astronomically large one that
371    /// tokio's `timeout`/`sleep` would silently treat as unbounded (they fall
372    /// back to `Instant::far_future()` rather than panicking), for a caller
373    /// reaching either of these levels directly.
374    ///
375    /// [`ServerConfig::load_from`]: crate::config::ServerConfig::load_from
376    /// [`ServerConfig::validate`]: crate::config::ServerConfig::validate
377    /// [`MAX_TIMEOUT_SECONDS`]: crate::config::MAX_TIMEOUT_SECONDS
378    ///
379    /// # Examples
380    ///
381    /// ```
382    /// use std::time::Duration;
383    /// use mcpls_core::config::LspServerConfig;
384    /// use mcpls_core::lsp::LspClient;
385    ///
386    /// let mut config = LspServerConfig::rust_analyzer();
387    /// config.request_timeout_seconds = 45;
388    /// let client = LspClient::new(config);
389    ///
390    /// assert_eq!(client.request_timeout(), Duration::from_secs(45));
391    /// ```
392    #[must_use]
393    pub fn request_timeout(&self) -> Duration {
394        Duration::from_secs(
395            self.config
396                .request_timeout_seconds
397                .clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
398        )
399    }
400
401    /// The timeout applied to completion (`textDocument/completion`) requests.
402    ///
403    /// Equal to [`Self::request_timeout`], capped at 10 seconds. Completions
404    /// cannot be configured above this cap by any
405    /// value of `request_timeout_seconds` — if that proves insufficient in
406    /// practice, the fix is a dedicated `completion_timeout_seconds` field,
407    /// not raising this cap.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// use std::time::Duration;
413    /// use mcpls_core::config::LspServerConfig;
414    /// use mcpls_core::lsp::LspClient;
415    ///
416    /// let mut config = LspServerConfig::rust_analyzer();
417    /// config.request_timeout_seconds = 300;
418    /// let client = LspClient::new(config);
419    ///
420    /// // Capped at 10s even though request_timeout_seconds is 300.
421    /// assert_eq!(client.completion_timeout(), Duration::from_secs(10));
422    /// assert!(client.completion_timeout() <= client.request_timeout());
423    /// ```
424    #[must_use]
425    pub fn completion_timeout(&self) -> Duration {
426        self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
427    }
428
429    /// The timeout applied to a single `codeAction/resolve` request.
430    ///
431    /// Equal to [`Self::request_timeout`], capped at 10 seconds.
432    ///
433    /// # Examples
434    ///
435    /// ```
436    /// use std::time::Duration;
437    /// use mcpls_core::config::LspServerConfig;
438    /// use mcpls_core::lsp::LspClient;
439    ///
440    /// let mut config = LspServerConfig::rust_analyzer();
441    /// config.request_timeout_seconds = 300;
442    /// let client = LspClient::new(config);
443    ///
444    /// // Capped at 10s even though request_timeout_seconds is 300.
445    /// assert_eq!(client.code_action_resolve_timeout(), Duration::from_secs(10));
446    /// assert!(client.code_action_resolve_timeout() <= client.request_timeout());
447    /// ```
448    #[must_use]
449    pub fn code_action_resolve_timeout(&self) -> Duration {
450        self.request_timeout().min(CODE_ACTION_RESOLVE_TIMEOUT_CAP)
451    }
452
453    /// Registers before enqueuing the send command (not before the message
454    /// loop's own transport read), cleaning up the entry if the send fails.
455    async fn register_and_send_request(
456        &self,
457        request: JsonRpcRequest,
458        response_tx: oneshot::Sender<Result<Value>>,
459    ) -> Result<()> {
460        let id = request.id.clone();
461
462        self.pending_requests
463            .lock()
464            .await
465            .insert(id.clone(), response_tx);
466
467        if self
468            .command_tx
469            .send(ClientCommand::SendRequest { request })
470            .await
471            .is_err()
472        {
473            self.pending_requests.lock().await.remove(&id);
474            return Err(Error::ServerTerminated);
475        }
476
477        Ok(())
478    }
479
480    /// Send request and wait for response with timeout.
481    ///
482    /// Automatically retries up to 3 times when the server returns error code
483    /// -32802 (`ServerCancelled`, any method) or -32801 (`ContentModified`,
484    /// only for methods in `CONTENT_MODIFIED_RETRY_METHODS`) -- gated by
485    /// `data.retriggerRequest` when present -- using exponential backoff
486    /// starting at 500 ms. Both codes share the same attempt budget: a
487    /// request that hits -32801 then -32802 does not get 8 attempts, only 4.
488    ///
489    /// This method owns the severity of LSP error-response logging (#392):
490    /// a transient error about to be retried logs at `warn!`, while `error!`
491    /// is reserved for an error actually surfaced to the caller -- retry
492    /// exhaustion or a non-retryable code/method combination. This is
493    /// deliberately not decided in `message_loop_inner`, which parses the
494    /// response before knowing whether a retry will follow.
495    ///
496    /// # Type Parameters
497    ///
498    /// * `P` - The type of the request parameters (must be serializable)
499    /// * `R` - The type of the response result (must be deserializable)
500    ///
501    /// # Errors
502    ///
503    /// Returns an error if:
504    /// - Server has shut down
505    /// - Request times out
506    /// - Response cannot be deserialized
507    /// - LSP server returns an error
508    pub async fn request<P, R>(
509        &self,
510        method: &str,
511        params: P,
512        timeout_duration: Duration,
513    ) -> Result<R>
514    where
515        P: Serialize,
516        R: DeserializeOwned,
517    {
518        let params_value = Self::omit_null_params(serde_json::to_value(params)?);
519        let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
520
521        for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
522            if attempt > 0 {
523                debug!(
524                    "Retrying {} (attempt {}/{}), backoff={}ms",
525                    method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
526                );
527                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
528                delay_ms *= 2;
529            }
530
531            let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
532            let (response_tx, response_rx) = oneshot::channel();
533            let request = JsonRpcRequest {
534                jsonrpc: JSONRPC_VERSION.to_string(),
535                id: id.clone(),
536                method: method.to_string(),
537                params: params_value.clone(),
538            };
539
540            debug!("Sending request: {} (id={:?})", method, id);
541
542            self.register_and_send_request(request, response_tx).await?;
543
544            let outcome = match timeout(timeout_duration, response_rx).await {
545                Ok(received) => received.map_err(|_| Error::ServerTerminated)?,
546                Err(_elapsed) => {
547                    // The response may still arrive after this point (the
548                    // server is just slow, not dead), but nothing will ever
549                    // read it again -- drop the now-orphaned entry instead of
550                    // leaking it in `pending_requests` forever.
551                    self.pending_requests.lock().await.remove(&id);
552                    return Err(Error::Timeout(timeout_duration.as_secs()));
553                }
554            };
555
556            match outcome {
557                Ok(result_value) => {
558                    return serde_json::from_value(result_value).map_err(|e| {
559                        Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
560                    });
561                }
562                Err(Error::LspServerError {
563                    code,
564                    message,
565                    data,
566                }) if (code == SERVER_CANCELLED_CODE
567                    || (LspErrorCodes::from(code) == LspErrorCodes::ContentModified
568                        && CONTENT_MODIFIED_RETRY_METHODS.contains(&method)))
569                    && Self::should_retrigger(data.as_ref()) =>
570                {
571                    if attempt == SERVER_CANCELLED_MAX_RETRIES {
572                        // Same "LSP error response: ..." prefix as the
573                        // non-retryable branch below -- a log-grep alert on
574                        // that prefix must catch every error actually
575                        // surfaced to the caller, retry exhaustion included.
576                        error!(
577                            "LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
578                            Self::truncate_error_message_for_log(&message),
579                            code,
580                            method,
581                            id
582                        );
583                        return Err(Error::LspServerError {
584                            code,
585                            message,
586                            data,
587                        });
588                    }
589                    warn!(
590                        "LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
591                        Self::truncate_error_message_for_log(&message),
592                        code,
593                        method,
594                        id
595                    );
596                    // continue loop for next attempt
597                }
598                Err(Error::LspServerError {
599                    code,
600                    message,
601                    data,
602                }) => {
603                    error!(
604                        "LSP error response: {} (code {}) on '{}' (id={:?})",
605                        Self::truncate_error_message_for_log(&message),
606                        code,
607                        method,
608                        id
609                    );
610                    return Err(Error::LspServerError {
611                        code,
612                        message,
613                        data,
614                    });
615                }
616                Err(e) => return Err(e),
617            }
618        }
619
620        Err(Error::ServerTerminated)
621    }
622
623    /// Send a typed LSP request, deriving both the method string and the
624    /// result type from `R`'s [`lsp_types::Request`] implementation so they
625    /// cannot drift independently -- unlike [`Self::request`], which takes
626    /// the method string and result type as two unchecked, hand-picked
627    /// values.
628    ///
629    /// # Errors
630    ///
631    /// See [`Self::request`].
632    pub async fn request_typed<R>(
633        &self,
634        params: R::Params,
635        timeout_duration: Duration,
636    ) -> Result<R::Result>
637    where
638        R: lsp_types::Request,
639    {
640        self.request(R::METHOD.as_str(), params, timeout_duration)
641            .await
642    }
643
644    /// Returns true when the error data from a retryable "please retry" LSP
645    /// response (`ServerCancelled` -32802 or `ContentModified` -32801)
646    /// indicates a retry should happen.
647    ///
648    /// The LSP spec defines `data.retriggerRequest` only for diagnostic
649    /// requests' `ServerCancelled` responses (`DiagnosticServerCancellationData`)
650    /// -- it is not part of the general `ServerCancelled` or `ContentModified`
651    /// contract for other methods. mcpls checks this field whenever it is
652    /// present regardless of method (harmless for non-diagnostic methods,
653    /// since a compliant server won't send it there) and defaults to
654    /// retrying when the field is absent, since retrying is the more useful
655    /// default for a request that would otherwise surface as a hard error to
656    /// the MCP caller.
657    fn should_retrigger(data: Option<&Value>) -> bool {
658        data.is_none_or(|v| {
659            v.get("retriggerRequest")
660                .and_then(Value::as_bool)
661                .unwrap_or(true)
662        })
663    }
664
665    /// Fail every request still parked in `pending_requests` with
666    /// `Error::ServerTerminated`, instead of leaving each to discover a dead
667    /// connection only when its own timeout elapses.
668    ///
669    /// Intended for a client that is about to be discarded -- e.g.
670    /// superseded by a respawned replacement for the same server -- so
671    /// callers still waiting on it unblock immediately.
672    pub(crate) async fn fail_pending_requests(&self) {
673        Self::drain_and_fail_pending(&self.pending_requests).await;
674    }
675
676    /// Drains `pending`, resolving each remaining sender to
677    /// `Err(Error::ServerTerminated)`. Shared by [`Self::fail_pending_requests`]
678    /// and [`Self::message_loop`]'s exit-path cleanup (#458).
679    async fn drain_and_fail_pending(pending: &Arc<Mutex<PendingRequests>>) {
680        for (_, sender) in pending.lock().await.drain() {
681            let _ = sender.send(Err(Error::ServerTerminated));
682        }
683    }
684
685    /// Send notification (fire-and-forget, no response expected).
686    ///
687    /// # Errors
688    ///
689    /// Returns an error if the server has shut down.
690    pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
691    where
692        P: Serialize,
693    {
694        let params_value = Self::omit_null_params(serde_json::to_value(params)?);
695
696        debug!("Sending notification: {}", method);
697
698        self.command_tx
699            .send(ClientCommand::SendNotification {
700                method: method.to_string(),
701                params: params_value,
702            })
703            .await
704            .map_err(|_| Error::ServerTerminated)?;
705
706        Ok(())
707    }
708
709    /// Send a typed LSP notification, deriving the method string from `N`'s
710    /// [`lsp_types::Notification`] implementation so it cannot drift from the
711    /// params type -- the notification counterpart to [`Self::request_typed`].
712    ///
713    /// # Errors
714    ///
715    /// See [`Self::notify`].
716    pub async fn notify_typed<N>(&self, params: N::Params) -> Result<()>
717    where
718        N: lsp_types::Notification,
719    {
720        self.notify(N::METHOD.as_str(), params).await
721    }
722
723    /// Shutdown client gracefully.
724    ///
725    /// This sends a shutdown command to the background task and waits for it to complete.
726    ///
727    /// # Errors
728    ///
729    /// Returns an error if the background task failed.
730    pub async fn shutdown(mut self) -> Result<()> {
731        debug!("Shutting down LSP client");
732
733        let _ = self.command_tx.send(ClientCommand::Shutdown).await;
734
735        if let Some(task) = self.receiver_task.take() {
736            task.await
737                .map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
738        }
739
740        *self.state.lock().await = super::ServerState::Shutdown;
741
742        Ok(())
743    }
744
745    /// Background task: handle message I/O.
746    ///
747    /// This task runs in the background, handling:
748    /// - Outbound requests and notifications
749    /// - Inbound responses and server notifications
750    /// - Matching responses to pending requests
751    async fn message_loop(
752        transport: (LspTransport, LspTransportReader),
753        mut command_rx: mpsc::Receiver<ClientCommand>,
754        pending_requests: Arc<Mutex<PendingRequests>>,
755        notification_tx: Option<mpsc::Sender<LspNotification>>,
756        lifecycle_tx: Option<mpsc::Sender<LspNotification>>,
757    ) -> Result<()> {
758        debug!("Message loop started");
759        let (mut transport, reader) = transport;
760        let (reader_handle, mut msg_rx) = spawn_reader_task(reader);
761        let result = {
762            // Aborts the reader task the instant `message_loop_inner` returns, even via `?` (#451).
763            let _abort_reader_on_drop = crate::AbortOnDrop(&reader_handle);
764            Self::message_loop_inner(
765                &mut transport,
766                &mut msg_rx,
767                &mut command_rx,
768                &pending_requests,
769                notification_tx.as_ref(),
770                lifecycle_tx.as_ref(),
771            )
772            .await
773        };
774        let _ = timeout(READER_TASK_ABORT_GRACE, reader_handle).await;
775        // Dropped before the drain, not just at fn-exit: otherwise a
776        // `register_and_send_request` racing the drain could still insert
777        // into `pending_requests` and succeed its `command_tx.send(..)`,
778        // leaving that entry unfailed until its own timeout (#458 S1). Once
779        // this is gone, every later `send` fails and the caller cleans up
780        // its own entry.
781        drop(command_rx);
782        // Runs after `message_loop_inner` returns, so a response the Shutdown
783        // drain (#451) already resolved is gone from the map by now -- only
784        // requests with no answer get failed here (#458).
785        Self::drain_and_fail_pending(&pending_requests).await;
786        if let Err(ref e) = result {
787            error!("Message loop exiting with error: {}", e);
788        } else {
789            debug!("Message loop exiting normally");
790        }
791        result
792    }
793
794    /// Maps a `null` params value to an omitted `params` field. LSP methods
795    /// with `params: void` (`shutdown`, `exit`) must go out without the key:
796    /// tsgo rejects `"params": null` with `-32602 expected empty, got: null`.
797    fn omit_null_params(params: Value) -> Option<Value> {
798        if params.is_null() { None } else { Some(params) }
799    }
800
801    /// Truncate an LSP server's error message for the `tracing::error!` log
802    /// line, bounding it to at most [`crate::util::MAX_LOG_STRING_BYTES`]
803    /// bytes (the full formatted string is slightly longer).
804    ///
805    /// Log-line use only -- the message forwarded to the MCP caller in
806    /// [`Error::LspServerError`] is truncated separately, to the larger
807    /// [`MAX_ERROR_MESSAGE_CALLER_BYTES`] (#313).
808    fn truncate_error_message_for_log(message: &str) -> String {
809        crate::util::truncate_str(message, crate::util::MAX_LOG_STRING_BYTES)
810    }
811
812    /// Which of the two notification lanes (P3) `notification` belongs on,
813    /// or `None` if it must be dropped before reaching either.
814    ///
815    /// `notification_tx` (returned as `"notification"`) carries diagnostics/
816    /// log/showMessage; `lifecycle_tx` (returned as `"lifecycle"`) carries
817    /// `$/progress` `begin`/`end` frames and `Other` (which carries e.g.
818    /// rust-analyzer's `experimental/serverStatus`) -- splitting them means
819    /// a high-volume diagnostics publisher can never starve out a
820    /// low-volume readiness signal, or vice versa. The lane name is
821    /// returned alongside the sender purely for the drop-warning log at the
822    /// call site (Fix 6) -- it plays no role in routing.
823    ///
824    /// A `$/progress` `report` frame -- the high-volume case a
825    /// `report`-per-package emitter like gopls can produce -- is classified
826    /// via [`crate::lsp::types::ProgressKind::from_value`] (shared with
827    /// `bridge::indexing::IndexingTracker::observe_progress` so the two
828    /// can't drift out of sync on which `kind`s are recognized -- Fix 9)
829    /// and dropped before it ever reaches either channel (S3). An
830    /// *unparseable* `$/progress` notification
831    /// (e.g. missing the mandatory `token` field) falls back to
832    /// `LspNotification::Other { method: "$/progress", .. }` at parse time
833    /// (`LspNotification::parse`) rather than `Progress`, so it must be
834    /// dropped here too by matching on `method` -- otherwise a server whose
835    /// `report` payloads fail to deserialize could bypass the `kind`-based
836    /// filter above entirely by sending malformed frames (security LOW /
837    /// M1).
838    fn notification_lane<'a>(
839        notification: &LspNotification,
840        notification_tx: Option<&'a mpsc::Sender<LspNotification>>,
841        lifecycle_tx: Option<&'a mpsc::Sender<LspNotification>>,
842    ) -> Option<(&'static str, &'a mpsc::Sender<LspNotification>)> {
843        match notification {
844            LspNotification::PublishDiagnostics(_)
845            | LspNotification::LogMessage(_)
846            | LspNotification::ShowMessage(_) => notification_tx.map(|tx| ("notification", tx)),
847            LspNotification::Progress(params) => {
848                crate::lsp::types::ProgressKind::from_value(&params.value)
849                    .and(lifecycle_tx)
850                    .map(|tx| ("lifecycle", tx))
851            }
852            LspNotification::Other { method, .. } if method.as_ref() == "$/progress" => None,
853            LspNotification::Other { .. } => lifecycle_tx.map(|tx| ("lifecycle", tx)),
854        }
855    }
856
857    #[allow(clippy::too_many_lines)]
858    async fn message_loop_inner(
859        transport: &mut LspTransport,
860        msg_rx: &mut mpsc::Receiver<Result<InboundMessage>>,
861        command_rx: &mut mpsc::Receiver<ClientCommand>,
862        pending_requests: &Arc<Mutex<PendingRequests>>,
863        notification_tx: Option<&mpsc::Sender<LspNotification>>,
864        lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
865    ) -> Result<()> {
866        loop {
867            tokio::select! {
868                Some(command) = command_rx.recv() => {
869                    match command {
870                        ClientCommand::SendRequest { request } => {
871                            let value = serde_json::to_value(&request)?;
872                            transport.send(&value).await?;
873                        }
874                        ClientCommand::SendNotification { method, params } => {
875                            let notification = serde_json::to_value(JsonRpcNotification {
876                                jsonrpc: JSONRPC_VERSION.to_string(),
877                                method,
878                                params,
879                            })?;
880                            transport.send(&notification).await?;
881                        }
882                        ClientCommand::Shutdown => {
883                            debug!("Client shutdown requested");
884                            // The reader task can run ahead of us (#451): drain whatever it
885                            // already queued instead of dropping it, or a response to a
886                            // concurrent caller's in-flight request would be silently lost.
887                            while let Ok(message) = msg_rx.try_recv() {
888                                match message {
889                                    Ok(m) => {
890                                        Self::handle_inbound_message(
891                                            transport,
892                                            m,
893                                            pending_requests,
894                                            notification_tx,
895                                            lifecycle_tx,
896                                        )
897                                        .await?;
898                                    }
899                                    Err(e) => {
900                                        error!("Transport receive error while draining on shutdown: {}", e);
901                                        break;
902                                    }
903                                }
904                            }
905                            break;
906                        }
907                    }
908                }
909
910                // Cancel-safe, unlike the `transport.receive()` this replaces (#451): reading itself happens off this `select!`.
911                message = msg_rx.recv() => {
912                    let message = match message {
913                        Some(Ok(m)) => m,
914                        Some(Err(e)) => {
915                            error!("Transport receive error: {}", e);
916                            return Err(e);
917                        }
918                        None => {
919                            // Reader task only exits after sending an `Err`, so this means it panicked.
920                            error!("Transport receive error: reader task ended unexpectedly");
921                            return Err(Error::ServerTerminated);
922                        }
923                    };
924                    Self::handle_inbound_message(
925                        transport,
926                        message,
927                        pending_requests,
928                        notification_tx,
929                        lifecycle_tx,
930                    )
931                    .await?;
932                }
933            }
934        }
935
936        Ok(())
937    }
938
939    /// Processes one fully-decoded inbound LSP message: resolves a matching
940    /// pending request, answers a server-initiated request, or forwards a
941    /// notification to its lane. Shared by `message_loop_inner`'s normal
942    /// `msg_rx.recv()` branch and its `Shutdown` drain, so a message handled
943    /// during either path behaves identically.
944    async fn handle_inbound_message(
945        transport: &mut LspTransport,
946        message: InboundMessage,
947        pending_requests: &Arc<Mutex<PendingRequests>>,
948        notification_tx: Option<&mpsc::Sender<LspNotification>>,
949        lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
950    ) -> Result<()> {
951        match message {
952            InboundMessage::Response(response) => {
953                trace!("Received response: id={:?}", response.id);
954
955                let sender = pending_requests.lock().await.remove(&response.id);
956
957                if let Some(sender) = sender {
958                    if let Some(error) = response.error {
959                        // Deliberately not logged at `error!` here: this fires
960                        // for every attempt, before `LspClient::request`'s retry
961                        // loop knows whether the error is transient and about to
962                        // be retried (-32802, or -32801 for an allowlisted
963                        // method). Logging unconditionally at this point would
964                        // emit a spurious ERROR line for errors that are retried
965                        // and succeed. `request` logs at `warn!` on retry and
966                        // `error!` once the error is actually surfaced to the
967                        // caller (retry exhaustion or a non-retryable error);
968                        // the response id is already traced above.
969                        trace!(
970                            "LSP error response: {} (code {})",
971                            Self::truncate_error_message_for_log(&error.message),
972                            error.code
973                        );
974                        // Truncated separately from the log line, to the larger
975                        // MAX_ERROR_MESSAGE_CALLER_BYTES -- the raw message is
976                        // unbounded and attacker-influenceable (#313), but a
977                        // log-line-sized cut would also clip legitimate long
978                        // errors before the model ever sees them (S2).
979                        let caller_message = crate::util::truncate_str(
980                            &error.message,
981                            MAX_ERROR_MESSAGE_CALLER_BYTES,
982                        );
983                        let _ = sender.send(Err(Error::LspServerError {
984                            code: error.code,
985                            message: caller_message,
986                            data: error.data,
987                        }));
988                    } else if let Some(result) = response.result {
989                        let _ = sender.send(Ok(result));
990                    } else {
991                        // LSP spec allows null result for some requests (e.g., hover with no info).
992                        // Treat as successful response with null value.
993                        trace!("Response with null result: {:?}", response.id);
994                        let _ = sender.send(Ok(Value::Null));
995                    }
996                } else {
997                    warn!(
998                        "Received response for unknown request ID: {:?}",
999                        response.id
1000                    );
1001                }
1002            }
1003            InboundMessage::Request(request) => {
1004                debug!(
1005                    "Received server request: {} (id={:?})",
1006                    request.method, request.id
1007                );
1008                let response = Self::server_request_response(request);
1009                let value = serde_json::to_value(&response)?;
1010                transport.send(&value).await?;
1011            }
1012            InboundMessage::Notification(notification) => {
1013                debug!("Received notification: {}", notification.method);
1014
1015                // Parse notification into typed variant
1016                let typed = LspNotification::parse(&notification.method, notification.params);
1017
1018                let destination = Self::notification_lane(&typed, notification_tx, lifecycle_tx);
1019
1020                if let Some((lane, tx)) = destination {
1021                    // Log diagnostics count since it's useful for debugging
1022                    if let LspNotification::PublishDiagnostics(ref params) = typed {
1023                        debug!(
1024                            "Forwarding diagnostics for {}: {} items",
1025                            params.uri.as_ref(),
1026                            params.diagnostics.len()
1027                        );
1028                    } else {
1029                        trace!("Forwarding notification: {:?}", typed);
1030                    }
1031
1032                    // Names lane and method -- the only diagnostic for a dropped frame.
1033                    if tx.try_send(typed).is_err() {
1034                        warn!(
1035                            "Dropping notification: lane={lane}, method={} \
1036                             (channel full or closed)",
1037                            notification.method
1038                        );
1039                    }
1040                }
1041            }
1042        }
1043
1044        Ok(())
1045    }
1046
1047    fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
1048        match Self::server_request_result(&request.method, request.params.as_ref()) {
1049            Ok(result) => JsonRpcResponse {
1050                jsonrpc: JSONRPC_VERSION.to_string(),
1051                id: request.id,
1052                result: Some(result),
1053                error: None,
1054            },
1055            Err(error) => JsonRpcResponse {
1056                jsonrpc: JSONRPC_VERSION.to_string(),
1057                id: request.id,
1058                result: None,
1059                error: Some(error),
1060            },
1061        }
1062    }
1063
1064    fn server_request_result(
1065        method: &str,
1066        params: Option<&Value>,
1067    ) -> std::result::Result<Value, JsonRpcError> {
1068        match method {
1069            "client/registerCapability"
1070            | "client/unregisterCapability"
1071            | "workspace/workspaceFolders"
1072            | "workspace/diagnostic/refresh"
1073            | "workspace/semanticTokens/refresh"
1074            | "workspace/inlayHint/refresh"
1075            | "workspace/codeLens/refresh"
1076            | "window/showMessageRequest"
1077            | "window/workDoneProgress/create" => Ok(Value::Null),
1078            "workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
1079            "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
1080            _ => Err(JsonRpcError {
1081                code: -32601,
1082                message: format!("Unhandled server request: {method}"),
1083                data: None,
1084            }),
1085        }
1086    }
1087
1088    fn workspace_configuration_result(params: Option<&Value>) -> Value {
1089        let item_count = params
1090            .and_then(|value| value.get("items"))
1091            .and_then(Value::as_array)
1092            .map_or(0, Vec::len);
1093
1094        Value::Array(vec![Value::Null; item_count])
1095    }
1096}
1097
1098#[cfg(test)]
1099#[allow(clippy::unwrap_used)]
1100mod tests {
1101    use super::*;
1102
1103    #[test]
1104    fn test_request_id_generation() {
1105        let counter = AtomicI64::new(1);
1106
1107        let id1 = counter.fetch_add(1, Ordering::SeqCst);
1108        let id2 = counter.fetch_add(1, Ordering::SeqCst);
1109        let id3 = counter.fetch_add(1, Ordering::SeqCst);
1110
1111        assert_eq!(id1, 1);
1112        assert_eq!(id2, 2);
1113        assert_eq!(id3, 3);
1114    }
1115
1116    #[test]
1117    fn test_client_creation() {
1118        let config = LspServerConfig::rust_analyzer();
1119
1120        let client = LspClient::new(config);
1121        assert_eq!(client.language_id(), "rust");
1122    }
1123
1124    #[test]
1125    fn test_client_clone() {
1126        let config = LspServerConfig::rust_analyzer();
1127        let client = LspClient::new(config);
1128
1129        #[allow(clippy::redundant_clone)]
1130        let cloned = client.clone();
1131        assert_eq!(cloned.language_id(), "rust");
1132
1133        assert!(
1134            cloned.receiver_task.is_none(),
1135            "Cloned client should not own receiver task"
1136        );
1137    }
1138
1139    #[test]
1140    fn test_request_timeout_and_completion_timeout_at_default() {
1141        let config = LspServerConfig::rust_analyzer();
1142        let client = LspClient::new(config);
1143
1144        assert_eq!(client.request_timeout(), Duration::from_secs(30));
1145        assert_eq!(client.completion_timeout(), Duration::from_secs(10));
1146    }
1147
1148    #[test]
1149    fn test_completion_timeout_clamps_to_ten_seconds() {
1150        for secs in [1, 2, 3, 30, 300] {
1151            let mut config = LspServerConfig::rust_analyzer();
1152            config.request_timeout_seconds = secs;
1153            let client = LspClient::new(config);
1154
1155            assert_eq!(
1156                client.completion_timeout(),
1157                Duration::from_secs(secs.min(10)),
1158                "request_timeout_seconds={secs}"
1159            );
1160            assert!(client.completion_timeout() <= client.request_timeout());
1161        }
1162    }
1163
1164    #[test]
1165    fn test_code_action_resolve_timeout_clamps_to_ten_seconds() {
1166        for secs in [1, 2, 3, 30, 300] {
1167            let mut config = LspServerConfig::rust_analyzer();
1168            config.request_timeout_seconds = secs;
1169            let client = LspClient::new(config);
1170
1171            assert_eq!(
1172                client.code_action_resolve_timeout(),
1173                Duration::from_secs(secs.min(10)),
1174                "request_timeout_seconds={secs}"
1175            );
1176            assert!(client.code_action_resolve_timeout() <= client.request_timeout());
1177        }
1178    }
1179
1180    #[test]
1181    fn test_request_timeout_clamps_zero_to_one_second() {
1182        let mut config = LspServerConfig::rust_analyzer();
1183        config.request_timeout_seconds = 0;
1184        let client = LspClient::new(config);
1185
1186        assert_eq!(client.request_timeout(), Duration::from_secs(1));
1187        assert_eq!(client.completion_timeout(), Duration::from_secs(1));
1188    }
1189
1190    #[test]
1191    fn test_request_timeout_clamps_above_max_to_max() {
1192        let mut config = LspServerConfig::rust_analyzer();
1193        config.request_timeout_seconds = u64::MAX;
1194        let client = LspClient::new(config);
1195
1196        assert_eq!(
1197            client.request_timeout(),
1198            Duration::from_secs(crate::config::MAX_TIMEOUT_SECONDS)
1199        );
1200    }
1201
1202    #[test]
1203    fn test_request_timeout_independent_per_server() {
1204        let mut config_a = LspServerConfig::rust_analyzer();
1205        config_a.request_timeout_seconds = 5;
1206        let mut config_b = LspServerConfig::pyright();
1207        config_b.request_timeout_seconds = 15;
1208
1209        let client_a = LspClient::new(config_a);
1210        let client_b = LspClient::new(config_b);
1211
1212        assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
1213        assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
1214    }
1215
1216    #[test]
1217    fn test_register_capability_request_is_acknowledged() {
1218        let request = JsonRpcRequest {
1219            jsonrpc: JSONRPC_VERSION.to_string(),
1220            id: RequestId::String("ts1".to_string()),
1221            method: "client/registerCapability".to_string(),
1222            params: Some(serde_json::json!({ "registrations": [] })),
1223        };
1224
1225        let response = LspClient::server_request_response(request);
1226
1227        assert_eq!(response.id, RequestId::String("ts1".to_string()));
1228        assert_eq!(response.result, Some(Value::Null));
1229        assert!(response.error.is_none());
1230    }
1231
1232    #[test]
1233    fn test_workspace_configuration_request_returns_null_per_item() {
1234        let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
1235            "items": [{ "section": "typescript" }, { "section": "editor" }]
1236        })));
1237
1238        assert_eq!(result, serde_json::json!([null, null]));
1239    }
1240
1241    /// P1: without this, no spec-compliant LSP server may ever initiate
1242    /// `$/progress` at all (per LSP 3.17, a server needs a successful
1243    /// `window/workDoneProgress/create` response before it may report
1244    /// server-initiated progress for a token).
1245    #[test]
1246    fn test_work_done_progress_create_request_is_acknowledged() {
1247        let request = JsonRpcRequest {
1248            jsonrpc: JSONRPC_VERSION.to_string(),
1249            id: RequestId::String("wdp1".to_string()),
1250            method: "window/workDoneProgress/create".to_string(),
1251            params: Some(serde_json::json!({ "token": "indexing" })),
1252        };
1253
1254        let response = LspClient::server_request_response(request);
1255
1256        assert_eq!(response.result, Some(Value::Null));
1257        assert!(response.error.is_none());
1258    }
1259
1260    /// S3 (Fix 7): a `report`-kind `$/progress` frame must never be
1261    /// enqueued on either notification lane -- the actual mechanism
1262    /// protecting against a `report`-per-package emitter like gopls
1263    /// overrunning the bounded lifecycle channel.
1264    #[test]
1265    fn test_report_progress_frame_reaches_neither_lane() {
1266        let (notification_tx, _notification_rx) = mpsc::channel(8);
1267        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
1268
1269        let report = LspNotification::Progress(lsp_types::ProgressParams {
1270            token: lsp_types::ProgressToken::Int(1),
1271            value: serde_json::json!({ "kind": "report", "percentage": 50 }),
1272        });
1273
1274        let destination =
1275            LspClient::notification_lane(&report, Some(&notification_tx), Some(&lifecycle_tx));
1276
1277        assert!(
1278            destination.is_none(),
1279            "a report-kind frame must be dropped before reaching either lane"
1280        );
1281    }
1282
1283    /// Fix 3 / M1: an unparseable `$/progress` notification (e.g. missing
1284    /// the mandatory `token` field) falls back to `LspNotification::Other {
1285    /// method: "$/progress", .. }` at parse time -- this must be dropped
1286    /// the same as a well-formed `report` frame, not routed to the
1287    /// lifecycle lane, or a server whose payloads fail to deserialize could
1288    /// bypass the kind-based filter entirely.
1289    #[test]
1290    fn test_malformed_progress_other_reaches_neither_lane() {
1291        let (notification_tx, _notification_rx) = mpsc::channel(8);
1292        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
1293
1294        let malformed = LspNotification::Other {
1295            method: std::borrow::Cow::Borrowed("$/progress"),
1296            params: None,
1297        };
1298
1299        let destination =
1300            LspClient::notification_lane(&malformed, Some(&notification_tx), Some(&lifecycle_tx));
1301
1302        assert!(
1303            destination.is_none(),
1304            "a malformed $/progress frame must be dropped, not routed to the lifecycle lane"
1305        );
1306    }
1307
1308    /// P3 sanity check alongside the two tests above: a `begin` frame and a
1309    /// genuine `Other` notification (e.g. rust-analyzer's
1310    /// `experimental/serverStatus`) must still reach the lifecycle lane --
1311    /// the report/malformed filters must not have overcorrected.
1312    #[test]
1313    fn test_begin_and_other_notifications_reach_lifecycle_lane() {
1314        let (notification_tx, _notification_rx) = mpsc::channel(8);
1315        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
1316
1317        let begin = LspNotification::Progress(lsp_types::ProgressParams {
1318            token: lsp_types::ProgressToken::Int(1),
1319            value: serde_json::json!({ "kind": "begin", "title": "Indexing" }),
1320        });
1321        assert_eq!(
1322            LspClient::notification_lane(&begin, Some(&notification_tx), Some(&lifecycle_tx))
1323                .map(|(lane, _)| lane),
1324            Some("lifecycle")
1325        );
1326
1327        let server_status = LspNotification::Other {
1328            method: std::borrow::Cow::Borrowed("experimental/serverStatus"),
1329            params: Some(serde_json::json!({ "quiescent": false })),
1330        };
1331        assert_eq!(
1332            LspClient::notification_lane(
1333                &server_status,
1334                Some(&notification_tx),
1335                Some(&lifecycle_tx)
1336            )
1337            .map(|(lane, _)| lane),
1338            Some("lifecycle")
1339        );
1340    }
1341
1342    #[test]
1343    fn test_unknown_server_request_returns_method_not_found() {
1344        let request = JsonRpcRequest {
1345            jsonrpc: JSONRPC_VERSION.to_string(),
1346            id: RequestId::String("unknown-1".to_string()),
1347            method: "custom/request".to_string(),
1348            params: None,
1349        };
1350
1351        let response = LspClient::server_request_response(request);
1352
1353        assert!(response.result.is_none());
1354        match response.error {
1355            Some(error) => {
1356                assert_eq!(error.code, -32601);
1357                assert_eq!(error.message, "Unhandled server request: custom/request");
1358            }
1359            None => panic!("unknown request should return error"),
1360        }
1361    }
1362
1363    #[tokio::test]
1364    async fn test_null_response_handling() {
1365        use crate::lsp::types::{JsonRpcResponse, RequestId};
1366
1367        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1368
1369        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
1370
1371        pending_requests
1372            .lock()
1373            .await
1374            .insert(RequestId::Number(1), response_tx);
1375
1376        let null_response = JsonRpcResponse {
1377            jsonrpc: "2.0".to_string(),
1378            id: RequestId::Number(1),
1379            result: None,
1380            error: None,
1381        };
1382
1383        let sender = pending_requests.lock().await.remove(&null_response.id);
1384        if let Some(sender) = sender {
1385            let _ = sender.send(Ok(Value::Null));
1386        }
1387
1388        let timeout_result =
1389            tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
1390
1391        assert!(timeout_result.is_ok(), "Should not timeout");
1392
1393        let channel_result = timeout_result.unwrap();
1394        assert!(
1395            channel_result.is_ok(),
1396            "Channel should not be closed: {:?}",
1397            channel_result.err()
1398        );
1399
1400        let response = channel_result.unwrap();
1401        assert!(
1402            response.is_ok(),
1403            "Should receive Ok(Value::Null), not Err: {:?}",
1404            response.err()
1405        );
1406
1407        let value = response.unwrap();
1408        assert_eq!(value, Value::Null, "Should receive Value::Null");
1409    }
1410
1411    #[tokio::test]
1412    async fn test_error_response_handling() {
1413        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
1414
1415        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1416        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
1417
1418        pending_requests
1419            .lock()
1420            .await
1421            .insert(RequestId::Number(1), response_tx);
1422
1423        let error_response = JsonRpcResponse {
1424            jsonrpc: "2.0".to_string(),
1425            id: RequestId::Number(1),
1426            result: None,
1427            error: Some(JsonRpcError {
1428                code: -32601,
1429                message: "Method not found".to_string(),
1430                data: None,
1431            }),
1432        };
1433
1434        let sender = pending_requests.lock().await.remove(&error_response.id);
1435        if let Some(sender) = sender
1436            && let Some(error) = error_response.error
1437        {
1438            let _ = sender.send(Err(Error::LspServerError {
1439                code: error.code,
1440                message: error.message,
1441                data: error.data,
1442            }));
1443        }
1444
1445        let result = response_rx.await.unwrap();
1446        assert!(result.is_err(), "Should receive error");
1447
1448        if let Err(Error::LspServerError { code, message, .. }) = result {
1449            assert_eq!(code, -32601);
1450            assert_eq!(message, "Method not found");
1451        } else {
1452            panic!("Expected LspServerError");
1453        }
1454    }
1455
1456    #[tokio::test]
1457    async fn test_unknown_request_id() {
1458        use crate::lsp::types::{JsonRpcResponse, RequestId};
1459
1460        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1461
1462        let response = JsonRpcResponse {
1463            jsonrpc: "2.0".to_string(),
1464            id: RequestId::Number(999),
1465            result: Some(Value::Null),
1466            error: None,
1467        };
1468
1469        let sender = pending_requests.lock().await.remove(&response.id);
1470        assert!(sender.is_none(), "Should not find sender for unknown ID");
1471    }
1472
1473    #[test]
1474    fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
1475        // 199 ASCII bytes followed by a 3-byte UTF-8 char ('€') straddles the byte-200 cut.
1476        let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
1477
1478        let truncated = LspClient::truncate_error_message_for_log(&message);
1479
1480        // Cutting before the multi-byte char keeps the message valid UTF-8 (no panic) and
1481        // pins the payload to 199 bytes, not 200.
1482        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
1483    }
1484
1485    #[test]
1486    fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
1487        let exact = "x".repeat(200);
1488        assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
1489        assert_eq!(LspClient::truncate_error_message_for_log(""), "");
1490    }
1491
1492    #[test]
1493    fn test_truncate_error_message_for_log_truncates_just_above_limit() {
1494        let message = "x".repeat(201);
1495        assert_eq!(
1496            LspClient::truncate_error_message_for_log(&message),
1497            format!("{}... (truncated)", "x".repeat(200))
1498        );
1499    }
1500
1501    #[test]
1502    fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
1503        // A 4-byte emoji run straddling every possible alignment near the byte-200 boundary.
1504        let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
1505
1506        let truncated = LspClient::truncate_error_message_for_log(&message);
1507
1508        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
1509    }
1510
1511    #[tokio::test]
1512    async fn test_concurrent_request_ids() {
1513        let counter = Arc::new(AtomicI64::new(1));
1514
1515        let counter1 = Arc::clone(&counter);
1516        let counter2 = Arc::clone(&counter);
1517        let counter3 = Arc::clone(&counter);
1518
1519        let handles = vec![
1520            tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
1521            tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
1522            tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
1523        ];
1524
1525        let mut ids = Vec::new();
1526        for handle in handles {
1527            ids.push(handle.await.unwrap());
1528        }
1529
1530        ids.sort_unstable();
1531        assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
1532    }
1533
1534    #[test]
1535    fn test_jsonrpc_version_constant() {
1536        assert_eq!(JSONRPC_VERSION, "2.0");
1537    }
1538
1539    /// #239 regression: a request that times out must remove its own entry
1540    /// from `pending_requests` instead of leaking it. `sleep` is used as the
1541    /// "server": it never writes anything to stdout, so no response can ever
1542    /// arrive and the request is guaranteed to time out rather than race a
1543    /// real answer.
1544    ///
1545    /// Unix-only: spawns a real `sleep` subprocess, which is unavailable on
1546    /// the Windows CI runner.
1547    #[cfg(unix)]
1548    #[tokio::test]
1549    async fn test_request_timeout_removes_pending_entry() {
1550        let mut child = tokio::process::Command::new("sleep")
1551            .arg("2")
1552            .stdin(std::process::Stdio::piped())
1553            .stdout(std::process::Stdio::piped())
1554            .kill_on_drop(true)
1555            .spawn()
1556            .unwrap();
1557        let stdin = child.stdin.take().unwrap();
1558        let stdout = child.stdout.take().unwrap();
1559
1560        let transport = LspTransport::new(stdin, stdout);
1561        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1562
1563        let result: Result<Value> = client
1564            .request(
1565                "textDocument/hover",
1566                serde_json::json!({}),
1567                Duration::from_millis(50),
1568            )
1569            .await;
1570
1571        assert!(matches!(result, Err(Error::Timeout(_))), "got {result:?}");
1572        assert!(
1573            client.pending_requests.lock().await.is_empty(),
1574            "timed-out request must not remain in pending_requests"
1575        );
1576    }
1577
1578    /// #249 continuation: a client about to be discarded (e.g. superseded by
1579    /// a respawned replacement) must fail every still-pending request
1580    /// immediately rather than leaving callers to wait out their timeout.
1581    #[tokio::test]
1582    async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
1583        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1584        let (command_tx, _command_rx) = mpsc::channel(1);
1585
1586        let client = LspClient {
1587            config: LspServerConfig::rust_analyzer(),
1588            state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
1589            request_counter: Arc::new(AtomicI64::new(1)),
1590            command_tx,
1591            pending_requests: Arc::clone(&pending_requests),
1592            receiver_task: None,
1593        };
1594
1595        let (tx1, rx1) = oneshot::channel::<Result<Value>>();
1596        let (tx2, rx2) = oneshot::channel::<Result<Value>>();
1597        pending_requests
1598            .lock()
1599            .await
1600            .insert(RequestId::Number(1), tx1);
1601        pending_requests
1602            .lock()
1603            .await
1604            .insert(RequestId::Number(2), tx2);
1605
1606        client.fail_pending_requests().await;
1607
1608        assert!(pending_requests.lock().await.is_empty());
1609        assert!(matches!(rx1.await.unwrap(), Err(Error::ServerTerminated)));
1610        assert!(matches!(rx2.await.unwrap(), Err(Error::ServerTerminated)));
1611    }
1612
1613    #[test]
1614    fn test_should_retrigger_defaults_to_true_when_data_absent() {
1615        assert!(LspClient::should_retrigger(None));
1616    }
1617
1618    #[test]
1619    fn test_should_retrigger_false_when_flag_false() {
1620        assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
1621            "retriggerRequest": false
1622        }))));
1623    }
1624
1625    #[test]
1626    fn test_should_retrigger_true_when_flag_true() {
1627        assert!(LspClient::should_retrigger(Some(&serde_json::json!({
1628            "retriggerRequest": true
1629        }))));
1630    }
1631
1632    /// Wire-level checks that `params: void` LSP methods go out without a
1633    /// `params` key. tsgo rejects `"params": null` on `shutdown` with
1634    /// `-32602 expected empty, got: null` and then ignores the `exit` that
1635    /// follows, so mcpls fell through to the kill-on-timeout path.
1636    mod void_params_wire {
1637        use tokio::io::BufReader;
1638
1639        use super::*;
1640        use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
1641
1642        #[tokio::test]
1643        async fn test_request_with_null_params_omits_params_key() {
1644            let (client, mut server) = fake_lsp_client();
1645
1646            let request_task = tokio::spawn(async move {
1647                client
1648                    .request::<_, Value>("shutdown", Value::Null, Duration::from_secs(5))
1649                    .await
1650            });
1651
1652            let mut reader = BufReader::new(&mut server.write_stdout);
1653            let request = read_framed_message(&mut reader).await;
1654
1655            assert_eq!(request["method"], "shutdown");
1656            assert!(
1657                request.get("params").is_none(),
1658                "null params must be omitted, got: {request}"
1659            );
1660
1661            write_response(&mut server.read_half_stdin, &request["id"], Value::Null).await;
1662            request_task.await.unwrap().unwrap();
1663        }
1664
1665        #[tokio::test]
1666        async fn test_notify_with_null_params_omits_params_key() {
1667            let (client, mut server) = fake_lsp_client();
1668
1669            client.notify("exit", Value::Null).await.unwrap();
1670
1671            let mut reader = BufReader::new(&mut server.write_stdout);
1672            let notification = read_framed_message(&mut reader).await;
1673
1674            assert_eq!(notification["method"], "exit");
1675            assert!(
1676                notification.get("params").is_none(),
1677                "null params must be omitted, got: {notification}"
1678            );
1679        }
1680
1681        #[tokio::test]
1682        async fn test_notify_with_empty_object_params_keeps_params_key() {
1683            let (client, mut server) = fake_lsp_client();
1684
1685            client
1686                .notify("initialized", lsp_types::InitializedParams {})
1687                .await
1688                .unwrap();
1689
1690            let mut reader = BufReader::new(&mut server.write_stdout);
1691            let notification = read_framed_message(&mut reader).await;
1692
1693            assert_eq!(notification["method"], "initialized");
1694            assert_eq!(
1695                notification["params"],
1696                serde_json::json!({}),
1697                "non-null params must still be sent"
1698            );
1699        }
1700    }
1701
1702    mod retry_behavior {
1703        use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
1704
1705        use super::*;
1706        use crate::test_lsp::{
1707            CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
1708            write_response as write_success_response,
1709        };
1710
1711        /// Writes a framed JSON-RPC retryable error response — either
1712        /// `ServerCancelled` (-32802) or `ContentModified` (-32801) — with a
1713        /// `data.retriggerRequest` flag.
1714        ///
1715        /// Kept local rather than promoted to the shared `test_lsp` harness:
1716        /// the `data.retriggerRequest` field is specific to this retry-logic
1717        /// test suite, unlike the generic success/error responses above.
1718        async fn write_retryable_error_response(
1719            stdin: &mut DuplexStream,
1720            id: &Value,
1721            code: i32,
1722            message: &str,
1723            retrigger: bool,
1724        ) {
1725            let response = serde_json::json!({
1726                "jsonrpc": "2.0",
1727                "id": id,
1728                "error": {
1729                    "code": code,
1730                    "message": message,
1731                    "data": { "retriggerRequest": retrigger },
1732                },
1733            });
1734            let content = serde_json::to_string(&response).unwrap();
1735            let header = format!("Content-Length: {}\r\n\r\n", content.len());
1736            stdin.write_all(header.as_bytes()).await.unwrap();
1737            stdin.write_all(content.as_bytes()).await.unwrap();
1738            stdin.flush().await.unwrap();
1739        }
1740
1741        // Not `start_paused`: the retry loop's real backoff sleeps
1742        // interleave with real async I/O on the duplex pipes below, and
1743        // paused virtual time does not reliably auto-advance across both.
1744        //
1745        // Also captures tracing output (#392): retry exhaustion is the one
1746        // scenario where every attempt but the last logs `warn!` and only
1747        // the last logs `error!`, so this doubles as that regression test
1748        // rather than duplicating the same ~3.5s wire choreography in a
1749        // second test just to assert on log severity.
1750        #[tokio::test]
1751        async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
1752            use tracing_subscriber::layer::SubscriberExt as _;
1753
1754            let (client, mut server) = fake_lsp_client();
1755            let captured = CapturedLogs::default();
1756            let subscriber = tracing_subscriber::registry().with(captured.clone());
1757            let guard = tracing::subscriber::set_default(subscriber);
1758
1759            let request_task = tokio::spawn(async move {
1760                client
1761                    .request::<_, Value>(
1762                        "textDocument/hover",
1763                        serde_json::json!({}),
1764                        Duration::from_secs(30),
1765                    )
1766                    .await
1767            });
1768
1769            let mut reader = BufReader::new(&mut server.write_stdout);
1770            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
1771            // attempt gets ServerCancelled, so retries must exhaust rather
1772            // than loop forever or swallow the error.
1773            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1774                let request = read_framed_message(&mut reader).await;
1775                let id = request["id"].clone();
1776                write_retryable_error_response(
1777                    &mut server.read_half_stdin,
1778                    &id,
1779                    SERVER_CANCELLED_CODE,
1780                    "server cancelled the request",
1781                    true,
1782                )
1783                .await;
1784            }
1785
1786            let result = request_task.await.unwrap();
1787
1788            match result {
1789                Err(Error::LspServerError {
1790                    code,
1791                    message,
1792                    data,
1793                }) => {
1794                    // Assert the exact original error surfaces, not merely
1795                    // "some error with this code" -- a freshly constructed
1796                    // placeholder error would satisfy a code-only check.
1797                    assert_eq!(code, SERVER_CANCELLED_CODE);
1798                    assert_eq!(message, "server cancelled the request");
1799                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1800                }
1801                other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
1802            }
1803
1804            drop(guard);
1805            let logs = captured.entries();
1806            assert_eq!(
1807                logs.iter()
1808                    .filter(|(level, _)| *level == tracing::Level::ERROR)
1809                    .count(),
1810                1,
1811                "exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
1812            );
1813            assert!(
1814                logs.iter()
1815                    .any(|(level, msg)| *level == tracing::Level::ERROR
1816                        && msg.contains("LSP error response")
1817                        && msg.contains("retries exhausted")),
1818                "expected an ERROR log sharing the 'LSP error response' prefix and naming \
1819                 retry exhaustion, got: {logs:?}"
1820            );
1821            assert_eq!(
1822                logs.iter()
1823                    .filter(
1824                        |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
1825                    )
1826                    .count(),
1827                usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
1828                "every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
1829            );
1830        }
1831
1832        #[tokio::test]
1833        async fn test_retrigger_false_returns_immediately_without_retry() {
1834            let (client, mut server) = fake_lsp_client();
1835
1836            let request_task = tokio::spawn(async move {
1837                client
1838                    .request::<_, Value>(
1839                        "textDocument/hover",
1840                        serde_json::json!({}),
1841                        Duration::from_secs(30),
1842                    )
1843                    .await
1844            });
1845
1846            let mut reader = BufReader::new(&mut server.write_stdout);
1847            let request = read_framed_message(&mut reader).await;
1848            let id = request["id"].clone();
1849            write_retryable_error_response(
1850                &mut server.read_half_stdin,
1851                &id,
1852                SERVER_CANCELLED_CODE,
1853                "server cancelled the request",
1854                false,
1855            )
1856            .await;
1857
1858            // With `retriggerRequest: false`, `should_retrigger`'s gate on
1859            // the retry branch must short-circuit the loop: the error
1860            // returns well under the first 500ms backoff, and no second
1861            // request is ever sent. If the `&& Self::should_retrigger(..)`
1862            // guard were ever dropped from the retry match arm, this would
1863            // instead retry and both assertions below would fail.
1864            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1865                .await
1866                .unwrap()
1867                .unwrap();
1868
1869            match result {
1870                Err(Error::LspServerError { code, .. }) => {
1871                    assert_eq!(code, SERVER_CANCELLED_CODE);
1872                }
1873                other => panic!("expected immediate ServerCancelled error, got {other:?}"),
1874            }
1875
1876            let second_request =
1877                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1878                    .await;
1879            assert!(
1880                second_request.is_err(),
1881                "no retry should have been sent after retriggerRequest: false"
1882            );
1883        }
1884
1885        #[tokio::test]
1886        async fn test_retry_succeeds_after_one_server_cancelled_response() {
1887            let (client, mut server) = fake_lsp_client();
1888
1889            let request_task = tokio::spawn(async move {
1890                client
1891                    .request::<_, Value>(
1892                        "textDocument/hover",
1893                        serde_json::json!({}),
1894                        Duration::from_secs(30),
1895                    )
1896                    .await
1897            });
1898
1899            let mut reader = BufReader::new(&mut server.write_stdout);
1900
1901            // First attempt is cancelled and must retrigger.
1902            let first = read_framed_message(&mut reader).await;
1903            write_retryable_error_response(
1904                &mut server.read_half_stdin,
1905                &first["id"].clone(),
1906                SERVER_CANCELLED_CODE,
1907                "server cancelled the request",
1908                true,
1909            )
1910            .await;
1911
1912            // Second attempt (after backoff) succeeds -- proves the loop
1913            // genuinely re-sends the request rather than just counting down.
1914            let second = read_framed_message(&mut reader).await;
1915            assert_ne!(
1916                first["id"], second["id"],
1917                "retry must use a fresh request id"
1918            );
1919            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1920            write_success_response(
1921                &mut server.read_half_stdin,
1922                &second["id"].clone(),
1923                expected_result.clone(),
1924            )
1925            .await;
1926
1927            let result = request_task.await.unwrap();
1928            assert_eq!(result.unwrap(), expected_result);
1929        }
1930
1931        #[tokio::test]
1932        async fn test_retry_exhaustion_returns_original_content_modified_error() {
1933            let (client, mut server) = fake_lsp_client();
1934
1935            let request_task = tokio::spawn(async move {
1936                client
1937                    .request::<_, Value>(
1938                        "textDocument/hover",
1939                        serde_json::json!({}),
1940                        Duration::from_secs(30),
1941                    )
1942                    .await
1943            });
1944
1945            let mut reader = BufReader::new(&mut server.write_stdout);
1946            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
1947            // attempt gets ContentModified, so retries must exhaust rather
1948            // than loop forever or swallow the error. -32801 shares the same
1949            // attempt budget as -32802 (FR-002), not an independent one.
1950            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1951                let request = read_framed_message(&mut reader).await;
1952                let id = request["id"].clone();
1953                write_retryable_error_response(
1954                    &mut server.read_half_stdin,
1955                    &id,
1956                    i32::from(LspErrorCodes::ContentModified),
1957                    "content modified",
1958                    true,
1959                )
1960                .await;
1961            }
1962
1963            let result = request_task.await.unwrap();
1964
1965            match result {
1966                Err(Error::LspServerError {
1967                    code,
1968                    message,
1969                    data,
1970                }) => {
1971                    // Assert the exact original error surfaces, not merely
1972                    // "some error with this code" -- a freshly constructed
1973                    // placeholder error would satisfy a code-only check.
1974                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1975                    assert_eq!(message, "content modified");
1976                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1977                }
1978                other => panic!("expected exhausted ContentModified error, got {other:?}"),
1979            }
1980        }
1981
1982        #[tokio::test]
1983        async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
1984            let (client, mut server) = fake_lsp_client();
1985
1986            let request_task = tokio::spawn(async move {
1987                client
1988                    .request::<_, Value>(
1989                        "textDocument/hover",
1990                        serde_json::json!({}),
1991                        Duration::from_secs(30),
1992                    )
1993                    .await
1994            });
1995
1996            let mut reader = BufReader::new(&mut server.write_stdout);
1997            let request = read_framed_message(&mut reader).await;
1998            let id = request["id"].clone();
1999            write_retryable_error_response(
2000                &mut server.read_half_stdin,
2001                &id,
2002                i32::from(LspErrorCodes::ContentModified),
2003                "content modified",
2004                false,
2005            )
2006            .await;
2007
2008            // Same `should_retrigger` gate as -32802: a non-spec-compliant
2009            // server sending `retriggerRequest: false` on -32801 must still
2010            // be honored (FR-006 resolution), short-circuiting the loop well
2011            // under the first 500ms backoff.
2012            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
2013                .await
2014                .unwrap()
2015                .unwrap();
2016
2017            match result {
2018                Err(Error::LspServerError { code, .. }) => {
2019                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
2020                }
2021                other => panic!("expected immediate ContentModified error, got {other:?}"),
2022            }
2023
2024            let second_request =
2025                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
2026                    .await;
2027            assert!(
2028                second_request.is_err(),
2029                "no retry should have been sent after retriggerRequest: false"
2030            );
2031        }
2032
2033        #[tokio::test]
2034        async fn test_retry_succeeds_after_one_content_modified_response() {
2035            let (client, mut server) = fake_lsp_client();
2036
2037            let request_task = tokio::spawn(async move {
2038                client
2039                    .request::<_, Value>(
2040                        "textDocument/hover",
2041                        serde_json::json!({}),
2042                        Duration::from_secs(30),
2043                    )
2044                    .await
2045            });
2046
2047            let mut reader = BufReader::new(&mut server.write_stdout);
2048
2049            // First attempt gets ContentModified and must retrigger.
2050            let first = read_framed_message(&mut reader).await;
2051            write_retryable_error_response(
2052                &mut server.read_half_stdin,
2053                &first["id"].clone(),
2054                i32::from(LspErrorCodes::ContentModified),
2055                "content modified",
2056                true,
2057            )
2058            .await;
2059
2060            // Second attempt (after backoff) succeeds -- proves the loop
2061            // genuinely re-sends the request rather than just counting down.
2062            let second = read_framed_message(&mut reader).await;
2063            assert_ne!(
2064                first["id"], second["id"],
2065                "retry must use a fresh request id"
2066            );
2067            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
2068            write_success_response(
2069                &mut server.read_half_stdin,
2070                &second["id"].clone(),
2071                expected_result.clone(),
2072            )
2073            .await;
2074
2075            let result = request_task.await.unwrap();
2076            assert_eq!(result.unwrap(), expected_result);
2077        }
2078
2079        #[tokio::test]
2080        async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
2081            let (client, mut server) = fake_lsp_client();
2082
2083            // `textDocument/rename` is deliberately excluded from
2084            // `CONTENT_MODIFIED_RETRY_METHODS` (its result is an edit the
2085            // caller applies at a position that may no longer be valid once
2086            // the document changed) -- a -32801 response for it must return
2087            // immediately even though `retriggerRequest: true` would pass
2088            // `should_retrigger`'s gate on its own.
2089            let request_task = tokio::spawn(async move {
2090                client
2091                    .request::<_, Value>(
2092                        "textDocument/rename",
2093                        serde_json::json!({}),
2094                        Duration::from_secs(30),
2095                    )
2096                    .await
2097            });
2098
2099            let mut reader = BufReader::new(&mut server.write_stdout);
2100            let request = read_framed_message(&mut reader).await;
2101            let id = request["id"].clone();
2102            write_retryable_error_response(
2103                &mut server.read_half_stdin,
2104                &id,
2105                i32::from(LspErrorCodes::ContentModified),
2106                "content modified",
2107                true,
2108            )
2109            .await;
2110
2111            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
2112                .await
2113                .unwrap()
2114                .unwrap();
2115
2116            match result {
2117                Err(Error::LspServerError { code, .. }) => {
2118                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
2119                }
2120                other => panic!("expected immediate ContentModified error, got {other:?}"),
2121            }
2122
2123            let second_request =
2124                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
2125                    .await;
2126            assert!(
2127                second_request.is_err(),
2128                "no retry should have been sent for a non-allowlisted method"
2129            );
2130        }
2131
2132        /// #313: an oversized, server-controlled error message must be
2133        /// truncated before it reaches the MCP caller in
2134        /// `Error::LspServerError`, not just before it is logged. Routes
2135        /// through the real `message_loop_inner` (via `fake_lsp_client`)
2136        /// rather than constructing the error by hand, so it actually
2137        /// exercises the fix.
2138        #[tokio::test]
2139        async fn test_oversized_error_message_truncated_for_caller() {
2140            let (client, mut server) = fake_lsp_client();
2141
2142            let request_task = tokio::spawn(async move {
2143                client
2144                    .request::<_, Value>(
2145                        "textDocument/hover",
2146                        serde_json::json!({}),
2147                        Duration::from_secs(30),
2148                    )
2149                    .await
2150            });
2151
2152            let mut reader = BufReader::new(&mut server.write_stdout);
2153            let request = read_framed_message(&mut reader).await;
2154            let id = request["id"].clone();
2155            let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
2156            write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
2157                .await;
2158
2159            let result = request_task.await.unwrap();
2160
2161            match result {
2162                Err(Error::LspServerError { code, message, .. }) => {
2163                    assert_eq!(code, -32603);
2164                    assert!(
2165                        message.len() < oversized_message.len(),
2166                        "caller-facing message must be truncated, got {} bytes",
2167                        message.len()
2168                    );
2169                    assert!(message.ends_with("... (truncated)"));
2170                }
2171                other => panic!("expected truncated LspServerError, got {other:?}"),
2172            }
2173        }
2174
2175        /// #313 S2: a legitimate error message longer than the log-line cap
2176        /// (`crate::util::MAX_LOG_STRING_BYTES`, 200 bytes) but shorter than
2177        /// the caller-facing cap must reach the MCP caller intact -- the
2178        /// caller-facing budget must not silently collapse to the log
2179        /// budget.
2180        #[tokio::test]
2181        async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
2182            let (client, mut server) = fake_lsp_client();
2183
2184            let request_task = tokio::spawn(async move {
2185                client
2186                    .request::<_, Value>(
2187                        "textDocument/hover",
2188                        serde_json::json!({}),
2189                        Duration::from_secs(30),
2190                    )
2191                    .await
2192            });
2193
2194            let mut reader = BufReader::new(&mut server.write_stdout);
2195            let request = read_framed_message(&mut reader).await;
2196            let id = request["id"].clone();
2197            let message = "x".repeat(crate::util::MAX_LOG_STRING_BYTES + 50);
2198            write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
2199
2200            let result = request_task.await.unwrap();
2201
2202            match result {
2203                Err(Error::LspServerError {
2204                    message: returned, ..
2205                }) => {
2206                    assert_eq!(
2207                        returned, message,
2208                        "message under the caller cap must not be truncated"
2209                    );
2210                }
2211                other => panic!("expected untruncated LspServerError, got {other:?}"),
2212            }
2213        }
2214
2215        /// #392: a `-32802`/`-32801` error that gets retried and then
2216        /// succeeds must not log at `error!` -- only a `warn!` "will retry"
2217        /// line -- so log-based monitoring does not false-positive on a
2218        /// transient error the retry loop silently recovers from.
2219        #[tokio::test]
2220        async fn test_retried_error_that_recovers_does_not_log_error_level() {
2221            use tracing_subscriber::layer::SubscriberExt as _;
2222
2223            let (client, mut server) = fake_lsp_client();
2224            let captured = CapturedLogs::default();
2225            let subscriber = tracing_subscriber::registry().with(captured.clone());
2226            let guard = tracing::subscriber::set_default(subscriber);
2227
2228            let request_task = tokio::spawn(async move {
2229                client
2230                    .request::<_, Value>(
2231                        "textDocument/hover",
2232                        serde_json::json!({}),
2233                        Duration::from_secs(30),
2234                    )
2235                    .await
2236            });
2237
2238            let mut reader = BufReader::new(&mut server.write_stdout);
2239
2240            let first = read_framed_message(&mut reader).await;
2241            write_retryable_error_response(
2242                &mut server.read_half_stdin,
2243                &first["id"].clone(),
2244                SERVER_CANCELLED_CODE,
2245                "server cancelled the request",
2246                true,
2247            )
2248            .await;
2249
2250            let second = read_framed_message(&mut reader).await;
2251            write_success_response(
2252                &mut server.read_half_stdin,
2253                &second["id"].clone(),
2254                serde_json::json!({ "contents": "resolved on retry" }),
2255            )
2256            .await;
2257
2258            let result = request_task.await.unwrap();
2259            assert!(result.is_ok(), "expected retry to recover, got {result:?}");
2260
2261            drop(guard);
2262            let logs = captured.entries();
2263            assert!(
2264                !logs
2265                    .iter()
2266                    .any(|(level, _)| *level == tracing::Level::ERROR),
2267                "a retried-and-recovered error must not log at ERROR, got: {logs:?}"
2268            );
2269            assert!(
2270                logs.iter().any(
2271                    |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
2272                ),
2273                "expected a WARN 'will retry' log line, got: {logs:?}"
2274            );
2275        }
2276
2277        /// #392: a `-32801` (`ContentModified`) error for a method outside
2278        /// `CONTENT_MODIFIED_RETRY_METHODS` is never retried, so it must
2279        /// still surface at `error!` on the very first attempt.
2280        #[tokio::test]
2281        async fn test_non_retryable_error_logs_error_level() {
2282            use tracing_subscriber::layer::SubscriberExt as _;
2283
2284            let (client, mut server) = fake_lsp_client();
2285            let captured = CapturedLogs::default();
2286            let subscriber = tracing_subscriber::registry().with(captured.clone());
2287            let guard = tracing::subscriber::set_default(subscriber);
2288
2289            let request_task = tokio::spawn(async move {
2290                client
2291                    .request::<_, Value>(
2292                        "textDocument/rename",
2293                        serde_json::json!({}),
2294                        Duration::from_secs(30),
2295                    )
2296                    .await
2297            });
2298
2299            let mut reader = BufReader::new(&mut server.write_stdout);
2300            let request = read_framed_message(&mut reader).await;
2301            let id = request["id"].clone();
2302            write_retryable_error_response(
2303                &mut server.read_half_stdin,
2304                &id,
2305                i32::from(LspErrorCodes::ContentModified),
2306                "content modified",
2307                true,
2308            )
2309            .await;
2310
2311            let result = request_task.await.unwrap();
2312            assert!(result.is_err(), "expected a non-retryable error");
2313
2314            drop(guard);
2315            let logs = captured.entries();
2316            assert!(
2317                logs.iter()
2318                    .any(|(level, msg)| *level == tracing::Level::ERROR
2319                        && msg.contains("LSP error response")
2320                        && msg.contains("content modified")),
2321                "a non-retryable error must still surface an ERROR log sharing the \
2322                 'LSP error response' prefix, got: {logs:?}"
2323            );
2324        }
2325    }
2326
2327    /// Regression coverage for #451 (`LspTransportReader::receive` moved off
2328    /// the `select!` and onto a dedicated reader task, see
2329    /// [`super::spawn_reader_task`]).
2330    mod reader_task_regression {
2331        use tokio::io::BufReader;
2332
2333        use super::*;
2334        use crate::test_lsp::{
2335            fake_lsp_client, inert_transport, read_framed_message, write_response,
2336        };
2337
2338        /// The reader task only ever sends `Err` through the channel before
2339        /// exiting (see `spawn_reader_task`) -- a `None` from `msg_rx.recv()`
2340        /// means the task disappeared some other way (e.g. panicked). This is
2341        /// the one branch in `message_loop_inner` that has no equivalent in
2342        /// the pre-#451 code, so it needs its own direct test rather than
2343        /// relying on the full `fake_lsp_client` harness to provoke it.
2344        #[tokio::test]
2345        async fn test_message_loop_inner_treats_reader_channel_close_as_server_terminated() {
2346            let (mut transport, _reader) = inert_transport();
2347            let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
2348            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
2349            let pending_requests: Arc<Mutex<PendingRequests>> =
2350                Arc::new(Mutex::new(HashMap::new()));
2351
2352            drop(msg_tx);
2353
2354            let result = LspClient::message_loop_inner(
2355                &mut transport,
2356                &mut msg_rx,
2357                &mut command_rx,
2358                &pending_requests,
2359                None,
2360                None,
2361            )
2362            .await;
2363
2364            assert!(
2365                matches!(result, Err(Error::ServerTerminated)),
2366                "got {result:?}"
2367            );
2368        }
2369
2370        /// As above, with a real pending request parked at the time the
2371        /// reader task disappears -- `message_loop_inner` itself never
2372        /// touches `pending_requests` on this branch, so a caller blocked on
2373        /// it only unblocks via `message_loop`'s post-loop
2374        /// `drain_and_fail_pending` (#458), invoked here the same way
2375        /// `message_loop` does after `message_loop_inner` returns. Driving
2376        /// this exact branch through the real `message_loop` would require
2377        /// the reader task to panic (the only other way `msg_rx.recv()`
2378        /// returns `None`), which isn't reproducible without a production
2379        /// seam -- see `test_message_loop_fails_pending_requests_on_transport_error_exit`
2380        /// for the sibling exit path exercised through the real function.
2381        #[tokio::test]
2382        async fn test_message_loop_inner_reader_gone_then_drain_fails_pending_request() {
2383            let (mut transport, _reader) = inert_transport();
2384            let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
2385            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
2386            let pending_requests: Arc<Mutex<PendingRequests>> =
2387                Arc::new(Mutex::new(HashMap::new()));
2388
2389            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
2390            pending_requests
2391                .lock()
2392                .await
2393                .insert(RequestId::Number(1), response_tx);
2394
2395            drop(msg_tx);
2396
2397            let result = LspClient::message_loop_inner(
2398                &mut transport,
2399                &mut msg_rx,
2400                &mut command_rx,
2401                &pending_requests,
2402                None,
2403                None,
2404            )
2405            .await;
2406            assert!(
2407                matches!(result, Err(Error::ServerTerminated)),
2408                "got {result:?}"
2409            );
2410
2411            LspClient::drain_and_fail_pending(&pending_requests).await;
2412
2413            let received = response_rx.await.unwrap();
2414            assert!(
2415                matches!(received, Err(Error::ServerTerminated)),
2416                "got {received:?}"
2417            );
2418            assert!(pending_requests.lock().await.is_empty());
2419        }
2420
2421        /// The reader task can run ahead of `select!` and have already queued
2422        /// a fully-decoded response in `msg_rx` by the time a concurrent
2423        /// `Shutdown` command is what `select!` happens to pick. Before the
2424        /// drain fix, that queued response was silently dropped instead of
2425        /// resolving its caller's pending request -- the caller would then
2426        /// block until its own `request_timeout_seconds` elapsed instead of
2427        /// failing fast or succeeding.
2428        #[tokio::test]
2429        async fn test_shutdown_drains_buffered_responses_instead_of_dropping_them() {
2430            let (mut transport, _reader) = inert_transport();
2431            let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
2432            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
2433            let pending_requests: Arc<Mutex<PendingRequests>> =
2434                Arc::new(Mutex::new(HashMap::new()));
2435
2436            let id = RequestId::Number(1);
2437            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
2438            pending_requests
2439                .lock()
2440                .await
2441                .insert(id.clone(), response_tx);
2442
2443            msg_tx
2444                .send(Ok(InboundMessage::Response(JsonRpcResponse {
2445                    jsonrpc: "2.0".to_string(),
2446                    id: id.clone(),
2447                    result: Some(serde_json::json!({ "ok": true })),
2448                    error: None,
2449                })))
2450                .await
2451                .unwrap();
2452            command_tx.send(ClientCommand::Shutdown).await.unwrap();
2453            drop(command_tx);
2454
2455            let result = LspClient::message_loop_inner(
2456                &mut transport,
2457                &mut msg_rx,
2458                &mut command_rx,
2459                &pending_requests,
2460                None,
2461                None,
2462            )
2463            .await;
2464
2465            assert!(result.is_ok(), "got {result:?}");
2466            // `Err` here means the sender was dropped without a reply -- i.e. the
2467            // buffered response was lost instead of resolving this request.
2468            let received = response_rx.await.unwrap();
2469            assert_eq!(received.unwrap(), serde_json::json!({ "ok": true }));
2470            assert!(
2471                pending_requests.lock().await.is_empty(),
2472                "the drained response must resolve its pending request entry"
2473            );
2474        }
2475
2476        /// #458: before the fix, none of `message_loop`'s exit paths failed
2477        /// requests still parked in `pending_requests` -- each caller stayed
2478        /// blocked in its own `timeout(..)` until `request_timeout` elapsed.
2479        /// Drives the real `message_loop` (not `_inner`) over
2480        /// [`inert_transport`], whose reader side is dropped, so the reader
2481        /// task hits a transport receive error immediately and
2482        /// `message_loop` exits through that path.
2483        #[tokio::test]
2484        async fn test_message_loop_fails_pending_requests_on_transport_error_exit() {
2485            let (transport, reader) = inert_transport();
2486            let (_command_tx, command_rx) = mpsc::channel::<ClientCommand>(1);
2487            let pending_requests: Arc<Mutex<PendingRequests>> =
2488                Arc::new(Mutex::new(HashMap::new()));
2489
2490            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
2491            pending_requests
2492                .lock()
2493                .await
2494                .insert(RequestId::Number(1), response_tx);
2495
2496            let result = LspClient::message_loop(
2497                (transport, reader),
2498                command_rx,
2499                Arc::clone(&pending_requests),
2500                None,
2501                None,
2502            )
2503            .await;
2504
2505            assert!(result.is_err(), "got {result:?}");
2506            let received = response_rx.await.unwrap();
2507            assert!(
2508                matches!(received, Err(Error::ServerTerminated)),
2509                "got {received:?}"
2510            );
2511            assert!(pending_requests.lock().await.is_empty());
2512        }
2513
2514        /// #458: unit-level check that composing `message_loop_inner`'s
2515        /// Shutdown `try_recv` drain (#451) with a subsequent
2516        /// `drain_and_fail_pending` call -- the same two pieces
2517        /// `message_loop` itself calls in that order -- does not re-fail a
2518        /// request already resolved by a buffered response, while still
2519        /// failing a truly unanswered one. This does not exercise
2520        /// `message_loop`'s own statement ordering (see
2521        /// `test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown`
2522        /// for that, driven through the real function).
2523        #[tokio::test]
2524        async fn test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending() {
2525            let (mut transport, _reader) = inert_transport();
2526            let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
2527            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
2528            let pending_requests: Arc<Mutex<PendingRequests>> =
2529                Arc::new(Mutex::new(HashMap::new()));
2530
2531            let answered_id = RequestId::Number(1);
2532            let (answered_tx, answered_rx) = oneshot::channel::<Result<Value>>();
2533            let unanswered_id = RequestId::Number(2);
2534            let (unanswered_tx, unanswered_rx) = oneshot::channel::<Result<Value>>();
2535            pending_requests
2536                .lock()
2537                .await
2538                .insert(answered_id.clone(), answered_tx);
2539            pending_requests
2540                .lock()
2541                .await
2542                .insert(unanswered_id.clone(), unanswered_tx);
2543
2544            msg_tx
2545                .send(Ok(InboundMessage::Response(JsonRpcResponse {
2546                    jsonrpc: "2.0".to_string(),
2547                    id: answered_id,
2548                    result: Some(serde_json::json!({ "ok": true })),
2549                    error: None,
2550                })))
2551                .await
2552                .unwrap();
2553            command_tx.send(ClientCommand::Shutdown).await.unwrap();
2554            drop(command_tx);
2555
2556            let inner_result = LspClient::message_loop_inner(
2557                &mut transport,
2558                &mut msg_rx,
2559                &mut command_rx,
2560                &pending_requests,
2561                None,
2562                None,
2563            )
2564            .await;
2565            assert!(inner_result.is_ok(), "got {inner_result:?}");
2566
2567            // Mirrors the drain `message_loop` runs after `message_loop_inner`
2568            // returns.
2569            LspClient::drain_and_fail_pending(&pending_requests).await;
2570
2571            let answered = answered_rx.await.unwrap();
2572            assert_eq!(answered.unwrap(), serde_json::json!({ "ok": true }));
2573
2574            let unanswered = unanswered_rx.await.unwrap();
2575            assert!(
2576                matches!(unanswered, Err(Error::ServerTerminated)),
2577                "got {unanswered:?}"
2578            );
2579            assert!(pending_requests.lock().await.is_empty());
2580        }
2581
2582        /// #458 S2: end-to-end proof that `message_loop`'s post-loop drain
2583        /// runs *after* `message_loop_inner` returns, driven through the
2584        /// real `message_loop` (via `fake_lsp_client`), not a hand-recreated
2585        /// sequence. Deterministic without depending on `select!`
2586        /// nondeterminism: the answered request is awaited to completion via
2587        /// the loop's ordinary response path *before* `shutdown` is ever
2588        /// sent, so nothing here relies on which branch `select!` happens to
2589        /// pick when both are ready -- that race is unit-tested separately in
2590        /// `test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending`.
2591        #[tokio::test]
2592        async fn test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown()
2593        {
2594            let (client, mut server) = fake_lsp_client();
2595            let mut reader = BufReader::new(&mut server.write_stdout);
2596
2597            let answered_client = client.clone();
2598            let answered_task = tokio::spawn(async move {
2599                answered_client
2600                    .request::<_, Value>(
2601                        "textDocument/hover",
2602                        serde_json::json!({}),
2603                        Duration::from_secs(30),
2604                    )
2605                    .await
2606            });
2607
2608            let request = read_framed_message(&mut reader).await;
2609            let id = request["id"].clone();
2610            write_response(
2611                &mut server.read_half_stdin,
2612                &id,
2613                serde_json::json!({ "ok": true }),
2614            )
2615            .await;
2616
2617            let answered = answered_task.await.unwrap().unwrap();
2618            assert_eq!(answered, serde_json::json!({ "ok": true }));
2619
2620            let unanswered_client = client.clone();
2621            let unanswered_task = tokio::spawn(async move {
2622                unanswered_client
2623                    .request::<_, Value>(
2624                        "textDocument/definition",
2625                        serde_json::json!({}),
2626                        Duration::from_secs(30),
2627                    )
2628                    .await
2629            });
2630
2631            // Reading the framed bytes back confirms the request was fully
2632            // sent, which only happens after it is already registered in
2633            // `pending_requests` -- so shutdown below cannot race its insert.
2634            let _unanswered_request = read_framed_message(&mut reader).await;
2635
2636            client.shutdown().await.unwrap();
2637
2638            let unanswered = tokio::time::timeout(Duration::from_millis(200), unanswered_task)
2639                .await
2640                .unwrap_or_else(|_| {
2641                    panic!(
2642                        "unanswered request must fail immediately on shutdown, not hang until its own timeout"
2643                    )
2644                })
2645                .unwrap();
2646            assert!(
2647                matches!(unanswered, Err(Error::ServerTerminated)),
2648                "got {unanswered:?}"
2649            );
2650        }
2651
2652        /// Drives dense, concurrent request/response traffic through the real
2653        /// `message_loop`/`spawn_reader_task` pair (via `fake_lsp_client`) and
2654        /// answers deliberately out of arrival order, so a bug that matched
2655        /// responses positionally instead of by id -- the failure mode a
2656        /// mid-frame desync would eventually cause -- would surface as a
2657        /// mismatched payload rather than a hang.
2658        #[tokio::test]
2659        async fn test_dense_concurrent_requests_all_resolve_to_matching_responses() {
2660            const REQUEST_COUNT: usize = 20;
2661            let (client, mut server) = fake_lsp_client();
2662
2663            // A manual push loop, not `.map(..).collect()`: `tokio::spawn`
2664            // must run eagerly for every `i` right here, before
2665            // `server_task` starts answering below -- a lazily-iterated
2666            // combinator would spawn (and thus send) each request only as
2667            // its `JoinHandle` is later awaited, one at a time, defeating
2668            // the "dense concurrent" setup this test needs.
2669            let mut request_tasks = Vec::with_capacity(REQUEST_COUNT);
2670            for i in 0..REQUEST_COUNT {
2671                let client = client.clone();
2672                request_tasks.push(tokio::spawn(async move {
2673                    client
2674                        .request::<_, Value>(
2675                            "textDocument/hover",
2676                            serde_json::json!({ "n": i }),
2677                            Duration::from_secs(30),
2678                        )
2679                        .await
2680                }));
2681            }
2682
2683            let server_task = tokio::spawn(async move {
2684                let mut reader = BufReader::new(&mut server.write_stdout);
2685                let mut requests = Vec::with_capacity(REQUEST_COUNT);
2686                for _ in 0..REQUEST_COUNT {
2687                    requests.push(read_framed_message(&mut reader).await);
2688                }
2689                for request in requests.into_iter().rev() {
2690                    let id = request["id"].clone();
2691                    let n = request["params"]["n"].clone();
2692                    write_response(
2693                        &mut server.read_half_stdin,
2694                        &id,
2695                        serde_json::json!({ "echo": n }),
2696                    )
2697                    .await;
2698                }
2699            });
2700
2701            server_task.await.unwrap();
2702
2703            for (i, task) in request_tasks.into_iter().enumerate() {
2704                let value = task
2705                    .await
2706                    .unwrap()
2707                    .unwrap_or_else(|e| panic!("request {i} failed: {e:?}"));
2708                assert_eq!(
2709                    value["echo"],
2710                    serde_json::json!(i),
2711                    "response for request {i} carried the wrong payload -- id/response mismatch"
2712                );
2713            }
2714        }
2715
2716        /// #458: a request issued after the client has already shut down
2717        /// must fail immediately via `register_and_send_request`'s
2718        /// `command_tx.send(..)` check, not hang until its own
2719        /// `request_timeout`. Wrapped in a short outer `timeout` so a
2720        /// regression back to the slow path fails this test instead of
2721        /// merely making it slow.
2722        #[tokio::test]
2723        async fn test_request_after_shutdown_fails_fast_instead_of_hanging() {
2724            let (client, _server) = fake_lsp_client();
2725            let post_shutdown_client = client.clone();
2726
2727            client.shutdown().await.unwrap();
2728
2729            let result = tokio::time::timeout(
2730                Duration::from_millis(200),
2731                post_shutdown_client.request::<_, Value>(
2732                    "textDocument/hover",
2733                    serde_json::json!({}),
2734                    Duration::from_secs(30),
2735                ),
2736            )
2737            .await
2738            .unwrap_or_else(|_| {
2739                panic!(
2740                    "request after shutdown must fail immediately, not hang until its own timeout"
2741                )
2742            });
2743
2744            assert!(
2745                matches!(result, Err(Error::ServerTerminated)),
2746                "got {result:?}"
2747            );
2748        }
2749    }
2750}