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::atomic::{AtomicI64, AtomicU64, Ordering};
5use std::sync::{Arc, Mutex as StdMutex, PoisonError};
6
7use lsp_types::{ErrorCodes, 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, Instant, timeout, timeout_at};
14use tracing::{debug, error, trace, warn};
15
16use crate::config::{LanguageId, LspServerConfig, LspSettings, ServerId};
17use crate::error::{BackgroundTask, Error, Result};
18use crate::lsp::transport::{LspTransport, LspTransportReader};
19use crate::lsp::types::{
20    InboundMessage, JsonRpcError, JsonRpcNotification, JsonRpcOutcome, JsonRpcReply,
21    JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, LspNotification, RequestId,
22};
23use crate::lsp::{DropCounter, DropLog, Lane, NotificationSink, ServerMessage};
24use crate::redaction::{RedactedText, Redactions};
25use crate::util::MAX_ERROR_MESSAGE_CALLER_BYTES;
26
27/// Maximum number of retry attempts for server-cancelled requests.
28const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
29
30/// Initial backoff delay for server-cancelled retries (milliseconds).
31const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
32
33/// Bounded capacity for the channel carrying fully-decoded inbound LSP
34/// messages from [`spawn_reader_task`]'s background task to
35/// [`LspClient::message_loop_inner`].
36///
37/// Backpressured (`send().await`, not `try_send`) unlike the best-effort
38/// notification/lifecycle lanes: dropping a frame here would desync
39/// request/response correlation or silently swallow a server-initiated
40/// request. Matches the command channel's capacity -- both lanes carry
41/// protocol-critical traffic at a similar cadence.
42const READER_CHANNEL_CAPACITY: usize = 100;
43
44/// How long [`LspClient::message_loop`] waits, after aborting the reader
45/// task it no longer drains, for that task to actually finish dropping its
46/// [`LspTransportReader`] (and the `ChildStdout` it owns).
47///
48/// `abort()` only requests cancellation -- the task's locals are dropped
49/// once the runtime next polls it, not synchronously at the call site.
50/// Mirrors `await_lsp_init_handle`'s reasoning in `crate::lib` for the same
51/// pattern. Short: the task is either already parked in a cancel-safe
52/// `.await` (aborts promptly) or has nothing left to do.
53const READER_TASK_ABORT_GRACE: Duration = Duration::from_secs(1);
54
55/// Deadline for a full LSP server shutdown (`shutdown` request, `exit`
56/// notification, message-loop stop and child exit). Cleanup after an abort
57/// may overrun it by a 50 ms settle.
58pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
59
60/// How many undecodable inbound frames in a row the reader tolerates before it
61/// treats the connection as broken.
62///
63/// A single frame that is not a valid message is dropped, so one bad message
64/// does not take the server down. A server that emits only garbage never makes
65/// progress, though: after this many consecutive undecodable frames the
66/// connection is torn down and the server respawned like any other lost
67/// connection.
68pub const MAX_CONSECUTIVE_UNDECODABLE_FRAMES: u32 = 64;
69
70const _: () = assert!(MAX_CONSECUTIVE_UNDECODABLE_FRAMES > 0);
71
72/// How long [`LspClient::shutdown_until`] waits for an aborted message loop
73/// to drop its command receiver before failing the pending requests.
74const SHUTDOWN_ABORT_SETTLE: Duration = Duration::from_millis(50);
75
76/// LSP request methods for which a `-32801` (`ContentModified`) error
77/// response is safe to retry automatically -- also declared to servers via
78/// `general.staleRequestSupport.retryOnContentModified` during initialize
79/// (see [`crate::lsp::LspServer`]'s handshake).
80///
81/// Per the LSP spec, `ContentModified` means the server noticed the document
82/// changed while it was computing a response; the (possibly stale) result may
83/// still be useful, or the client may choose to cancel the request instead.
84/// mcpls chooses to retry, which is safe for a read-only/idempotent request
85/// (hover, references, diagnostics, ...): a stale response is simply
86/// discarded and superseded by a fresh one.
87///
88/// Deliberately excludes every method in
89/// `crate::bridge::translator::edits` (`textDocument/rename`,
90/// `textDocument/formatting`, `textDocument/codeAction`): their result is an
91/// edit the MCP caller applies, and `-32801` means the document changed since
92/// the request was issued, so a retry at the original position could return
93/// an edit for content the caller no longer expects (e.g. renaming a
94/// different symbol than the one originally at that position).
95///
96/// This list only gates `-32801`. `-32802` (`ServerCancelled`) retry is
97/// unaffected and keeps retrying unconditionally for every method, as before.
98pub const CONTENT_MODIFIED_RETRY_METHODS: &[&str] = &[
99    "textDocument/signatureHelp",
100    "textDocument/inlayHint",
101    "textDocument/completion",
102    "textDocument/prepareCallHierarchy",
103    "callHierarchy/incomingCalls",
104    "callHierarchy/outgoingCalls",
105    "textDocument/diagnostic",
106    "textDocument/hover",
107    "textDocument/definition",
108    "textDocument/references",
109    "textDocument/implementation",
110    "textDocument/typeDefinition",
111    "textDocument/documentSymbol",
112    "workspace/symbol",
113];
114
115/// Upper bound on the effective timeout for completion requests, regardless
116/// of `request_timeout_seconds`.
117///
118/// Completions are latency-sensitive: a completion list that takes longer
119/// than this is no longer useful to the caller. This is a deliberate MVP
120/// ceiling, not an oversight — completions cannot be configured above this
121/// value today. See [`LspClient::completion_timeout`].
122const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
123
124/// Upper bound on the effective timeout for a single `codeAction/resolve`
125/// request, regardless of `request_timeout_seconds`.
126///
127/// `handle_code_actions` (`bridge::translator::edits`) resolves up to
128/// `MAX_CODE_ACTION_RESOLVES` deferred actions concurrently after the
129/// initial `textDocument/codeAction` response; an uncapped per-resolve
130/// timeout would let a large `request_timeout_seconds` configuration make
131/// one tool call wait far longer than a caller expects for what is meant to
132/// be a best-effort follow-up. Mirrors [`COMPLETION_TIMEOUT_CAP`]'s
133/// reasoning. See [`LspClient::code_action_resolve_timeout`].
134const CODE_ACTION_RESOLVE_TIMEOUT_CAP: Duration = Duration::from_secs(10);
135
136/// Type alias for pending request tracking map.
137type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
138
139/// Spawns the dedicated background task that owns `reader` exclusively and
140/// decodes inbound LSP frames in a loop, handing each one to
141/// [`LspClient::message_loop_inner`] over the returned channel.
142///
143/// This is the fix for #451: [`LspTransportReader::receive`] is not
144/// cancel-safe, so it must never run as a branch of the `select!` in
145/// `message_loop_inner`, which also waits on `command_rx`. Running it here
146/// instead, on a task driven only by its own `.await`s, means it can never
147/// be cancelled mid-frame.
148///
149/// The task exits after sending one `Err` (I/O failure or EOF), once
150/// `message_loop_inner` drops its end of the channel (e.g. on shutdown), or
151/// when the returned [`JoinHandle`] is aborted. Callers must abort it once
152/// they stop draining the channel -- see [`LspClient::message_loop`], the
153/// only caller -- otherwise it stays parked in a blocking read holding the
154/// underlying `ChildStdout` open indefinitely: not itself a correctness bug
155/// (`LspServer::is_dead`, lifecycle.rs, checks the child process directly via
156/// `try_wait()` and doesn't care whether we still hold its stdout open), but
157/// a leaked task and file descriptor for the lifetime of that connection.
158fn spawn_reader_task(
159    mut reader: LspTransportReader,
160) -> (JoinHandle<()>, mpsc::Receiver<Result<InboundMessage>>) {
161    let (tx, rx) = mpsc::channel(READER_CHANNEL_CAPACITY);
162    let handle = tokio::spawn(async move {
163        let mut run = UndecodableRun::default();
164        loop {
165            let message = reader
166                .receive()
167                .await
168                .and_then(|message| run.observe(message));
169            let is_err = message.is_err();
170            if tx.send(message).await.is_err() || is_err {
171                break;
172            }
173        }
174    });
175    (handle, rx)
176}
177
178/// The current run of consecutive undecodable inbound frames.
179#[derive(Debug, Default)]
180struct UndecodableRun {
181    in_a_row: u32,
182    drops: DropCounter,
183}
184
185impl UndecodableRun {
186    /// Passes `message` on. A decodable message ends the run; an undecodable
187    /// one extends it. Drops are logged at `warn` at most once a minute, with
188    /// the number dropped since the last line, and at `debug` otherwise, so a
189    /// server that spams garbage cannot flood the log.
190    ///
191    /// # Errors
192    ///
193    /// [`Error::LspProtocolError`] once the run reaches
194    /// [`MAX_CONSECUTIVE_UNDECODABLE_FRAMES`], so the connection is torn down.
195    fn observe(&mut self, message: InboundMessage) -> Result<InboundMessage> {
196        self.observe_at(message, std::time::Instant::now())
197    }
198
199    fn observe_at(
200        &mut self,
201        message: InboundMessage,
202        now: std::time::Instant,
203    ) -> Result<InboundMessage> {
204        if !message.is_undecodable() {
205            self.in_a_row = 0;
206            return Ok(message);
207        }
208        self.in_a_row = self.in_a_row.saturating_add(1);
209        if self.in_a_row >= MAX_CONSECUTIVE_UNDECODABLE_FRAMES {
210            return Err(Error::LspProtocolError(RedactedText::fixed(
211                "too many consecutive undecodable messages",
212            )));
213        }
214        if let Some(suppressed) = self.drops.record(now) {
215            warn!(
216                "Dropped an undecodable LSP message ({suppressed} others dropped since the last \
217                 warning); further ones are logged at debug for a minute"
218            );
219        } else {
220            debug!(
221                "Dropped an undecodable LSP message ({} in a row)",
222                self.in_a_row
223            );
224        }
225        Ok(message)
226    }
227}
228
229/// Why requests still pending on a client are being failed, and so which
230/// error they resolve with.
231#[derive(Debug, Clone, Default)]
232pub enum PendingFailure {
233    /// The connection is gone.
234    #[default]
235    Terminated,
236    /// The server was deliberately restarted; the caller may retry.
237    Restarted(ServerId),
238}
239
240impl PendingFailure {
241    fn error(&self) -> Error {
242        match self {
243            Self::Terminated => Error::ServerTerminated,
244            Self::Restarted(server_id) => Error::ServerRestarted {
245                server_id: server_id.clone(),
246            },
247        }
248    }
249}
250
251/// LSP client with async request/response handling.
252///
253/// This client manages communication with an LSP server, handling:
254/// - Concurrent requests with unique ID tracking
255/// - Background message loop for receiving responses
256/// - Timeout support for all requests
257/// - Graceful shutdown
258#[derive(Debug)]
259pub struct LspClient {
260    /// Configuration for this LSP server.
261    config: LspServerConfig,
262
263    /// Current server state.
264    state: Arc<Mutex<super::ServerState>>,
265
266    /// Atomic counter for request IDs.
267    request_counter: Arc<AtomicI64>,
268
269    /// Command sender for outbound messages.
270    command_tx: mpsc::Sender<ClientCommand>,
271
272    /// Requests awaiting a response, shared with the background message loop.
273    ///
274    /// Exposed here (not just captured by the loop) so [`Self::request`] can
275    /// remove its own entry on timeout instead of leaking it, and so a
276    /// connection known to be dead can fail its stragglers immediately via
277    /// [`Self::fail_pending_requests`] rather than leaving each to discover
278    /// that only when its own timeout elapses.
279    pending_requests: Arc<Mutex<PendingRequests>>,
280
281    /// The error stragglers in `pending_requests` fail with, shared by every
282    /// clone and by the message loop's exit-path drain.
283    pending_failure: Arc<StdMutex<PendingFailure>>,
284
285    /// Background receiver task handle.
286    receiver_task: Option<JoinHandle<Result<()>>>,
287
288    /// Secrets hidden from text derived from server output.
289    redactions: Arc<Redactions>,
290
291    /// Identity of the connection, shared by every clone.
292    connection_id: ConnectionId,
293}
294
295/// Identity of one client connection, and so of one server process.
296///
297/// Clones of a client share it; every client built by this process gets a
298/// distinct one, so a result tied to a replaced process is told apart from the
299/// running one.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub struct ConnectionId(u64);
302
303impl ConnectionId {
304    fn next() -> Self {
305        static NEXT: AtomicU64 = AtomicU64::new(1);
306        Self(NEXT.fetch_add(1, Ordering::Relaxed))
307    }
308}
309
310impl Clone for LspClient {
311    /// Creates a clone that shares the underlying connection.
312    ///
313    /// The clone does not own the receiver task and cannot perform shutdown.
314    /// All clones share the same command channel for sending requests.
315    fn clone(&self) -> Self {
316        Self {
317            config: self.config.clone(),
318            state: Arc::clone(&self.state),
319            request_counter: Arc::clone(&self.request_counter),
320            command_tx: self.command_tx.clone(),
321            pending_requests: Arc::clone(&self.pending_requests),
322            pending_failure: Arc::clone(&self.pending_failure),
323            receiver_task: None,
324            redactions: Arc::clone(&self.redactions),
325            connection_id: self.connection_id,
326        }
327    }
328}
329
330/// Commands for client control.
331enum ClientCommand {
332    /// Send a request and wait for response.
333    SendRequest { request: JsonRpcRequest },
334    /// Send a notification (no response expected).
335    SendNotification {
336        method: String,
337        params: Option<Value>,
338    },
339    /// Shutdown the client.
340    Shutdown,
341}
342
343impl LspClient {
344    /// Create a new LSP client with the given configuration.
345    ///
346    /// The client starts in an uninitialized state. Call `initialize()` to
347    /// start the server and complete the initialization handshake.
348    #[must_use]
349    pub fn new(config: LspServerConfig) -> Self {
350        // Placeholder channel - the receiver is intentionally dropped since
351        // the client starts uninitialized. A real channel is created when
352        // `from_transport` or `from_transport_with_notifications` is called.
353        let (command_tx, _command_rx) = mpsc::channel(1); // Minimal capacity for placeholder
354
355        Self {
356            config,
357            state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
358            request_counter: Arc::new(AtomicI64::new(1)),
359            command_tx,
360            pending_requests: Arc::new(Mutex::new(HashMap::new())),
361            pending_failure: Arc::default(),
362            receiver_task: None,
363            redactions: Arc::default(),
364            connection_id: ConnectionId::next(),
365        }
366    }
367
368    /// Create client from transport (for testing or custom spawning).
369    ///
370    /// This method initializes the background message loop with the provided transport.
371    #[cfg(test)]
372    pub(crate) fn from_transport(
373        config: LspServerConfig,
374        transport: (LspTransport, LspTransportReader),
375    ) -> Self {
376        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
377        let request_counter = Arc::new(AtomicI64::new(1));
378        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
379        let pending_failure = Arc::<StdMutex<PendingFailure>>::default();
380
381        let (command_tx, command_rx) = mpsc::channel(100);
382
383        let receiver_task = tokio::spawn(Self::message_loop(
384            transport,
385            command_rx,
386            Arc::clone(&pending_requests),
387            Arc::clone(&pending_failure),
388            None,
389            None,
390            Arc::default(),
391            Self::shared_settings(&config),
392        ));
393
394        Self {
395            config,
396            state,
397            request_counter,
398            command_tx,
399            pending_requests,
400            pending_failure,
401            receiver_task: Some(receiver_task),
402            redactions: Arc::default(),
403            connection_id: ConnectionId::next(),
404        }
405    }
406
407    /// Create client from transport with notification forwarding.
408    ///
409    /// Notifications are parsed and split across two lanes (P3): diagnostics/
410    /// log/showMessage go through `notification_tx`; `$/progress` `begin`/
411    /// `end` frames and unrecognized notifications (`LspNotification::Other`,
412    /// which carries rust-analyzer's `experimental/serverStatus`) go through
413    /// `lifecycle_tx` instead. A `$/progress` `report` frame is never
414    /// enqueued on either lane -- see [`Self::message_loop_inner`].
415    pub(crate) fn from_transport_with_notifications(
416        config: LspServerConfig,
417        transport: (LspTransport, LspTransportReader),
418        notification_tx: NotificationSink,
419        lifecycle_tx: mpsc::Sender<LspNotification>,
420        redactions: Arc<Redactions>,
421    ) -> Self {
422        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
423        let request_counter = Arc::new(AtomicI64::new(1));
424        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
425        let pending_failure = Arc::<StdMutex<PendingFailure>>::default();
426
427        let (command_tx, command_rx) = mpsc::channel(100);
428
429        let receiver_task = tokio::spawn(Self::message_loop(
430            transport,
431            command_rx,
432            Arc::clone(&pending_requests),
433            Arc::clone(&pending_failure),
434            Some(notification_tx),
435            Some(lifecycle_tx),
436            Arc::clone(&redactions),
437            Self::shared_settings(&config),
438        ));
439
440        Self {
441            config,
442            state,
443            request_counter,
444            command_tx,
445            pending_requests,
446            pending_failure,
447            receiver_task: Some(receiver_task),
448            redactions,
449            connection_id: ConnectionId::next(),
450        }
451    }
452
453    /// The configured settings, shared with the message loop.
454    fn shared_settings(config: &LspServerConfig) -> Option<Arc<LspSettings>> {
455        config.settings.clone().map(Arc::new)
456    }
457
458    /// Identity of the connection this client and its clones talk over.
459    pub(crate) const fn connection_id(&self) -> ConnectionId {
460        self.connection_id
461    }
462
463    /// Secrets this client hides from text derived from server output.
464    pub(crate) const fn redactions(&self) -> &Arc<Redactions> {
465        &self.redactions
466    }
467
468    /// Get the language ID for this client.
469    #[must_use]
470    pub const fn language_id(&self) -> &LanguageId {
471        &self.config.language_id
472    }
473
474    /// Get the current server state.
475    pub async fn state(&self) -> super::ServerState {
476        *self.state.lock().await
477    }
478
479    /// The timeout applied to a single LSP request attempt, derived from
480    /// [`LspServerConfig::request_timeout_seconds`].
481    ///
482    /// This bounds one attempt, not a whole tool call: [`Self::request`]
483    /// retries up to `SERVER_CANCELLED_MAX_RETRIES` (3) additional times on a
484    /// `-32802` (`ServerCancelled`) or `-32801` (`ContentModified`) response,
485    /// sharing one attempt budget between the two codes, so the worst-case
486    /// latency for a single tool call is `4 * request_timeout() + 3.5s` (the
487    /// sum of the retry backoff delays).
488    ///
489    /// Exception: `get_code_actions` (`bridge::translator::edits`) can add a
490    /// second, concurrent round of requests on top of this bound -- up to
491    /// `MAX_CODE_ACTION_RESOLVES` `codeAction/resolve` calls, each retried
492    /// under the same rules but bounded by [`Self::code_action_resolve_timeout`]
493    /// rather than this timeout. Since those run concurrently with each
494    /// other (not with the initial `textDocument/codeAction` request), the
495    /// worst case for that one tool call is
496    /// `(4 * request_timeout() + 3.5s) + (4 * code_action_resolve_timeout() + 3.5s)`,
497    /// not a multiple scaling with the number of resolved actions.
498    ///
499    /// The value is a [`TimeoutSecs`], so it is already within 1 second to
500    /// [`MAX_TIMEOUT_SECONDS`] and needs no clamping here.
501    ///
502    /// [`TimeoutSecs`]: crate::config::TimeoutSecs
503    /// [`MAX_TIMEOUT_SECONDS`]: crate::config::MAX_TIMEOUT_SECONDS
504    ///
505    /// # Examples
506    ///
507    /// ```
508    /// use std::time::Duration;
509    /// use mcpls_core::config::{LspServerConfig, TimeoutSecs};
510    /// use mcpls_core::lsp::LspClient;
511    ///
512    /// let mut config = LspServerConfig::rust_analyzer();
513    /// config.request_timeout_seconds = TimeoutSecs::new(45).unwrap();
514    /// let client = LspClient::new(config);
515    ///
516    /// assert_eq!(client.request_timeout(), Duration::from_secs(45));
517    /// ```
518    #[must_use]
519    pub const fn request_timeout(&self) -> Duration {
520        self.config.request_timeout_seconds.as_duration()
521    }
522
523    /// The timeout applied to completion (`textDocument/completion`) requests.
524    ///
525    /// Equal to [`Self::request_timeout`], capped at 10 seconds. Completions
526    /// cannot be configured above this cap by any
527    /// value of `request_timeout_seconds` — if that proves insufficient in
528    /// practice, the fix is a dedicated `completion_timeout_seconds` field,
529    /// not raising this cap.
530    ///
531    /// # Examples
532    ///
533    /// ```
534    /// use std::time::Duration;
535    /// use mcpls_core::config::{LspServerConfig, TimeoutSecs};
536    /// use mcpls_core::lsp::LspClient;
537    ///
538    /// let mut config = LspServerConfig::rust_analyzer();
539    /// config.request_timeout_seconds = TimeoutSecs::new(300).unwrap();
540    /// let client = LspClient::new(config);
541    ///
542    /// // Capped at 10s even though request_timeout_seconds is 300.
543    /// assert_eq!(client.completion_timeout(), Duration::from_secs(10));
544    /// assert!(client.completion_timeout() <= client.request_timeout());
545    /// ```
546    #[must_use]
547    pub fn completion_timeout(&self) -> Duration {
548        self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
549    }
550
551    /// The timeout applied to a single `codeAction/resolve` request.
552    ///
553    /// Equal to [`Self::request_timeout`], capped at 10 seconds.
554    ///
555    /// # Examples
556    ///
557    /// ```
558    /// use std::time::Duration;
559    /// use mcpls_core::config::{LspServerConfig, TimeoutSecs};
560    /// use mcpls_core::lsp::LspClient;
561    ///
562    /// let mut config = LspServerConfig::rust_analyzer();
563    /// config.request_timeout_seconds = TimeoutSecs::new(300).unwrap();
564    /// let client = LspClient::new(config);
565    ///
566    /// // Capped at 10s even though request_timeout_seconds is 300.
567    /// assert_eq!(client.code_action_resolve_timeout(), Duration::from_secs(10));
568    /// assert!(client.code_action_resolve_timeout() <= client.request_timeout());
569    /// ```
570    #[must_use]
571    pub fn code_action_resolve_timeout(&self) -> Duration {
572        self.request_timeout().min(CODE_ACTION_RESOLVE_TIMEOUT_CAP)
573    }
574
575    /// Registers before enqueuing the send command (not before the message
576    /// loop's own transport read), cleaning up the entry if the send fails.
577    async fn register_and_send_request(
578        &self,
579        request: JsonRpcRequest,
580        response_tx: oneshot::Sender<Result<Value>>,
581    ) -> Result<()> {
582        let id = request.id.clone();
583
584        self.pending_requests
585            .lock()
586            .await
587            .insert(id.clone(), response_tx);
588
589        if self
590            .command_tx
591            .send(ClientCommand::SendRequest { request })
592            .await
593            .is_err()
594        {
595            self.pending_requests.lock().await.remove(&id);
596            return Err(Error::ServerTerminated);
597        }
598
599        Ok(())
600    }
601
602    /// Send request and wait for response with timeout.
603    ///
604    /// Automatically retries up to 3 times when the server returns error code
605    /// -32802 (`ServerCancelled`, any method) or -32801 (`ContentModified`,
606    /// only for methods in `CONTENT_MODIFIED_RETRY_METHODS`) -- gated by
607    /// `data.retriggerRequest` when present -- using exponential backoff
608    /// starting at 500 ms. Both codes share the same attempt budget: a
609    /// request that hits -32801 then -32802 does not get 8 attempts, only 4.
610    ///
611    /// This method owns the severity of LSP error-response logging (#392):
612    /// a transient error about to be retried logs at `warn!`, while `error!`
613    /// is reserved for an error actually surfaced to the caller -- retry
614    /// exhaustion or a non-retryable code/method combination. This is
615    /// deliberately not decided in `message_loop_inner`, which parses the
616    /// response before knowing whether a retry will follow.
617    ///
618    /// # Type Parameters
619    ///
620    /// * `P` - The type of the request parameters (must be serializable)
621    /// * `R` - The type of the response result (must be deserializable)
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if:
626    /// - Server has shut down
627    /// - Request times out
628    /// - Response cannot be deserialized
629    /// - LSP server returns an error
630    pub async fn request<P, R>(
631        &self,
632        method: &str,
633        params: P,
634        timeout_duration: Duration,
635    ) -> Result<R>
636    where
637        P: Serialize,
638        R: DeserializeOwned,
639    {
640        Box::pin(self.request_inner(method, params, timeout_duration))
641            .await
642            .map_err(UnclassifiedError::surface)
643    }
644
645    async fn request_inner<P, R>(
646        &self,
647        method: &str,
648        params: P,
649        timeout_duration: Duration,
650    ) -> std::result::Result<R, UnclassifiedError>
651    where
652        P: Serialize,
653        R: DeserializeOwned,
654    {
655        let params_value = Self::omit_null_params(
656            serde_json::to_value(params).map_err(|e| UnclassifiedError::logged(e.into()))?,
657        );
658        let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
659
660        for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
661            if attempt > 0 {
662                debug!(
663                    "Retrying {} (attempt {}/{}), backoff={}ms",
664                    method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
665                );
666                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
667                delay_ms = delay_ms.saturating_mul(2);
668            }
669
670            let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
671            let (response_tx, response_rx) = oneshot::channel();
672            let request = JsonRpcRequest {
673                jsonrpc: JsonRpcVersion,
674                id: id.clone(),
675                method: method.to_string(),
676                params: params_value.clone(),
677            };
678
679            debug!("Sending request: {} (id={:?})", method, id);
680
681            self.register_and_send_request(request, response_tx)
682                .await
683                .map_err(UnclassifiedError::logged)?;
684
685            let outcome = match timeout(timeout_duration, response_rx).await {
686                Ok(received) => {
687                    received.map_err(|_| UnclassifiedError::logged(Error::ServerTerminated))?
688                }
689                Err(_elapsed) => {
690                    // The response may still arrive after this point (the
691                    // server is just slow, not dead), but nothing will ever
692                    // read it again -- drop the now-orphaned entry instead of
693                    // leaking it in `pending_requests` forever.
694                    self.pending_requests.lock().await.remove(&id);
695                    return Err(UnclassifiedError::logged(Error::Timeout(timeout_duration)));
696                }
697            };
698
699            match outcome {
700                Ok(result_value) => {
701                    return serde_json::from_value(result_value).map_err(|e| {
702                        UnclassifiedError::logged(
703                            self.redactions.protocol_error(format_args!(
704                                "Failed to deserialize response: {e}"
705                            )),
706                        )
707                    });
708                }
709                Err(Error::LspServerError {
710                    code,
711                    message,
712                    data,
713                }) if (LspErrorCodes::from(code) == LspErrorCodes::ServerCancelled
714                    || (LspErrorCodes::from(code) == LspErrorCodes::ContentModified
715                        && CONTENT_MODIFIED_RETRY_METHODS.contains(&method)))
716                    && Self::should_retrigger(data.as_ref()) =>
717                {
718                    if attempt == SERVER_CANCELLED_MAX_RETRIES {
719                        // Same "LSP error response: ..." prefix as the
720                        // non-retryable branch below -- a log-grep alert on
721                        // that prefix must catch every error actually
722                        // surfaced to the caller, retry exhaustion included.
723                        error!(
724                            "LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
725                            Self::truncate_error_message_for_log(&message),
726                            code,
727                            method,
728                            id
729                        );
730                        return Err(UnclassifiedError::logged(Error::LspServerError {
731                            code,
732                            message,
733                            data,
734                        }));
735                    }
736                    warn!(
737                        "LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
738                        Self::truncate_error_message_for_log(&message),
739                        code,
740                        method,
741                        id
742                    );
743                    // continue loop for next attempt
744                }
745                Err(Error::LspServerError {
746                    code,
747                    message,
748                    data,
749                }) => {
750                    return Err(UnclassifiedError::server_response(
751                        ServerErrorResponse::new(code, message, data),
752                        method,
753                        id,
754                    ));
755                }
756                Err(e) => return Err(UnclassifiedError::logged(e)),
757            }
758        }
759
760        Err(UnclassifiedError::logged(Error::ServerTerminated))
761    }
762
763    /// Like [`Self::request_typed`], but a server error response is returned
764    /// unlogged, wrapped in an [`UnclassifiedError`] so the caller decides
765    /// whether it is an error surfaced to the user or an expected outcome.
766    ///
767    /// # Errors
768    ///
769    /// See [`Self::request`].
770    pub(crate) async fn request_typed_classified<R>(
771        &self,
772        params: R::Params,
773        timeout_duration: Duration,
774    ) -> std::result::Result<R::Result, UnclassifiedError>
775    where
776        R: lsp_types::Request,
777    {
778        Box::pin(self.request_inner(R::METHOD.as_str(), params, timeout_duration)).await
779    }
780
781    /// Send a typed LSP request, deriving both the method string and the
782    /// result type from `R`'s [`lsp_types::Request`] implementation so they
783    /// cannot drift independently -- unlike [`Self::request`], which takes
784    /// the method string and result type as two unchecked, hand-picked
785    /// values.
786    ///
787    /// # Errors
788    ///
789    /// See [`Self::request`].
790    pub async fn request_typed<R>(
791        &self,
792        params: R::Params,
793        timeout_duration: Duration,
794    ) -> Result<R::Result>
795    where
796        R: lsp_types::Request,
797    {
798        self.request(R::METHOD.as_str(), params, timeout_duration)
799            .await
800    }
801
802    /// Returns true when the error data from a retryable "please retry" LSP
803    /// response (`ServerCancelled` -32802 or `ContentModified` -32801)
804    /// indicates a retry should happen.
805    ///
806    /// The LSP spec defines `data.retriggerRequest` only for diagnostic
807    /// requests' `ServerCancelled` responses (`DiagnosticServerCancellationData`)
808    /// -- it is not part of the general `ServerCancelled` or `ContentModified`
809    /// contract for other methods. mcpls checks this field whenever it is
810    /// present regardless of method (harmless for non-diagnostic methods,
811    /// since a compliant server won't send it there) and defaults to
812    /// retrying when the field is absent, since retrying is the more useful
813    /// default for a request that would otherwise surface as a hard error to
814    /// the MCP caller.
815    fn should_retrigger(data: Option<&Value>) -> bool {
816        data.is_none_or(|v| {
817            v.get("retriggerRequest")
818                .and_then(Value::as_bool)
819                .unwrap_or(true)
820        })
821    }
822
823    /// Whether the message loop this client owns has stopped (it returned
824    /// or panicked), leaving the connection permanently dead.
825    ///
826    /// Always `false` for a clone or a placeholder client: only the owning
827    /// client holds the loop handle.
828    pub(crate) fn is_message_loop_finished(&self) -> bool {
829        self.receiver_task
830            .as_ref()
831            .is_some_and(JoinHandle::is_finished)
832    }
833
834    /// Fail every request still parked in `pending_requests` with
835    /// `Error::ServerTerminated`, instead of leaving each to discover a dead
836    /// connection only when its own timeout elapses.
837    ///
838    /// Intended for a client that is about to be discarded -- e.g.
839    /// superseded by a respawned replacement for the same server -- so
840    /// callers still waiting on it unblock immediately.
841    pub(crate) async fn fail_pending_requests(&self) {
842        Self::drain_and_fail_pending(&self.pending_requests, &self.pending_failure).await;
843    }
844
845    /// Make every request still pending, or failed later by the loop's exit
846    /// drain, resolve with [`Error::ServerRestarted`] instead of
847    /// [`Error::ServerTerminated`].
848    pub(crate) fn mark_restarted(&self, server_id: ServerId) {
849        *self
850            .pending_failure
851            .lock()
852            .unwrap_or_else(PoisonError::into_inner) = PendingFailure::Restarted(server_id);
853    }
854
855    /// Drains `pending`, resolving each remaining sender to
856    /// `Err(Error::ServerTerminated)`. Shared by [`Self::fail_pending_requests`]
857    /// and [`Self::message_loop`]'s exit-path cleanup (#458).
858    async fn drain_and_fail_pending(
859        pending: &Arc<Mutex<PendingRequests>>,
860        failure: &StdMutex<PendingFailure>,
861    ) {
862        for (_, sender) in pending.lock().await.drain() {
863            let error = failure
864                .lock()
865                .unwrap_or_else(PoisonError::into_inner)
866                .error();
867            let _ = sender.send(Err(error));
868        }
869    }
870
871    /// Send notification (fire-and-forget, no response expected).
872    ///
873    /// # Errors
874    ///
875    /// Returns an error if the server has shut down.
876    pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
877    where
878        P: Serialize,
879    {
880        let params_value = Self::omit_null_params(serde_json::to_value(params)?);
881
882        debug!("Sending notification: {}", method);
883
884        self.command_tx
885            .send(ClientCommand::SendNotification {
886                method: method.to_string(),
887                params: params_value,
888            })
889            .await
890            .map_err(|_| Error::ServerTerminated)?;
891
892        Ok(())
893    }
894
895    /// Send a typed LSP notification, deriving the method string from `N`'s
896    /// [`lsp_types::Notification`] implementation so it cannot drift from the
897    /// params type -- the notification counterpart to [`Self::request_typed`].
898    ///
899    /// # Errors
900    ///
901    /// See [`Self::notify`].
902    pub async fn notify_typed<N>(&self, params: N::Params) -> Result<()>
903    where
904        N: lsp_types::Notification,
905    {
906        self.notify(N::METHOD.as_str(), params).await
907    }
908
909    /// Shutdown client gracefully, with a deadline of [`SHUTDOWN_TIMEOUT`].
910    ///
911    /// This sends a shutdown command to the background task and waits for it to complete.
912    /// Only the client that owns the message loop (the one held by
913    /// [`LspServer`](crate::lsp::LspServer)) can abort a wedged loop; calling this on a
914    /// clone still stops the shared loop for every clone but cannot abort it.
915    ///
916    /// # Errors
917    ///
918    /// Returns [`Error::ShutdownTimeout`] if the loop did not stop in time (it is then
919    /// aborted and every pending request fails with [`Error::ServerTerminated`]), or
920    /// another error if the background task failed.
921    #[allow(
922        clippy::arithmetic_side_effects,
923        reason = "SHUTDOWN_TIMEOUT is a small constant"
924    )]
925    pub async fn shutdown(mut self) -> Result<()> {
926        self.shutdown_until(Instant::now() + SHUTDOWN_TIMEOUT).await
927    }
928
929    /// As [`Self::shutdown`], with a caller-chosen absolute deadline.
930    pub(crate) async fn shutdown_until(&mut self, deadline: Instant) -> Result<()> {
931        debug!("Shutting down LSP client");
932
933        let mut task = self.receiver_task.take();
934        let command_tx = &self.command_tx;
935        let outcome = timeout_at(deadline, async {
936            let _ = command_tx.send(ClientCommand::Shutdown).await;
937            match task.as_mut() {
938                Some(task) => match task.await {
939                    Ok(result) => result,
940                    Err(source) => Err(Error::TaskFailed {
941                        task: BackgroundTask::LspReceiver,
942                        source,
943                    }),
944                },
945                None => Ok(()),
946            }
947        })
948        .await;
949
950        let result = outcome.unwrap_or(Err(Error::ShutdownTimeout));
951        if matches!(result, Err(Error::ShutdownTimeout)) {
952            if let Some(task) = task {
953                task.abort();
954                // Lets the aborted loop drop `command_rx`, so a racing
955                // request cannot enqueue after the drain below (#458).
956                let _ = timeout(SHUTDOWN_ABORT_SETTLE, task).await;
957            }
958            Self::drain_and_fail_pending(&self.pending_requests, &self.pending_failure).await;
959        }
960
961        *self.state.lock().await = super::ServerState::Shutdown;
962
963        result
964    }
965
966    /// Background task: handle message I/O.
967    ///
968    /// This task runs in the background, handling:
969    /// - Outbound requests and notifications
970    /// - Inbound responses and server notifications
971    /// - Matching responses to pending requests
972    #[allow(
973        clippy::too_many_arguments,
974        reason = "one-to-one with the spawned loop's channels and shared state"
975    )]
976    async fn message_loop(
977        transport: (LspTransport, LspTransportReader),
978        mut command_rx: mpsc::Receiver<ClientCommand>,
979        pending_requests: Arc<Mutex<PendingRequests>>,
980        pending_failure: Arc<StdMutex<PendingFailure>>,
981        notification_tx: Option<NotificationSink>,
982        lifecycle_tx: Option<mpsc::Sender<LspNotification>>,
983        redactions: Arc<Redactions>,
984        settings: Option<Arc<LspSettings>>,
985    ) -> Result<()> {
986        debug!("Message loop started");
987        let (mut transport, reader) = transport;
988        let (reader_handle, mut msg_rx) = spawn_reader_task(reader);
989        let result = {
990            // Aborts the reader task the instant `message_loop_inner` returns, even via `?` (#451).
991            let _abort_reader_on_drop = crate::util::AbortOnDrop(&reader_handle);
992            Self::message_loop_inner(
993                &mut transport,
994                &mut msg_rx,
995                &mut command_rx,
996                &pending_requests,
997                notification_tx.as_ref(),
998                lifecycle_tx.as_ref(),
999                &redactions,
1000                settings.as_deref(),
1001            )
1002            .await
1003        };
1004        let _ = timeout(READER_TASK_ABORT_GRACE, reader_handle).await;
1005        // Dropped before the drain, not just at fn-exit: otherwise a
1006        // `register_and_send_request` racing the drain could still insert
1007        // into `pending_requests` and succeed its `command_tx.send(..)`,
1008        // leaving that entry unfailed until its own timeout (#458 S1). Once
1009        // this is gone, every later `send` fails and the caller cleans up
1010        // its own entry.
1011        drop(command_rx);
1012        // Runs after `message_loop_inner` returns, so a response the Shutdown
1013        // drain (#451) already resolved is gone from the map by now -- only
1014        // requests with no answer get failed here (#458).
1015        Self::drain_and_fail_pending(&pending_requests, &pending_failure).await;
1016        if let Err(ref e) = result {
1017            error!("Message loop exiting with error: {}", e);
1018        } else {
1019            debug!("Message loop exiting normally");
1020        }
1021        result
1022    }
1023
1024    /// Maps a `null` params value to an omitted `params` field. LSP methods
1025    /// with `params: void` (`shutdown`, `exit`) must go out without the key:
1026    /// tsgo rejects `"params": null` with `-32602 expected empty, got: null`.
1027    fn omit_null_params(params: Value) -> Option<Value> {
1028        if params.is_null() { None } else { Some(params) }
1029    }
1030
1031    /// Truncate an LSP server's error message for the `tracing::error!` log
1032    /// line, bounding it to at most [`crate::util::MAX_LOG_STRING_BYTES`]
1033    /// bytes (the full formatted string is slightly longer).
1034    ///
1035    /// Log-line use only -- the message forwarded to the MCP caller in
1036    /// [`Error::LspServerError`] is truncated separately, to the larger
1037    /// [`MAX_ERROR_MESSAGE_CALLER_BYTES`] (#313).
1038    fn truncate_error_message_for_log(message: &str) -> String {
1039        crate::util::truncate_str(message, crate::util::MAX_LOG_STRING_BYTES)
1040    }
1041
1042    /// Which of the two notification lanes (P3) `notification` belongs on,
1043    /// or `None` if it must be dropped before reaching either.
1044    ///
1045    /// `notification_tx` (returned as `"notification"`) carries diagnostics/
1046    /// log/showMessage; `lifecycle_tx` (returned as `"lifecycle"`) carries
1047    /// `$/progress` `begin`/`end` frames and `Other` (which carries e.g.
1048    /// rust-analyzer's `experimental/serverStatus`) -- splitting them means
1049    /// a high-volume diagnostics publisher can never starve out a
1050    /// low-volume readiness signal, or vice versa. The lane name is
1051    /// returned alongside the sender purely for the drop-warning log at the
1052    /// call site (Fix 6) -- it plays no role in routing.
1053    ///
1054    /// A `$/progress` `report` frame -- the high-volume case a
1055    /// `report`-per-package emitter like gopls can produce -- is classified
1056    /// via [`crate::lsp::types::ProgressKind::from_value`] (shared with
1057    /// `bridge::indexing::IndexingTracker::observe_progress` so the two
1058    /// can't drift out of sync on which `kind`s are recognized -- Fix 9)
1059    /// and dropped before it ever reaches either channel (S3). An
1060    /// *unparseable* `$/progress` notification
1061    /// (e.g. missing the mandatory `token` field) falls back to
1062    /// `LspNotification::Other { method: "$/progress", .. }` at parse time
1063    /// (`LspNotification::parse`) rather than `Progress`, so it must be
1064    /// dropped here too by matching on `method` -- otherwise a server whose
1065    /// `report` payloads fail to deserialize could bypass the `kind`-based
1066    /// filter above entirely by sending malformed frames (security LOW /
1067    /// M1).
1068    /// Whether `notification` is handed to the log/lifecycle lanes, as opposed
1069    /// to the diagnostics mailbox or the floor.
1070    fn reaches_a_lane(
1071        notification: &LspNotification,
1072        has_sink: bool,
1073        lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
1074    ) -> bool {
1075        match notification {
1076            LspNotification::PublishDiagnostics(_) => false,
1077            LspNotification::LogMessage(_) | LspNotification::ShowMessage(_) => has_sink,
1078            other => Self::lifecycle_lane(other, lifecycle_tx).is_some(),
1079        }
1080    }
1081
1082    fn lifecycle_lane<'a>(
1083        notification: &LspNotification,
1084        lifecycle_tx: Option<&'a mpsc::Sender<LspNotification>>,
1085    ) -> Option<&'a mpsc::Sender<LspNotification>> {
1086        match notification {
1087            LspNotification::PublishDiagnostics(_)
1088            | LspNotification::LogMessage(_)
1089            | LspNotification::ShowMessage(_) => None,
1090            LspNotification::Progress(params) => {
1091                crate::lsp::types::ProgressKind::from_value(&params.value).and(lifecycle_tx)
1092            }
1093            LspNotification::Other { method, .. } if method.as_ref() == "$/progress" => None,
1094            LspNotification::Other { .. } => lifecycle_tx,
1095        }
1096    }
1097
1098    #[allow(
1099        clippy::too_many_lines,
1100        clippy::too_many_arguments,
1101        reason = "one select loop over the transport, channels and shared state"
1102    )]
1103    async fn message_loop_inner(
1104        transport: &mut LspTransport,
1105        msg_rx: &mut mpsc::Receiver<Result<InboundMessage>>,
1106        command_rx: &mut mpsc::Receiver<ClientCommand>,
1107        pending_requests: &Arc<Mutex<PendingRequests>>,
1108        notification_tx: Option<&NotificationSink>,
1109        lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
1110        redactions: &Redactions,
1111        settings: Option<&LspSettings>,
1112    ) -> Result<()> {
1113        loop {
1114            tokio::select! {
1115                Some(command) = command_rx.recv() => {
1116                    match command {
1117                        ClientCommand::SendRequest { request } => {
1118                            let value = serde_json::to_value(&request)?;
1119                            transport.send(&value).await?;
1120                        }
1121                        ClientCommand::SendNotification { method, params } => {
1122                            let notification = serde_json::to_value(JsonRpcNotification {
1123                                jsonrpc: JsonRpcVersion,
1124                                method,
1125                                params,
1126                            })?;
1127                            transport.send(&notification).await?;
1128                        }
1129                        ClientCommand::Shutdown => {
1130                            debug!("Client shutdown requested");
1131                            // The reader task can run ahead of us (#451): drain whatever it
1132                            // already queued instead of dropping it, or a response to a
1133                            // concurrent caller's in-flight request would be silently lost.
1134                            while let Ok(message) = msg_rx.try_recv() {
1135                                match message {
1136                                    Ok(m) => {
1137                                        Self::handle_inbound_message(
1138                                            transport,
1139                                            m,
1140                                            pending_requests,
1141                                            notification_tx,
1142                                            lifecycle_tx,
1143                                            redactions,
1144                                            settings,
1145                                        )
1146                                        .await?;
1147                                    }
1148                                    Err(e) => {
1149                                        error!("Transport receive error while draining on shutdown: {}", e);
1150                                        break;
1151                                    }
1152                                }
1153                            }
1154                            break;
1155                        }
1156                    }
1157                }
1158
1159                // Cancel-safe, unlike the `transport.receive()` this replaces (#451): reading itself happens off this `select!`.
1160                message = msg_rx.recv() => {
1161                    let message = match message {
1162                        Some(Ok(m)) => m,
1163                        Some(Err(e)) => {
1164                            error!("Transport receive error: {}", e);
1165                            return Err(e);
1166                        }
1167                        None => {
1168                            // Reader task only exits after sending an `Err`, so this means it panicked.
1169                            error!("Transport receive error: reader task ended unexpectedly");
1170                            return Err(Error::ServerTerminated);
1171                        }
1172                    };
1173                    Self::handle_inbound_message(
1174                        transport,
1175                        message,
1176                        pending_requests,
1177                        notification_tx,
1178                        lifecycle_tx,
1179                        redactions,
1180                        settings,
1181                    )
1182                    .await?;
1183                }
1184            }
1185        }
1186
1187        Ok(())
1188    }
1189
1190    /// Hands `response` to the request waiting for it, if any.
1191    async fn complete_pending(
1192        pending_requests: &Arc<Mutex<PendingRequests>>,
1193        response: JsonRpcResponse,
1194        redactions: &Redactions,
1195    ) {
1196        trace!("Received response: id={:?}", response.id);
1197
1198        let sender = pending_requests.lock().await.remove(&response.id);
1199
1200        if let Some(sender) = sender {
1201            match response.outcome {
1202                JsonRpcOutcome::Error(error) => {
1203                    // Deliberately not logged at `error!` here: this fires
1204                    // for every attempt, before `LspClient::request`'s retry
1205                    // loop knows whether the error is transient and about to
1206                    // be retried (-32802, or -32801 for an allowlisted
1207                    // method). Logging unconditionally at this point would
1208                    // emit a spurious ERROR line for errors that are retried
1209                    // and succeed. `request` logs at `warn!` on retry and
1210                    // `error!` once the error is actually surfaced to the
1211                    // caller (retry exhaustion or a non-retryable error);
1212                    // the response id is already traced above.
1213                    let message = redactions.apply(&error.message);
1214                    trace!(
1215                        "LSP error response: {} (code {})",
1216                        Self::truncate_error_message_for_log(&message),
1217                        error.code
1218                    );
1219                    // Truncated separately from the log line, to the larger
1220                    // MAX_ERROR_MESSAGE_CALLER_BYTES -- the raw message is
1221                    // unbounded and attacker-influenceable (#313), but a
1222                    // log-line-sized cut would also clip legitimate long
1223                    // errors before the model ever sees them (S2).
1224                    let caller_message =
1225                        crate::util::truncate_str(&message, MAX_ERROR_MESSAGE_CALLER_BYTES);
1226                    let _ = sender.send(Err(Error::LspServerError {
1227                        code: error.code,
1228                        message: caller_message,
1229                        data: error.data,
1230                    }));
1231                }
1232                JsonRpcOutcome::Result(result) => {
1233                    let _ = sender.send(Ok(result));
1234                }
1235            }
1236        } else {
1237            warn!(
1238                "Received response for unknown request ID: {:?}",
1239                response.id
1240            );
1241        }
1242    }
1243
1244    /// Processes one fully-decoded inbound LSP message: resolves a matching
1245    /// pending request, answers a server-initiated request, or forwards a
1246    /// notification to its lane. Shared by `message_loop_inner`'s normal
1247    /// `msg_rx.recv()` branch and its `Shutdown` drain, so a message handled
1248    /// during either path behaves identically.
1249    async fn handle_inbound_message(
1250        transport: &mut LspTransport,
1251        message: InboundMessage,
1252        pending_requests: &Arc<Mutex<PendingRequests>>,
1253        notification_tx: Option<&NotificationSink>,
1254        lifecycle_tx: Option<&mpsc::Sender<LspNotification>>,
1255        redactions: &Redactions,
1256        settings: Option<&LspSettings>,
1257    ) -> Result<()> {
1258        match message {
1259            InboundMessage::Response(response) => {
1260                Self::complete_pending(pending_requests, response, redactions).await;
1261            }
1262            InboundMessage::UndecodableResponse { id } => {
1263                warn!("Failed the request answered by an undecodable response: id={id:?}");
1264                let sender = pending_requests.lock().await.remove(&id);
1265                if let Some(sender) = sender {
1266                    let _ = sender.send(Err(Error::LspProtocolError(RedactedText::fixed(
1267                        "undecodable response",
1268                    ))));
1269                }
1270            }
1271            InboundMessage::UndecodableRequest { id } => {
1272                debug!("Answering an undecodable server request with an error: id={id:?}");
1273                let reply = JsonRpcReply::new(
1274                    id,
1275                    JsonRpcOutcome::Error(JsonRpcError::new(
1276                        ErrorCodes::InvalidRequest,
1277                        "Invalid Request: the request could not be decoded",
1278                    )),
1279                );
1280                transport.send(&serde_json::to_value(&reply)?).await?;
1281            }
1282            InboundMessage::UndecodableNotification | InboundMessage::UndecodableFrame => {}
1283            InboundMessage::Request(request) => {
1284                debug!(
1285                    "Received server request: {} (id={:?})",
1286                    request.method, request.id
1287                );
1288                let reply = Self::server_request_reply(request, settings);
1289                let value = serde_json::to_value(&reply)?;
1290                transport.send(&value).await?;
1291            }
1292            InboundMessage::Notification(notification) => {
1293                debug!("Received notification: {}", notification.method);
1294
1295                // Parse notification into typed variant
1296                let mut typed = LspNotification::parse(&notification.method, notification.params);
1297                Self::redact_notification(&mut typed, redactions);
1298
1299                if tracing::enabled!(tracing::Level::TRACE)
1300                    && Self::reaches_a_lane(&typed, notification_tx.is_some(), lifecycle_tx)
1301                {
1302                    trace!(
1303                        "Forwarding notification: {}",
1304                        redactions.apply(&format!("{typed:?}"))
1305                    );
1306                }
1307
1308                // Diagnostics have their own delivery path: the mailbox coalesces
1309                // per file and records what it cannot hold, so a burst is never
1310                // dropped unrecorded.
1311                let delivered = match typed {
1312                    LspNotification::PublishDiagnostics(params) => {
1313                        if let Some(sink) = notification_tx {
1314                            debug!(
1315                                "Forwarding diagnostics for {}: {} items",
1316                                params.uri.as_ref(),
1317                                params.diagnostics.len()
1318                            );
1319                            sink.publishes().publish(params);
1320                        }
1321                        return Ok(());
1322                    }
1323                    LspNotification::LogMessage(params) => notification_tx.map(|sink| {
1324                        (
1325                            Lane::Notification,
1326                            sink.messages().try_send(ServerMessage::Log(params)).is_ok(),
1327                        )
1328                    }),
1329                    LspNotification::ShowMessage(params) => notification_tx.map(|sink| {
1330                        (
1331                            Lane::Notification,
1332                            sink.messages()
1333                                .try_send(ServerMessage::Show(params))
1334                                .is_ok(),
1335                        )
1336                    }),
1337                    other => Self::lifecycle_lane(&other, lifecycle_tx)
1338                        .map(|tx| (Lane::Lifecycle, tx.try_send(other).is_ok())),
1339                };
1340
1341                if let Some((lane, false)) = delivered {
1342                    DropLog::of(lane).record(&notification.method);
1343                }
1344            }
1345        }
1346
1347        Ok(())
1348    }
1349
1350    /// Redacts the human-readable text a server controls before it reaches
1351    /// the notification cache, from where MCP tools and the diagnostics
1352    /// resource return it.
1353    pub(crate) fn redact_notification(notification: &mut LspNotification, redactions: &Redactions) {
1354        if redactions.is_empty() {
1355            return;
1356        }
1357        match notification {
1358            LspNotification::LogMessage(params) => {
1359                redactions.redact_in_place(&mut params.message);
1360            }
1361            LspNotification::ShowMessage(params) => {
1362                redactions.redact_in_place(&mut params.message);
1363            }
1364            LspNotification::PublishDiagnostics(params) => {
1365                for diagnostic in &mut params.diagnostics {
1366                    redactions.redact_diagnostic(diagnostic);
1367                }
1368            }
1369            LspNotification::Progress(params) => redactions.redact_progress(&mut params.value),
1370            LspNotification::Other { .. } => {}
1371        }
1372    }
1373
1374    fn server_request_reply(
1375        request: JsonRpcRequest,
1376        settings: Option<&LspSettings>,
1377    ) -> JsonRpcReply {
1378        let outcome =
1379            Self::server_request_result(&request.method, request.params.as_ref(), settings);
1380        JsonRpcReply::new(request.id, outcome.into())
1381    }
1382
1383    fn server_request_result(
1384        method: &str,
1385        params: Option<&Value>,
1386        settings: Option<&LspSettings>,
1387    ) -> std::result::Result<Value, JsonRpcError> {
1388        match method {
1389            "client/registerCapability"
1390            | "client/unregisterCapability"
1391            | "workspace/workspaceFolders"
1392            | "workspace/diagnostic/refresh"
1393            | "workspace/semanticTokens/refresh"
1394            | "workspace/inlayHint/refresh"
1395            | "workspace/codeLens/refresh"
1396            | "window/showMessageRequest"
1397            | "window/workDoneProgress/create" => Ok(Value::Null),
1398            "workspace/configuration" => Self::workspace_configuration_result(params, settings),
1399            "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
1400            _ => Err(JsonRpcError::new(
1401                ErrorCodes::MethodNotFound,
1402                format!("Unhandled server request: {method}"),
1403            )),
1404        }
1405    }
1406
1407    /// Answers `workspace/configuration`: one entry per requested item.
1408    ///
1409    /// Without settings every item gets `Null`, whatever its shape and even
1410    /// when `items` is absent (the reply before settings existed). With
1411    /// settings an item gets its section (the whole object when `section` is
1412    /// absent, `Null` when it is unknown, not a string, or the item is not an
1413    /// object); only a missing
1414    /// `items` list is answered with `-32602`. `scopeUri` is never read.
1415    fn workspace_configuration_result(
1416        params: Option<&Value>,
1417        settings: Option<&LspSettings>,
1418    ) -> std::result::Result<Value, JsonRpcError> {
1419        let items = params
1420            .and_then(|params| params.get("items"))
1421            .and_then(Value::as_array);
1422        let Some(settings) = settings else {
1423            return Ok(Value::Array(vec![Value::Null; items.map_or(0, Vec::len)]));
1424        };
1425        let items = items.ok_or_else(|| {
1426            JsonRpcError::new(
1427                ErrorCodes::InvalidParams,
1428                "Invalid params for workspace/configuration",
1429            )
1430        })?;
1431        let sections = items
1432            .iter()
1433            .map(|item| match ConfigurationItem::parse(item) {
1434                ConfigurationItem::Whole => settings.section(None),
1435                ConfigurationItem::Section(section) => settings.section(Some(section)),
1436                ConfigurationItem::Uninterpretable => Value::Null,
1437            })
1438            .collect();
1439        Ok(Value::Array(sections))
1440    }
1441}
1442
1443/// Logs a server error response that reached the caller, under the prefix a
1444/// log-grep alert watches for.
1445fn log_surfaced_error(method: &str, id: &RequestId, code: i32, message: &str) {
1446    error!(
1447        "LSP error response: {} (code {}) on '{}' (id={:?})",
1448        LspClient::truncate_error_message_for_log(message),
1449        code,
1450        method,
1451        id
1452    );
1453}
1454
1455/// A failed request whose severity is still undecided.
1456///
1457/// A server error response is logged only once the caller says whether it is
1458/// surfaced as an error ([`Self::surface`], ERROR) or turned into an expected
1459/// outcome ([`Self::handled`], DEBUG). There is deliberately no conversion
1460/// into [`Error`], so `?` does not compile and the choice cannot be skipped.
1461#[derive(Debug)]
1462#[must_use = "call `surface()` or `handled()` to log and unwrap the error"]
1463pub struct UnclassifiedError(Box<Unclassified>);
1464
1465/// An error response from the server, kept as the [`Error::LspServerError`] it
1466/// becomes so a caller can classify it, and constructible from nothing else.
1467#[derive(Debug)]
1468struct ServerErrorResponse {
1469    code: i32,
1470    message: String,
1471    data: Option<Value>,
1472    /// The [`Error`] built from the fields above, once a caller asked to see it.
1473    error: std::sync::OnceLock<Error>,
1474}
1475
1476impl ServerErrorResponse {
1477    const fn new(code: i32, message: String, data: Option<Value>) -> Self {
1478        Self {
1479            code,
1480            message,
1481            data,
1482            error: std::sync::OnceLock::new(),
1483        }
1484    }
1485
1486    /// The code and message of the response.
1487    const fn code_and_message(&self) -> (i32, &str) {
1488        (self.code, self.message.as_str())
1489    }
1490
1491    /// The response as the error it becomes, for classification.
1492    fn as_error(&self) -> &Error {
1493        self.error.get_or_init(|| Error::LspServerError {
1494            code: self.code,
1495            message: self.message.clone(),
1496            data: self.data.clone(),
1497        })
1498    }
1499
1500    fn into_error(self) -> Error {
1501        self.error.into_inner().unwrap_or(Error::LspServerError {
1502            code: self.code,
1503            message: self.message,
1504            data: self.data,
1505        })
1506    }
1507}
1508
1509/// The two states of an [`UnclassifiedError`]: a failure already logged where
1510/// it happened, and a server error response nobody has logged yet.
1511#[derive(Debug)]
1512enum Unclassified {
1513    Logged(Error),
1514    ServerResponse {
1515        response: ServerErrorResponse,
1516        method: String,
1517        id: RequestId,
1518    },
1519}
1520
1521impl Unclassified {
1522    fn error(&self) -> &Error {
1523        match self {
1524            Self::Logged(error) => error,
1525            Self::ServerResponse { response, .. } => response.as_error(),
1526        }
1527    }
1528
1529    /// Hands over the error, first passing an unlogged server error response
1530    /// to `report` as (method, id, code, message).
1531    fn finish(self, report: impl FnOnce(&str, &RequestId, i32, &str)) -> Error {
1532        match self {
1533            Self::Logged(error) => error,
1534            Self::ServerResponse {
1535                response,
1536                method,
1537                id,
1538            } => {
1539                let (code, message) = response.code_and_message();
1540                report(&method, &id, code, message);
1541                response.into_error()
1542            }
1543        }
1544    }
1545}
1546
1547fn log_handled_error(method: &str, id: &RequestId, code: i32, message: &str) {
1548    debug!(
1549        "LSP error response handled by the caller: {} (code {}) on '{}' (id={:?})",
1550        LspClient::truncate_error_message_for_log(message),
1551        code,
1552        method,
1553        id
1554    );
1555}
1556
1557impl UnclassifiedError {
1558    fn logged(error: Error) -> Self {
1559        Self(Box::new(Unclassified::Logged(error)))
1560    }
1561
1562    fn server_response(response: ServerErrorResponse, method: &str, id: RequestId) -> Self {
1563        Self(Box::new(Unclassified::ServerResponse {
1564            response,
1565            method: method.to_owned(),
1566            id,
1567        }))
1568    }
1569
1570    /// Hands over the state, leaving a logged placeholder behind so that the
1571    /// `Drop` below has nothing left to report.
1572    fn take(mut self) -> Unclassified {
1573        std::mem::replace(&mut *self.0, Unclassified::Logged(Error::ServerTerminated))
1574    }
1575
1576    /// The wrapped error, for classification before logging.
1577    pub(crate) fn error(&self) -> &Error {
1578        self.0.error()
1579    }
1580
1581    /// The error returned to the caller as a failure; a server error response
1582    /// is logged at ERROR.
1583    pub(crate) fn surface(self) -> Error {
1584        self.take().finish(log_surfaced_error)
1585    }
1586
1587    /// The error of an expected outcome; a server error response is logged at
1588    /// DEBUG.
1589    pub(crate) fn handled(self) -> Error {
1590        self.take().finish(log_handled_error)
1591    }
1592}
1593
1594/// A dropped, unclassified server error response is logged at ERROR, so
1595/// `.ok()`, `let _ =` or an ignoring match arm cannot hide it.
1596impl Drop for UnclassifiedError {
1597    fn drop(&mut self) {
1598        let inner = std::mem::replace(&mut *self.0, Unclassified::Logged(Error::ServerTerminated));
1599        drop(inner.finish(log_surfaced_error));
1600    }
1601}
1602
1603/// How one `workspace/configuration` item selects part of the settings.
1604enum ConfigurationItem<'a> {
1605    Whole,
1606    Section(&'a str),
1607    Uninterpretable,
1608}
1609
1610impl<'a> ConfigurationItem<'a> {
1611    fn parse(item: &'a Value) -> Self {
1612        let Value::Object(object) = item else {
1613            return Self::Uninterpretable;
1614        };
1615        match object.get("section") {
1616            None | Some(Value::Null) => Self::Whole,
1617            Some(Value::String(section)) => Self::Section(section),
1618            Some(_) => Self::Uninterpretable,
1619        }
1620    }
1621}
1622
1623#[cfg(test)]
1624mod tests {
1625    use std::assert_matches;
1626
1627    use super::*;
1628    use crate::config::TimeoutSecs;
1629    use crate::lsp::types::JsonRpcResponse;
1630
1631    #[test]
1632    fn test_unclassified_server_response_always_reports_code_and_message() {
1633        let unclassified = UnclassifiedError::server_response(
1634            ServerErrorResponse::new(-32001, "boom".to_owned(), None),
1635            "textDocument/hover",
1636            RequestId::Number(3),
1637        );
1638        let mut reported = None;
1639
1640        let error = unclassified.take().finish(|method, id, code, message| {
1641            reported = Some((method.to_owned(), id.clone(), code, message.to_owned()));
1642        });
1643
1644        assert_eq!(
1645            reported,
1646            Some((
1647                "textDocument/hover".to_owned(),
1648                RequestId::Number(3),
1649                -32001,
1650                "boom".to_owned()
1651            ))
1652        );
1653        assert_matches!(error, Error::LspServerError { code: -32001, .. });
1654    }
1655
1656    /// The `trace!` of a forwarded notification must not print a configured
1657    /// secret that the server echoed into an unrecognized notification.
1658    #[tokio::test]
1659    async fn test_forwarded_notification_trace_is_redacted() {
1660        use tracing_subscriber::prelude::*;
1661
1662        use crate::test_lsp::CapturedLogs;
1663
1664        let redactions =
1665            Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
1666        let logs = CapturedLogs::default();
1667        let _guard = tracing::subscriber::set_default(
1668            tracing_subscriber::registry()
1669                .with(tracing_subscriber::filter::LevelFilter::TRACE)
1670                .with(logs.clone()),
1671        );
1672        let (mut transport, _reader) = LspTransport::new(tokio::io::sink(), tokio::io::empty());
1673        let (lifecycle_tx, mut lifecycle_rx) = mpsc::channel(4);
1674        let pending = Arc::new(Mutex::new(HashMap::new()));
1675        let notification = InboundMessage::Notification(JsonRpcNotification {
1676            jsonrpc: JsonRpcVersion,
1677            method: "custom/echo".to_string(),
1678            params: Some(serde_json::json!({"env": "SuperSecretValue123"})),
1679        });
1680
1681        LspClient::handle_inbound_message(
1682            &mut transport,
1683            notification,
1684            &pending,
1685            None,
1686            Some(&lifecycle_tx),
1687            &redactions,
1688            None,
1689        )
1690        .await
1691        .unwrap();
1692
1693        assert!(lifecycle_rx.recv().await.is_some());
1694        let output = logs.messages().join("\n");
1695        assert!(output.contains("Forwarding notification"), "{output}");
1696        assert!(!output.contains("SuperSecretValue123"), "{output}");
1697    }
1698
1699    fn secret_diagnostics_params() -> Value {
1700        let range = serde_json::json!({
1701            "start": {"line": 0, "character": 0},
1702            "end": {"line": 0, "character": 1}
1703        });
1704        serde_json::json!({
1705            "uri": "file:///w/main.rs",
1706            "diagnostics": [{
1707                "range": range,
1708                "message": "leak SuperSecretValue123",
1709                "data": {"hint": ["SuperSecretValue123"]}
1710            }]
1711        })
1712    }
1713
1714    /// #583: the reader task redacts a pushed diagnostic before it reaches the
1715    /// notification lane, from where the cache and the MCP tools read it.
1716    #[tokio::test]
1717    async fn test_reader_task_forwards_pushed_diagnostics_redacted() {
1718        use crate::test_lsp::{fake_lsp_client_with_redactions, write_notification};
1719
1720        let redactions =
1721            Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
1722        let (_client, mut server, mut lanes) = fake_lsp_client_with_redactions(redactions);
1723
1724        write_notification(
1725            &mut server.read_half_stdin,
1726            "textDocument/publishDiagnostics",
1727            secret_diagnostics_params(),
1728        )
1729        .await;
1730
1731        let forwarded = timeout(Duration::from_secs(2), lanes.publishes.recv())
1732            .await
1733            .unwrap()
1734            .unwrap();
1735        let shown = format!("{forwarded:?}");
1736        assert!(shown.contains("[redacted:API_TOKEN]"), "{shown}");
1737        assert!(!shown.contains("SuperSecretValue123"), "{shown}");
1738    }
1739
1740    /// With nothing to hide the notification is forwarded as received.
1741    #[test]
1742    fn test_redact_notification_leaves_text_alone_without_secrets() {
1743        let mut notification = LspNotification::parse(
1744            "textDocument/publishDiagnostics",
1745            Some(secret_diagnostics_params()),
1746        );
1747        let before = format!("{notification:?}");
1748
1749        LspClient::redact_notification(&mut notification, &Redactions::default());
1750        assert_eq!(format!("{notification:?}"), before);
1751
1752        let unrelated = Redactions::new([("API_TOKEN".to_owned(), "NotInTheText99".to_owned())]);
1753        LspClient::redact_notification(&mut notification, &unrelated);
1754        assert_eq!(format!("{notification:?}"), before);
1755
1756        let matching =
1757            Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
1758        LspClient::redact_notification(&mut notification, &matching);
1759        assert!(!format!("{notification:?}").contains("SuperSecretValue123"));
1760    }
1761
1762    #[test]
1763    fn test_request_id_generation() {
1764        let counter = AtomicI64::new(1);
1765
1766        let id1 = counter.fetch_add(1, Ordering::SeqCst);
1767        let id2 = counter.fetch_add(1, Ordering::SeqCst);
1768        let id3 = counter.fetch_add(1, Ordering::SeqCst);
1769
1770        assert_eq!(id1, 1);
1771        assert_eq!(id2, 2);
1772        assert_eq!(id3, 3);
1773    }
1774
1775    #[test]
1776    fn test_client_creation() {
1777        let config = LspServerConfig::rust_analyzer();
1778
1779        let client = LspClient::new(config);
1780        assert_eq!(client.language_id(), "rust");
1781    }
1782
1783    #[test]
1784    fn test_client_clone() {
1785        let config = LspServerConfig::rust_analyzer();
1786        let client = LspClient::new(config);
1787
1788        #[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
1789        let cloned = client.clone();
1790        assert_eq!(cloned.language_id(), "rust");
1791
1792        assert!(
1793            cloned.receiver_task.is_none(),
1794            "Cloned client should not own receiver task"
1795        );
1796    }
1797
1798    #[test]
1799    fn test_request_timeout_and_completion_timeout_at_default() {
1800        let config = LspServerConfig::rust_analyzer();
1801        let client = LspClient::new(config);
1802
1803        assert_eq!(client.request_timeout(), Duration::from_secs(30));
1804        assert_eq!(client.completion_timeout(), Duration::from_secs(10));
1805    }
1806
1807    #[test]
1808    fn test_completion_timeout_clamps_to_ten_seconds() {
1809        for secs in [1, 2, 3, 30, 300] {
1810            let mut config = LspServerConfig::rust_analyzer();
1811            config.request_timeout_seconds = TimeoutSecs::new(secs).unwrap();
1812            let client = LspClient::new(config);
1813
1814            assert_eq!(
1815                client.completion_timeout(),
1816                Duration::from_secs(secs.min(10)),
1817                "request_timeout_seconds={secs}"
1818            );
1819            assert!(client.completion_timeout() <= client.request_timeout());
1820        }
1821    }
1822
1823    #[test]
1824    fn test_code_action_resolve_timeout_clamps_to_ten_seconds() {
1825        for secs in [1, 2, 3, 30, 300] {
1826            let mut config = LspServerConfig::rust_analyzer();
1827            config.request_timeout_seconds = TimeoutSecs::new(secs).unwrap();
1828            let client = LspClient::new(config);
1829
1830            assert_eq!(
1831                client.code_action_resolve_timeout(),
1832                Duration::from_secs(secs.min(10)),
1833                "request_timeout_seconds={secs}"
1834            );
1835            assert!(client.code_action_resolve_timeout() <= client.request_timeout());
1836        }
1837    }
1838
1839    #[test]
1840    fn test_request_timeout_independent_per_server() {
1841        let mut config_a = LspServerConfig::rust_analyzer();
1842        config_a.request_timeout_seconds = TimeoutSecs::new(5).unwrap();
1843        let mut config_b = LspServerConfig::pyright();
1844        config_b.request_timeout_seconds = TimeoutSecs::new(15).unwrap();
1845
1846        let client_a = LspClient::new(config_a);
1847        let client_b = LspClient::new(config_b);
1848
1849        assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
1850        assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
1851    }
1852
1853    #[test]
1854    fn test_register_capability_request_is_acknowledged() {
1855        let request = JsonRpcRequest {
1856            jsonrpc: JsonRpcVersion,
1857            id: RequestId::String("ts1".to_string()),
1858            method: "client/registerCapability".to_string(),
1859            params: Some(serde_json::json!({ "registrations": [] })),
1860        };
1861
1862        let reply = reply_json(request);
1863
1864        assert_eq!(reply["id"], serde_json::json!("ts1"));
1865        assert_eq!(reply["jsonrpc"], serde_json::json!("2.0"));
1866        assert_eq!(reply["result"], Value::Null);
1867        assert!(reply.get("error").is_none());
1868    }
1869
1870    #[test]
1871    fn test_workspace_configuration_request_returns_null_per_item_without_settings() {
1872        let result = LspClient::workspace_configuration_result(
1873            Some(&serde_json::json!({
1874                "items": [{ "section": "typescript" }, { "section": "editor" }]
1875            })),
1876            None,
1877        );
1878
1879        assert_eq!(result.unwrap(), serde_json::json!([null, null]));
1880    }
1881
1882    fn sample_settings() -> LspSettings {
1883        serde_json::from_value(serde_json::json!({
1884            "python.analysis.typeCheckingMode": "strict",
1885            "gopls": { "ui.semanticTokens": true }
1886        }))
1887        .unwrap()
1888    }
1889
1890    #[test]
1891    fn test_workspace_configuration_serves_settings_sections() {
1892        let settings = sample_settings();
1893        let result = LspClient::workspace_configuration_result(
1894            Some(&serde_json::json!({
1895                "items": [
1896                    { "section": "python.analysis" },
1897                    { "section": "gopls", "scopeUri": "file:///ignored" },
1898                    { "section": "missing" },
1899                    {}
1900                ]
1901            })),
1902            Some(&settings),
1903        )
1904        .unwrap();
1905
1906        assert_eq!(
1907            result,
1908            serde_json::json!([
1909                { "typeCheckingMode": "strict" },
1910                { "ui.semanticTokens": true },
1911                null,
1912                settings.to_value()
1913            ])
1914        );
1915    }
1916
1917    #[test]
1918    fn test_workspace_configuration_non_object_item_gets_null() {
1919        let settings = sample_settings();
1920        let result = LspClient::workspace_configuration_result(
1921            Some(&serde_json::json!({
1922                "items": [
1923                    { "section": "python.analysis" },
1924                    { "section": "missing.x" },
1925                    {},
1926                    { "section": 5 },
1927                    "x",
1928                    null,
1929                    7
1930                ]
1931            })),
1932            Some(&settings),
1933        )
1934        .unwrap();
1935
1936        assert_eq!(
1937            result,
1938            serde_json::json!([
1939                { "typeCheckingMode": "strict" },
1940                null,
1941                settings.to_value(),
1942                null,
1943                null,
1944                null,
1945                null
1946            ])
1947        );
1948    }
1949
1950    #[tokio::test]
1951    async fn test_message_loop_answers_workspace_configuration_from_settings() {
1952        use tokio::io::BufReader;
1953
1954        use crate::test_lsp::{fake_lsp_client_with_config, read_framed_message, write_request};
1955
1956        let mut config = LspServerConfig::rust_analyzer();
1957        config.settings = Some(sample_settings());
1958        let (_client, mut server) = fake_lsp_client_with_config(config);
1959
1960        write_request(
1961            &mut server.read_half_stdin,
1962            &serde_json::json!(7),
1963            "workspace/configuration",
1964            serde_json::json!({ "items": [{ "section": "python.analysis", "scopeUri": 1 }] }),
1965        )
1966        .await;
1967        let mut reader = BufReader::new(&mut server.write_stdout);
1968        let response = read_framed_message(&mut reader).await;
1969
1970        assert_eq!(response["id"], 7);
1971        assert_eq!(
1972            response["result"],
1973            serde_json::json!([{ "typeCheckingMode": "strict" }])
1974        );
1975    }
1976
1977    #[test]
1978    fn test_workspace_configuration_is_lenient_about_item_shape() {
1979        let settings = sample_settings();
1980        let params = serde_json::json!({
1981            "items": [
1982                { "section": 5 },
1983                { "section": null },
1984                3,
1985                { "section": "gopls", "scopeUri": 42 }
1986            ]
1987        });
1988
1989        let with = LspClient::workspace_configuration_result(Some(&params), Some(&settings));
1990        let without = LspClient::workspace_configuration_result(Some(&params), None);
1991
1992        assert_eq!(
1993            with.unwrap(),
1994            serde_json::json!([
1995                null,
1996                settings.to_value(),
1997                null,
1998                { "ui.semanticTokens": true }
1999            ])
2000        );
2001        assert_eq!(
2002            without.unwrap(),
2003            serde_json::json!([null, null, null, null])
2004        );
2005    }
2006
2007    #[test]
2008    fn test_workspace_configuration_without_settings_matches_the_old_reply_for_bad_params() {
2009        for params in [
2010            None,
2011            Some(serde_json::json!({})),
2012            Some(serde_json::json!({ "items": "x" })),
2013        ] {
2014            let result = LspClient::workspace_configuration_result(params.as_ref(), None);
2015            assert_eq!(result.unwrap(), serde_json::json!([]));
2016        }
2017    }
2018
2019    #[test]
2020    fn test_workspace_configuration_with_settings_rejects_missing_items() {
2021        for params in [None, Some(serde_json::json!({ "items": "nope" }))] {
2022            let error = LspClient::workspace_configuration_result(
2023                params.as_ref(),
2024                Some(&sample_settings()),
2025            )
2026            .unwrap_err();
2027            assert_eq!(error.code, -32602);
2028        }
2029    }
2030
2031    /// P1: without this, no spec-compliant LSP server may ever initiate
2032    /// `$/progress` at all (per LSP 3.17, a server needs a successful
2033    /// `window/workDoneProgress/create` response before it may report
2034    /// server-initiated progress for a token).
2035    #[test]
2036    fn test_work_done_progress_create_request_is_acknowledged() {
2037        let request = JsonRpcRequest {
2038            jsonrpc: JsonRpcVersion,
2039            id: RequestId::String("wdp1".to_string()),
2040            method: "window/workDoneProgress/create".to_string(),
2041            params: Some(serde_json::json!({ "token": "indexing" })),
2042        };
2043
2044        let reply = reply_json(request);
2045
2046        assert_eq!(reply["result"], Value::Null);
2047        assert!(reply.get("error").is_none());
2048    }
2049
2050    /// S3 (Fix 7): a `report`-kind `$/progress` frame must never be
2051    /// enqueued on either notification lane -- the actual mechanism
2052    /// protecting against a `report`-per-package emitter like gopls
2053    /// overrunning the bounded lifecycle channel.
2054    #[test]
2055    fn test_report_progress_frame_reaches_neither_lane() {
2056        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
2057
2058        let report = LspNotification::Progress(lsp_types::ProgressParams {
2059            token: lsp_types::ProgressToken::Int(1),
2060            value: serde_json::json!({ "kind": "report", "percentage": 50 }),
2061        });
2062
2063        let destination = LspClient::lifecycle_lane(&report, Some(&lifecycle_tx));
2064
2065        assert!(
2066            destination.is_none(),
2067            "a report-kind frame must be dropped before reaching either lane"
2068        );
2069    }
2070
2071    /// Fix 3 / M1: an unparseable `$/progress` notification (e.g. missing
2072    /// the mandatory `token` field) falls back to `LspNotification::Other {
2073    /// method: "$/progress", .. }` at parse time -- this must be dropped
2074    /// the same as a well-formed `report` frame, not routed to the
2075    /// lifecycle lane, or a server whose payloads fail to deserialize could
2076    /// bypass the kind-based filter entirely.
2077    #[test]
2078    fn test_malformed_progress_other_reaches_neither_lane() {
2079        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
2080
2081        let malformed = LspNotification::Other {
2082            method: std::borrow::Cow::Borrowed("$/progress"),
2083            params: None,
2084        };
2085
2086        let destination = LspClient::lifecycle_lane(&malformed, Some(&lifecycle_tx));
2087
2088        assert!(
2089            destination.is_none(),
2090            "a malformed $/progress frame must be dropped, not routed to the lifecycle lane"
2091        );
2092    }
2093
2094    /// P3 sanity check alongside the two tests above: a `begin` frame and a
2095    /// genuine `Other` notification (e.g. rust-analyzer's
2096    /// `experimental/serverStatus`) must still reach the lifecycle lane --
2097    /// the report/malformed filters must not have overcorrected.
2098    #[test]
2099    fn test_begin_and_other_notifications_reach_lifecycle_lane() {
2100        let (lifecycle_tx, _lifecycle_rx) = mpsc::channel(8);
2101
2102        let begin = LspNotification::Progress(lsp_types::ProgressParams {
2103            token: lsp_types::ProgressToken::Int(1),
2104            value: serde_json::json!({ "kind": "begin", "title": "Indexing" }),
2105        });
2106        assert!(LspClient::lifecycle_lane(&begin, Some(&lifecycle_tx)).is_some());
2107
2108        let server_status = LspNotification::Other {
2109            method: std::borrow::Cow::Borrowed("experimental/serverStatus"),
2110            params: Some(serde_json::json!({ "quiescent": false })),
2111        };
2112        assert!(LspClient::lifecycle_lane(&server_status, Some(&lifecycle_tx)).is_some());
2113    }
2114
2115    #[test]
2116    fn test_unknown_server_request_returns_method_not_found() {
2117        let request = JsonRpcRequest {
2118            jsonrpc: JsonRpcVersion,
2119            id: RequestId::String("unknown-1".to_string()),
2120            method: "custom/request".to_string(),
2121            params: None,
2122        };
2123
2124        let reply = reply_json(request);
2125
2126        assert!(reply.get("result").is_none());
2127        assert_eq!(reply["error"]["code"], serde_json::json!(-32601));
2128        assert_eq!(
2129            reply["error"]["message"],
2130            serde_json::json!("Unhandled server request: custom/request")
2131        );
2132    }
2133
2134    fn reply_json(request: JsonRpcRequest) -> Value {
2135        serde_json::to_value(LspClient::server_request_reply(request, None)).unwrap()
2136    }
2137
2138    #[tokio::test]
2139    async fn test_null_response_handling() {
2140        use crate::lsp::types::{JsonRpcResponse, RequestId};
2141
2142        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
2143
2144        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
2145
2146        pending_requests
2147            .lock()
2148            .await
2149            .insert(RequestId::Number(1), response_tx);
2150
2151        let null_response = JsonRpcResponse {
2152            jsonrpc: JsonRpcVersion,
2153            id: RequestId::Number(1),
2154            outcome: JsonRpcOutcome::Result(Value::Null),
2155        };
2156
2157        let sender = pending_requests.lock().await.remove(&null_response.id);
2158        if let Some(sender) = sender {
2159            let _ = sender.send(Ok(Value::Null));
2160        }
2161
2162        let timeout_result =
2163            tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
2164
2165        assert!(timeout_result.is_ok(), "Should not timeout");
2166
2167        let channel_result = timeout_result.unwrap();
2168        assert!(
2169            channel_result.is_ok(),
2170            "Channel should not be closed: {:?}",
2171            channel_result.err()
2172        );
2173
2174        let response = channel_result.unwrap();
2175        assert!(
2176            response.is_ok(),
2177            "Should receive Ok(Value::Null), not Err: {:?}",
2178            response.err()
2179        );
2180
2181        let value = response.unwrap();
2182        assert_eq!(value, Value::Null, "Should receive Value::Null");
2183    }
2184
2185    #[tokio::test]
2186    async fn test_error_response_handling() {
2187        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
2188
2189        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
2190        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
2191
2192        pending_requests
2193            .lock()
2194            .await
2195            .insert(RequestId::Number(1), response_tx);
2196
2197        let error_response = JsonRpcResponse {
2198            jsonrpc: JsonRpcVersion,
2199            id: RequestId::Number(1),
2200            outcome: JsonRpcOutcome::Error(JsonRpcError {
2201                code: -32601,
2202                message: "Method not found".to_string(),
2203                data: None,
2204            }),
2205        };
2206
2207        let sender = pending_requests.lock().await.remove(&error_response.id);
2208        if let Some(sender) = sender
2209            && let JsonRpcOutcome::Error(error) = error_response.outcome
2210        {
2211            let _ = sender.send(Err(Error::LspServerError {
2212                code: error.code,
2213                message: error.message,
2214                data: error.data,
2215            }));
2216        }
2217
2218        let result = response_rx.await.unwrap();
2219        assert!(result.is_err(), "Should receive error");
2220
2221        if let Err(Error::LspServerError { code, message, .. }) = result {
2222            assert_eq!(code, -32601);
2223            assert_eq!(message, "Method not found");
2224        } else {
2225            panic!("Expected LspServerError");
2226        }
2227    }
2228
2229    #[tokio::test]
2230    async fn test_unknown_request_id() {
2231        use crate::lsp::types::{JsonRpcResponse, RequestId};
2232
2233        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
2234
2235        let response = JsonRpcResponse {
2236            jsonrpc: JsonRpcVersion,
2237            id: RequestId::Number(999),
2238            outcome: JsonRpcOutcome::Result(Value::Null),
2239        };
2240
2241        let sender = pending_requests.lock().await.remove(&response.id);
2242        assert!(sender.is_none(), "Should not find sender for unknown ID");
2243    }
2244
2245    #[test]
2246    fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
2247        // 199 ASCII bytes followed by a 3-byte UTF-8 char ('€') straddles the byte-200 cut.
2248        let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
2249
2250        let truncated = LspClient::truncate_error_message_for_log(&message);
2251
2252        // Cutting before the multi-byte char keeps the message valid UTF-8 (no panic) and
2253        // pins the payload to 199 bytes, not 200.
2254        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
2255    }
2256
2257    #[test]
2258    fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
2259        let exact = "x".repeat(200);
2260        assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
2261        assert_eq!(LspClient::truncate_error_message_for_log(""), "");
2262    }
2263
2264    #[test]
2265    fn test_truncate_error_message_for_log_truncates_just_above_limit() {
2266        let message = "x".repeat(201);
2267        assert_eq!(
2268            LspClient::truncate_error_message_for_log(&message),
2269            format!("{}... (truncated)", "x".repeat(200))
2270        );
2271    }
2272
2273    #[test]
2274    fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
2275        // A 4-byte emoji run straddling every possible alignment near the byte-200 boundary.
2276        let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
2277
2278        let truncated = LspClient::truncate_error_message_for_log(&message);
2279
2280        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
2281    }
2282
2283    #[tokio::test]
2284    async fn test_concurrent_request_ids() {
2285        let counter = Arc::new(AtomicI64::new(1));
2286
2287        let counter1 = Arc::clone(&counter);
2288        let counter2 = Arc::clone(&counter);
2289        let counter3 = Arc::clone(&counter);
2290
2291        let handles = vec![
2292            tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
2293            tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
2294            tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
2295        ];
2296
2297        let mut ids = Vec::new();
2298        for handle in handles {
2299            ids.push(handle.await.unwrap());
2300        }
2301
2302        ids.sort_unstable();
2303        assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
2304    }
2305
2306    #[test]
2307    fn test_jsonrpc_version_constant() {
2308        assert_eq!(
2309            serde_json::to_value(JsonRpcVersion).unwrap(),
2310            serde_json::json!("2.0")
2311        );
2312    }
2313
2314    /// #239 regression: a request that times out must remove its own entry
2315    /// from `pending_requests` instead of leaking it. `sleep` is used as the
2316    /// "server": it never writes anything to stdout, so no response can ever
2317    /// arrive and the request is guaranteed to time out rather than race a
2318    /// real answer.
2319    ///
2320    /// Unix-only: spawns a real `sleep` subprocess, which is unavailable on
2321    /// the Windows CI runner.
2322    #[cfg(unix)]
2323    #[tokio::test]
2324    async fn test_request_timeout_removes_pending_entry() {
2325        let mut child = tokio::process::Command::new("sleep")
2326            .arg("2")
2327            .stdin(std::process::Stdio::piped())
2328            .stdout(std::process::Stdio::piped())
2329            .kill_on_drop(true)
2330            .spawn()
2331            .unwrap();
2332        let stdin = child.stdin.take().unwrap();
2333        let stdout = child.stdout.take().unwrap();
2334
2335        let transport = LspTransport::new(stdin, stdout);
2336        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
2337
2338        let result: Result<Value> = client
2339            .request(
2340                "textDocument/hover",
2341                serde_json::json!({}),
2342                Duration::from_millis(50),
2343            )
2344            .await;
2345
2346        assert_matches!(result, Err(Error::Timeout(_)), "got {result:?}");
2347        assert!(
2348            client.pending_requests.lock().await.is_empty(),
2349            "timed-out request must not remain in pending_requests"
2350        );
2351    }
2352
2353    /// #249 continuation: a client about to be discarded (e.g. superseded by
2354    /// a respawned replacement) must fail every still-pending request
2355    /// immediately rather than leaving callers to wait out their timeout.
2356    #[tokio::test]
2357    async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
2358        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
2359        let (command_tx, _command_rx) = mpsc::channel(1);
2360
2361        let client = LspClient {
2362            config: LspServerConfig::rust_analyzer(),
2363            state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
2364            request_counter: Arc::new(AtomicI64::new(1)),
2365            command_tx,
2366            pending_requests: Arc::clone(&pending_requests),
2367            pending_failure: Arc::default(),
2368            receiver_task: None,
2369            redactions: Arc::default(),
2370            connection_id: ConnectionId::next(),
2371        };
2372
2373        let (tx1, rx1) = oneshot::channel::<Result<Value>>();
2374        let (tx2, rx2) = oneshot::channel::<Result<Value>>();
2375        pending_requests
2376            .lock()
2377            .await
2378            .insert(RequestId::Number(1), tx1);
2379        pending_requests
2380            .lock()
2381            .await
2382            .insert(RequestId::Number(2), tx2);
2383
2384        client.fail_pending_requests().await;
2385
2386        assert!(pending_requests.lock().await.is_empty());
2387        assert_matches!(rx1.await.unwrap(), Err(Error::ServerTerminated));
2388        assert_matches!(rx2.await.unwrap(), Err(Error::ServerTerminated));
2389    }
2390
2391    /// Pins where a request is cancelled (#518): once the request is on the
2392    /// wire, a timeout or a dropped caller must leave the client able to
2393    /// serve the next request, with no stale reply crossing over to it. The
2394    /// paused clock makes the timeout deterministic.
2395    mod timeout_cancellation {
2396        use tokio::io::BufReader;
2397        use tokio::task::JoinHandle;
2398
2399        use super::*;
2400        use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
2401
2402        const TIMEOUT: Duration = Duration::from_secs(5);
2403
2404        fn spawn_hover(client: &LspClient) -> JoinHandle<Result<Value>> {
2405            let client = client.clone();
2406            tokio::spawn(async move {
2407                client
2408                    .request("textDocument/hover", serde_json::json!({}), TIMEOUT)
2409                    .await
2410            })
2411        }
2412
2413        #[tokio::test(start_paused = true)]
2414        async fn test_timeout_while_awaiting_reply_clears_pending_and_discards_late_reply() {
2415            let (client, mut server) = fake_lsp_client();
2416            let mut reader = BufReader::new(&mut server.write_stdout);
2417
2418            let first = spawn_hover(&client);
2419            let first_request = read_framed_message(&mut reader).await;
2420
2421            tokio::time::advance(TIMEOUT + Duration::from_secs(1)).await;
2422            assert_matches!(
2423                first.await.unwrap(),
2424                Err(Error::Timeout(limit)) if limit == TIMEOUT,
2425                "request awaiting its reply must time out"
2426            );
2427            assert!(
2428                client.pending_requests.lock().await.is_empty(),
2429                "timed-out request must not remain in pending_requests"
2430            );
2431
2432            write_response(
2433                &mut server.read_half_stdin,
2434                &first_request["id"],
2435                serde_json::json!("stale"),
2436            )
2437            .await;
2438
2439            let second = spawn_hover(&client);
2440            let second_request = read_framed_message(&mut reader).await;
2441            assert_ne!(second_request["id"], first_request["id"]);
2442            write_response(
2443                &mut server.read_half_stdin,
2444                &second_request["id"],
2445                serde_json::json!("fresh"),
2446            )
2447            .await;
2448
2449            assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("fresh"));
2450            assert!(client.pending_requests.lock().await.is_empty());
2451        }
2452
2453        #[tokio::test]
2454        async fn test_undecodable_response_fails_only_its_request() {
2455            use crate::test_lsp::write_raw_frame;
2456
2457            let (client, mut server) = fake_lsp_client();
2458            let mut reader = BufReader::new(&mut server.write_stdout);
2459
2460            let first = spawn_hover(&client);
2461            let first_request = read_framed_message(&mut reader).await;
2462            let deep = format!("{}1{}", "{\"parent\":".repeat(200), "}".repeat(200));
2463            write_raw_frame(
2464                &mut server.read_half_stdin,
2465                &format!(
2466                    r#"{{"jsonrpc":"2.0","id":{},"result":{deep}}}"#,
2467                    first_request["id"]
2468                ),
2469            )
2470            .await;
2471
2472            assert_matches!(
2473                first.await.unwrap(),
2474                Err(Error::LspProtocolError(message)) if message.as_str() == "undecodable response"
2475            );
2476
2477            let second = spawn_hover(&client);
2478            let second_request = read_framed_message(&mut reader).await;
2479            write_response(
2480                &mut server.read_half_stdin,
2481                &second_request["id"],
2482                serde_json::json!("alive"),
2483            )
2484            .await;
2485            assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("alive"));
2486            assert!(client.pending_requests.lock().await.is_empty());
2487        }
2488
2489        fn deeply_nested() -> String {
2490            format!("{}1{}", "{\"parent\":".repeat(200), "}".repeat(200))
2491        }
2492
2493        /// #681: invalid UTF-8 in a response body fails its own request only.
2494        #[tokio::test]
2495        async fn test_invalid_utf8_response_fails_only_its_request() {
2496            use tokio::io::AsyncWriteExt as _;
2497
2498            let (client, mut server) = fake_lsp_client();
2499            let mut reader = BufReader::new(&mut server.write_stdout);
2500
2501            let first = spawn_hover(&client);
2502            let first_request = read_framed_message(&mut reader).await;
2503            let mut body = format!(
2504                r#"{{"jsonrpc":"2.0","id":{},"result":"x"#,
2505                first_request["id"]
2506            )
2507            .into_bytes();
2508            body.extend_from_slice(&[0xff, 0xfe]);
2509            body.extend_from_slice(br#"y"}"#);
2510            let stdin = &mut server.read_half_stdin;
2511            stdin
2512                .write_all(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes())
2513                .await
2514                .unwrap();
2515            stdin.write_all(&body).await.unwrap();
2516            stdin.flush().await.unwrap();
2517
2518            assert_matches!(
2519                first.await.unwrap(),
2520                Err(Error::LspProtocolError(message)) if message.as_str() == "undecodable response"
2521            );
2522
2523            let second = spawn_hover(&client);
2524            let second_request = read_framed_message(&mut reader).await;
2525            write_response(
2526                &mut server.read_half_stdin,
2527                &second_request["id"],
2528                serde_json::json!("alive"),
2529            )
2530            .await;
2531            assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("alive"));
2532        }
2533
2534        /// #681: undecodable frames are warned about at most once a minute,
2535        /// with the number dropped silently since the last warning.
2536        #[test]
2537        fn test_undecodable_warning_is_time_limited_and_counts_the_suppressed() {
2538            use tracing_subscriber::layer::SubscriberExt as _;
2539
2540            let captured = crate::test_lsp::CapturedLogs::default();
2541            let _guard = tracing::subscriber::set_default(
2542                tracing_subscriber::registry().with(captured.clone()),
2543            );
2544            let mut run = UndecodableRun::default();
2545            let start = std::time::Instant::now();
2546            let at = |secs| start + Duration::from_secs(secs);
2547            for secs in [0, 1, 2, 30] {
2548                run.observe_at(InboundMessage::UndecodableFrame, at(secs))
2549                    .unwrap();
2550            }
2551            run.observe_at(InboundMessage::UndecodableFrame, at(61))
2552                .unwrap();
2553
2554            let warnings: Vec<_> = captured
2555                .entries()
2556                .into_iter()
2557                .filter(|(level, _)| *level == tracing::Level::WARN)
2558                .map(|(_, message)| message)
2559                .collect();
2560            assert_eq!(warnings.len(), 2, "{warnings:?}");
2561            assert!(warnings[0].contains("(0 others"), "{warnings:?}");
2562            assert!(warnings[1].contains("(3 others"), "{warnings:?}");
2563        }
2564
2565        #[tokio::test]
2566        async fn test_undecodable_server_request_is_answered_with_an_error() {
2567            use crate::test_lsp::write_raw_frame;
2568
2569            let (client, mut server) = fake_lsp_client();
2570            let mut reader = BufReader::new(&mut server.write_stdout);
2571
2572            write_raw_frame(
2573                &mut server.read_half_stdin,
2574                &format!(
2575                    r#"{{"jsonrpc":"2.0","id":"srv-1","method":"client/registerCapability","params":{}}}"#,
2576                    deeply_nested()
2577                ),
2578            )
2579            .await;
2580            let reply = read_framed_message(&mut reader).await;
2581
2582            assert_eq!(reply["id"], serde_json::json!("srv-1"));
2583            assert_eq!(reply["error"]["code"], serde_json::json!(-32600));
2584
2585            let hover = spawn_hover(&client);
2586            let request = read_framed_message(&mut reader).await;
2587            write_response(
2588                &mut server.read_half_stdin,
2589                &request["id"],
2590                serde_json::json!("alive"),
2591            )
2592            .await;
2593            assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
2594        }
2595
2596        #[tokio::test]
2597        async fn test_undecodable_notification_is_dropped_and_the_connection_stays_up() {
2598            use crate::test_lsp::write_raw_frame;
2599
2600            let (client, mut server) = fake_lsp_client();
2601            let mut reader = BufReader::new(&mut server.write_stdout);
2602            let hover = spawn_hover(&client);
2603            let request = read_framed_message(&mut reader).await;
2604
2605            write_raw_frame(
2606                &mut server.read_half_stdin,
2607                &format!(
2608                    r#"{{"jsonrpc":"2.0","method":"window/logMessage","params":{}}}"#,
2609                    deeply_nested()
2610                ),
2611            )
2612            .await;
2613            write_response(
2614                &mut server.read_half_stdin,
2615                &request["id"],
2616                serde_json::json!("alive"),
2617            )
2618            .await;
2619
2620            assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
2621        }
2622
2623        #[tokio::test]
2624        async fn test_a_run_of_undecodable_frames_ends_the_connection() {
2625            use crate::test_lsp::write_raw_frame;
2626
2627            let (client, mut server) = fake_lsp_client();
2628            let mut reader = BufReader::new(&mut server.write_stdout);
2629            let hover = spawn_hover(&client);
2630            let _request = read_framed_message(&mut reader).await;
2631
2632            for _ in 0..MAX_CONSECUTIVE_UNDECODABLE_FRAMES {
2633                write_raw_frame(&mut server.read_half_stdin, "{not json").await;
2634            }
2635
2636            assert_matches!(hover.await.unwrap(), Err(Error::ServerTerminated));
2637        }
2638
2639        #[tokio::test]
2640        async fn test_a_decodable_message_ends_a_run_of_undecodable_frames() {
2641            use crate::test_lsp::write_raw_frame;
2642
2643            let (client, mut server) = fake_lsp_client();
2644            let mut reader = BufReader::new(&mut server.write_stdout);
2645            let hover = spawn_hover(&client);
2646            let request = read_framed_message(&mut reader).await;
2647
2648            let almost = MAX_CONSECUTIVE_UNDECODABLE_FRAMES - 1;
2649            for _ in 0..2 {
2650                for _ in 0..almost {
2651                    write_raw_frame(&mut server.read_half_stdin, "{not json").await;
2652                }
2653                write_raw_frame(
2654                    &mut server.read_half_stdin,
2655                    r#"{"jsonrpc":"2.0","method":"$/progress"}"#,
2656                )
2657                .await;
2658            }
2659            write_response(
2660                &mut server.read_half_stdin,
2661                &request["id"],
2662                serde_json::json!("alive"),
2663            )
2664            .await;
2665
2666            assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
2667        }
2668
2669        #[tokio::test(start_paused = true)]
2670        async fn test_dropped_caller_after_send_is_cleared_by_late_reply() {
2671            let (client, mut server) = fake_lsp_client();
2672            let mut reader = BufReader::new(&mut server.write_stdout);
2673
2674            let first = spawn_hover(&client);
2675            let first_request = read_framed_message(&mut reader).await;
2676            first.abort();
2677            assert!(first.await.unwrap_err().is_cancelled());
2678
2679            write_response(
2680                &mut server.read_half_stdin,
2681                &first_request["id"],
2682                serde_json::json!("orphaned"),
2683            )
2684            .await;
2685
2686            let second = spawn_hover(&client);
2687            let second_request = read_framed_message(&mut reader).await;
2688            write_response(
2689                &mut server.read_half_stdin,
2690                &second_request["id"],
2691                serde_json::json!("fresh"),
2692            )
2693            .await;
2694
2695            assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("fresh"));
2696            assert!(
2697                client.pending_requests.lock().await.is_empty(),
2698                "the orphaned reply must clear the abandoned entry"
2699            );
2700        }
2701    }
2702
2703    #[test]
2704    fn test_should_retrigger_defaults_to_true_when_data_absent() {
2705        assert!(LspClient::should_retrigger(None));
2706    }
2707
2708    #[test]
2709    fn test_should_retrigger_false_when_flag_false() {
2710        assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
2711            "retriggerRequest": false
2712        }))));
2713    }
2714
2715    #[test]
2716    fn test_should_retrigger_true_when_flag_true() {
2717        assert!(LspClient::should_retrigger(Some(&serde_json::json!({
2718            "retriggerRequest": true
2719        }))));
2720    }
2721
2722    /// Wire-level checks that `params: void` LSP methods go out without a
2723    /// `params` key. tsgo rejects `"params": null` on `shutdown` with
2724    /// `-32602 expected empty, got: null` and then ignores the `exit` that
2725    /// follows, so mcpls fell through to the kill-on-timeout path.
2726    mod void_params_wire {
2727        use tokio::io::BufReader;
2728
2729        use super::*;
2730        use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
2731
2732        #[tokio::test]
2733        async fn test_request_with_null_params_omits_params_key() {
2734            let (client, mut server) = fake_lsp_client();
2735
2736            let request_task = tokio::spawn(async move {
2737                client
2738                    .request::<_, Value>("shutdown", Value::Null, Duration::from_secs(5))
2739                    .await
2740            });
2741
2742            let mut reader = BufReader::new(&mut server.write_stdout);
2743            let request = read_framed_message(&mut reader).await;
2744
2745            assert_eq!(request["method"], "shutdown");
2746            assert!(
2747                request.get("params").is_none(),
2748                "null params must be omitted, got: {request}"
2749            );
2750
2751            write_response(&mut server.read_half_stdin, &request["id"], Value::Null).await;
2752            request_task.await.unwrap().unwrap();
2753        }
2754
2755        #[tokio::test]
2756        async fn test_notify_with_null_params_omits_params_key() {
2757            let (client, mut server) = fake_lsp_client();
2758
2759            client.notify("exit", Value::Null).await.unwrap();
2760
2761            let mut reader = BufReader::new(&mut server.write_stdout);
2762            let notification = read_framed_message(&mut reader).await;
2763
2764            assert_eq!(notification["method"], "exit");
2765            assert!(
2766                notification.get("params").is_none(),
2767                "null params must be omitted, got: {notification}"
2768            );
2769        }
2770
2771        #[tokio::test]
2772        async fn test_notify_with_empty_object_params_keeps_params_key() {
2773            let (client, mut server) = fake_lsp_client();
2774
2775            client
2776                .notify("initialized", lsp_types::InitializedParams {})
2777                .await
2778                .unwrap();
2779
2780            let mut reader = BufReader::new(&mut server.write_stdout);
2781            let notification = read_framed_message(&mut reader).await;
2782
2783            assert_eq!(notification["method"], "initialized");
2784            assert_eq!(
2785                notification["params"],
2786                serde_json::json!({}),
2787                "non-null params must still be sent"
2788            );
2789        }
2790    }
2791
2792    mod retry_behavior {
2793        use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
2794
2795        use super::*;
2796        use crate::test_lsp::{
2797            CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
2798            write_response as write_success_response,
2799        };
2800
2801        /// Writes a framed JSON-RPC retryable error response — either
2802        /// `ServerCancelled` (-32802) or `ContentModified` (-32801) — with a
2803        /// `data.retriggerRequest` flag.
2804        ///
2805        /// Kept local rather than promoted to the shared `test_lsp` harness:
2806        /// the `data.retriggerRequest` field is specific to this retry-logic
2807        /// test suite, unlike the generic success/error responses above.
2808        async fn write_retryable_error_response(
2809            stdin: &mut DuplexStream,
2810            id: &Value,
2811            code: i32,
2812            message: &str,
2813            retrigger: bool,
2814        ) {
2815            let response = serde_json::json!({
2816                "jsonrpc": "2.0",
2817                "id": id,
2818                "error": {
2819                    "code": code,
2820                    "message": message,
2821                    "data": { "retriggerRequest": retrigger },
2822                },
2823            });
2824            let content = serde_json::to_string(&response).unwrap();
2825            let header = format!("Content-Length: {}\r\n\r\n", content.len());
2826            stdin.write_all(header.as_bytes()).await.unwrap();
2827            stdin.write_all(content.as_bytes()).await.unwrap();
2828            stdin.flush().await.unwrap();
2829        }
2830
2831        // Not `start_paused`: the retry loop's real backoff sleeps
2832        // interleave with real async I/O on the duplex pipes below, and
2833        // paused virtual time does not reliably auto-advance across both.
2834        //
2835        // Also captures tracing output (#392): retry exhaustion is the one
2836        // scenario where every attempt but the last logs `warn!` and only
2837        // the last logs `error!`, so this doubles as that regression test
2838        // rather than duplicating the same ~3.5s wire choreography in a
2839        // second test just to assert on log severity.
2840        #[tokio::test]
2841        async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
2842            use tracing_subscriber::layer::SubscriberExt as _;
2843
2844            let (client, mut server) = fake_lsp_client();
2845            let captured = CapturedLogs::default();
2846            let subscriber = tracing_subscriber::registry().with(captured.clone());
2847            let guard = tracing::subscriber::set_default(subscriber);
2848
2849            let request_task = tokio::spawn(async move {
2850                client
2851                    .request::<_, Value>(
2852                        "textDocument/hover",
2853                        serde_json::json!({}),
2854                        Duration::from_secs(30),
2855                    )
2856                    .await
2857            });
2858
2859            let mut reader = BufReader::new(&mut server.write_stdout);
2860            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
2861            // attempt gets ServerCancelled, so retries must exhaust rather
2862            // than loop forever or swallow the error.
2863            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
2864                let request = read_framed_message(&mut reader).await;
2865                let id = request["id"].clone();
2866                write_retryable_error_response(
2867                    &mut server.read_half_stdin,
2868                    &id,
2869                    i32::from(LspErrorCodes::ServerCancelled),
2870                    "server cancelled the request",
2871                    true,
2872                )
2873                .await;
2874            }
2875
2876            let result = request_task.await.unwrap();
2877
2878            match result {
2879                Err(Error::LspServerError {
2880                    code,
2881                    message,
2882                    data,
2883                }) => {
2884                    // Assert the exact original error surfaces, not merely
2885                    // "some error with this code" -- a freshly constructed
2886                    // placeholder error would satisfy a code-only check.
2887                    assert_eq!(code, i32::from(LspErrorCodes::ServerCancelled));
2888                    assert_eq!(message, "server cancelled the request");
2889                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
2890                }
2891                other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
2892            }
2893
2894            drop(guard);
2895            let logs = captured.entries();
2896            assert_eq!(
2897                logs.iter()
2898                    .filter(|(level, _)| *level == tracing::Level::ERROR)
2899                    .count(),
2900                1,
2901                "exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
2902            );
2903            assert!(
2904                logs.iter()
2905                    .any(|(level, msg)| *level == tracing::Level::ERROR
2906                        && msg.contains("LSP error response")
2907                        && msg.contains("retries exhausted")),
2908                "expected an ERROR log sharing the 'LSP error response' prefix and naming \
2909                 retry exhaustion, got: {logs:?}"
2910            );
2911            assert_eq!(
2912                logs.iter()
2913                    .filter(
2914                        |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
2915                    )
2916                    .count(),
2917                usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
2918                "every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
2919            );
2920        }
2921
2922        #[tokio::test]
2923        async fn test_retrigger_false_returns_immediately_without_retry() {
2924            let (client, mut server) = fake_lsp_client();
2925
2926            let request_task = tokio::spawn(async move {
2927                client
2928                    .request::<_, Value>(
2929                        "textDocument/hover",
2930                        serde_json::json!({}),
2931                        Duration::from_secs(30),
2932                    )
2933                    .await
2934            });
2935
2936            let mut reader = BufReader::new(&mut server.write_stdout);
2937            let request = read_framed_message(&mut reader).await;
2938            let id = request["id"].clone();
2939            write_retryable_error_response(
2940                &mut server.read_half_stdin,
2941                &id,
2942                i32::from(LspErrorCodes::ServerCancelled),
2943                "server cancelled the request",
2944                false,
2945            )
2946            .await;
2947
2948            // With `retriggerRequest: false`, `should_retrigger`'s gate on
2949            // the retry branch must short-circuit the loop: the error
2950            // returns well under the first 500ms backoff, and no second
2951            // request is ever sent. If the `&& Self::should_retrigger(..)`
2952            // guard were ever dropped from the retry match arm, this would
2953            // instead retry and both assertions below would fail.
2954            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
2955                .await
2956                .unwrap()
2957                .unwrap();
2958
2959            match result {
2960                Err(Error::LspServerError { code, .. }) => {
2961                    assert_eq!(code, i32::from(LspErrorCodes::ServerCancelled));
2962                }
2963                other => panic!("expected immediate ServerCancelled error, got {other:?}"),
2964            }
2965
2966            let second_request =
2967                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
2968                    .await;
2969            assert!(
2970                second_request.is_err(),
2971                "no retry should have been sent after retriggerRequest: false"
2972            );
2973        }
2974
2975        #[tokio::test]
2976        async fn test_retry_succeeds_after_one_server_cancelled_response() {
2977            let (client, mut server) = fake_lsp_client();
2978
2979            let request_task = tokio::spawn(async move {
2980                client
2981                    .request::<_, Value>(
2982                        "textDocument/hover",
2983                        serde_json::json!({}),
2984                        Duration::from_secs(30),
2985                    )
2986                    .await
2987            });
2988
2989            let mut reader = BufReader::new(&mut server.write_stdout);
2990
2991            // First attempt is cancelled and must retrigger.
2992            let first = read_framed_message(&mut reader).await;
2993            write_retryable_error_response(
2994                &mut server.read_half_stdin,
2995                &first["id"].clone(),
2996                i32::from(LspErrorCodes::ServerCancelled),
2997                "server cancelled the request",
2998                true,
2999            )
3000            .await;
3001
3002            // Second attempt (after backoff) succeeds -- proves the loop
3003            // genuinely re-sends the request rather than just counting down.
3004            let second = read_framed_message(&mut reader).await;
3005            assert_ne!(
3006                first["id"], second["id"],
3007                "retry must use a fresh request id"
3008            );
3009            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
3010            write_success_response(
3011                &mut server.read_half_stdin,
3012                &second["id"].clone(),
3013                expected_result.clone(),
3014            )
3015            .await;
3016
3017            let result = request_task.await.unwrap();
3018            assert_eq!(result.unwrap(), expected_result);
3019        }
3020
3021        #[tokio::test]
3022        async fn test_retry_exhaustion_returns_original_content_modified_error() {
3023            let (client, mut server) = fake_lsp_client();
3024
3025            let request_task = tokio::spawn(async move {
3026                client
3027                    .request::<_, Value>(
3028                        "textDocument/hover",
3029                        serde_json::json!({}),
3030                        Duration::from_secs(30),
3031                    )
3032                    .await
3033            });
3034
3035            let mut reader = BufReader::new(&mut server.write_stdout);
3036            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
3037            // attempt gets ContentModified, so retries must exhaust rather
3038            // than loop forever or swallow the error. -32801 shares the same
3039            // attempt budget as -32802 (FR-002), not an independent one.
3040            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
3041                let request = read_framed_message(&mut reader).await;
3042                let id = request["id"].clone();
3043                write_retryable_error_response(
3044                    &mut server.read_half_stdin,
3045                    &id,
3046                    i32::from(LspErrorCodes::ContentModified),
3047                    "content modified",
3048                    true,
3049                )
3050                .await;
3051            }
3052
3053            let result = request_task.await.unwrap();
3054
3055            match result {
3056                Err(Error::LspServerError {
3057                    code,
3058                    message,
3059                    data,
3060                }) => {
3061                    // Assert the exact original error surfaces, not merely
3062                    // "some error with this code" -- a freshly constructed
3063                    // placeholder error would satisfy a code-only check.
3064                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
3065                    assert_eq!(message, "content modified");
3066                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
3067                }
3068                other => panic!("expected exhausted ContentModified error, got {other:?}"),
3069            }
3070        }
3071
3072        #[tokio::test]
3073        async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
3074            let (client, mut server) = fake_lsp_client();
3075
3076            let request_task = tokio::spawn(async move {
3077                client
3078                    .request::<_, Value>(
3079                        "textDocument/hover",
3080                        serde_json::json!({}),
3081                        Duration::from_secs(30),
3082                    )
3083                    .await
3084            });
3085
3086            let mut reader = BufReader::new(&mut server.write_stdout);
3087            let request = read_framed_message(&mut reader).await;
3088            let id = request["id"].clone();
3089            write_retryable_error_response(
3090                &mut server.read_half_stdin,
3091                &id,
3092                i32::from(LspErrorCodes::ContentModified),
3093                "content modified",
3094                false,
3095            )
3096            .await;
3097
3098            // Same `should_retrigger` gate as -32802: a non-spec-compliant
3099            // server sending `retriggerRequest: false` on -32801 must still
3100            // be honored (FR-006 resolution), short-circuiting the loop well
3101            // under the first 500ms backoff.
3102            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
3103                .await
3104                .unwrap()
3105                .unwrap();
3106
3107            match result {
3108                Err(Error::LspServerError { code, .. }) => {
3109                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
3110                }
3111                other => panic!("expected immediate ContentModified error, got {other:?}"),
3112            }
3113
3114            let second_request =
3115                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
3116                    .await;
3117            assert!(
3118                second_request.is_err(),
3119                "no retry should have been sent after retriggerRequest: false"
3120            );
3121        }
3122
3123        #[tokio::test]
3124        async fn test_retry_succeeds_after_one_content_modified_response() {
3125            let (client, mut server) = fake_lsp_client();
3126
3127            let request_task = tokio::spawn(async move {
3128                client
3129                    .request::<_, Value>(
3130                        "textDocument/hover",
3131                        serde_json::json!({}),
3132                        Duration::from_secs(30),
3133                    )
3134                    .await
3135            });
3136
3137            let mut reader = BufReader::new(&mut server.write_stdout);
3138
3139            // First attempt gets ContentModified and must retrigger.
3140            let first = read_framed_message(&mut reader).await;
3141            write_retryable_error_response(
3142                &mut server.read_half_stdin,
3143                &first["id"].clone(),
3144                i32::from(LspErrorCodes::ContentModified),
3145                "content modified",
3146                true,
3147            )
3148            .await;
3149
3150            // Second attempt (after backoff) succeeds -- proves the loop
3151            // genuinely re-sends the request rather than just counting down.
3152            let second = read_framed_message(&mut reader).await;
3153            assert_ne!(
3154                first["id"], second["id"],
3155                "retry must use a fresh request id"
3156            );
3157            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
3158            write_success_response(
3159                &mut server.read_half_stdin,
3160                &second["id"].clone(),
3161                expected_result.clone(),
3162            )
3163            .await;
3164
3165            let result = request_task.await.unwrap();
3166            assert_eq!(result.unwrap(), expected_result);
3167        }
3168
3169        #[tokio::test]
3170        async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
3171            let (client, mut server) = fake_lsp_client();
3172
3173            // `textDocument/rename` is deliberately excluded from
3174            // `CONTENT_MODIFIED_RETRY_METHODS` (its result is an edit the
3175            // caller applies at a position that may no longer be valid once
3176            // the document changed) -- a -32801 response for it must return
3177            // immediately even though `retriggerRequest: true` would pass
3178            // `should_retrigger`'s gate on its own.
3179            let request_task = tokio::spawn(async move {
3180                client
3181                    .request::<_, Value>(
3182                        "textDocument/rename",
3183                        serde_json::json!({}),
3184                        Duration::from_secs(30),
3185                    )
3186                    .await
3187            });
3188
3189            let mut reader = BufReader::new(&mut server.write_stdout);
3190            let request = read_framed_message(&mut reader).await;
3191            let id = request["id"].clone();
3192            write_retryable_error_response(
3193                &mut server.read_half_stdin,
3194                &id,
3195                i32::from(LspErrorCodes::ContentModified),
3196                "content modified",
3197                true,
3198            )
3199            .await;
3200
3201            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
3202                .await
3203                .unwrap()
3204                .unwrap();
3205
3206            match result {
3207                Err(Error::LspServerError { code, .. }) => {
3208                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
3209                }
3210                other => panic!("expected immediate ContentModified error, got {other:?}"),
3211            }
3212
3213            let second_request =
3214                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
3215                    .await;
3216            assert!(
3217                second_request.is_err(),
3218                "no retry should have been sent for a non-allowlisted method"
3219            );
3220        }
3221
3222        /// #313: an oversized, server-controlled error message must be
3223        /// truncated before it reaches the MCP caller in
3224        /// `Error::LspServerError`, not just before it is logged. Routes
3225        /// through the real `message_loop_inner` (via `fake_lsp_client`)
3226        /// rather than constructing the error by hand, so it actually
3227        /// exercises the fix.
3228        #[tokio::test]
3229        async fn test_oversized_error_message_truncated_for_caller() {
3230            let (client, mut server) = fake_lsp_client();
3231
3232            let request_task = tokio::spawn(async move {
3233                client
3234                    .request::<_, Value>(
3235                        "textDocument/hover",
3236                        serde_json::json!({}),
3237                        Duration::from_secs(30),
3238                    )
3239                    .await
3240            });
3241
3242            let mut reader = BufReader::new(&mut server.write_stdout);
3243            let request = read_framed_message(&mut reader).await;
3244            let id = request["id"].clone();
3245            let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
3246            write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
3247                .await;
3248
3249            let result = request_task.await.unwrap();
3250
3251            match result {
3252                Err(Error::LspServerError { code, message, .. }) => {
3253                    assert_eq!(code, -32603);
3254                    assert!(
3255                        message.len() < oversized_message.len(),
3256                        "caller-facing message must be truncated, got {} bytes",
3257                        message.len()
3258                    );
3259                    assert!(message.ends_with("... (truncated)"));
3260                }
3261                other => panic!("expected truncated LspServerError, got {other:?}"),
3262            }
3263        }
3264
3265        /// #313 S2: a legitimate error message longer than the log-line cap
3266        /// (`crate::util::MAX_LOG_STRING_BYTES`, 200 bytes) but shorter than
3267        /// the caller-facing cap must reach the MCP caller intact -- the
3268        /// caller-facing budget must not silently collapse to the log
3269        /// budget.
3270        #[tokio::test]
3271        async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
3272            let (client, mut server) = fake_lsp_client();
3273
3274            let request_task = tokio::spawn(async move {
3275                client
3276                    .request::<_, Value>(
3277                        "textDocument/hover",
3278                        serde_json::json!({}),
3279                        Duration::from_secs(30),
3280                    )
3281                    .await
3282            });
3283
3284            let mut reader = BufReader::new(&mut server.write_stdout);
3285            let request = read_framed_message(&mut reader).await;
3286            let id = request["id"].clone();
3287            let message = "x".repeat(crate::util::MAX_LOG_STRING_BYTES + 50);
3288            write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
3289
3290            let result = request_task.await.unwrap();
3291
3292            match result {
3293                Err(Error::LspServerError {
3294                    message: returned, ..
3295                }) => {
3296                    assert_eq!(
3297                        returned, message,
3298                        "message under the caller cap must not be truncated"
3299                    );
3300                }
3301                other => panic!("expected untruncated LspServerError, got {other:?}"),
3302            }
3303        }
3304
3305        /// #392: a `-32802`/`-32801` error that gets retried and then
3306        /// succeeds must not log at `error!` -- only a `warn!` "will retry"
3307        /// line -- so log-based monitoring does not false-positive on a
3308        /// transient error the retry loop silently recovers from.
3309        #[tokio::test]
3310        async fn test_retried_error_that_recovers_does_not_log_error_level() {
3311            use tracing_subscriber::layer::SubscriberExt as _;
3312
3313            let (client, mut server) = fake_lsp_client();
3314            let captured = CapturedLogs::default();
3315            let subscriber = tracing_subscriber::registry().with(captured.clone());
3316            let guard = tracing::subscriber::set_default(subscriber);
3317
3318            let request_task = tokio::spawn(async move {
3319                client
3320                    .request::<_, Value>(
3321                        "textDocument/hover",
3322                        serde_json::json!({}),
3323                        Duration::from_secs(30),
3324                    )
3325                    .await
3326            });
3327
3328            let mut reader = BufReader::new(&mut server.write_stdout);
3329
3330            let first = read_framed_message(&mut reader).await;
3331            write_retryable_error_response(
3332                &mut server.read_half_stdin,
3333                &first["id"].clone(),
3334                i32::from(LspErrorCodes::ServerCancelled),
3335                "server cancelled the request",
3336                true,
3337            )
3338            .await;
3339
3340            let second = read_framed_message(&mut reader).await;
3341            write_success_response(
3342                &mut server.read_half_stdin,
3343                &second["id"].clone(),
3344                serde_json::json!({ "contents": "resolved on retry" }),
3345            )
3346            .await;
3347
3348            let result = request_task.await.unwrap();
3349            assert!(result.is_ok(), "expected retry to recover, got {result:?}");
3350
3351            drop(guard);
3352            let logs = captured.entries();
3353            assert!(
3354                !logs
3355                    .iter()
3356                    .any(|(level, _)| *level == tracing::Level::ERROR),
3357                "a retried-and-recovered error must not log at ERROR, got: {logs:?}"
3358            );
3359            assert!(
3360                logs.iter().any(
3361                    |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
3362                ),
3363                "expected a WARN 'will retry' log line, got: {logs:?}"
3364            );
3365        }
3366
3367        /// #392: a `-32801` (`ContentModified`) error for a method outside
3368        /// `CONTENT_MODIFIED_RETRY_METHODS` is never retried, so it must
3369        /// still surface at `error!` on the very first attempt.
3370        #[tokio::test]
3371        async fn test_non_retryable_error_logs_error_level() {
3372            use tracing_subscriber::layer::SubscriberExt as _;
3373
3374            let (client, mut server) = fake_lsp_client();
3375            let captured = CapturedLogs::default();
3376            let subscriber = tracing_subscriber::registry().with(captured.clone());
3377            let guard = tracing::subscriber::set_default(subscriber);
3378
3379            let request_task = tokio::spawn(async move {
3380                client
3381                    .request::<_, Value>(
3382                        "textDocument/rename",
3383                        serde_json::json!({}),
3384                        Duration::from_secs(30),
3385                    )
3386                    .await
3387            });
3388
3389            let mut reader = BufReader::new(&mut server.write_stdout);
3390            let request = read_framed_message(&mut reader).await;
3391            let id = request["id"].clone();
3392            write_retryable_error_response(
3393                &mut server.read_half_stdin,
3394                &id,
3395                i32::from(LspErrorCodes::ContentModified),
3396                "content modified",
3397                true,
3398            )
3399            .await;
3400
3401            let result = request_task.await.unwrap();
3402            assert!(result.is_err(), "expected a non-retryable error");
3403
3404            drop(guard);
3405            let logs = captured.entries();
3406            assert!(
3407                logs.iter()
3408                    .any(|(level, msg)| *level == tracing::Level::ERROR
3409                        && msg.contains("LSP error response")
3410                        && msg.contains("content modified")),
3411                "a non-retryable error must still surface an ERROR log sharing the \
3412                 'LSP error response' prefix, got: {logs:?}"
3413            );
3414        }
3415    }
3416
3417    /// Regression coverage for #451 (`LspTransportReader::receive` moved off
3418    /// the `select!` and onto a dedicated reader task, see
3419    /// [`super::spawn_reader_task`]).
3420    mod reader_task_regression {
3421        use tokio::io::BufReader;
3422
3423        use super::*;
3424        use crate::test_lsp::{
3425            fake_lsp_client, inert_transport, read_framed_message, write_response,
3426        };
3427
3428        /// The reader task only ever sends `Err` through the channel before
3429        /// exiting (see `spawn_reader_task`) -- a `None` from `msg_rx.recv()`
3430        /// means the task disappeared some other way (e.g. panicked). This is
3431        /// the one branch in `message_loop_inner` that has no equivalent in
3432        /// the pre-#451 code, so it needs its own direct test rather than
3433        /// relying on the full `fake_lsp_client` harness to provoke it.
3434        #[tokio::test]
3435        async fn test_message_loop_inner_treats_reader_channel_close_as_server_terminated() {
3436            let (mut transport, _reader) = inert_transport();
3437            let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
3438            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
3439            let pending_requests: Arc<Mutex<PendingRequests>> =
3440                Arc::new(Mutex::new(HashMap::new()));
3441
3442            drop(msg_tx);
3443
3444            let result = LspClient::message_loop_inner(
3445                &mut transport,
3446                &mut msg_rx,
3447                &mut command_rx,
3448                &pending_requests,
3449                None,
3450                None,
3451                &Redactions::default(),
3452                None,
3453            )
3454            .await;
3455
3456            assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
3457        }
3458
3459        /// As above, with a real pending request parked at the time the
3460        /// reader task disappears -- `message_loop_inner` itself never
3461        /// touches `pending_requests` on this branch, so a caller blocked on
3462        /// it only unblocks via `message_loop`'s post-loop
3463        /// `drain_and_fail_pending` (#458), invoked here the same way
3464        /// `message_loop` does after `message_loop_inner` returns. Driving
3465        /// this exact branch through the real `message_loop` would require
3466        /// the reader task to panic (the only other way `msg_rx.recv()`
3467        /// returns `None`), which isn't reproducible without a production
3468        /// seam -- see `test_message_loop_fails_pending_requests_on_transport_error_exit`
3469        /// for the sibling exit path exercised through the real function.
3470        #[tokio::test]
3471        async fn test_message_loop_inner_reader_gone_then_drain_fails_pending_request() {
3472            let (mut transport, _reader) = inert_transport();
3473            let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
3474            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
3475            let pending_requests: Arc<Mutex<PendingRequests>> =
3476                Arc::new(Mutex::new(HashMap::new()));
3477
3478            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
3479            pending_requests
3480                .lock()
3481                .await
3482                .insert(RequestId::Number(1), response_tx);
3483
3484            drop(msg_tx);
3485
3486            let result = LspClient::message_loop_inner(
3487                &mut transport,
3488                &mut msg_rx,
3489                &mut command_rx,
3490                &pending_requests,
3491                None,
3492                None,
3493                &Redactions::default(),
3494                None,
3495            )
3496            .await;
3497            assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
3498
3499            LspClient::drain_and_fail_pending(&pending_requests, &StdMutex::default()).await;
3500
3501            let received = response_rx.await.unwrap();
3502            assert_matches!(received, Err(Error::ServerTerminated), "got {received:?}");
3503            assert!(pending_requests.lock().await.is_empty());
3504        }
3505
3506        /// The reader task can run ahead of `select!` and have already queued
3507        /// a fully-decoded response in `msg_rx` by the time a concurrent
3508        /// `Shutdown` command is what `select!` happens to pick. Before the
3509        /// drain fix, that queued response was silently dropped instead of
3510        /// resolving its caller's pending request -- the caller would then
3511        /// block until its own `request_timeout_seconds` elapsed instead of
3512        /// failing fast or succeeding.
3513        #[tokio::test]
3514        async fn test_shutdown_drains_buffered_responses_instead_of_dropping_them() {
3515            let (mut transport, _reader) = inert_transport();
3516            let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
3517            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
3518            let pending_requests: Arc<Mutex<PendingRequests>> =
3519                Arc::new(Mutex::new(HashMap::new()));
3520
3521            let id = RequestId::Number(1);
3522            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
3523            pending_requests
3524                .lock()
3525                .await
3526                .insert(id.clone(), response_tx);
3527
3528            msg_tx
3529                .send(Ok(InboundMessage::Response(JsonRpcResponse {
3530                    jsonrpc: JsonRpcVersion,
3531                    id: id.clone(),
3532                    outcome: JsonRpcOutcome::Result(serde_json::json!({ "ok": true })),
3533                })))
3534                .await
3535                .unwrap();
3536            command_tx.send(ClientCommand::Shutdown).await.unwrap();
3537            drop(command_tx);
3538
3539            let result = LspClient::message_loop_inner(
3540                &mut transport,
3541                &mut msg_rx,
3542                &mut command_rx,
3543                &pending_requests,
3544                None,
3545                None,
3546                &Redactions::default(),
3547                None,
3548            )
3549            .await;
3550
3551            assert!(result.is_ok(), "got {result:?}");
3552            // `Err` here means the sender was dropped without a reply -- i.e. the
3553            // buffered response was lost instead of resolving this request.
3554            let received = response_rx.await.unwrap();
3555            assert_eq!(received.unwrap(), serde_json::json!({ "ok": true }));
3556            assert!(
3557                pending_requests.lock().await.is_empty(),
3558                "the drained response must resolve its pending request entry"
3559            );
3560        }
3561
3562        /// #458: before the fix, none of `message_loop`'s exit paths failed
3563        /// requests still parked in `pending_requests` -- each caller stayed
3564        /// blocked in its own `timeout(..)` until `request_timeout` elapsed.
3565        /// Drives the real `message_loop` (not `_inner`) over
3566        /// [`inert_transport`], whose reader side is dropped, so the reader
3567        /// task hits a transport receive error immediately and
3568        /// `message_loop` exits through that path.
3569        #[tokio::test]
3570        async fn test_message_loop_fails_pending_requests_on_transport_error_exit() {
3571            let (transport, reader) = inert_transport();
3572            let (_command_tx, command_rx) = mpsc::channel::<ClientCommand>(1);
3573            let pending_requests: Arc<Mutex<PendingRequests>> =
3574                Arc::new(Mutex::new(HashMap::new()));
3575
3576            let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
3577            pending_requests
3578                .lock()
3579                .await
3580                .insert(RequestId::Number(1), response_tx);
3581
3582            let result = LspClient::message_loop(
3583                (transport, reader),
3584                command_rx,
3585                Arc::clone(&pending_requests),
3586                Arc::default(),
3587                None,
3588                None,
3589                Arc::default(),
3590                None,
3591            )
3592            .await;
3593
3594            assert!(result.is_err(), "got {result:?}");
3595            let received = response_rx.await.unwrap();
3596            assert_matches!(received, Err(Error::ServerTerminated), "got {received:?}");
3597            assert!(pending_requests.lock().await.is_empty());
3598        }
3599
3600        /// #458: unit-level check that composing `message_loop_inner`'s
3601        /// Shutdown `try_recv` drain (#451) with a subsequent
3602        /// `drain_and_fail_pending` call -- the same two pieces
3603        /// `message_loop` itself calls in that order -- does not re-fail a
3604        /// request already resolved by a buffered response, while still
3605        /// failing a truly unanswered one. This does not exercise
3606        /// `message_loop`'s own statement ordering (see
3607        /// `test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown`
3608        /// for that, driven through the real function).
3609        #[tokio::test]
3610        async fn test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending() {
3611            let (mut transport, _reader) = inert_transport();
3612            let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
3613            let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
3614            let pending_requests: Arc<Mutex<PendingRequests>> =
3615                Arc::new(Mutex::new(HashMap::new()));
3616
3617            let answered_id = RequestId::Number(1);
3618            let (answered_tx, answered_rx) = oneshot::channel::<Result<Value>>();
3619            let unanswered_id = RequestId::Number(2);
3620            let (unanswered_tx, unanswered_rx) = oneshot::channel::<Result<Value>>();
3621            pending_requests
3622                .lock()
3623                .await
3624                .insert(answered_id.clone(), answered_tx);
3625            pending_requests
3626                .lock()
3627                .await
3628                .insert(unanswered_id.clone(), unanswered_tx);
3629
3630            msg_tx
3631                .send(Ok(InboundMessage::Response(JsonRpcResponse {
3632                    jsonrpc: JsonRpcVersion,
3633                    id: answered_id,
3634                    outcome: JsonRpcOutcome::Result(serde_json::json!({ "ok": true })),
3635                })))
3636                .await
3637                .unwrap();
3638            command_tx.send(ClientCommand::Shutdown).await.unwrap();
3639            drop(command_tx);
3640
3641            let inner_result = LspClient::message_loop_inner(
3642                &mut transport,
3643                &mut msg_rx,
3644                &mut command_rx,
3645                &pending_requests,
3646                None,
3647                None,
3648                &Redactions::default(),
3649                None,
3650            )
3651            .await;
3652            assert!(inner_result.is_ok(), "got {inner_result:?}");
3653
3654            // Mirrors the drain `message_loop` runs after `message_loop_inner`
3655            // returns.
3656            LspClient::drain_and_fail_pending(&pending_requests, &StdMutex::default()).await;
3657
3658            let answered = answered_rx.await.unwrap();
3659            assert_eq!(answered.unwrap(), serde_json::json!({ "ok": true }));
3660
3661            let unanswered = unanswered_rx.await.unwrap();
3662            assert_matches!(
3663                unanswered,
3664                Err(Error::ServerTerminated),
3665                "got {unanswered:?}"
3666            );
3667            assert!(pending_requests.lock().await.is_empty());
3668        }
3669
3670        /// #458 S2: end-to-end proof that `message_loop`'s post-loop drain
3671        /// runs *after* `message_loop_inner` returns, driven through the
3672        /// real `message_loop` (via `fake_lsp_client`), not a hand-recreated
3673        /// sequence. Deterministic without depending on `select!`
3674        /// nondeterminism: the answered request is awaited to completion via
3675        /// the loop's ordinary response path *before* `shutdown` is ever
3676        /// sent, so nothing here relies on which branch `select!` happens to
3677        /// pick when both are ready -- that race is unit-tested separately in
3678        /// `test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending`.
3679        #[tokio::test]
3680        async fn test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown()
3681        {
3682            let (client, mut server) = fake_lsp_client();
3683            let mut reader = BufReader::new(&mut server.write_stdout);
3684
3685            let answered_client = client.clone();
3686            let answered_task = tokio::spawn(async move {
3687                answered_client
3688                    .request::<_, Value>(
3689                        "textDocument/hover",
3690                        serde_json::json!({}),
3691                        Duration::from_secs(30),
3692                    )
3693                    .await
3694            });
3695
3696            let request = read_framed_message(&mut reader).await;
3697            let id = request["id"].clone();
3698            write_response(
3699                &mut server.read_half_stdin,
3700                &id,
3701                serde_json::json!({ "ok": true }),
3702            )
3703            .await;
3704
3705            let answered = answered_task.await.unwrap().unwrap();
3706            assert_eq!(answered, serde_json::json!({ "ok": true }));
3707
3708            let unanswered_client = client.clone();
3709            let unanswered_task = tokio::spawn(async move {
3710                unanswered_client
3711                    .request::<_, Value>(
3712                        "textDocument/definition",
3713                        serde_json::json!({}),
3714                        Duration::from_secs(30),
3715                    )
3716                    .await
3717            });
3718
3719            // Reading the framed bytes back confirms the request was fully
3720            // sent, which only happens after it is already registered in
3721            // `pending_requests` -- so shutdown below cannot race its insert.
3722            let _unanswered_request = read_framed_message(&mut reader).await;
3723
3724            client.shutdown().await.unwrap();
3725
3726            let unanswered = tokio::time::timeout(Duration::from_millis(200), unanswered_task)
3727                .await
3728                .unwrap_or_else(|_| {
3729                    panic!(
3730                        "unanswered request must fail immediately on shutdown, not hang until its own timeout"
3731                    )
3732                })
3733                .unwrap();
3734            assert_matches!(
3735                unanswered,
3736                Err(Error::ServerTerminated),
3737                "got {unanswered:?}"
3738            );
3739        }
3740
3741        /// Drives dense, concurrent request/response traffic through the real
3742        /// `message_loop`/`spawn_reader_task` pair (via `fake_lsp_client`) and
3743        /// answers deliberately out of arrival order, so a bug that matched
3744        /// responses positionally instead of by id -- the failure mode a
3745        /// mid-frame desync would eventually cause -- would surface as a
3746        /// mismatched payload rather than a hang.
3747        #[tokio::test]
3748        async fn test_dense_concurrent_requests_all_resolve_to_matching_responses() {
3749            const REQUEST_COUNT: usize = 20;
3750            let (client, mut server) = fake_lsp_client();
3751
3752            // A manual push loop, not `.map(..).collect()`: `tokio::spawn`
3753            // must run eagerly for every `i` right here, before
3754            // `server_task` starts answering below -- a lazily-iterated
3755            // combinator would spawn (and thus send) each request only as
3756            // its `JoinHandle` is later awaited, one at a time, defeating
3757            // the "dense concurrent" setup this test needs.
3758            let mut request_tasks = Vec::with_capacity(REQUEST_COUNT);
3759            for i in 0..REQUEST_COUNT {
3760                let client = client.clone();
3761                request_tasks.push(tokio::spawn(async move {
3762                    client
3763                        .request::<_, Value>(
3764                            "textDocument/hover",
3765                            serde_json::json!({ "n": i }),
3766                            Duration::from_secs(30),
3767                        )
3768                        .await
3769                }));
3770            }
3771
3772            let server_task = tokio::spawn(async move {
3773                let mut reader = BufReader::new(&mut server.write_stdout);
3774                let mut requests = Vec::with_capacity(REQUEST_COUNT);
3775                for _ in 0..REQUEST_COUNT {
3776                    requests.push(read_framed_message(&mut reader).await);
3777                }
3778                for request in requests.into_iter().rev() {
3779                    let id = request["id"].clone();
3780                    let n = request["params"]["n"].clone();
3781                    write_response(
3782                        &mut server.read_half_stdin,
3783                        &id,
3784                        serde_json::json!({ "echo": n }),
3785                    )
3786                    .await;
3787                }
3788            });
3789
3790            server_task.await.unwrap();
3791
3792            for (i, task) in request_tasks.into_iter().enumerate() {
3793                let value = task
3794                    .await
3795                    .unwrap()
3796                    .unwrap_or_else(|e| panic!("request {i} failed: {e:?}"));
3797                assert_eq!(
3798                    value["echo"],
3799                    serde_json::json!(i),
3800                    "response for request {i} carried the wrong payload -- id/response mismatch"
3801                );
3802            }
3803        }
3804
3805        /// Shutting down a clone stops the loop for every clone but cannot
3806        /// abort it, and the owner can still shut down afterwards.
3807        #[tokio::test]
3808        async fn test_shutdown_on_clone_stops_shared_loop_then_owner_shutdown_succeeds() {
3809            let (client, _server) = fake_lsp_client();
3810
3811            client.clone().shutdown().await.unwrap();
3812
3813            let request = timeout(
3814                Duration::from_secs(2),
3815                client.request::<_, Value>("x/y", serde_json::json!({}), Duration::from_secs(30)),
3816            )
3817            .await
3818            .unwrap_or_else(|_| panic!("request on a stopped loop must fail fast"));
3819            assert_matches!(request, Err(Error::ServerTerminated), "got {request:?}");
3820
3821            client.shutdown().await.unwrap();
3822        }
3823
3824        /// A deadline that has already passed still aborts the loop, reports
3825        /// `ShutdownTimeout`, and leaves the client in the `Shutdown` state.
3826        #[tokio::test]
3827        async fn test_shutdown_until_expired_deadline_reports_timeout() {
3828            let (mut client, _server) = fake_lsp_client();
3829            let probe = client.clone();
3830
3831            let result = client
3832                .shutdown_until(Instant::now() - Duration::from_millis(50))
3833                .await;
3834
3835            assert_matches!(result, Err(Error::ShutdownTimeout), "got {result:?}");
3836            assert_matches!(probe.state().await, crate::lsp::ServerState::Shutdown);
3837        }
3838
3839        /// A message loop wedged writing to a server that never reads must not
3840        /// hang `shutdown_until`: it returns `ShutdownTimeout` at the deadline
3841        /// and fails the requests still in flight.
3842        #[tokio::test]
3843        async fn test_shutdown_until_aborts_wedged_loop_and_fails_pending() {
3844            let (mut client, mut server) = fake_lsp_client();
3845            let mut reader = BufReader::new(&mut server.write_stdout);
3846
3847            let in_flight_client = client.clone();
3848            let in_flight = tokio::spawn(async move {
3849                in_flight_client
3850                    .request::<_, Value>(
3851                        "textDocument/hover",
3852                        serde_json::json!({}),
3853                        Duration::from_secs(30),
3854                    )
3855                    .await
3856            });
3857            let _ = read_framed_message(&mut reader).await;
3858
3859            // Larger than the mock pipe's capacity, so the loop blocks in `send`.
3860            client
3861                .notify("x/wedge", serde_json::json!({ "p": "a".repeat(1 << 20) }))
3862                .await
3863                .unwrap();
3864
3865            let deadline = Instant::now() + Duration::from_millis(200);
3866            let result = timeout(Duration::from_secs(5), client.shutdown_until(deadline))
3867                .await
3868                .unwrap_or_else(|_| panic!("shutdown_until must honor its deadline"));
3869            assert_matches!(result, Err(Error::ShutdownTimeout), "got {result:?}");
3870
3871            let in_flight = timeout(Duration::from_secs(1), in_flight)
3872                .await
3873                .unwrap_or_else(|_| panic!("in-flight request must be failed by the drain"))
3874                .unwrap();
3875            assert_matches!(in_flight, Err(Error::ServerTerminated), "got {in_flight:?}");
3876        }
3877
3878        /// A client marked restarted fails its stragglers with the retryable
3879        /// `ServerRestarted`, both from the explicit drain and from the
3880        /// message loop's exit drain.
3881        #[tokio::test]
3882        async fn test_marked_restarted_client_fails_pending_with_server_restarted() {
3883            let (mut client, mut server) = fake_lsp_client();
3884            let mut reader = BufReader::new(&mut server.write_stdout);
3885
3886            let in_flight_client = client.clone();
3887            let in_flight = tokio::spawn(async move {
3888                in_flight_client
3889                    .request::<_, Value>(
3890                        "textDocument/hover",
3891                        serde_json::json!({}),
3892                        Duration::from_secs(30),
3893                    )
3894                    .await
3895            });
3896            let _ = read_framed_message(&mut reader).await;
3897
3898            client.mark_restarted(ServerId::from_static("rust"));
3899            client
3900                .shutdown_until(Instant::now() + Duration::from_millis(200))
3901                .await
3902                .ok();
3903
3904            let in_flight = timeout(Duration::from_secs(1), in_flight)
3905                .await
3906                .unwrap_or_else(|_| panic!("in-flight request must be failed by the drain"))
3907                .unwrap();
3908            assert!(
3909                matches!(&in_flight, Err(Error::ServerRestarted { server_id }) if server_id.as_str() == "rust"),
3910                "got {in_flight:?}"
3911            );
3912        }
3913
3914        /// #458: a request issued after the client has already shut down
3915        /// must fail immediately via `register_and_send_request`'s
3916        /// `command_tx.send(..)` check, not hang until its own
3917        /// `request_timeout`. Wrapped in a short outer `timeout` so a
3918        /// regression back to the slow path fails this test instead of
3919        /// merely making it slow.
3920        #[tokio::test]
3921        async fn test_request_after_shutdown_fails_fast_instead_of_hanging() {
3922            let (client, _server) = fake_lsp_client();
3923            let post_shutdown_client = client.clone();
3924
3925            client.shutdown().await.unwrap();
3926
3927            let result = tokio::time::timeout(
3928                Duration::from_millis(200),
3929                post_shutdown_client.request::<_, Value>(
3930                    "textDocument/hover",
3931                    serde_json::json!({}),
3932                    Duration::from_secs(30),
3933                ),
3934            )
3935            .await
3936            .unwrap_or_else(|_| {
3937                panic!(
3938                    "request after shutdown must fail immediately, not hang until its own timeout"
3939                )
3940            });
3941
3942            assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
3943        }
3944    }
3945}