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 serde::Serialize;
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10use tokio::sync::{Mutex, mpsc, oneshot};
11use tokio::task::JoinHandle;
12use tokio::time::{Duration, timeout};
13use tracing::{debug, error, trace, warn};
14
15use crate::config::LspServerConfig;
16use crate::error::{Error, Result};
17use crate::lsp::transport::LspTransport;
18use crate::lsp::types::{
19    InboundMessage, JsonRpcError, JsonRpcRequest, JsonRpcResponse, LspNotification, RequestId,
20};
21
22/// JSON-RPC protocol version.
23const JSONRPC_VERSION: &str = "2.0";
24
25/// LSP error code returned when the server cancels a request and wants the client to retry.
26const SERVER_CANCELLED_CODE: i32 = -32802;
27
28/// Maximum number of retry attempts for server-cancelled requests.
29const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
30
31/// Initial backoff delay for server-cancelled retries (milliseconds).
32const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
33
34/// Byte-length threshold for truncating an LSP error message before logging it.
35///
36/// Kept short since this feeds a single `tracing::error!` log line, not the
37/// MCP caller -- see `MAX_ERROR_MESSAGE_CALLER_BYTES` for that budget.
38const MAX_ERROR_MESSAGE_LOG_BYTES: usize = 200;
39
40/// Byte-length threshold for the LSP error message forwarded to the MCP
41/// caller in [`Error::LspServerError`] (#313).
42///
43/// Deliberately much larger than `MAX_ERROR_MESSAGE_LOG_BYTES`: a
44/// legitimate LSP error (e.g. a verbose rust-analyzer type-mismatch
45/// diagnostic reported through an error response) can run into the low
46/// kilobytes, and that detail is useful to the calling model -- a log line
47/// should stay terse, but a truncated-to-200-bytes error handed to the
48/// model would cut off real content on every longer-but-honest error. Still
49/// far below #311's 256 KiB cache-entry cap: this string is echoed directly
50/// into the MCP tool result / model context, not merely cached.
51const MAX_ERROR_MESSAGE_CALLER_BYTES: usize = 4 * 1024;
52
53/// Upper bound on the effective timeout for completion requests, regardless
54/// of `request_timeout_seconds`.
55///
56/// Completions are latency-sensitive: a completion list that takes longer
57/// than this is no longer useful to the caller. This is a deliberate MVP
58/// ceiling, not an oversight — completions cannot be configured above this
59/// value today. See [`LspClient::completion_timeout`].
60const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
61
62/// Type alias for pending request tracking map.
63type PendingRequests = HashMap<RequestId, oneshot::Sender<Result<Value>>>;
64
65/// LSP client with async request/response handling.
66///
67/// This client manages communication with an LSP server, handling:
68/// - Concurrent requests with unique ID tracking
69/// - Background message loop for receiving responses
70/// - Timeout support for all requests
71/// - Graceful shutdown
72#[derive(Debug)]
73pub struct LspClient {
74    /// Configuration for this LSP server.
75    config: LspServerConfig,
76
77    /// Current server state.
78    state: Arc<Mutex<super::ServerState>>,
79
80    /// Atomic counter for request IDs.
81    request_counter: Arc<AtomicI64>,
82
83    /// Command sender for outbound messages.
84    command_tx: mpsc::Sender<ClientCommand>,
85
86    /// Requests awaiting a response, shared with the background message loop.
87    ///
88    /// Exposed here (not just captured by the loop) so [`Self::request`] can
89    /// remove its own entry on timeout instead of leaking it, and so a
90    /// connection known to be dead can fail its stragglers immediately via
91    /// [`Self::fail_pending_requests`] rather than leaving each to discover
92    /// that only when its own timeout elapses.
93    pending_requests: Arc<Mutex<PendingRequests>>,
94
95    /// Background receiver task handle.
96    receiver_task: Option<JoinHandle<Result<()>>>,
97}
98
99impl Clone for LspClient {
100    /// Creates a clone that shares the underlying connection.
101    ///
102    /// The clone does not own the receiver task and cannot perform shutdown.
103    /// All clones share the same command channel for sending requests.
104    fn clone(&self) -> Self {
105        Self {
106            config: self.config.clone(),
107            state: Arc::clone(&self.state),
108            request_counter: Arc::clone(&self.request_counter),
109            command_tx: self.command_tx.clone(),
110            pending_requests: Arc::clone(&self.pending_requests),
111            receiver_task: None,
112        }
113    }
114}
115
116/// Commands for client control.
117enum ClientCommand {
118    /// Send a request and wait for response.
119    SendRequest {
120        request: JsonRpcRequest,
121        response_tx: oneshot::Sender<Result<Value>>,
122    },
123    /// Send a notification (no response expected).
124    SendNotification {
125        method: String,
126        params: Option<Value>,
127    },
128    /// Shutdown the client.
129    Shutdown,
130}
131
132impl LspClient {
133    /// Create a new LSP client with the given configuration.
134    ///
135    /// The client starts in an uninitialized state. Call `initialize()` to
136    /// start the server and complete the initialization handshake.
137    #[must_use]
138    pub fn new(config: LspServerConfig) -> Self {
139        // Placeholder channel - the receiver is intentionally dropped since
140        // the client starts uninitialized. A real channel is created when
141        // `from_transport` or `from_transport_with_notifications` is called.
142        let (command_tx, _command_rx) = mpsc::channel(1); // Minimal capacity for placeholder
143
144        Self {
145            config,
146            state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
147            request_counter: Arc::new(AtomicI64::new(1)),
148            command_tx,
149            pending_requests: Arc::new(Mutex::new(HashMap::new())),
150            receiver_task: None,
151        }
152    }
153
154    /// Create client from transport (for testing or custom spawning).
155    ///
156    /// This method initializes the background message loop with the provided transport.
157    #[cfg(test)]
158    pub(crate) fn from_transport(config: LspServerConfig, transport: LspTransport) -> Self {
159        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
160        let request_counter = Arc::new(AtomicI64::new(1));
161        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
162
163        let (command_tx, command_rx) = mpsc::channel(100);
164
165        let receiver_task = tokio::spawn(Self::message_loop(
166            transport,
167            command_rx,
168            Arc::clone(&pending_requests),
169            None,
170        ));
171
172        Self {
173            config,
174            state,
175            request_counter,
176            command_tx,
177            pending_requests,
178            receiver_task: Some(receiver_task),
179        }
180    }
181
182    /// Create client from transport with notification forwarding.
183    ///
184    /// Notifications received from the LSP server will be parsed and sent
185    /// through the provided channel.
186    pub(crate) fn from_transport_with_notifications(
187        config: LspServerConfig,
188        transport: LspTransport,
189        notification_tx: mpsc::Sender<LspNotification>,
190    ) -> Self {
191        let state = Arc::new(Mutex::new(super::ServerState::Initializing));
192        let request_counter = Arc::new(AtomicI64::new(1));
193        let pending_requests = Arc::new(Mutex::new(HashMap::new()));
194
195        let (command_tx, command_rx) = mpsc::channel(100);
196
197        let receiver_task = tokio::spawn(Self::message_loop(
198            transport,
199            command_rx,
200            Arc::clone(&pending_requests),
201            Some(notification_tx),
202        ));
203
204        Self {
205            config,
206            state,
207            request_counter,
208            command_tx,
209            pending_requests,
210            receiver_task: Some(receiver_task),
211        }
212    }
213
214    /// Get the language ID for this client.
215    #[must_use]
216    pub fn language_id(&self) -> &str {
217        &self.config.language_id
218    }
219
220    /// Get the current server state.
221    pub async fn state(&self) -> super::ServerState {
222        *self.state.lock().await
223    }
224
225    /// The timeout applied to a single LSP request attempt, derived from
226    /// [`LspServerConfig::request_timeout_seconds`].
227    ///
228    /// This bounds one attempt, not a whole tool call: [`Self::request`]
229    /// retries up to `SERVER_CANCELLED_MAX_RETRIES` (3) additional times on a
230    /// `-32802` (`ServerCancelled`) response, so the worst-case latency for a
231    /// single tool call is `4 * request_timeout() + 3.5s` (the sum of the
232    /// retry backoff delays).
233    ///
234    /// The configured value is clamped to the range from 1 second to
235    /// [`MAX_TIMEOUT_SECONDS`]. [`crate::serve`]/[`crate::serve_with`] now
236    /// validate the top-level `ServerConfig` (via [`ServerConfig::validate`],
237    /// which rejects `request_timeout_seconds` that is `0` or greater than
238    /// [`MAX_TIMEOUT_SECONDS`]) regardless of whether it came from
239    /// [`ServerConfig::load_from`] or was built programmatically by the
240    /// caller. But `Self::new`, [`super::LspServer::spawn`], and
241    /// [`super::LspServer::spawn_batch`] are all `pub` and take an
242    /// [`LspServerConfig`] (or [`super::ServerInitConfig`] wrapping one)
243    /// directly, bypassing that top-level validation entirely — it operates
244    /// on the top-level `ServerConfig`, not the per-server one. This clamp is
245    /// the last line of defense against a zero-duration timeout that would
246    /// fail every request instantly, or an astronomically large one that
247    /// tokio's `timeout`/`sleep` would silently treat as unbounded (they fall
248    /// back to `Instant::far_future()` rather than panicking), for a caller
249    /// reaching either of these levels directly.
250    ///
251    /// [`ServerConfig::load_from`]: crate::config::ServerConfig::load_from
252    /// [`ServerConfig::validate`]: crate::config::ServerConfig::validate
253    /// [`MAX_TIMEOUT_SECONDS`]: crate::config::MAX_TIMEOUT_SECONDS
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use std::time::Duration;
259    /// use mcpls_core::config::LspServerConfig;
260    /// use mcpls_core::lsp::LspClient;
261    ///
262    /// let mut config = LspServerConfig::rust_analyzer();
263    /// config.request_timeout_seconds = 45;
264    /// let client = LspClient::new(config);
265    ///
266    /// assert_eq!(client.request_timeout(), Duration::from_secs(45));
267    /// ```
268    #[must_use]
269    pub fn request_timeout(&self) -> Duration {
270        Duration::from_secs(
271            self.config
272                .request_timeout_seconds
273                .clamp(1, crate::config::MAX_TIMEOUT_SECONDS),
274        )
275    }
276
277    /// The timeout applied to completion (`textDocument/completion`) requests.
278    ///
279    /// Equal to [`Self::request_timeout`], capped at 10 seconds. Completions
280    /// cannot be configured above this cap by any
281    /// value of `request_timeout_seconds` — if that proves insufficient in
282    /// practice, the fix is a dedicated `completion_timeout_seconds` field,
283    /// not raising this cap.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use std::time::Duration;
289    /// use mcpls_core::config::LspServerConfig;
290    /// use mcpls_core::lsp::LspClient;
291    ///
292    /// let mut config = LspServerConfig::rust_analyzer();
293    /// config.request_timeout_seconds = 300;
294    /// let client = LspClient::new(config);
295    ///
296    /// // Capped at 10s even though request_timeout_seconds is 300.
297    /// assert_eq!(client.completion_timeout(), Duration::from_secs(10));
298    /// assert!(client.completion_timeout() <= client.request_timeout());
299    /// ```
300    #[must_use]
301    pub fn completion_timeout(&self) -> Duration {
302        self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
303    }
304
305    /// Send request and wait for response with timeout.
306    ///
307    /// Automatically retries up to 3 times when the server returns error code
308    /// -32802 (`ServerCancelled`) with `data.retriggerRequest == true`, using
309    /// exponential backoff starting at 500 ms.
310    ///
311    /// # Type Parameters
312    ///
313    /// * `P` - The type of the request parameters (must be serializable)
314    /// * `R` - The type of the response result (must be deserializable)
315    ///
316    /// # Errors
317    ///
318    /// Returns an error if:
319    /// - Server has shut down
320    /// - Request times out
321    /// - Response cannot be deserialized
322    /// - LSP server returns an error
323    pub async fn request<P, R>(
324        &self,
325        method: &str,
326        params: P,
327        timeout_duration: Duration,
328    ) -> Result<R>
329    where
330        P: Serialize,
331        R: DeserializeOwned,
332    {
333        let params_value = serde_json::to_value(params)?;
334        let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
335
336        for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
337            if attempt > 0 {
338                debug!(
339                    "Retrying {} after ServerCancelled (attempt {}/{}), backoff={}ms",
340                    method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
341                );
342                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
343                delay_ms *= 2;
344            }
345
346            let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
347            let (response_tx, response_rx) = oneshot::channel();
348            let request = JsonRpcRequest {
349                jsonrpc: JSONRPC_VERSION.to_string(),
350                id: id.clone(),
351                method: method.to_string(),
352                params: Some(params_value.clone()),
353            };
354
355            debug!("Sending request: {} (id={:?})", method, id);
356
357            self.command_tx
358                .send(ClientCommand::SendRequest {
359                    request,
360                    response_tx,
361                })
362                .await
363                .map_err(|_| Error::ServerTerminated)?;
364
365            let outcome = match timeout(timeout_duration, response_rx).await {
366                Ok(received) => received.map_err(|_| Error::ServerTerminated)?,
367                Err(_elapsed) => {
368                    // The response may still arrive after this point (the
369                    // server is just slow, not dead), but nothing will ever
370                    // read it again -- drop the now-orphaned entry instead of
371                    // leaking it in `pending_requests` forever.
372                    self.pending_requests.lock().await.remove(&id);
373                    return Err(Error::Timeout(timeout_duration.as_secs()));
374                }
375            };
376
377            match outcome {
378                Ok(result_value) => {
379                    return serde_json::from_value(result_value).map_err(|e| {
380                        Error::LspProtocolError(format!("Failed to deserialize response: {e}"))
381                    });
382                }
383                Err(Error::LspServerError {
384                    code,
385                    ref message,
386                    ref data,
387                }) if code == SERVER_CANCELLED_CODE && Self::should_retrigger(data.as_ref()) => {
388                    warn!(
389                        "ServerCancelled (-32802) on '{}', will retry: {}",
390                        method, message
391                    );
392                    if attempt == SERVER_CANCELLED_MAX_RETRIES {
393                        return Err(Error::LspServerError {
394                            code,
395                            message: message.clone(),
396                            data: data.clone(),
397                        });
398                    }
399                    // continue loop for next attempt
400                }
401                Err(e) => return Err(e),
402            }
403        }
404
405        Err(Error::ServerTerminated)
406    }
407
408    /// Returns true when the error data from a `ServerCancelled` (-32802) response
409    /// indicates the server wants the client to retrigger the request.
410    ///
411    /// Per the LSP specification, `data.retriggerRequest == true` is the signal.
412    /// When `data` is absent (older servers), we default to retrying anyway because
413    /// code -32802 is exclusively used for this purpose.
414    fn should_retrigger(data: Option<&Value>) -> bool {
415        data.is_none_or(|v| {
416            v.get("retriggerRequest")
417                .and_then(Value::as_bool)
418                .unwrap_or(true)
419        })
420    }
421
422    /// Fail every request still parked in `pending_requests` with
423    /// `Error::ServerTerminated`, instead of leaving each to discover a dead
424    /// connection only when its own timeout elapses.
425    ///
426    /// Intended for a client that is about to be discarded -- e.g.
427    /// superseded by a respawned replacement for the same server -- so
428    /// callers still waiting on it unblock immediately.
429    pub(crate) async fn fail_pending_requests(&self) {
430        let mut pending = self.pending_requests.lock().await;
431        for (_, sender) in pending.drain() {
432            let _ = sender.send(Err(Error::ServerTerminated));
433        }
434    }
435
436    /// Send notification (fire-and-forget, no response expected).
437    ///
438    /// # Errors
439    ///
440    /// Returns an error if the server has shut down.
441    pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
442    where
443        P: Serialize,
444    {
445        let params_value = serde_json::to_value(params)?;
446
447        debug!("Sending notification: {}", method);
448
449        self.command_tx
450            .send(ClientCommand::SendNotification {
451                method: method.to_string(),
452                params: Some(params_value),
453            })
454            .await
455            .map_err(|_| Error::ServerTerminated)?;
456
457        Ok(())
458    }
459
460    /// Shutdown client gracefully.
461    ///
462    /// This sends a shutdown command to the background task and waits for it to complete.
463    ///
464    /// # Errors
465    ///
466    /// Returns an error if the background task failed.
467    pub async fn shutdown(mut self) -> Result<()> {
468        debug!("Shutting down LSP client");
469
470        let _ = self.command_tx.send(ClientCommand::Shutdown).await;
471
472        if let Some(task) = self.receiver_task.take() {
473            task.await
474                .map_err(|e| Error::Transport(format!("Receiver task failed: {e}")))??;
475        }
476
477        *self.state.lock().await = super::ServerState::Shutdown;
478
479        Ok(())
480    }
481
482    /// Background task: handle message I/O.
483    ///
484    /// This task runs in the background, handling:
485    /// - Outbound requests and notifications
486    /// - Inbound responses and server notifications
487    /// - Matching responses to pending requests
488    async fn message_loop(
489        mut transport: LspTransport,
490        mut command_rx: mpsc::Receiver<ClientCommand>,
491        pending_requests: Arc<Mutex<PendingRequests>>,
492        notification_tx: Option<mpsc::Sender<LspNotification>>,
493    ) -> Result<()> {
494        debug!("Message loop started");
495        let result = Self::message_loop_inner(
496            &mut transport,
497            &mut command_rx,
498            &pending_requests,
499            notification_tx.as_ref(),
500        )
501        .await;
502        if let Err(ref e) = result {
503            error!("Message loop exiting with error: {}", e);
504        } else {
505            debug!("Message loop exiting normally");
506        }
507        result
508    }
509
510    /// Truncate an LSP server's error message for the `tracing::error!` log
511    /// line, bounding it to at most [`MAX_ERROR_MESSAGE_LOG_BYTES`] bytes
512    /// (the full formatted string is slightly longer).
513    ///
514    /// Log-line use only -- the message forwarded to the MCP caller in
515    /// [`Error::LspServerError`] is truncated separately, to the larger
516    /// [`MAX_ERROR_MESSAGE_CALLER_BYTES`] (#313).
517    fn truncate_error_message_for_log(message: &str) -> String {
518        crate::util::truncate_str(message, MAX_ERROR_MESSAGE_LOG_BYTES)
519    }
520
521    async fn message_loop_inner(
522        transport: &mut LspTransport,
523        command_rx: &mut mpsc::Receiver<ClientCommand>,
524        pending_requests: &Arc<Mutex<PendingRequests>>,
525        notification_tx: Option<&mpsc::Sender<LspNotification>>,
526    ) -> Result<()> {
527        loop {
528            tokio::select! {
529                Some(command) = command_rx.recv() => {
530                    match command {
531                        ClientCommand::SendRequest { request, response_tx } => {
532                            pending_requests.lock().await.insert(
533                                request.id.clone(),
534                                response_tx,
535                            );
536
537                            let value = serde_json::to_value(&request)?;
538                            transport.send(&value).await?;
539                        }
540                        ClientCommand::SendNotification { method, params } => {
541                            let notification = serde_json::json!({
542                                "jsonrpc": "2.0",
543                                "method": method,
544                                "params": params,
545                            });
546                            transport.send(&notification).await?;
547                        }
548                        ClientCommand::Shutdown => {
549                            debug!("Client shutdown requested");
550                            break;
551                        }
552                    }
553                }
554
555                message = transport.receive() => {
556                    let message = match message {
557                        Ok(m) => m,
558                        Err(e) => {
559                            error!("Transport receive error: {}", e);
560                            return Err(e);
561                        }
562                    };
563                    match message {
564                        InboundMessage::Response(response) => {
565                            trace!("Received response: id={:?}", response.id);
566
567                            let sender = pending_requests.lock().await.remove(&response.id);
568
569                            if let Some(sender) = sender {
570                                if let Some(error) = response.error {
571                                    let log_message = Self::truncate_error_message_for_log(&error.message);
572                                    error!("LSP error response: {} (code {})", log_message, error.code);
573                                    // Truncated separately from the log line, to the larger
574                                    // MAX_ERROR_MESSAGE_CALLER_BYTES -- the raw message is
575                                    // unbounded and attacker-influenceable (#313), but a
576                                    // log-line-sized cut would also clip legitimate long
577                                    // errors before the model ever sees them (S2).
578                                    let caller_message = crate::util::truncate_str(
579                                        &error.message,
580                                        MAX_ERROR_MESSAGE_CALLER_BYTES,
581                                    );
582                                    let _ = sender.send(Err(Error::LspServerError {
583                                        code: error.code,
584                                        message: caller_message,
585                                        data: error.data,
586                                    }));
587                                } else if let Some(result) = response.result {
588                                    let _ = sender.send(Ok(result));
589                                } else {
590                                    // LSP spec allows null result for some requests (e.g., hover with no info).
591                                    // Treat as successful response with null value.
592                                    trace!("Response with null result: {:?}", response.id);
593                                    let _ = sender.send(Ok(Value::Null));
594                                }
595                            } else {
596                                warn!("Received response for unknown request ID: {:?}", response.id);
597                            }
598                        }
599                        InboundMessage::Request(request) => {
600                            debug!(
601                                "Received server request: {} (id={:?})",
602                                request.method, request.id
603                            );
604                            let response = Self::server_request_response(request);
605                            let value = serde_json::to_value(&response)?;
606                            transport.send(&value).await?;
607                        }
608                        InboundMessage::Notification(notification) => {
609                            debug!("Received notification: {}", notification.method);
610
611                            // Parse notification into typed variant
612                            let typed = LspNotification::parse(&notification.method, notification.params);
613
614                            // Forward to notification handler if sender is available
615                            if let Some(tx) = notification_tx {
616                                // Log diagnostics count since it's useful for debugging
617                                if let LspNotification::PublishDiagnostics(ref params) = typed {
618                                    debug!(
619                                        "Forwarding diagnostics for {}: {} items",
620                                        params.uri.as_str(),
621                                        params.diagnostics.len()
622                                    );
623                                } else {
624                                    trace!("Forwarding notification: {:?}", typed);
625                                }
626
627                                // Send the notification with backpressure handling
628                                if tx.try_send(typed).is_err() {
629                                    warn!("Notification channel full or closed, dropping notification");
630                                }
631                            }
632                        }
633                    }
634                }
635            }
636        }
637
638        Ok(())
639    }
640
641    fn server_request_response(request: JsonRpcRequest) -> JsonRpcResponse {
642        match Self::server_request_result(&request.method, request.params.as_ref()) {
643            Ok(result) => JsonRpcResponse {
644                jsonrpc: JSONRPC_VERSION.to_string(),
645                id: request.id,
646                result: Some(result),
647                error: None,
648            },
649            Err(error) => JsonRpcResponse {
650                jsonrpc: JSONRPC_VERSION.to_string(),
651                id: request.id,
652                result: None,
653                error: Some(error),
654            },
655        }
656    }
657
658    fn server_request_result(
659        method: &str,
660        params: Option<&Value>,
661    ) -> std::result::Result<Value, JsonRpcError> {
662        match method {
663            "client/registerCapability"
664            | "client/unregisterCapability"
665            | "workspace/workspaceFolders"
666            | "workspace/diagnostic/refresh"
667            | "workspace/semanticTokens/refresh"
668            | "workspace/inlayHint/refresh"
669            | "workspace/codeLens/refresh"
670            | "window/showMessageRequest" => Ok(Value::Null),
671            "workspace/configuration" => Ok(Self::workspace_configuration_result(params)),
672            "workspace/applyEdit" => Ok(serde_json::json!({ "applied": false })),
673            _ => Err(JsonRpcError {
674                code: -32601,
675                message: format!("Unhandled server request: {method}"),
676                data: None,
677            }),
678        }
679    }
680
681    fn workspace_configuration_result(params: Option<&Value>) -> Value {
682        let item_count = params
683            .and_then(|value| value.get("items"))
684            .and_then(Value::as_array)
685            .map_or(0, Vec::len);
686
687        Value::Array(vec![Value::Null; item_count])
688    }
689}
690
691#[cfg(test)]
692#[allow(clippy::unwrap_used)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn test_request_id_generation() {
698        let counter = AtomicI64::new(1);
699
700        let id1 = counter.fetch_add(1, Ordering::SeqCst);
701        let id2 = counter.fetch_add(1, Ordering::SeqCst);
702        let id3 = counter.fetch_add(1, Ordering::SeqCst);
703
704        assert_eq!(id1, 1);
705        assert_eq!(id2, 2);
706        assert_eq!(id3, 3);
707    }
708
709    #[test]
710    fn test_client_creation() {
711        let config = LspServerConfig::rust_analyzer();
712
713        let client = LspClient::new(config);
714        assert_eq!(client.language_id(), "rust");
715    }
716
717    #[test]
718    fn test_client_clone() {
719        let config = LspServerConfig::rust_analyzer();
720        let client = LspClient::new(config);
721
722        #[allow(clippy::redundant_clone)]
723        let cloned = client.clone();
724        assert_eq!(cloned.language_id(), "rust");
725
726        assert!(
727            cloned.receiver_task.is_none(),
728            "Cloned client should not own receiver task"
729        );
730    }
731
732    #[test]
733    fn test_request_timeout_and_completion_timeout_at_default() {
734        let config = LspServerConfig::rust_analyzer();
735        let client = LspClient::new(config);
736
737        assert_eq!(client.request_timeout(), Duration::from_secs(30));
738        assert_eq!(client.completion_timeout(), Duration::from_secs(10));
739    }
740
741    #[test]
742    fn test_completion_timeout_clamps_to_ten_seconds() {
743        for secs in [1, 2, 3, 30, 300] {
744            let mut config = LspServerConfig::rust_analyzer();
745            config.request_timeout_seconds = secs;
746            let client = LspClient::new(config);
747
748            assert_eq!(
749                client.completion_timeout(),
750                Duration::from_secs(secs.min(10)),
751                "request_timeout_seconds={secs}"
752            );
753            assert!(client.completion_timeout() <= client.request_timeout());
754        }
755    }
756
757    #[test]
758    fn test_request_timeout_clamps_zero_to_one_second() {
759        let mut config = LspServerConfig::rust_analyzer();
760        config.request_timeout_seconds = 0;
761        let client = LspClient::new(config);
762
763        assert_eq!(client.request_timeout(), Duration::from_secs(1));
764        assert_eq!(client.completion_timeout(), Duration::from_secs(1));
765    }
766
767    #[test]
768    fn test_request_timeout_clamps_above_max_to_max() {
769        let mut config = LspServerConfig::rust_analyzer();
770        config.request_timeout_seconds = u64::MAX;
771        let client = LspClient::new(config);
772
773        assert_eq!(
774            client.request_timeout(),
775            Duration::from_secs(crate::config::MAX_TIMEOUT_SECONDS)
776        );
777    }
778
779    #[test]
780    fn test_request_timeout_independent_per_server() {
781        let mut config_a = LspServerConfig::rust_analyzer();
782        config_a.request_timeout_seconds = 5;
783        let mut config_b = LspServerConfig::pyright();
784        config_b.request_timeout_seconds = 15;
785
786        let client_a = LspClient::new(config_a);
787        let client_b = LspClient::new(config_b);
788
789        assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
790        assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
791    }
792
793    #[test]
794    fn test_register_capability_request_is_acknowledged() {
795        let request = JsonRpcRequest {
796            jsonrpc: JSONRPC_VERSION.to_string(),
797            id: RequestId::String("ts1".to_string()),
798            method: "client/registerCapability".to_string(),
799            params: Some(serde_json::json!({ "registrations": [] })),
800        };
801
802        let response = LspClient::server_request_response(request);
803
804        assert_eq!(response.id, RequestId::String("ts1".to_string()));
805        assert_eq!(response.result, Some(Value::Null));
806        assert!(response.error.is_none());
807    }
808
809    #[test]
810    fn test_workspace_configuration_request_returns_null_per_item() {
811        let result = LspClient::workspace_configuration_result(Some(&serde_json::json!({
812            "items": [{ "section": "typescript" }, { "section": "editor" }]
813        })));
814
815        assert_eq!(result, serde_json::json!([null, null]));
816    }
817
818    #[test]
819    fn test_unknown_server_request_returns_method_not_found() {
820        let request = JsonRpcRequest {
821            jsonrpc: JSONRPC_VERSION.to_string(),
822            id: RequestId::String("unknown-1".to_string()),
823            method: "custom/request".to_string(),
824            params: None,
825        };
826
827        let response = LspClient::server_request_response(request);
828
829        assert!(response.result.is_none());
830        match response.error {
831            Some(error) => {
832                assert_eq!(error.code, -32601);
833                assert_eq!(error.message, "Unhandled server request: custom/request");
834            }
835            None => panic!("unknown request should return error"),
836        }
837    }
838
839    #[tokio::test]
840    async fn test_null_response_handling() {
841        use crate::lsp::types::{JsonRpcResponse, RequestId};
842
843        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
844
845        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
846
847        pending_requests
848            .lock()
849            .await
850            .insert(RequestId::Number(1), response_tx);
851
852        let null_response = JsonRpcResponse {
853            jsonrpc: "2.0".to_string(),
854            id: RequestId::Number(1),
855            result: None,
856            error: None,
857        };
858
859        let sender = pending_requests.lock().await.remove(&null_response.id);
860        if let Some(sender) = sender {
861            let _ = sender.send(Ok(Value::Null));
862        }
863
864        let timeout_result =
865            tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
866
867        assert!(timeout_result.is_ok(), "Should not timeout");
868
869        let channel_result = timeout_result.unwrap();
870        assert!(
871            channel_result.is_ok(),
872            "Channel should not be closed: {:?}",
873            channel_result.err()
874        );
875
876        let response = channel_result.unwrap();
877        assert!(
878            response.is_ok(),
879            "Should receive Ok(Value::Null), not Err: {:?}",
880            response.err()
881        );
882
883        let value = response.unwrap();
884        assert_eq!(value, Value::Null, "Should receive Value::Null");
885    }
886
887    #[tokio::test]
888    async fn test_error_response_handling() {
889        use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
890
891        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
892        let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
893
894        pending_requests
895            .lock()
896            .await
897            .insert(RequestId::Number(1), response_tx);
898
899        let error_response = JsonRpcResponse {
900            jsonrpc: "2.0".to_string(),
901            id: RequestId::Number(1),
902            result: None,
903            error: Some(JsonRpcError {
904                code: -32601,
905                message: "Method not found".to_string(),
906                data: None,
907            }),
908        };
909
910        let sender = pending_requests.lock().await.remove(&error_response.id);
911        if let Some(sender) = sender
912            && let Some(error) = error_response.error
913        {
914            let _ = sender.send(Err(Error::LspServerError {
915                code: error.code,
916                message: error.message,
917                data: error.data,
918            }));
919        }
920
921        let result = response_rx.await.unwrap();
922        assert!(result.is_err(), "Should receive error");
923
924        if let Err(Error::LspServerError { code, message, .. }) = result {
925            assert_eq!(code, -32601);
926            assert_eq!(message, "Method not found");
927        } else {
928            panic!("Expected LspServerError");
929        }
930    }
931
932    #[tokio::test]
933    async fn test_unknown_request_id() {
934        use crate::lsp::types::{JsonRpcResponse, RequestId};
935
936        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
937
938        let response = JsonRpcResponse {
939            jsonrpc: "2.0".to_string(),
940            id: RequestId::Number(999),
941            result: Some(Value::Null),
942            error: None,
943        };
944
945        let sender = pending_requests.lock().await.remove(&response.id);
946        assert!(sender.is_none(), "Should not find sender for unknown ID");
947    }
948
949    #[test]
950    fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
951        // 199 ASCII bytes followed by a 3-byte UTF-8 char ('€') straddles the byte-200 cut.
952        let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
953
954        let truncated = LspClient::truncate_error_message_for_log(&message);
955
956        // Cutting before the multi-byte char keeps the message valid UTF-8 (no panic) and
957        // pins the payload to 199 bytes, not 200.
958        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
959    }
960
961    #[test]
962    fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
963        let exact = "x".repeat(200);
964        assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
965        assert_eq!(LspClient::truncate_error_message_for_log(""), "");
966    }
967
968    #[test]
969    fn test_truncate_error_message_for_log_truncates_just_above_limit() {
970        let message = "x".repeat(201);
971        assert_eq!(
972            LspClient::truncate_error_message_for_log(&message),
973            format!("{}... (truncated)", "x".repeat(200))
974        );
975    }
976
977    #[test]
978    fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
979        // A 4-byte emoji run straddling every possible alignment near the byte-200 boundary.
980        let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
981
982        let truncated = LspClient::truncate_error_message_for_log(&message);
983
984        assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
985    }
986
987    #[tokio::test]
988    async fn test_concurrent_request_ids() {
989        let counter = Arc::new(AtomicI64::new(1));
990
991        let counter1 = Arc::clone(&counter);
992        let counter2 = Arc::clone(&counter);
993        let counter3 = Arc::clone(&counter);
994
995        let handles = vec![
996            tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
997            tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
998            tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
999        ];
1000
1001        let mut ids = Vec::new();
1002        for handle in handles {
1003            ids.push(handle.await.unwrap());
1004        }
1005
1006        ids.sort_unstable();
1007        assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
1008    }
1009
1010    #[test]
1011    fn test_jsonrpc_version_constant() {
1012        assert_eq!(JSONRPC_VERSION, "2.0");
1013    }
1014
1015    /// #239 regression: a request that times out must remove its own entry
1016    /// from `pending_requests` instead of leaking it. `sleep` is used as the
1017    /// "server": it never writes anything to stdout, so no response can ever
1018    /// arrive and the request is guaranteed to time out rather than race a
1019    /// real answer.
1020    ///
1021    /// Unix-only: spawns a real `sleep` subprocess, which is unavailable on
1022    /// the Windows CI runner.
1023    #[cfg(unix)]
1024    #[tokio::test]
1025    async fn test_request_timeout_removes_pending_entry() {
1026        let mut child = tokio::process::Command::new("sleep")
1027            .arg("2")
1028            .stdin(std::process::Stdio::piped())
1029            .stdout(std::process::Stdio::piped())
1030            .kill_on_drop(true)
1031            .spawn()
1032            .unwrap();
1033        let stdin = child.stdin.take().unwrap();
1034        let stdout = child.stdout.take().unwrap();
1035
1036        let transport = LspTransport::new(stdin, stdout);
1037        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1038
1039        let result: Result<Value> = client
1040            .request(
1041                "textDocument/hover",
1042                serde_json::json!({}),
1043                Duration::from_millis(50),
1044            )
1045            .await;
1046
1047        assert!(matches!(result, Err(Error::Timeout(_))), "got {result:?}");
1048        assert!(
1049            client.pending_requests.lock().await.is_empty(),
1050            "timed-out request must not remain in pending_requests"
1051        );
1052    }
1053
1054    /// #249 continuation: a client about to be discarded (e.g. superseded by
1055    /// a respawned replacement) must fail every still-pending request
1056    /// immediately rather than leaving callers to wait out their timeout.
1057    #[tokio::test]
1058    async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
1059        let pending_requests: Arc<Mutex<PendingRequests>> = Arc::new(Mutex::new(HashMap::new()));
1060        let (command_tx, _command_rx) = mpsc::channel(1);
1061
1062        let client = LspClient {
1063            config: LspServerConfig::rust_analyzer(),
1064            state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
1065            request_counter: Arc::new(AtomicI64::new(1)),
1066            command_tx,
1067            pending_requests: Arc::clone(&pending_requests),
1068            receiver_task: None,
1069        };
1070
1071        let (tx1, rx1) = oneshot::channel::<Result<Value>>();
1072        let (tx2, rx2) = oneshot::channel::<Result<Value>>();
1073        pending_requests
1074            .lock()
1075            .await
1076            .insert(RequestId::Number(1), tx1);
1077        pending_requests
1078            .lock()
1079            .await
1080            .insert(RequestId::Number(2), tx2);
1081
1082        client.fail_pending_requests().await;
1083
1084        assert!(pending_requests.lock().await.is_empty());
1085        assert!(matches!(rx1.await.unwrap(), Err(Error::ServerTerminated)));
1086        assert!(matches!(rx2.await.unwrap(), Err(Error::ServerTerminated)));
1087    }
1088
1089    #[test]
1090    fn test_should_retrigger_defaults_to_true_when_data_absent() {
1091        assert!(LspClient::should_retrigger(None));
1092    }
1093
1094    #[test]
1095    fn test_should_retrigger_false_when_flag_false() {
1096        assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
1097            "retriggerRequest": false
1098        }))));
1099    }
1100
1101    #[test]
1102    fn test_should_retrigger_true_when_flag_true() {
1103        assert!(LspClient::should_retrigger(Some(&serde_json::json!({
1104            "retriggerRequest": true
1105        }))));
1106    }
1107
1108    mod retry_behavior {
1109        use std::process::Stdio;
1110
1111        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
1112        use tokio::process::{Child, ChildStdin, ChildStdout, Command};
1113
1114        use super::*;
1115        use crate::config::LspServerConfig;
1116
1117        struct FakeServer {
1118            _write_half: Child,
1119            _read_half: Child,
1120            read_half_stdin: ChildStdin,
1121            write_stdout: ChildStdout,
1122        }
1123
1124        fn fake_lsp_client() -> (LspClient, FakeServer) {
1125            let mut write_half = Command::new("cat")
1126                .stdin(Stdio::piped())
1127                .stdout(Stdio::piped())
1128                .kill_on_drop(true)
1129                .spawn()
1130                .unwrap();
1131            let write_stdin = write_half.stdin.take().unwrap();
1132            let write_stdout = write_half.stdout.take().unwrap();
1133
1134            let mut read_half = Command::new("cat")
1135                .stdin(Stdio::piped())
1136                .stdout(Stdio::piped())
1137                .kill_on_drop(true)
1138                .spawn()
1139                .unwrap();
1140            let read_stdout = read_half.stdout.take().unwrap();
1141            let read_stdin = read_half.stdin.take().unwrap();
1142
1143            let transport = LspTransport::new(write_stdin, read_stdout);
1144            let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
1145
1146            (
1147                client,
1148                FakeServer {
1149                    _write_half: write_half,
1150                    _read_half: read_half,
1151                    read_half_stdin: read_stdin,
1152                    write_stdout,
1153                },
1154            )
1155        }
1156
1157        /// Reads one `Content-Length`-framed JSON-RPC message off `reader`.
1158        async fn read_framed_message(reader: &mut BufReader<&mut ChildStdout>) -> Value {
1159            let mut content_length = None;
1160            let mut line = String::new();
1161            loop {
1162                line.clear();
1163                reader.read_line(&mut line).await.unwrap();
1164                if line == "\r\n" || line == "\n" {
1165                    break;
1166                }
1167                if let Some((key, value)) = line.trim_end().split_once(':')
1168                    && key.trim().eq_ignore_ascii_case("content-length")
1169                {
1170                    content_length = Some(value.trim().parse::<usize>().unwrap());
1171                }
1172            }
1173            let mut buf = vec![0u8; content_length.unwrap()];
1174            reader.read_exact(&mut buf).await.unwrap();
1175            serde_json::from_slice(&buf).unwrap()
1176        }
1177
1178        /// Writes a framed JSON-RPC `ServerCancelled` (-32802) error response.
1179        async fn write_server_cancelled_response(
1180            stdin: &mut ChildStdin,
1181            id: &Value,
1182            retrigger: bool,
1183        ) {
1184            let response = serde_json::json!({
1185                "jsonrpc": "2.0",
1186                "id": id,
1187                "error": {
1188                    "code": SERVER_CANCELLED_CODE,
1189                    "message": "server cancelled the request",
1190                    "data": { "retriggerRequest": retrigger },
1191                },
1192            });
1193            let content = serde_json::to_string(&response).unwrap();
1194            let header = format!("Content-Length: {}\r\n\r\n", content.len());
1195            stdin.write_all(header.as_bytes()).await.unwrap();
1196            stdin.write_all(content.as_bytes()).await.unwrap();
1197            stdin.flush().await.unwrap();
1198        }
1199
1200        /// Writes a framed JSON-RPC error response with an arbitrary code/message.
1201        async fn write_error_response(
1202            stdin: &mut ChildStdin,
1203            id: &Value,
1204            code: i32,
1205            message: &str,
1206        ) {
1207            let response = serde_json::json!({
1208                "jsonrpc": "2.0",
1209                "id": id,
1210                "error": { "code": code, "message": message },
1211            });
1212            let content = serde_json::to_string(&response).unwrap();
1213            let header = format!("Content-Length: {}\r\n\r\n", content.len());
1214            stdin.write_all(header.as_bytes()).await.unwrap();
1215            stdin.write_all(content.as_bytes()).await.unwrap();
1216            stdin.flush().await.unwrap();
1217        }
1218
1219        /// Writes a framed JSON-RPC success response.
1220        async fn write_success_response(stdin: &mut ChildStdin, id: &Value, result: Value) {
1221            let response = serde_json::json!({
1222                "jsonrpc": "2.0",
1223                "id": id,
1224                "result": result,
1225            });
1226            let content = serde_json::to_string(&response).unwrap();
1227            let header = format!("Content-Length: {}\r\n\r\n", content.len());
1228            stdin.write_all(header.as_bytes()).await.unwrap();
1229            stdin.write_all(content.as_bytes()).await.unwrap();
1230            stdin.flush().await.unwrap();
1231        }
1232
1233        // Not `start_paused`: the retry loop's real backoff sleeps
1234        // interleave with real subprocess pipe I/O below, and paused
1235        // virtual time does not reliably auto-advance across both.
1236        #[tokio::test]
1237        async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
1238            let (client, mut server) = fake_lsp_client();
1239
1240            let request_task = tokio::spawn(async move {
1241                client
1242                    .request::<_, Value>(
1243                        "textDocument/hover",
1244                        serde_json::json!({}),
1245                        Duration::from_secs(30),
1246                    )
1247                    .await
1248            });
1249
1250            let mut reader = BufReader::new(&mut server.write_stdout);
1251            // Initial attempt plus SERVER_CANCELLED_MAX_RETRIES retries: every
1252            // attempt gets ServerCancelled, so retries must exhaust rather
1253            // than loop forever or swallow the error.
1254            for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
1255                let request = read_framed_message(&mut reader).await;
1256                let id = request["id"].clone();
1257                write_server_cancelled_response(&mut server.read_half_stdin, &id, true).await;
1258            }
1259
1260            let result = request_task.await.unwrap();
1261
1262            match result {
1263                Err(Error::LspServerError {
1264                    code,
1265                    message,
1266                    data,
1267                }) => {
1268                    // Assert the exact original error surfaces, not merely
1269                    // "some error with this code" -- a freshly constructed
1270                    // placeholder error would satisfy a code-only check.
1271                    assert_eq!(code, SERVER_CANCELLED_CODE);
1272                    assert_eq!(message, "server cancelled the request");
1273                    assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
1274                }
1275                other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
1276            }
1277        }
1278
1279        #[tokio::test]
1280        async fn test_retrigger_false_returns_immediately_without_retry() {
1281            let (client, mut server) = fake_lsp_client();
1282
1283            let request_task = tokio::spawn(async move {
1284                client
1285                    .request::<_, Value>(
1286                        "textDocument/hover",
1287                        serde_json::json!({}),
1288                        Duration::from_secs(30),
1289                    )
1290                    .await
1291            });
1292
1293            let mut reader = BufReader::new(&mut server.write_stdout);
1294            let request = read_framed_message(&mut reader).await;
1295            let id = request["id"].clone();
1296            write_server_cancelled_response(&mut server.read_half_stdin, &id, false).await;
1297
1298            // With `retriggerRequest: false`, `should_retrigger`'s gate on
1299            // the retry branch must short-circuit the loop: the error
1300            // returns well under the first 500ms backoff, and no second
1301            // request is ever sent. If the `&& Self::should_retrigger(..)`
1302            // guard were ever dropped from the retry match arm, this would
1303            // instead retry and both assertions below would fail.
1304            let result = tokio::time::timeout(Duration::from_millis(200), request_task)
1305                .await
1306                .unwrap()
1307                .unwrap();
1308
1309            match result {
1310                Err(Error::LspServerError { code, .. }) => {
1311                    assert_eq!(code, SERVER_CANCELLED_CODE);
1312                }
1313                other => panic!("expected immediate ServerCancelled error, got {other:?}"),
1314            }
1315
1316            let second_request =
1317                tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
1318                    .await;
1319            assert!(
1320                second_request.is_err(),
1321                "no retry should have been sent after retriggerRequest: false"
1322            );
1323        }
1324
1325        #[tokio::test]
1326        async fn test_retry_succeeds_after_one_server_cancelled_response() {
1327            let (client, mut server) = fake_lsp_client();
1328
1329            let request_task = tokio::spawn(async move {
1330                client
1331                    .request::<_, Value>(
1332                        "textDocument/hover",
1333                        serde_json::json!({}),
1334                        Duration::from_secs(30),
1335                    )
1336                    .await
1337            });
1338
1339            let mut reader = BufReader::new(&mut server.write_stdout);
1340
1341            // First attempt is cancelled and must retrigger.
1342            let first = read_framed_message(&mut reader).await;
1343            write_server_cancelled_response(
1344                &mut server.read_half_stdin,
1345                &first["id"].clone(),
1346                true,
1347            )
1348            .await;
1349
1350            // Second attempt (after backoff) succeeds -- proves the loop
1351            // genuinely re-sends the request rather than just counting down.
1352            let second = read_framed_message(&mut reader).await;
1353            assert_ne!(
1354                first["id"], second["id"],
1355                "retry must use a fresh request id"
1356            );
1357            let expected_result = serde_json::json!({ "contents": "resolved on retry" });
1358            write_success_response(
1359                &mut server.read_half_stdin,
1360                &second["id"].clone(),
1361                expected_result.clone(),
1362            )
1363            .await;
1364
1365            let result = request_task.await.unwrap();
1366            assert_eq!(result.unwrap(), expected_result);
1367        }
1368
1369        /// #313: an oversized, server-controlled error message must be
1370        /// truncated before it reaches the MCP caller in
1371        /// `Error::LspServerError`, not just before it is logged. Routes
1372        /// through the real `message_loop_inner` (via `fake_lsp_client`)
1373        /// rather than constructing the error by hand, so it actually
1374        /// exercises the fix.
1375        #[tokio::test]
1376        async fn test_oversized_error_message_truncated_for_caller() {
1377            let (client, mut server) = fake_lsp_client();
1378
1379            let request_task = tokio::spawn(async move {
1380                client
1381                    .request::<_, Value>(
1382                        "textDocument/hover",
1383                        serde_json::json!({}),
1384                        Duration::from_secs(30),
1385                    )
1386                    .await
1387            });
1388
1389            let mut reader = BufReader::new(&mut server.write_stdout);
1390            let request = read_framed_message(&mut reader).await;
1391            let id = request["id"].clone();
1392            let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
1393            write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
1394                .await;
1395
1396            let result = request_task.await.unwrap();
1397
1398            match result {
1399                Err(Error::LspServerError { code, message, .. }) => {
1400                    assert_eq!(code, -32603);
1401                    assert!(
1402                        message.len() < oversized_message.len(),
1403                        "caller-facing message must be truncated, got {} bytes",
1404                        message.len()
1405                    );
1406                    assert!(message.ends_with("... (truncated)"));
1407                }
1408                other => panic!("expected truncated LspServerError, got {other:?}"),
1409            }
1410        }
1411
1412        /// #313 S2: a legitimate error message longer than the log-line cap
1413        /// (`MAX_ERROR_MESSAGE_LOG_BYTES`, 200 bytes) but shorter than the
1414        /// caller-facing cap must reach the MCP caller intact -- the
1415        /// caller-facing budget must not silently collapse to the log
1416        /// budget.
1417        #[tokio::test]
1418        async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
1419            let (client, mut server) = fake_lsp_client();
1420
1421            let request_task = tokio::spawn(async move {
1422                client
1423                    .request::<_, Value>(
1424                        "textDocument/hover",
1425                        serde_json::json!({}),
1426                        Duration::from_secs(30),
1427                    )
1428                    .await
1429            });
1430
1431            let mut reader = BufReader::new(&mut server.write_stdout);
1432            let request = read_framed_message(&mut reader).await;
1433            let id = request["id"].clone();
1434            let message = "x".repeat(MAX_ERROR_MESSAGE_LOG_BYTES + 50);
1435            write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
1436
1437            let result = request_task.await.unwrap();
1438
1439            match result {
1440                Err(Error::LspServerError {
1441                    message: returned, ..
1442                }) => {
1443                    assert_eq!(
1444                        returned, message,
1445                        "message under the caller cap must not be truncated"
1446                    );
1447                }
1448                other => panic!("expected untruncated LspServerError, got {other:?}"),
1449            }
1450        }
1451    }
1452}