Skip to main content

mcpls_core/lsp/
client.rs

1//! LSP client implementation with async request/response handling.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicI64, Ordering};
6
7use lsp_types::LspErrorCodes;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11use tokio::sync::{Mutex, mpsc, oneshot};
12use tokio::task::JoinHandle;
13use tokio::time::{Duration, timeout};
14use tracing::{debug, error, trace, warn};
15
16use crate::config::LspServerConfig;
17use crate::error::{Error, Result};
18use crate::lsp::transport::LspTransport;
19use crate::lsp::types::{
20    InboundMessage, JsonRpcError, JsonRpcRequest, JsonRpcResponse, LspNotification, RequestId,
21};
22
23/// JSON-RPC protocol version.
24const JSONRPC_VERSION: &str = "2.0";
25
26/// LSP error code returned when the server cancels a request and wants the client to retry.
27const SERVER_CANCELLED_CODE: i32 = -32802;
28
29/// Maximum number of retry attempts for server-cancelled requests.
30const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
31
32/// Initial backoff delay for server-cancelled retries (milliseconds).
33const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
34
35/// LSP request methods for which a `-32801` (`ContentModified`) error
36/// response is safe to retry automatically -- also declared to servers via
37/// `general.staleRequestSupport.retryOnContentModified` during initialize
38/// (see [`crate::lsp::LspServer`]'s handshake).
39///
40/// Per the LSP spec, `ContentModified` means the server noticed the document
41/// changed while it was computing a response; the (possibly stale) result may
42/// still be useful, or the client may choose to cancel the request instead.
43/// mcpls chooses to retry, which is safe for a read-only/idempotent request
44/// (hover, references, diagnostics, ...): a stale response is simply
45/// discarded and superseded by a fresh one.
46///
47/// Deliberately excludes every method in
48/// `crate::bridge::translator::edits` (`textDocument/rename`,
49/// `textDocument/formatting`, `textDocument/codeAction`): their result is an
50/// edit the MCP caller applies, and `-32801` means the document changed since
51/// the request was issued, so a retry at the original position could return
52/// an edit for content the caller no longer expects (e.g. renaming a
53/// different symbol than the one originally at that position).
54///
55/// This list only gates `-32801`. `-32802` (`ServerCancelled`) retry is
56/// unaffected and keeps retrying unconditionally for every method, as before.
57pub const CONTENT_MODIFIED_RETRY_METHODS: &[&str] = &[
58    "textDocument/signatureHelp",
59    "textDocument/inlayHint",
60    "textDocument/completion",
61    "textDocument/prepareCallHierarchy",
62    "callHierarchy/incomingCalls",
63    "callHierarchy/outgoingCalls",
64    "textDocument/diagnostic",
65    "textDocument/hover",
66    "textDocument/definition",
67    "textDocument/references",
68    "textDocument/implementation",
69    "textDocument/typeDefinition",
70    "textDocument/documentSymbol",
71    "workspace/symbol",
72];
73
74/// Byte-length threshold for truncating an LSP error message before logging it.
75///
76/// Kept short since this feeds a single log line in [`LspClient::request`]
77/// -- `warn!` while a transient error is being retried, `error!` once it is
78/// actually surfaced to the caller -- not the MCP caller itself; see
79/// `MAX_ERROR_MESSAGE_CALLER_BYTES` for that budget.
80const MAX_ERROR_MESSAGE_LOG_BYTES: usize = 200;
81
82/// Byte-length threshold for the LSP error message forwarded to the MCP
83/// caller in [`Error::LspServerError`] (#313).
84///
85/// Deliberately much larger than `MAX_ERROR_MESSAGE_LOG_BYTES`: a
86/// legitimate LSP error (e.g. a verbose rust-analyzer type-mismatch
87/// diagnostic reported through an error response) can run into the low
88/// kilobytes, and that detail is useful to the calling model -- a log line
89/// should stay terse, but a truncated-to-200-bytes error handed to the
90/// model would cut off real content on every longer-but-honest error. Still
91/// far below #311's 256 KiB cache-entry cap: this string is echoed directly
92/// into the MCP tool result / model context, not merely cached.
93const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
94
95/// Upper bound on the effective timeout for completion requests, regardless
96/// of `request_timeout_seconds`.
97///
98/// Completions are latency-sensitive: a completion list that takes longer
99/// than this is no longer useful to the caller. This is a deliberate MVP
100/// ceiling, not an oversight — completions cannot be configured above this
101/// value today. See [`LspClient::completion_timeout`].
102const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
103
104/// Type alias for pending request tracking map.
105type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
106
107/// LSP client with async request/response handling.
108///
109/// This client manages communication with an LSP server, handling:
110/// - Concurrent requests with unique ID tracking
111/// - Background message loop for receiving responses
112/// - Timeout support for all requests
113/// - Graceful shutdown
114#[derive(Debug)]
115pub struct LspClient {
116    /// Configuration for this LSP server.
117    config: LspServerConfig,
118
119    /// Current server state.
120    state: Arc<Mutex<super::ServerState>>,
121
122    /// Atomic counter for request IDs.
123    request_counter: Arc<AtomicI64>,
124
125    /// Command sender for outbound messages.
126    command_tx: mpsc::Sender<ClientCommand>,
127
128    /// Requests awaiting a response, shared with the background message loop.
129    ///
130    /// Exposed here (not just captured by the loop) so [`Self::request`] can
131    /// remove its own entry on timeout instead of leaking it, and so a
132    /// connection known to be dead can fail its stragglers immediately via
133    /// [`Self::fail_pending_requests`] rather than leaving each to discover
134    /// that only when its own timeout elapses.
135    pending_requests: Arc<Mutex<PendingRequests>>,
136
137    /// Background receiver task handle.
138    receiver_task: Option<JoinHandle<Result<()>>>,
139}
140
141impl Clone for LspClient {
142    /// Creates a clone that shares the underlying connection.
143    ///
144    /// The clone does not own the receiver task and cannot perform shutdown.
145    /// All clones share the same command channel for sending requests.
146    fn clone(&self) -> Self {
147        Self {
148            config: self.config.clone(),
149            state: Arc::clone(&self.state),
150            request_counter: Arc::clone(&self.request_counter),
151            command_tx: self.command_tx.clone(),
152            pending_requests: Arc::clone(&self.pending_requests),
153            receiver_task: None,
154        }
155    }
156}
157
158/// Commands for client control.
159enum ClientCommand {
160    /// Send a request and wait for response.
161    SendRequest {
162        request: JsonRpcRequest,
163        response_tx: oneshot::Sender<Result<Value>>,
164    },
165    /// Send a notification (no response expected).
166    SendNotification {
167        method: String,
168        params: Option<Value>,
169    },
170    /// Shutdown the client.
171    Shutdown,
172}
173
174impl LspClient {
175    /// Create a new LSP client with the given configuration.
176    ///
177    /// The client starts in an uninitialized state. Call `initialize()` to
178    /// start the server and complete the initialization handshake.
179    #[must_use]
180    pub fn new(config: LspServerConfig) -> Self {
181        // Placeholder channel - the receiver is intentionally dropped since
182        // the client starts uninitialized. A real channel is created when
183        // `from_transport` or `from_transport_with_notifications` is called.
184        let (command_tx, _command_rx) = mpsc::channel(1); // Minimal capacity for placeholder
185
186        Self {
187            config,
188            state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
189            request_counter: Arc::new(AtomicI64::new(1)),
190            command_tx,
191            pending_requests: Arc::new(Mutex::new(HashMap::new())),
192            receiver_task: None,
193        }
194    }
195
196    /// Create client from transport (for testing or custom spawning).
197    ///
198    /// This method initializes the background message loop with the provided transport.
199    #[cfg(test)]
200    pub(crate) fn from_transport(config: LspServerConfig, transport: LspTransport) -> Self {
201        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
202        let request_counter = Arc::new(AtomicI64::new(1));
203        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
204
205        let (command_tx, command_rx) = mpsc::channel(100);
206
207        let receiver_task = tokio::spawn(Self::message_loop(
208            transport,
209            command_rx,
210            Arc::clone(&pending_requests),
211            None,
212        ));
213
214        Self {
215            config,
216            state,
217            request_counter,
218            command_tx,
219            pending_requests,
220            receiver_task: Some(receiver_task),
221        }
222    }
223
224    /// Create client from transport with notification forwarding.
225    ///
226    /// Notifications received from the LSP server will be parsed and sent
227    /// through the provided channel.
228    pub(crate) fn from_transport_with_notifications(
229        config: LspServerConfig,
230        transport: LspTransport,
231        notification_tx: mpsc::Sender<LspNotification>,
232    ) -> Self {
233        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
234        let request_counter = Arc::new(AtomicI64::new(1));
235        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
236
237        let (command_tx, command_rx) = mpsc::channel(100);
238
239        let receiver_task = tokio::spawn(Self::message_loop(
240            transport,
241            command_rx,
242            Arc::clone(&pending_requests),
243            Some(notification_tx),
244        ));
245
246        Self {
247            config,
248            state,
249            request_counter,
250            command_tx,
251            pending_requests,
252            receiver_task: Some(receiver_task),
253        }
254    }
255
256    /// Get the language ID for this client.
257    #[must_use]
258    pub fn language_id(&self) -> &str {
259        &self.config.language_id
260    }
261
262    /// Get the current server state.
263    pub async fn state(&self) -> super::ServerState {
264        *self.state.lock().await
265    }
266
267    /// The timeout applied to a single LSP request attempt, derived from
268    /// [`LspServerConfig::request_timeout_seconds`].
269    ///
270    /// This bounds one attempt, not a whole tool call: [`Self::request`]
271    /// retries up to `SERVER_CANCELLED_MAX_RETRIES` (3) additional times on a
272    /// `-32802` (`ServerCancelled`) or `-32801` (`ContentModified`) response,
273    /// sharing one attempt budget between the two codes, so the worst-case
274    /// latency for a single tool call is `4 * request_timeout() + 3.5s` (the
275    /// sum of the retry backoff delays).
276    ///
277    /// The configured value is clamped to the range from 1 second to
278    /// [`MAX_TIMEOUT_SECONDS`]. [`crate::serve`]/[`crate::serve_with`] now
279    /// validate the top-level `ServerConfig` (via [`ServerConfig::validate`],
280    /// which rejects `request_timeout_seconds` that is `0` or greater than
281    /// [`MAX_TIMEOUT_SECONDS`]) regardless of whether it came from
282    /// [`ServerConfig::load_from`] or was built programmatically by the
283    /// caller. But `Self::new`, [`super::LspServer::spawn`], and
284    /// [`super::LspServer::spawn_batch`] are all `pub` and take an
285    /// [`LspServerConfig`] (or [`super::ServerInitConfig`] wrapping one)
286    /// directly, bypassing that top-level validation entirely — it operates
287    /// on the top-level `ServerConfig`, not the per-server one. This clamp is
288    /// the last line of defense against a zero-duration timeout that would
289    /// fail every request instantly, or an astronomically large one that
290    /// tokio's `timeout`/`sleep` would silently treat as unbounded (they fall
291    /// back to `Instant::far_future()` rather than panicking), for a caller
292    /// reaching either of these levels directly.
293    ///
294    /// [`ServerConfig::load_from`]: crate::config::ServerConfig::load_from
295    /// [`ServerConfig::validate`]: crate::config::ServerConfig::validate
296    /// [`MAX_TIMEOUT_SECONDS`]: crate::config::MAX_TIMEOUT_SECONDS
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use std::time::Duration;
302    /// use mcpls_core::config::LspServerConfig;
303    /// use mcpls_core::lsp::LspClient;
304    ///
305    /// let mut config = LspServerConfig::rust_analyzer();
306    /// config.request_timeout_seconds = 45;
307    /// let client = LspClient::new(config);
308    ///
309    /// assert_eq!(client.request_timeout(), Duration::from_secs(45));
310    /// ```
311    #[must_use]
312    pub fn request_timeout(&self) -> Duration {
313        Duration::from_secs(
314            self.config
315                .request_timeout_seconds
316                .clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
317        )
318    }
319
320    /// The timeout applied to completion (`textDocument/completion`) requests.
321    ///
322    /// Equal to [`Self::request_timeout`], capped at 10 seconds. Completions
323    /// cannot be configured above this cap by any
324    /// value of `request_timeout_seconds` — if that proves insufficient in
325    /// practice, the fix is a dedicated `completion_timeout_seconds` field,
326    /// not raising this cap.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use std::time::Duration;
332    /// use mcpls_core::config::LspServerConfig;
333    /// use mcpls_core::lsp::LspClient;
334    ///
335    /// let mut config = LspServerConfig::rust_analyzer();
336    /// config.request_timeout_seconds = 300;
337    /// let client = LspClient::new(config);
338    ///
339    /// // Capped at 10s even though request_timeout_seconds is 300.
340    /// assert_eq!(client.completion_timeout(), Duration::from_secs(10));
341    /// assert!(client.completion_timeout() <= client.request_timeout());
342    /// ```
343    #[must_use]
344    pub fn completion_timeout(&self) -> Duration {
345        self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
346    }
347
348    /// Send request and wait for response with timeout.
349    ///
350    /// Automatically retries up to 3 times when the server returns error code
351    /// -32802 (`ServerCancelled`, any method) or -32801 (`ContentModified`,
352    /// only for methods in `CONTENT_MODIFIED_RETRY_METHODS`) -- gated by
353    /// `data.retriggerRequest` when present -- using exponential backoff
354    /// starting at 500 ms. Both codes share the same attempt budget: a
355    /// request that hits -32801 then -32802 does not get 8 attempts, only 4.
356    ///
357    /// This method owns the severity of LSP error-response logging (#392):
358    /// a transient error about to be retried logs at `warn!`, while `error!`
359    /// is reserved for an error actually surfaced to the caller -- retry
360    /// exhaustion or a non-retryable code/method combination. This is
361    /// deliberately not decided in `message_loop_inner`, which parses the
362    /// response before knowing whether a retry will follow.
363    ///
364    /// # Type Parameters
365    ///
366    /// * `P` - The type of the request parameters (must be serializable)
367    /// * `R` - The type of the response result (must be deserializable)
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if:
372    /// - Server has shut down
373    /// - Request times out
374    /// - Response cannot be deserialized
375    /// - LSP server returns an error
376    pub async fn request<P, R>(
377        &self,
378        method: &str,
379        params: P,
380        timeout_duration: Duration,
381    ) -> Result<R>
382    where
383        P: Serialize,
384        R: DeserializeOwned,
385    {
386        let params_value = serde_json::to_value(params)?;
387        let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
388
389        for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
390            if attempt > 0 {
391                debug!(
392                    "Retrying {} (attempt {}/{}), backoff={}ms",
393                    method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
394                );
395                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
396                delay_ms *= 2;
397            }
398
399            let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
400            let (response_tx, response_rx) = oneshot::channel();
401            let request = JsonRpcRequest {
402                jsonrpc: JSONRPC_VERSION.to_string(),
403                id: id.clone(),
404                method: method.to_string(),
405                params: Some(params_value.clone()),
406            };
407
408            debug!("Sending request: {} (id={:?})", method, id);
409
410            self.command_tx
411                .send(ClientCommand::SendRequest {
412                    request,
413                    response_tx,
414                })
415                .await
416                .map_err(|_| Error::ServerTerminated)?;
417
418            let outcome = match timeout(timeout_duration, response_rx).await {
419                Ok(received) => received.map_err(|_| Error::ServerTerminated)?,
420                Err(_elapsed) => {
421                    // The response may still arrive after this point (the
422                    // server is just slow, not dead), but nothing will ever
423                    // read it again -- drop the now-orphaned entry instead of
424                    // leaking it in `pending_requests` forever.
425                    self.pending_requests.lock().await.remove(&id);
426                    return Err(Error::Timeout(timeout_duration.as_secs()));
427                }
428            };
429
430            match outcome {
431                Ok(result_value) => {
432                    return serde_json::from_value(result_value).map_err(|e| {
433                        Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
434                    });
435                }
436                Err(Error::LspServerError {
437                    code,
438                    message,
439                    data,
440                }) if (code == SERVER_CANCELLED_CODE
441                    || (LspErrorCodes::from(code) == LspErrorCodes::ContentModified
442                        && CONTENT_MODIFIED_RETRY_METHODS.contains(&method)))
443                    && Self::should_retrigger(data.as_ref()) =>
444                {
445                    if attempt == SERVER_CANCELLED_MAX_RETRIES {
446                        // Same "LSP error response: ..." prefix as the
447                        // non-retryable branch below -- a log-grep alert on
448                        // that prefix must catch every error actually
449                        // surfaced to the caller, retry exhaustion included.
450                        error!(
451                            "LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
452                            Self::truncate_error_message_for_log(&message),
453                            code,
454                            method,
455                            id
456                        );
457                        return Err(Error::LspServerError {
458                            code,
459                            message,
460                            data,
461                        });
462                    }
463                    warn!(
464                        "LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
465                        Self::truncate_error_message_for_log(&message),
466                        code,
467                        method,
468                        id
469                    );
470                    // continue loop for next attempt
471                }
472                Err(Error::LspServerError {
473                    code,
474                    message,
475                    data,
476                }) => {
477                    error!(
478                        "LSP error response: {} (code {}) on '{}' (id={:?})",
479                        Self::truncate_error_message_for_log(&message),
480                        code,
481                        method,
482                        id
483                    );
484                    return Err(Error::LspServerError {
485                        code,
486                        message,
487                        data,
488                    });
489                }
490                Err(e) => return Err(e),
491            }
492        }
493
494        Err(Error::ServerTerminated)
495    }
496
497    /// Send a typed LSP request, deriving both the method string and the
498    /// result type from `R`'s [`lsp_types::Request`] implementation so they
499    /// cannot drift independently -- unlike [`Self::request`], which takes
500    /// the method string and result type as two unchecked, hand-picked
501    /// values.
502    ///
503    /// # Errors
504    ///
505    /// See [`Self::request`].
506    pub async fn request_typed<R>(
507        &self,
508        params: R::Params,
509        timeout_duration: Duration,
510    ) -> Result<R::Result>
511    where
512        R: lsp_types::Request,
513    {
514        self.request(R::METHOD.as_str(), params, timeout_duration)
515            .await
516    }
517
518    /// Returns true when the error data from a retryable "please retry" LSP
519    /// response (`ServerCancelled` -32802 or `ContentModified` -32801)
520    /// indicates a retry should happen.
521    ///
522    /// The LSP spec defines `data.retriggerRequest` only for diagnostic
523    /// requests' `ServerCancelled` responses (`DiagnosticServerCancellationData`)
524    /// -- it is not part of the general `ServerCancelled` or `ContentModified`
525    /// contract for other methods. mcpls checks this field whenever it is
526    /// present regardless of method (harmless for non-diagnostic methods,
527    /// since a compliant server won't send it there) and defaults to
528    /// retrying when the field is absent, since retrying is the more useful
529    /// default for a request that would otherwise surface as a hard error to
530    /// the MCP caller.
531    fn should_retrigger(data: Option<&Value>) -> bool {
532        data.is_none_or(|v| {
533            v.get("retriggerRequest")
534                .and_then(Value::as_bool)
535                .unwrap_or(true)
536        })
537    }
538
539    /// Fail every request still parked in `pending_requests` with
540    /// `Error::ServerTerminated`, instead of leaving each to discover a dead
541    /// connection only when its own timeout elapses.
542    ///
543    /// Intended for a client that is about to be discarded -- e.g.
544    /// superseded by a respawned replacement for the same server -- so
545    /// callers still waiting on it unblock immediately.
546    pub(crate) async fn fail_pending_requests(&self) {
547        let mut pending = self.pending_requests.lock().await;
548        for (_, sender) in pending.drain() {
549            let _ = sender.send(Err(Error::ServerTerminated));
550        }
551    }
552
553    /// Send notification (fire-and-forget, no response expected).
554    ///
555    /// # Errors
556    ///
557    /// Returns an error if the server has shut down.
558    pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
559    where
560        P: Serialize,
561    {
562        let params_value = serde_json::to_value(params)?;
563
564        debug!("Sending notification: {}", method);
565
566        self.command_tx
567            .send(ClientCommand::SendNotification {
568                method: method.to_string(),
569                params: Some(params_value),
570            })
571            .await
572            .map_err(|_| Error::ServerTerminated)?;
573
574        Ok(())
575    }
576
577    /// Shutdown client gracefully.
578    ///
579    /// This sends a shutdown command to the background task and waits for it to complete.
580    ///
581    /// # Errors
582    ///
583    /// Returns an error if the background task failed.
584    pub async fn shutdown(mut self) -> Result<()> {
585        debug!("Shutting down LSP client");
586
587        let _ = self.command_tx.send(ClientCommand::Shutdown).await;
588
589        if let Some(task) = self.receiver_task.take() {
590            task.await
591                .map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
592        }
593
594        *self.state.lock().await = super::ServerState::Shutdown;
595
596        Ok(())
597    }
598
599    /// Background task: handle message I/O.
600    ///
601    /// This task runs in the background, handling:
602    /// - Outbound requests and notifications
603    /// - Inbound responses and server notifications
604    /// - Matching responses to pending requests
605    async fn message_loop(
606        mut transport: LspTransport,
607        mut command_rx: mpsc::Receiver<ClientCommand>,
608        pending_requests: Arc<Mutex<PendingRequests>>,
609        notification_tx: Option<mpsc::Sender<LspNotification>>,
610    ) -> Result<()> {
611        debug!("Message loop started");
612        let result = Self::message_loop_inner(
613            &mut transport,
614            &mut command_rx,
615            &pending_requests,
616            notification_tx.as_ref(),
617        )
618        .await;
619        if let Err(ref e) = result {
620            error!("Message loop exiting with error: {}", e);
621        } else {
622            debug!("Message loop exiting normally");
623        }
624        result
625    }
626
627    /// Truncate an LSP server's error message for the `tracing::error!` log
628    /// line, bounding it to at most [`MAX_ERROR_MESSAGE_LOG_BYTES`] bytes
629    /// (the full formatted string is slightly longer).
630    ///
631    /// Log-line use only -- the message forwarded to the MCP caller in
632    /// [`Error::LspServerError`] is truncated separately, to the larger
633    /// [`MAX_ERROR_MESSAGE_CALLER_BYTES`] (#313).
634    fn truncate_error_message_for_log(message: &str) -> String {
635        crate::util::truncate_str(message, MAX_ERROR_MESSAGE_LOG_BYTES)
636    }
637
638    async fn message_loop_inner(
639        transport: &mut LspTransport,
640        command_rx: &mut mpsc::Receiver<ClientCommand>,
641        pending_requests: &Arc<Mutex<PendingRequests>>,
642        notification_tx: Option<&mpsc::Sender<LspNotification>>,
643    ) -> Result<()> {
644        loop {
645            tokio::select! {
646                Some(command) = command_rx.recv() => {
647                    match command {
648                        ClientCommand::SendRequest { request, response_tx } => {
649                            pending_requests.lock().await.insert(
650                                request.id.clone(),
651                                response_tx,
652                            );
653
654                            let value = serde_json::to_value(&request)?;
655                            transport.send(&value).await?;
656                        }
657                        ClientCommand::SendNotification { method, params } => {
658                            let notification = serde_json::json!({
659                                "jsonrpc": "2.0",
660                                "method": method,
661                                "params": params,
662                            });
663                            transport.send(&notification).await?;
664                        }
665                        ClientCommand::Shutdown => {
666                            debug!("Client shutdown requested");
667                            break;
668                        }
669                    }
670                }
671
672                message = transport.receive() => {
673                    let message = match message {
674                        Ok(m) => m,
675                        Err(e) => {
676                            error!("Transport receive error: {}", e);
677                            return Err(e);
678                        }
679                    };
680                    match message {
681                        InboundMessage::Response(response) => {
682                            trace!("Received response: id={:?}", response.id);
683
684                            let sender = pending_requests.lock().await.remove(&response.id);
685
686                            if let Some(sender) = sender {
687                                if let Some(error) = response.error {
688                                    // Deliberately not logged at `error!` here: this fires
689                                    // for every attempt, before `LspClient::request`'s retry
690                                    // loop knows whether the error is transient and about to
691                                    // be retried (-32802, or -32801 for an allowlisted
692                                    // method). Logging unconditionally at this point would
693                                    // emit a spurious ERROR line for errors that are retried
694                                    // and succeed. `request` logs at `warn!` on retry and
695                                    // `error!` once the error is actually surfaced to the
696                                    // caller (retry exhaustion or a non-retryable error);
697                                    // the response id is already traced above.
698                                    trace!(
699                                        "LSP error response: {} (code {})",
700                                        Self::truncate_error_message_for_log(&error.message),
701                                        error.code
702                                    );
703                                    // Truncated separately from the log line, to the larger
704                                    // MAX_ERROR_MESSAGE_CALLER_BYTES -- the raw message is
705                                    // unbounded and attacker-influenceable (#313), but a
706                                    // log-line-sized cut would also clip legitimate long
707                                    // errors before the model ever sees them (S2).
708                                    let caller_message = crate::util::truncate_str(
709                                        &error.message,
710                                        MAX_ERROR_MESSAGE_CALLER_BYTES,
711                                    );
712                                    let _ = sender.send(Err(Error::LspServerError {
713                                        code: error.code,
714                                        message: caller_message,
715                                        data: error.data,
716                                    }));
717                                } else if let Some(result) = response.result {
718                                    let _ = sender.send(Ok(result));
719                                } else {
720                                    // LSP spec allows null result for some requests (e.g., hover with no info).
721                                    // Treat as successful response with null value.
722                                    trace!("Response with null result: {:?}", response.id);
723                                    let _ = sender.send(Ok(Value::Null));
724                                }
725                            } else {
726                                warn!("Received response for unknown request ID: {:?}", response.id);
727                            }
728                        }
729                        InboundMessage::Request(request) => {
730                            debug!(
731                                "Received server request: {} (id={:?})",
732                                request.method, request.id
733                            );
734                            let response = Self::server_request_response(request);
735                            let value = serde_json::to_value(&response)?;
736                            transport.send(&value).await?;
737                        }
738                        InboundMessage::Notification(notification) => {
739                            debug!("Received notification: {}", notification.method);
740
741                            // Parse notification into typed variant
742                            let typed = LspNotification::parse(&notification.method, notification.params);
743
744                            // Forward to notification handler if sender is available
745                            if let Some(tx) = notification_tx {
746                                // Log diagnostics count since it's useful for debugging
747                                if let LspNotification::PublishDiagnostics(ref params) = typed {
748                                    debug!(
749                                        "Forwarding diagnostics for {}: {} items",
750                                        params.uri.as_ref(),
751                                        params.diagnostics.len()
752                                    );
753                                } else {
754                                    trace!("Forwarding notification: {:?}", typed);
755                                }
756
757                                // Send the notification with backpressure handling
758                                if tx.try_send(typed).is_err() {
759                                    warn!("Notification channel full or closed, dropping notification");
760                                }
761                            }
762                        }
763                    }
764                }
765            }
766        }
767
768        Ok(())
769    }
770
771    fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
772        match Self::server_request_result(&request.method, request.params.as_ref()) {
773            Ok(result) => JsonRpcResponse {
774                jsonrpc: JSONRPC_VERSION.to_string(),
775                id: request.id,
776                result: Some(result),
777                error: None,
778            },
779            Err(error) => JsonRpcResponse {
780                jsonrpc: JSONRPC_VERSION.to_string(),
781                id: request.id,
782                result: None,
783                error: Some(error),
784            },
785        }
786    }
787
788    fn server_request_result(
789        method: &str,
790        params: Option<&Value>,
791    ) -> std::result::Result<Value, JsonRpcError> {
792        match method {
793            "client/registerCapability"
794            | "client/unregisterCapability"
795            | "workspace/workspaceFolders"
796            | "workspace/diagnostic/refresh"
797            | "workspace/semanticTokens/refresh"
798            | "workspace/inlayHint/refresh"
799            | "workspace/codeLens/refresh"
800            | "window/showMessageRequest" => Ok(Value::Null),
801            "workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
802            "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
803            _ => Err(JsonRpcError {
804                code: -32601,
805                message: format!("Unhandled server request: {method}"),
806                data: None,
807            }),
808        }
809    }
810
811    fn workspace_configuration_result(params: Option<&Value>) -> Value {
812        let item_count = params
813            .and_then(|value| value.get("items"))
814            .and_then(Value::as_array)
815            .map_or(0, Vec::len);
816
817        Value::Array(vec![Value::Null; item_count])
818    }
819}
820
821#[cfg(test)]
822#[allow(clippy::unwrap_used)]
823mod tests {
824    use super::*;
825
826    #[test]
827    fn test_request_id_generation() {
828        let counter = AtomicI64::new(1);
829
830        let id1 = counter.fetch_add(1, Ordering::SeqCst);
831        let id2 = counter.fetch_add(1, Ordering::SeqCst);
832        let id3 = counter.fetch_add(1, Ordering::SeqCst);
833
834        assert_eq!(id1, 1);
835        assert_eq!(id2, 2);
836        assert_eq!(id3, 3);
837    }
838
839    #[test]
840    fn test_client_creation() {
841        let config = LspServerConfig::rust_analyzer();
842
843        let client = LspClient::new(config);
844        assert_eq!(client.language_id(), "rust");
845    }
846
847    #[test]
848    fn test_client_clone() {
849        let config = LspServerConfig::rust_analyzer();
850        let client = LspClient::new(config);
851
852        #[allow(clippy::redundant_clone)]
853        let cloned = client.clone();
854        assert_eq!(cloned.language_id(), "rust");
855
856        assert!(
857            cloned.receiver_task.is_none(),
858            "Cloned client should not own receiver task"
859        );
860    }
861
862    #[test]
863    fn test_request_timeout_and_completion_timeout_at_default() {
864        let config = LspServerConfig::rust_analyzer();
865        let client = LspClient::new(config);
866
867        assert_eq!(client.request_timeout(), Duration::from_secs(30));
868        assert_eq!(client.completion_timeout(), Duration::from_secs(10));
869    }
870
871    #[test]
872    fn test_completion_timeout_clamps_to_ten_seconds() {
873        for secs in [1, 2, 3, 30, 300] {
874            let mut config = LspServerConfig::rust_analyzer();
875            config.request_timeout_seconds = secs;
876            let client = LspClient::new(config);
877
878            assert_eq!(
879                client.completion_timeout(),
880                Duration::from_secs(secs.min(10)),
881                "request_timeout_seconds={secs}"
882            );
883            assert!(client.completion_timeout() <= client.request_timeout());
884        }
885    }
886
887    #[test]
888    fn test_request_timeout_clamps_zero_to_one_second() {
889        let mut config = LspServerConfig::rust_analyzer();
890        config.request_timeout_seconds = 0;
891        let client = LspClient::new(config);
892
893        assert_eq!(client.request_timeout(), Duration::from_secs(1));
894        assert_eq!(client.completion_timeout(), Duration::from_secs(1));
895    }
896
897    #[test]
898    fn test_request_timeout_clamps_above_max_to_max() {
899        let mut config = LspServerConfig::rust_analyzer();
900        config.request_timeout_seconds = u64::MAX;
901        let client = LspClient::new(config);
902
903        assert_eq!(
904            client.request_timeout(),
905            Duration::from_secs(crate::config::MAX_TIMEOUT_SECONDS)
906        );
907    }
908
909    #[test]
910    fn test_request_timeout_independent_per_server() {
911        let mut config_a = LspServerConfig::rust_analyzer();
912        config_a.request_timeout_seconds = 5;
913        let mut config_b = LspServerConfig::pyright();
914        config_b.request_timeout_seconds = 15;
915
916        let client_a = LspClient::new(config_a);
917        let client_b = LspClient::new(config_b);
918
919        assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
920        assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
921    }
922
923    #[test]
924    fn test_register_capability_request_is_acknowledged() {
925        let request = JsonRpcRequest {
926            jsonrpc: JSONRPC_VERSION.to_string(),
927            id: RequestId::String("ts1".to_string()),
928            method: "client/registerCapability".to_string(),
929            params: Some(serde_json::json!({ "registrations": [] })),
930        };
931
932        let response = LspClient::server_request_response(request);
933
934        assert_eq!(response.id, RequestId::String("ts1".to_string()));
935        assert_eq!(response.result, Some(Value::Null));
936        assert!(response.error.is_none());
937    }
938
939    #[test]
940    fn test_workspace_configuration_request_returns_null_per_item() {
941        let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
942            "items": [{ "section": "typescript" }, { "section": "editor" }]
943        })));
944
945        assert_eq!(result, serde_json::json!([null, null]));
946    }
947
948    #[test]
949    fn test_unknown_server_request_returns_method_not_found() {
950        let request = JsonRpcRequest {
951            jsonrpc: JSONRPC_VERSION.to_string(),
952            id: RequestId::String("unknown-1".to_string()),
953            method: "custom/request".to_string(),
954            params: None,
955        };
956
957        let response = LspClient::server_request_response(request);
958
959        assert!(response.result.is_none());
960        match response.error {
961            Some(error) => {
962                assert_eq!(error.code, -32601);
963                assert_eq!(error.message, "Unhandled server request: custom/request");
964            }
965            None => panic!("unknown request should return error"),
966        }
967    }
968
969    #[tokio::test]
970    async fn test_null_response_handling() {
971        use crate::lsp::types::{JsonRpcResponse, RequestId};
972
973        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
974
975        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
976
977        pending_requests
978            .lock()
979            .await
980            .insert(RequestId::Number(1), response_tx);
981
982        let null_response = JsonRpcResponse {
983            jsonrpc: "2.0".to_string(),
984            id: RequestId::Number(1),
985            result: None,
986            error: None,
987        };
988
989        let sender = pending_requests.lock().await.remove(&null_response.id);
990        if let Some(sender) = sender {
991            let _ = sender.send(Ok(Value::Null));
992        }
993
994        let timeout_result =
995            tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
996
997        assert!(timeout_result.is_ok(), "Should not timeout");
998
999        let channel_result = timeout_result.unwrap();
1000        assert!(
1001            channel_result.is_ok(),
1002            "Channel should not be closed: {:?}",
1003            channel_result.err()
1004        );
1005
1006        let response = channel_result.unwrap();
1007        assert!(
1008            response.is_ok(),
1009            "Should receive Ok(Value::Null), not Err: {:?}",
1010            response.err()
1011        );
1012
1013        let value = response.unwrap();
1014        assert_eq!(value, Value::Null, "Should receive Value::Null");
1015    }
1016
1017    #[tokio::test]
1018    async fn test_error_response_handling() {
1019        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
1020
1021        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1022        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
1023
1024        pending_requests
1025            .lock()
1026            .await
1027            .insert(RequestId::Number(1), response_tx);
1028
1029        let error_response = JsonRpcResponse {
1030            jsonrpc: "2.0".to_string(),
1031            id: RequestId::Number(1),
1032            result: None,
1033            error: Some(JsonRpcError {
1034                code: -32601,
1035                message: "Method not found".to_string(),
1036                data: None,
1037            }),
1038        };
1039
1040        let sender = pending_requests.lock().await.remove(&error_response.id);
1041        if let Some(sender) = sender
1042            && let Some(error) = error_response.error
1043        {
1044            let _ = sender.send(Err(Error::LspServerError {
1045                code: error.code,
1046                message: error.message,
1047                data: error.data,
1048            }));
1049        }
1050
1051        let result = response_rx.await.unwrap();
1052        assert!(result.is_err(), "Should receive error");
1053
1054        if let Err(Error::LspServerError { code, message, .. }) = result {
1055            assert_eq!(code, -32601);
1056            assert_eq!(message, "Method not found");
1057        } else {
1058            panic!("Expected LspServerError");
1059        }
1060    }
1061
1062    #[tokio::test]
1063    async fn test_unknown_request_id() {
1064        use crate::lsp::types::{JsonRpcResponse, RequestId};
1065
1066        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1067
1068        let response = JsonRpcResponse {
1069            jsonrpc: "2.0".to_string(),
1070            id: RequestId::Number(999),
1071            result: Some(Value::Null),
1072            error: None,
1073        };
1074
1075        let sender = pending_requests.lock().await.remove(&response.id);
1076        assert!(sender.is_none(), "Should not find sender for unknown ID");
1077    }
1078
1079    #[test]
1080    fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
1081        // 199 ASCII bytes followed by a 3-byte UTF-8 char ('€') straddles the byte-200 cut.
1082        let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
1083
1084        let truncated = LspClient::truncate_error_message_for_log(&message);
1085
1086        // Cutting before the multi-byte char keeps the message valid UTF-8 (no panic) and
1087        // pins the payload to 199 bytes, not 200.
1088        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
1089    }
1090
1091    #[test]
1092    fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
1093        let exact = "x".repeat(200);
1094        assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
1095        assert_eq!(LspClient::truncate_error_message_for_log(""), "");
1096    }
1097
1098    #[test]
1099    fn test_truncate_error_message_for_log_truncates_just_above_limit() {
1100        let message = "x".repeat(201);
1101        assert_eq!(
1102            LspClient::truncate_error_message_for_log(&message),
1103            format!("{}... (truncated)", "x".repeat(200))
1104        );
1105    }
1106
1107    #[test]
1108    fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
1109        // A 4-byte emoji run straddling every possible alignment near the byte-200 boundary.
1110        let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
1111
1112        let truncated = LspClient::truncate_error_message_for_log(&message);
1113
1114        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
1115    }
1116
1117    #[tokio::test]
1118    async fn test_concurrent_request_ids() {
1119        let counter = Arc::new(AtomicI64::new(1));
1120
1121        let counter1 = Arc::clone(&counter);
1122        let counter2 = Arc::clone(&counter);
1123        let counter3 = Arc::clone(&counter);
1124
1125        let handles = vec![
1126            tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
1127            tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
1128            tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
1129        ];
1130
1131        let mut ids = Vec::new();
1132        for handle in handles {
1133            ids.push(handle.await.unwrap());
1134        }
1135
1136        ids.sort_unstable();
1137        assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
1138    }
1139
1140    #[test]
1141    fn test_jsonrpc_version_constant() {
1142        assert_eq!(JSONRPC_VERSION, "2.0");
1143    }
1144
1145    /// #239 regression: a request that times out must remove its own entry
1146    /// from `pending_requests` instead of leaking it. `sleep` is used as the
1147    /// "server": it never writes anything to stdout, so no response can ever
1148    /// arrive and the request is guaranteed to time out rather than race a
1149    /// real answer.
1150    ///
1151    /// Unix-only: spawns a real `sleep` subprocess, which is unavailable on
1152    /// the Windows CI runner.
1153    #[cfg(unix)]
1154    #[tokio::test]
1155    async fn test_request_timeout_removes_pending_entry() {
1156        let mut child = tokio::process::Command::new("sleep")
1157            .arg("2")
1158            .stdin(std::process::Stdio::piped())
1159            .stdout(std::process::Stdio::piped())
1160            .kill_on_drop(true)
1161            .spawn()
1162            .unwrap();
1163        let stdin = child.stdin.take().unwrap();
1164        let stdout = child.stdout.take().unwrap();
1165
1166        let transport = LspTransport::new(stdin, stdout);
1167        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1168
1169        let result: Result<Value> = client
1170            .request(
1171                "textDocument/hover",
1172                serde_json::json!({}),
1173                Duration::from_millis(50),
1174            )
1175            .await;
1176
1177        assert!(matches!(result, Err(Error::Timeout(_))), "got {result:?}");
1178        assert!(
1179            client.pending_requests.lock().await.is_empty(),
1180            "timed-out request must not remain in pending_requests"
1181        );
1182    }
1183
1184    /// #249 continuation: a client about to be discarded (e.g. superseded by
1185    /// a respawned replacement) must fail every still-pending request
1186    /// immediately rather than leaving callers to wait out their timeout.
1187    #[tokio::test]
1188    async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
1189        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1190        let (command_tx, _command_rx) = mpsc::channel(1);
1191
1192        let client = LspClient {
1193            config: LspServerConfig::rust_analyzer(),
1194            state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
1195            request_counter: Arc::new(AtomicI64::new(1)),
1196            command_tx,
1197            pending_requests: Arc::clone(&pending_requests),
1198            receiver_task: None,
1199        };
1200
1201        let (tx1, rx1) = oneshot::channel::<Result<Value>>();
1202        let (tx2, rx2) = oneshot::channel::<Result<Value>>();
1203        pending_requests
1204            .lock()
1205            .await
1206            .insert(RequestId::Number(1), tx1);
1207        pending_requests
1208            .lock()
1209            .await
1210            .insert(RequestId::Number(2), tx2);
1211
1212        client.fail_pending_requests().await;
1213
1214        assert!(pending_requests.lock().await.is_empty());
1215        assert!(matches!(rx1.await.unwrap(), Err(Error::ServerTerminated)));
1216        assert!(matches!(rx2.await.unwrap(), Err(Error::ServerTerminated)));
1217    }
1218
1219    #[test]
1220    fn test_should_retrigger_defaults_to_true_when_data_absent() {
1221        assert!(LspClient::should_retrigger(None));
1222    }
1223
1224    #[test]
1225    fn test_should_retrigger_false_when_flag_false() {
1226        assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
1227            "retriggerRequest": false
1228        }))));
1229    }
1230
1231    #[test]
1232    fn test_should_retrigger_true_when_flag_true() {
1233        assert!(LspClient::should_retrigger(Some(&serde_json::json!({
1234            "retriggerRequest": true
1235        }))));
1236    }
1237
1238    mod retry_behavior {
1239        use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
1240
1241        use super::*;
1242        use crate::test_lsp::{
1243            CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
1244            write_response as write_success_response,
1245        };
1246
1247        /// Writes a framed JSON-RPC retryable error response — either
1248        /// `ServerCancelled` (-32802) or `ContentModified` (-32801) — with a
1249        /// `data.retriggerRequest` flag.
1250        ///
1251        /// Kept local rather than promoted to the shared `test_lsp` harness:
1252        /// the `data.retriggerRequest` field is specific to this retry-logic
1253        /// test suite, unlike the generic success/error responses above.
1254        async fn write_retryable_error_response(
1255            stdin: &mut DuplexStream,
1256            id: &Value,
1257            code: i32,
1258            message: &str,
1259            retrigger: bool,
1260        ) {
1261            let response = serde_json::json!({
1262                "jsonrpc": "2.0",
1263                "id": id,
1264                "error": {
1265                    "code": code,
1266                    "message": message,
1267                    "data": { "retriggerRequest": retrigger },
1268                },
1269            });
1270            let content = serde_json::to_string(&response).unwrap();
1271            let header = format!("Content-Length: {}\r\n\r\n", content.len());
1272            stdin.write_all(header.as_bytes()).await.unwrap();
1273            stdin.write_all(content.as_bytes()).await.unwrap();
1274            stdin.flush().await.unwrap();
1275        }
1276
1277        // Not `start_paused`: the retry loop's real backoff sleeps
1278        // interleave with real async I/O on the duplex pipes below, and
1279        // paused virtual time does not reliably auto-advance across both.
1280        //
1281        // Also captures tracing output (#392): retry exhaustion is the one
1282        // scenario where every attempt but the last logs `warn!` and only
1283        // the last logs `error!`, so this doubles as that regression test
1284        // rather than duplicating the same ~3.5s wire choreography in a
1285        // second test just to assert on log severity.
1286        #[tokio::test]
1287        async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
1288            use tracing_subscriber::layer::SubscriberExt as _;
1289
1290            let (client, mut server) = fake_lsp_client();
1291            let captured = CapturedLogs::default();
1292            let subscriber = tracing_subscriber::registry().with(captured.clone());
1293            let guard = tracing::subscriber::set_default(subscriber);
1294
1295            let request_task = tokio::spawn(async move {
1296                client
1297                    .request::<_, Value>(
1298                        "textDocument/hover",
1299                        serde_json::json!({}),
1300                        Duration::from_secs(30),
1301                    )
1302                    .await
1303            });
1304
1305            let mut reader = BufReader::new(&mut server.write_stdout);
1306            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
1307            // attempt gets ServerCancelled, so retries must exhaust rather
1308            // than loop forever or swallow the error.
1309            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1310                let request = read_framed_message(&mut reader).await;
1311                let id = request["id"].clone();
1312                write_retryable_error_response(
1313                    &mut server.read_half_stdin,
1314                    &id,
1315                    SERVER_CANCELLED_CODE,
1316                    "server cancelled the request",
1317                    true,
1318                )
1319                .await;
1320            }
1321
1322            let result = request_task.await.unwrap();
1323
1324            match result {
1325                Err(Error::LspServerError {
1326                    code,
1327                    message,
1328                    data,
1329                }) => {
1330                    // Assert the exact original error surfaces, not merely
1331                    // "some error with this code" -- a freshly constructed
1332                    // placeholder error would satisfy a code-only check.
1333                    assert_eq!(code, SERVER_CANCELLED_CODE);
1334                    assert_eq!(message, "server cancelled the request");
1335                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1336                }
1337                other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
1338            }
1339
1340            drop(guard);
1341            let logs = captured.entries();
1342            assert_eq!(
1343                logs.iter()
1344                    .filter(|(level, _)| *level == tracing::Level::ERROR)
1345                    .count(),
1346                1,
1347                "exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
1348            );
1349            assert!(
1350                logs.iter()
1351                    .any(|(level, msg)| *level == tracing::Level::ERROR
1352                        && msg.contains("LSP error response")
1353                        && msg.contains("retries exhausted")),
1354                "expected an ERROR log sharing the 'LSP error response' prefix and naming \
1355                 retry exhaustion, got: {logs:?}"
1356            );
1357            assert_eq!(
1358                logs.iter()
1359                    .filter(
1360                        |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
1361                    )
1362                    .count(),
1363                usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
1364                "every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
1365            );
1366        }
1367
1368        #[tokio::test]
1369        async fn test_retrigger_false_returns_immediately_without_retry() {
1370            let (client, mut server) = fake_lsp_client();
1371
1372            let request_task = tokio::spawn(async move {
1373                client
1374                    .request::<_, Value>(
1375                        "textDocument/hover",
1376                        serde_json::json!({}),
1377                        Duration::from_secs(30),
1378                    )
1379                    .await
1380            });
1381
1382            let mut reader = BufReader::new(&mut server.write_stdout);
1383            let request = read_framed_message(&mut reader).await;
1384            let id = request["id"].clone();
1385            write_retryable_error_response(
1386                &mut server.read_half_stdin,
1387                &id,
1388                SERVER_CANCELLED_CODE,
1389                "server cancelled the request",
1390                false,
1391            )
1392            .await;
1393
1394            // With `retriggerRequest: false`, `should_retrigger`'s gate on
1395            // the retry branch must short-circuit the loop: the error
1396            // returns well under the first 500ms backoff, and no second
1397            // request is ever sent. If the `&& Self::should_retrigger(..)`
1398            // guard were ever dropped from the retry match arm, this would
1399            // instead retry and both assertions below would fail.
1400            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1401                .await
1402                .unwrap()
1403                .unwrap();
1404
1405            match result {
1406                Err(Error::LspServerError { code, .. }) => {
1407                    assert_eq!(code, SERVER_CANCELLED_CODE);
1408                }
1409                other => panic!("expected immediate ServerCancelled error, got {other:?}"),
1410            }
1411
1412            let second_request =
1413                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1414                    .await;
1415            assert!(
1416                second_request.is_err(),
1417                "no retry should have been sent after retriggerRequest: false"
1418            );
1419        }
1420
1421        #[tokio::test]
1422        async fn test_retry_succeeds_after_one_server_cancelled_response() {
1423            let (client, mut server) = fake_lsp_client();
1424
1425            let request_task = tokio::spawn(async move {
1426                client
1427                    .request::<_, Value>(
1428                        "textDocument/hover",
1429                        serde_json::json!({}),
1430                        Duration::from_secs(30),
1431                    )
1432                    .await
1433            });
1434
1435            let mut reader = BufReader::new(&mut server.write_stdout);
1436
1437            // First attempt is cancelled and must retrigger.
1438            let first = read_framed_message(&mut reader).await;
1439            write_retryable_error_response(
1440                &mut server.read_half_stdin,
1441                &first["id"].clone(),
1442                SERVER_CANCELLED_CODE,
1443                "server cancelled the request",
1444                true,
1445            )
1446            .await;
1447
1448            // Second attempt (after backoff) succeeds -- proves the loop
1449            // genuinely re-sends the request rather than just counting down.
1450            let second = read_framed_message(&mut reader).await;
1451            assert_ne!(
1452                first["id"], second["id"],
1453                "retry must use a fresh request id"
1454            );
1455            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1456            write_success_response(
1457                &mut server.read_half_stdin,
1458                &second["id"].clone(),
1459                expected_result.clone(),
1460            )
1461            .await;
1462
1463            let result = request_task.await.unwrap();
1464            assert_eq!(result.unwrap(), expected_result);
1465        }
1466
1467        #[tokio::test]
1468        async fn test_retry_exhaustion_returns_original_content_modified_error() {
1469            let (client, mut server) = fake_lsp_client();
1470
1471            let request_task = tokio::spawn(async move {
1472                client
1473                    .request::<_, Value>(
1474                        "textDocument/hover",
1475                        serde_json::json!({}),
1476                        Duration::from_secs(30),
1477                    )
1478                    .await
1479            });
1480
1481            let mut reader = BufReader::new(&mut server.write_stdout);
1482            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
1483            // attempt gets ContentModified, so retries must exhaust rather
1484            // than loop forever or swallow the error. -32801 shares the same
1485            // attempt budget as -32802 (FR-002), not an independent one.
1486            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1487                let request = read_framed_message(&mut reader).await;
1488                let id = request["id"].clone();
1489                write_retryable_error_response(
1490                    &mut server.read_half_stdin,
1491                    &id,
1492                    i32::from(LspErrorCodes::ContentModified),
1493                    "content modified",
1494                    true,
1495                )
1496                .await;
1497            }
1498
1499            let result = request_task.await.unwrap();
1500
1501            match result {
1502                Err(Error::LspServerError {
1503                    code,
1504                    message,
1505                    data,
1506                }) => {
1507                    // Assert the exact original error surfaces, not merely
1508                    // "some error with this code" -- a freshly constructed
1509                    // placeholder error would satisfy a code-only check.
1510                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1511                    assert_eq!(message, "content modified");
1512                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1513                }
1514                other => panic!("expected exhausted ContentModified error, got {other:?}"),
1515            }
1516        }
1517
1518        #[tokio::test]
1519        async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
1520            let (client, mut server) = fake_lsp_client();
1521
1522            let request_task = tokio::spawn(async move {
1523                client
1524                    .request::<_, Value>(
1525                        "textDocument/hover",
1526                        serde_json::json!({}),
1527                        Duration::from_secs(30),
1528                    )
1529                    .await
1530            });
1531
1532            let mut reader = BufReader::new(&mut server.write_stdout);
1533            let request = read_framed_message(&mut reader).await;
1534            let id = request["id"].clone();
1535            write_retryable_error_response(
1536                &mut server.read_half_stdin,
1537                &id,
1538                i32::from(LspErrorCodes::ContentModified),
1539                "content modified",
1540                false,
1541            )
1542            .await;
1543
1544            // Same `should_retrigger` gate as -32802: a non-spec-compliant
1545            // server sending `retriggerRequest: false` on -32801 must still
1546            // be honored (FR-006 resolution), short-circuiting the loop well
1547            // under the first 500ms backoff.
1548            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1549                .await
1550                .unwrap()
1551                .unwrap();
1552
1553            match result {
1554                Err(Error::LspServerError { code, .. }) => {
1555                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1556                }
1557                other => panic!("expected immediate ContentModified error, got {other:?}"),
1558            }
1559
1560            let second_request =
1561                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1562                    .await;
1563            assert!(
1564                second_request.is_err(),
1565                "no retry should have been sent after retriggerRequest: false"
1566            );
1567        }
1568
1569        #[tokio::test]
1570        async fn test_retry_succeeds_after_one_content_modified_response() {
1571            let (client, mut server) = fake_lsp_client();
1572
1573            let request_task = tokio::spawn(async move {
1574                client
1575                    .request::<_, Value>(
1576                        "textDocument/hover",
1577                        serde_json::json!({}),
1578                        Duration::from_secs(30),
1579                    )
1580                    .await
1581            });
1582
1583            let mut reader = BufReader::new(&mut server.write_stdout);
1584
1585            // First attempt gets ContentModified and must retrigger.
1586            let first = read_framed_message(&mut reader).await;
1587            write_retryable_error_response(
1588                &mut server.read_half_stdin,
1589                &first["id"].clone(),
1590                i32::from(LspErrorCodes::ContentModified),
1591                "content modified",
1592                true,
1593            )
1594            .await;
1595
1596            // Second attempt (after backoff) succeeds -- proves the loop
1597            // genuinely re-sends the request rather than just counting down.
1598            let second = read_framed_message(&mut reader).await;
1599            assert_ne!(
1600                first["id"], second["id"],
1601                "retry must use a fresh request id"
1602            );
1603            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1604            write_success_response(
1605                &mut server.read_half_stdin,
1606                &second["id"].clone(),
1607                expected_result.clone(),
1608            )
1609            .await;
1610
1611            let result = request_task.await.unwrap();
1612            assert_eq!(result.unwrap(), expected_result);
1613        }
1614
1615        #[tokio::test]
1616        async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
1617            let (client, mut server) = fake_lsp_client();
1618
1619            // `textDocument/rename` is deliberately excluded from
1620            // `CONTENT_MODIFIED_RETRY_METHODS` (its result is an edit the
1621            // caller applies at a position that may no longer be valid once
1622            // the document changed) -- a -32801 response for it must return
1623            // immediately even though `retriggerRequest: true` would pass
1624            // `should_retrigger`'s gate on its own.
1625            let request_task = tokio::spawn(async move {
1626                client
1627                    .request::<_, Value>(
1628                        "textDocument/rename",
1629                        serde_json::json!({}),
1630                        Duration::from_secs(30),
1631                    )
1632                    .await
1633            });
1634
1635            let mut reader = BufReader::new(&mut server.write_stdout);
1636            let request = read_framed_message(&mut reader).await;
1637            let id = request["id"].clone();
1638            write_retryable_error_response(
1639                &mut server.read_half_stdin,
1640                &id,
1641                i32::from(LspErrorCodes::ContentModified),
1642                "content modified",
1643                true,
1644            )
1645            .await;
1646
1647            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1648                .await
1649                .unwrap()
1650                .unwrap();
1651
1652            match result {
1653                Err(Error::LspServerError { code, .. }) => {
1654                    assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
1655                }
1656                other => panic!("expected immediate ContentModified error, got {other:?}"),
1657            }
1658
1659            let second_request =
1660                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1661                    .await;
1662            assert!(
1663                second_request.is_err(),
1664                "no retry should have been sent for a non-allowlisted method"
1665            );
1666        }
1667
1668        /// #313: an oversized, server-controlled error message must be
1669        /// truncated before it reaches the MCP caller in
1670        /// `Error::LspServerError`, not just before it is logged. Routes
1671        /// through the real `message_loop_inner` (via `fake_lsp_client`)
1672        /// rather than constructing the error by hand, so it actually
1673        /// exercises the fix.
1674        #[tokio::test]
1675        async fn test_oversized_error_message_truncated_for_caller() {
1676            let (client, mut server) = fake_lsp_client();
1677
1678            let request_task = tokio::spawn(async move {
1679                client
1680                    .request::<_, Value>(
1681                        "textDocument/hover",
1682                        serde_json::json!({}),
1683                        Duration::from_secs(30),
1684                    )
1685                    .await
1686            });
1687
1688            let mut reader = BufReader::new(&mut server.write_stdout);
1689            let request = read_framed_message(&mut reader).await;
1690            let id = request["id"].clone();
1691            let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
1692            write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
1693                .await;
1694
1695            let result = request_task.await.unwrap();
1696
1697            match result {
1698                Err(Error::LspServerError { code, message, .. }) => {
1699                    assert_eq!(code, -32603);
1700                    assert!(
1701                        message.len() < oversized_message.len(),
1702                        "caller-facing message must be truncated, got {} bytes",
1703                        message.len()
1704                    );
1705                    assert!(message.ends_with("... (truncated)"));
1706                }
1707                other => panic!("expected truncated LspServerError, got {other:?}"),
1708            }
1709        }
1710
1711        /// #313 S2: a legitimate error message longer than the log-line cap
1712        /// (`MAX_ERROR_MESSAGE_LOG_BYTES`, 200 bytes) but shorter than the
1713        /// caller-facing cap must reach the MCP caller intact -- the
1714        /// caller-facing budget must not silently collapse to the log
1715        /// budget.
1716        #[tokio::test]
1717        async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
1718            let (client, mut server) = fake_lsp_client();
1719
1720            let request_task = tokio::spawn(async move {
1721                client
1722                    .request::<_, Value>(
1723                        "textDocument/hover",
1724                        serde_json::json!({}),
1725                        Duration::from_secs(30),
1726                    )
1727                    .await
1728            });
1729
1730            let mut reader = BufReader::new(&mut server.write_stdout);
1731            let request = read_framed_message(&mut reader).await;
1732            let id = request["id"].clone();
1733            let message = "x".repeat(MAX_ERROR_MESSAGE_LOG_BYTES + 50);
1734            write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
1735
1736            let result = request_task.await.unwrap();
1737
1738            match result {
1739                Err(Error::LspServerError {
1740                    message: returned, ..
1741                }) => {
1742                    assert_eq!(
1743                        returned, message,
1744                        "message under the caller cap must not be truncated"
1745                    );
1746                }
1747                other => panic!("expected untruncated LspServerError, got {other:?}"),
1748            }
1749        }
1750
1751        /// #392: a `-32802`/`-32801` error that gets retried and then
1752        /// succeeds must not log at `error!` -- only a `warn!` "will retry"
1753        /// line -- so log-based monitoring does not false-positive on a
1754        /// transient error the retry loop silently recovers from.
1755        #[tokio::test]
1756        async fn test_retried_error_that_recovers_does_not_log_error_level() {
1757            use tracing_subscriber::layer::SubscriberExt as _;
1758
1759            let (client, mut server) = fake_lsp_client();
1760            let captured = CapturedLogs::default();
1761            let subscriber = tracing_subscriber::registry().with(captured.clone());
1762            let guard = tracing::subscriber::set_default(subscriber);
1763
1764            let request_task = tokio::spawn(async move {
1765                client
1766                    .request::<_, Value>(
1767                        "textDocument/hover",
1768                        serde_json::json!({}),
1769                        Duration::from_secs(30),
1770                    )
1771                    .await
1772            });
1773
1774            let mut reader = BufReader::new(&mut server.write_stdout);
1775
1776            let first = read_framed_message(&mut reader).await;
1777            write_retryable_error_response(
1778                &mut server.read_half_stdin,
1779                &first["id"].clone(),
1780                SERVER_CANCELLED_CODE,
1781                "server cancelled the request",
1782                true,
1783            )
1784            .await;
1785
1786            let second = read_framed_message(&mut reader).await;
1787            write_success_response(
1788                &mut server.read_half_stdin,
1789                &second["id"].clone(),
1790                serde_json::json!({ "contents": "resolved on retry" }),
1791            )
1792            .await;
1793
1794            let result = request_task.await.unwrap();
1795            assert!(result.is_ok(), "expected retry to recover, got {result:?}");
1796
1797            drop(guard);
1798            let logs = captured.entries();
1799            assert!(
1800                !logs
1801                    .iter()
1802                    .any(|(level, _)| *level == tracing::Level::ERROR),
1803                "a retried-and-recovered error must not log at ERROR, got: {logs:?}"
1804            );
1805            assert!(
1806                logs.iter().any(
1807                    |(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
1808                ),
1809                "expected a WARN 'will retry' log line, got: {logs:?}"
1810            );
1811        }
1812
1813        /// #392: a `-32801` (`ContentModified`) error for a method outside
1814        /// `CONTENT_MODIFIED_RETRY_METHODS` is never retried, so it must
1815        /// still surface at `error!` on the very first attempt.
1816        #[tokio::test]
1817        async fn test_non_retryable_error_logs_error_level() {
1818            use tracing_subscriber::layer::SubscriberExt as _;
1819
1820            let (client, mut server) = fake_lsp_client();
1821            let captured = CapturedLogs::default();
1822            let subscriber = tracing_subscriber::registry().with(captured.clone());
1823            let guard = tracing::subscriber::set_default(subscriber);
1824
1825            let request_task = tokio::spawn(async move {
1826                client
1827                    .request::<_, Value>(
1828                        "textDocument/rename",
1829                        serde_json::json!({}),
1830                        Duration::from_secs(30),
1831                    )
1832                    .await
1833            });
1834
1835            let mut reader = BufReader::new(&mut server.write_stdout);
1836            let request = read_framed_message(&mut reader).await;
1837            let id = request["id"].clone();
1838            write_retryable_error_response(
1839                &mut server.read_half_stdin,
1840                &id,
1841                i32::from(LspErrorCodes::ContentModified),
1842                "content modified",
1843                true,
1844            )
1845            .await;
1846
1847            let result = request_task.await.unwrap();
1848            assert!(result.is_err(), "expected a non-retryable error");
1849
1850            drop(guard);
1851            let logs = captured.entries();
1852            assert!(
1853                logs.iter()
1854                    .any(|(level, msg)| *level == tracing::Level::ERROR
1855                        && msg.contains("LSP error response")
1856                        && msg.contains("content modified")),
1857                "a non-retryable error must still surface an ERROR log sharing the \
1858                 'LSP error response' prefix, got: {logs:?}"
1859            );
1860        }
1861    }
1862}