Skip to main content

mcpls_core/lsp/
client.rs

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