mcpls_core/lsp/
lifecycle.rs

1//! LSP server lifecycle management.
2//!
3//! This module handles the complete lifecycle of an LSP server:
4//! 1. Spawn server process
5//! 2. Initialize → initialized handshake
6//! 3. Capability negotiation
7//! 4. Active request handling
8//! 5. Graceful shutdown sequence
9
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::process::Stdio;
13use std::str::FromStr;
14
15use lsp_types::{
16    ClientCapabilities, ClientInfo, GeneralClientCapabilities, InitializeParams, InitializeResult,
17    InitializedParams, PositionEncodingKind, ServerCapabilities, Uri, WorkspaceFolder,
18};
19use tokio::process::Command;
20use tokio::time::Duration;
21use tracing::{debug, info};
22
23use crate::config::LspServerConfig;
24use crate::error::{Error, Result, ServerSpawnFailure};
25use crate::lsp::client::LspClient;
26use crate::lsp::transport::LspTransport;
27
28/// State of an LSP server connection.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ServerState {
31    /// Server has not been initialized.
32    Uninitialized,
33    /// Server is currently initializing.
34    Initializing,
35    /// Server is ready to handle requests.
36    Ready,
37    /// Server is shutting down.
38    ShuttingDown,
39    /// Server has been shut down.
40    Shutdown,
41}
42
43impl ServerState {
44    /// Check if the server is ready to handle requests.
45    #[must_use]
46    pub const fn is_ready(&self) -> bool {
47        matches!(self, Self::Ready)
48    }
49
50    /// Check if the server can accept new requests.
51    #[must_use]
52    pub const fn can_accept_requests(&self) -> bool {
53        matches!(self, Self::Ready)
54    }
55}
56
57/// Configuration for LSP server initialization.
58#[derive(Debug, Clone)]
59pub struct ServerInitConfig {
60    /// LSP server configuration.
61    pub server_config: LspServerConfig,
62    /// Workspace root paths.
63    pub workspace_roots: Vec<PathBuf>,
64    /// Initialization options (server-specific JSON).
65    pub initialization_options: Option<serde_json::Value>,
66}
67
68/// Result of attempting to spawn multiple LSP servers.
69///
70/// This type enables graceful degradation by collecting both
71/// successful initializations and failures. Use the helper methods
72/// to inspect the outcome and make decisions about how to proceed.
73///
74/// # Examples
75///
76/// ```
77/// use mcpls_core::lsp::ServerInitResult;
78/// use mcpls_core::error::ServerSpawnFailure;
79///
80/// let mut result = ServerInitResult::new();
81///
82/// // Check for different scenarios
83/// if result.all_failed() {
84///     eprintln!("All servers failed to initialize");
85/// } else if result.partial_success() {
86///     println!("Some servers succeeded, some failed");
87/// } else if result.has_servers() {
88///     println!("All servers initialized successfully");
89/// }
90/// ```
91#[derive(Debug)]
92pub struct ServerInitResult {
93    /// Successfully initialized servers (`language_id` -> server).
94    pub servers: HashMap<String, LspServer>,
95    /// Failures that occurred during spawn attempts.
96    pub failures: Vec<ServerSpawnFailure>,
97}
98
99impl ServerInitResult {
100    /// Create a new empty result.
101    #[must_use]
102    pub fn new() -> Self {
103        Self {
104            servers: HashMap::new(),
105            failures: Vec::new(),
106        }
107    }
108
109    /// Check if any servers were successfully initialized.
110    ///
111    /// Returns `true` if at least one server is available for use.
112    #[must_use]
113    pub fn has_servers(&self) -> bool {
114        !self.servers.is_empty()
115    }
116
117    /// Check if all attempted servers failed.
118    ///
119    /// Returns `true` only if there were failures and no servers succeeded.
120    /// Returns `false` for empty results (no servers configured).
121    #[must_use]
122    pub fn all_failed(&self) -> bool {
123        self.servers.is_empty() && !self.failures.is_empty()
124    }
125
126    /// Check if some but not all servers failed.
127    ///
128    /// Returns `true` if there are both successful servers and failures.
129    #[must_use]
130    pub fn partial_success(&self) -> bool {
131        !self.servers.is_empty() && !self.failures.is_empty()
132    }
133
134    /// Get the number of successfully initialized servers.
135    #[must_use]
136    pub fn server_count(&self) -> usize {
137        self.servers.len()
138    }
139
140    /// Get the number of failures.
141    #[must_use]
142    pub fn failure_count(&self) -> usize {
143        self.failures.len()
144    }
145
146    /// Add a successful server.
147    ///
148    /// If a server with the same `language_id` already exists, it will be replaced.
149    pub fn add_server(&mut self, language_id: String, server: LspServer) {
150        self.servers.insert(language_id, server);
151    }
152
153    /// Add a failure.
154    pub fn add_failure(&mut self, failure: ServerSpawnFailure) {
155        self.failures.push(failure);
156    }
157}
158
159impl Default for ServerInitResult {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165/// Managed LSP server instance with capabilities and encoding.
166pub struct LspServer {
167    client: LspClient,
168    capabilities: ServerCapabilities,
169    position_encoding: PositionEncodingKind,
170    /// Child process handle. Kept alive for process lifetime management.
171    /// When dropped, the process is terminated via SIGKILL (`kill_on_drop`).
172    _child: tokio::process::Child,
173}
174
175impl std::fmt::Debug for LspServer {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.debug_struct("LspServer")
178            .field("client", &self.client)
179            .field("capabilities", &self.capabilities)
180            .field("position_encoding", &self.position_encoding)
181            .field("_child", &"<process>")
182            .finish()
183    }
184}
185
186impl LspServer {
187    /// Spawn and initialize LSP server.
188    ///
189    /// This performs the complete initialization sequence:
190    /// 1. Spawns the LSP server as a child process
191    /// 2. Sends initialize request with client capabilities
192    /// 3. Receives server capabilities from initialize response
193    /// 4. Sends initialized notification
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if:
198    /// - Server process fails to spawn
199    /// - Initialize request fails or times out
200    /// - Server returns error during initialization
201    pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
202        info!(
203            "Spawning LSP server: {} {:?}",
204            config.server_config.command, config.server_config.args
205        );
206
207        let mut child = Command::new(&config.server_config.command)
208            .args(&config.server_config.args)
209            .stdin(Stdio::piped())
210            .stdout(Stdio::piped())
211            .stderr(Stdio::null())
212            .kill_on_drop(true)
213            .spawn()
214            .map_err(|e| Error::ServerSpawnFailed {
215                command: config.server_config.command.clone(),
216                source: e,
217            })?;
218
219        let stdin = child
220            .stdin
221            .take()
222            .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
223        let stdout = child
224            .stdout
225            .take()
226            .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
227
228        let transport = LspTransport::new(stdin, stdout);
229        let client = LspClient::from_transport(config.server_config.clone(), transport);
230
231        let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
232
233        info!("LSP server initialized successfully");
234
235        Ok(Self {
236            client,
237            capabilities,
238            position_encoding,
239            _child: child,
240        })
241    }
242
243    /// Perform LSP initialization handshake.
244    ///
245    /// Sends initialize request and waits for response, then sends initialized notification.
246    async fn initialize(
247        client: &LspClient,
248        config: &ServerInitConfig,
249    ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
250        debug!("Sending initialize request");
251
252        let workspace_folders: Vec<WorkspaceFolder> = config
253            .workspace_roots
254            .iter()
255            .map(|root| {
256                let path_str = root.to_str().ok_or_else(|| {
257                    let root_display = root.display();
258                    Error::InvalidUri(format!("Invalid UTF-8 in path: {root_display}"))
259                })?;
260                let uri_str = if cfg!(windows) {
261                    format!("file:///{}", path_str.replace('\\', "/"))
262                } else {
263                    format!("file://{path_str}")
264                };
265                let uri = Uri::from_str(&uri_str).map_err(|_| {
266                    let root_display = root.display();
267                    Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
268                })?;
269                Ok(WorkspaceFolder {
270                    uri,
271                    name: root
272                        .file_name()
273                        .and_then(|n| n.to_str())
274                        .unwrap_or("workspace")
275                        .to_string(),
276                })
277            })
278            .collect::<Result<Vec<_>>>()?;
279
280        let params = InitializeParams {
281            process_id: Some(std::process::id()),
282            #[allow(deprecated)]
283            root_uri: None,
284            initialization_options: config.initialization_options.clone(),
285            capabilities: ClientCapabilities {
286                general: Some(GeneralClientCapabilities {
287                    position_encodings: Some(vec![
288                        PositionEncodingKind::UTF8,
289                        PositionEncodingKind::UTF16,
290                    ]),
291                    ..Default::default()
292                }),
293                text_document: Some(lsp_types::TextDocumentClientCapabilities {
294                    hover: Some(lsp_types::HoverClientCapabilities {
295                        dynamic_registration: Some(false),
296                        content_format: Some(vec![
297                            lsp_types::MarkupKind::Markdown,
298                            lsp_types::MarkupKind::PlainText,
299                        ]),
300                    }),
301                    definition: Some(lsp_types::GotoCapability {
302                        dynamic_registration: Some(false),
303                        link_support: Some(true),
304                    }),
305                    references: Some(lsp_types::ReferenceClientCapabilities {
306                        dynamic_registration: Some(false),
307                    }),
308                    ..Default::default()
309                }),
310                workspace: Some(lsp_types::WorkspaceClientCapabilities {
311                    workspace_folders: Some(true),
312                    ..Default::default()
313                }),
314                ..Default::default()
315            },
316            client_info: Some(ClientInfo {
317                name: "mcpls".to_string(),
318                version: Some(env!("CARGO_PKG_VERSION").to_string()),
319            }),
320            workspace_folders: Some(workspace_folders),
321            ..Default::default()
322        };
323
324        let result: InitializeResult = client
325            .request("initialize", params, Duration::from_secs(30))
326            .await
327            .map_err(|e| Error::LspInitFailed {
328                message: format!("Initialize request failed: {e}"),
329            })?;
330
331        let position_encoding = result
332            .capabilities
333            .position_encoding
334            .clone()
335            .unwrap_or(PositionEncodingKind::UTF16);
336
337        debug!(
338            "Server capabilities received, encoding: {:?}",
339            position_encoding
340        );
341
342        client
343            .notify("initialized", InitializedParams {})
344            .await
345            .map_err(|e| Error::LspInitFailed {
346                message: format!("Initialized notification failed: {e}"),
347            })?;
348
349        Ok((result.capabilities, position_encoding))
350    }
351
352    /// Get server capabilities.
353    #[must_use]
354    pub const fn capabilities(&self) -> &ServerCapabilities {
355        &self.capabilities
356    }
357
358    /// Get negotiated position encoding.
359    #[must_use]
360    pub fn position_encoding(&self) -> PositionEncodingKind {
361        self.position_encoding.clone()
362    }
363
364    /// Get client for making requests.
365    #[must_use]
366    pub const fn client(&self) -> &LspClient {
367        &self.client
368    }
369
370    /// Shutdown server gracefully.
371    ///
372    /// Sends shutdown request, waits for response, then sends exit notification.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if shutdown sequence fails.
377    pub async fn shutdown(self) -> Result<()> {
378        debug!("Shutting down LSP server");
379
380        let _: serde_json::Value = self
381            .client
382            .request("shutdown", serde_json::Value::Null, Duration::from_secs(5))
383            .await?;
384
385        self.client.notify("exit", serde_json::Value::Null).await?;
386
387        self.client.shutdown().await?;
388
389        info!("LSP server shut down successfully");
390        Ok(())
391    }
392
393    /// Spawn multiple LSP servers in batch mode with graceful degradation.
394    ///
395    /// Attempts to spawn and initialize all configured servers. If some servers
396    /// fail to spawn, the successful servers are still returned. This enables
397    /// graceful degradation where the system can continue to operate with
398    /// partial functionality.
399    ///
400    /// # Behavior
401    ///
402    /// - Attempts to spawn each server sequentially
403    /// - Logs success (info) and failure (error) for each server
404    /// - Accumulates successful servers and failures
405    /// - Never panics or returns early - attempts all servers
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// use mcpls_core::lsp::{LspServer, ServerInitConfig};
411    /// use mcpls_core::config::LspServerConfig;
412    /// use std::path::PathBuf;
413    ///
414    /// # async fn example() {
415    /// let configs = vec![
416    ///     ServerInitConfig {
417    ///         server_config: LspServerConfig::rust_analyzer(),
418    ///         workspace_roots: vec![PathBuf::from("/workspace")],
419    ///         initialization_options: None,
420    ///     },
421    ///     ServerInitConfig {
422    ///         server_config: LspServerConfig::pyright(),
423    ///         workspace_roots: vec![PathBuf::from("/workspace")],
424    ///         initialization_options: None,
425    ///     },
426    /// ];
427    ///
428    /// let result = LspServer::spawn_batch(&configs).await;
429    ///
430    /// if result.has_servers() {
431    ///     println!("Successfully spawned {} servers", result.server_count());
432    /// }
433    ///
434    /// if result.partial_success() {
435    ///     eprintln!("Warning: {} servers failed", result.failure_count());
436    /// }
437    /// # }
438    /// ```
439    pub async fn spawn_batch(configs: &[ServerInitConfig]) -> ServerInitResult {
440        let mut result = ServerInitResult::new();
441
442        for config in configs {
443            let language_id = config.server_config.language_id.clone();
444            let command = config.server_config.command.clone();
445
446            match Self::spawn(config.clone()).await {
447                Ok(server) => {
448                    info!(
449                        "Successfully spawned LSP server: {} ({})",
450                        language_id, command
451                    );
452                    result.add_server(language_id, server);
453                }
454                Err(e) => {
455                    tracing::error!(
456                        "Failed to spawn LSP server: {} ({}): {}",
457                        language_id,
458                        command,
459                        e
460                    );
461                    result.add_failure(ServerSpawnFailure {
462                        language_id,
463                        command,
464                        message: e.to_string(),
465                    });
466                }
467            }
468        }
469
470        result
471    }
472}
473
474#[cfg(test)]
475#[allow(clippy::unwrap_used)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_server_state_ready() {
481        assert!(ServerState::Ready.is_ready());
482        assert!(ServerState::Ready.can_accept_requests());
483    }
484
485    #[test]
486    fn test_server_state_uninitialized() {
487        assert!(!ServerState::Uninitialized.is_ready());
488        assert!(!ServerState::Uninitialized.can_accept_requests());
489    }
490
491    #[test]
492    fn test_server_state_initializing() {
493        assert!(!ServerState::Initializing.is_ready());
494        assert!(!ServerState::Initializing.can_accept_requests());
495    }
496
497    #[test]
498    fn test_server_state_shutting_down() {
499        assert!(!ServerState::ShuttingDown.is_ready());
500        assert!(!ServerState::ShuttingDown.can_accept_requests());
501    }
502
503    #[test]
504    fn test_server_state_shutdown() {
505        assert!(!ServerState::Shutdown.is_ready());
506        assert!(!ServerState::Shutdown.can_accept_requests());
507    }
508
509    #[test]
510    fn test_server_state_equality() {
511        assert_eq!(ServerState::Ready, ServerState::Ready);
512        assert_ne!(ServerState::Ready, ServerState::Uninitialized);
513        assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
514    }
515
516    #[test]
517    fn test_server_state_clone() {
518        let state = ServerState::Ready;
519        let cloned = state;
520        assert_eq!(state, cloned);
521    }
522
523    #[test]
524    fn test_server_state_debug() {
525        let state = ServerState::Ready;
526        let debug_str = format!("{state:?}");
527        assert!(debug_str.contains("Ready"));
528    }
529
530    #[test]
531    fn test_server_init_config_clone() {
532        let config = ServerInitConfig {
533            server_config: LspServerConfig::rust_analyzer(),
534            workspace_roots: vec![PathBuf::from("/tmp/workspace")],
535            initialization_options: Some(serde_json::json!({"key": "value"})),
536        };
537
538        #[allow(clippy::redundant_clone)]
539        let cloned = config.clone();
540        assert_eq!(cloned.server_config.language_id, "rust");
541        assert_eq!(cloned.workspace_roots.len(), 1);
542    }
543
544    #[test]
545    fn test_server_init_config_debug() {
546        let config = ServerInitConfig {
547            server_config: LspServerConfig::pyright(),
548            workspace_roots: vec![],
549            initialization_options: None,
550        };
551
552        let debug_str = format!("{config:?}");
553        assert!(debug_str.contains("python"));
554        assert!(debug_str.contains("pyright"));
555    }
556
557    #[test]
558    fn test_server_init_config_with_options() {
559        use std::collections::HashMap;
560
561        let init_opts = serde_json::json!({
562            "settings": {
563                "python": {
564                    "analysis": {
565                        "typeCheckingMode": "strict"
566                    }
567                }
568            }
569        });
570
571        let mut env = HashMap::new();
572        env.insert("PYTHONPATH".to_string(), "/usr/lib".to_string());
573
574        let config = ServerInitConfig {
575            server_config: LspServerConfig {
576                language_id: "python".to_string(),
577                command: "pyright-langserver".to_string(),
578                args: vec!["--stdio".to_string()],
579                env,
580                file_patterns: vec!["**/*.py".to_string()],
581                initialization_options: Some(init_opts.clone()),
582                timeout_seconds: 10,
583            },
584            workspace_roots: vec![PathBuf::from("/workspace")],
585            initialization_options: Some(init_opts),
586        };
587
588        assert!(config.initialization_options.is_some());
589        assert_eq!(config.workspace_roots.len(), 1);
590    }
591
592    #[test]
593    fn test_server_init_config_empty_workspace() {
594        let config = ServerInitConfig {
595            server_config: LspServerConfig::typescript(),
596            workspace_roots: vec![],
597            initialization_options: None,
598        };
599
600        assert!(config.workspace_roots.is_empty());
601    }
602
603    #[test]
604    fn test_server_init_config_multiple_workspaces() {
605        let config = ServerInitConfig {
606            server_config: LspServerConfig::rust_analyzer(),
607            workspace_roots: vec![
608                PathBuf::from("/workspace1"),
609                PathBuf::from("/workspace2"),
610                PathBuf::from("/workspace3"),
611            ],
612            initialization_options: None,
613        };
614
615        assert_eq!(config.workspace_roots.len(), 3);
616    }
617
618    #[tokio::test]
619    async fn test_lsp_server_getters() {
620        use lsp_types::ServerCapabilities;
621
622        let mock_child = tokio::process::Command::new("echo")
623            .stdin(Stdio::piped())
624            .stdout(Stdio::piped())
625            .kill_on_drop(true)
626            .spawn()
627            .unwrap();
628
629        let mock_stdin = tokio::process::Command::new("cat")
630            .stdin(Stdio::piped())
631            .spawn()
632            .unwrap()
633            .stdin
634            .take()
635            .unwrap();
636
637        let mock_stdout = tokio::process::Command::new("echo")
638            .stdout(Stdio::piped())
639            .spawn()
640            .unwrap()
641            .stdout
642            .take()
643            .unwrap();
644
645        let transport = LspTransport::new(mock_stdin, mock_stdout);
646        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
647
648        let server = LspServer {
649            client,
650            capabilities: ServerCapabilities::default(),
651            position_encoding: PositionEncodingKind::UTF8,
652            _child: mock_child,
653        };
654
655        assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
656        assert!(server.capabilities().text_document_sync.is_none());
657
658        let debug_str = format!("{server:?}");
659        assert!(debug_str.contains("LspServer"));
660        assert!(debug_str.contains("<process>"));
661    }
662
663    #[test]
664    fn test_server_init_result_new_empty() {
665        let result = ServerInitResult::new();
666        assert!(!result.has_servers());
667        assert!(!result.all_failed());
668        assert!(!result.partial_success());
669        assert_eq!(result.server_count(), 0);
670        assert_eq!(result.failure_count(), 0);
671    }
672
673    #[test]
674    fn test_server_init_result_default() {
675        let result = ServerInitResult::default();
676        assert!(!result.has_servers());
677        assert_eq!(result.server_count(), 0);
678        assert_eq!(result.failure_count(), 0);
679    }
680
681    #[test]
682    fn test_server_init_result_all_failures() {
683        let mut result = ServerInitResult::new();
684
685        result.add_failure(ServerSpawnFailure {
686            language_id: "rust".to_string(),
687            command: "rust-analyzer".to_string(),
688            message: "not found".to_string(),
689        });
690
691        result.add_failure(ServerSpawnFailure {
692            language_id: "python".to_string(),
693            command: "pyright".to_string(),
694            message: "permission denied".to_string(),
695        });
696
697        assert!(!result.has_servers());
698        assert!(result.all_failed());
699        assert!(!result.partial_success());
700        assert_eq!(result.server_count(), 0);
701        assert_eq!(result.failure_count(), 2);
702    }
703
704    #[tokio::test]
705    async fn test_server_init_result_all_success() {
706        let mut result = ServerInitResult::new();
707
708        let mock_child1 = tokio::process::Command::new("echo")
709            .stdin(Stdio::piped())
710            .stdout(Stdio::piped())
711            .kill_on_drop(true)
712            .spawn()
713            .unwrap();
714
715        let mock_stdin1 = tokio::process::Command::new("cat")
716            .stdin(Stdio::piped())
717            .spawn()
718            .unwrap()
719            .stdin
720            .take()
721            .unwrap();
722
723        let mock_stdout1 = tokio::process::Command::new("echo")
724            .stdout(Stdio::piped())
725            .spawn()
726            .unwrap()
727            .stdout
728            .take()
729            .unwrap();
730
731        let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
732        let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
733
734        let server1 = LspServer {
735            client: client1,
736            capabilities: lsp_types::ServerCapabilities::default(),
737            position_encoding: PositionEncodingKind::UTF8,
738            _child: mock_child1,
739        };
740
741        result.add_server("rust".to_string(), server1);
742
743        assert!(result.has_servers());
744        assert!(!result.all_failed());
745        assert!(!result.partial_success());
746        assert_eq!(result.server_count(), 1);
747        assert_eq!(result.failure_count(), 0);
748    }
749
750    #[tokio::test]
751    async fn test_server_init_result_partial_success() {
752        let mut result = ServerInitResult::new();
753
754        let mock_child = tokio::process::Command::new("echo")
755            .stdin(Stdio::piped())
756            .stdout(Stdio::piped())
757            .kill_on_drop(true)
758            .spawn()
759            .unwrap();
760
761        let mock_stdin = tokio::process::Command::new("cat")
762            .stdin(Stdio::piped())
763            .spawn()
764            .unwrap()
765            .stdin
766            .take()
767            .unwrap();
768
769        let mock_stdout = tokio::process::Command::new("echo")
770            .stdout(Stdio::piped())
771            .spawn()
772            .unwrap()
773            .stdout
774            .take()
775            .unwrap();
776
777        let transport = LspTransport::new(mock_stdin, mock_stdout);
778        let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
779
780        let server = LspServer {
781            client,
782            capabilities: lsp_types::ServerCapabilities::default(),
783            position_encoding: PositionEncodingKind::UTF8,
784            _child: mock_child,
785        };
786
787        result.add_server("rust".to_string(), server);
788
789        result.add_failure(ServerSpawnFailure {
790            language_id: "python".to_string(),
791            command: "pyright".to_string(),
792            message: "not found".to_string(),
793        });
794
795        assert!(result.has_servers());
796        assert!(!result.all_failed());
797        assert!(result.partial_success());
798        assert_eq!(result.server_count(), 1);
799        assert_eq!(result.failure_count(), 1);
800    }
801
802    #[tokio::test]
803    async fn test_server_init_result_multiple_servers() {
804        let mut result = ServerInitResult::new();
805
806        for i in 0..3 {
807            let mock_child = tokio::process::Command::new("echo")
808                .stdin(Stdio::piped())
809                .stdout(Stdio::piped())
810                .kill_on_drop(true)
811                .spawn()
812                .unwrap();
813
814            let mock_stdin = tokio::process::Command::new("cat")
815                .stdin(Stdio::piped())
816                .spawn()
817                .unwrap()
818                .stdin
819                .take()
820                .unwrap();
821
822            let mock_stdout = tokio::process::Command::new("echo")
823                .stdout(Stdio::piped())
824                .spawn()
825                .unwrap()
826                .stdout
827                .take()
828                .unwrap();
829
830            let transport = LspTransport::new(mock_stdin, mock_stdout);
831            let config = if i == 0 {
832                LspServerConfig::rust_analyzer()
833            } else if i == 1 {
834                LspServerConfig::pyright()
835            } else {
836                LspServerConfig::typescript()
837            };
838            let client = LspClient::from_transport(config.clone(), transport);
839
840            let server = LspServer {
841                client,
842                capabilities: lsp_types::ServerCapabilities::default(),
843                position_encoding: PositionEncodingKind::UTF8,
844                _child: mock_child,
845            };
846
847            result.add_server(config.language_id, server);
848        }
849
850        assert!(result.has_servers());
851        assert!(!result.all_failed());
852        assert!(!result.partial_success());
853        assert_eq!(result.server_count(), 3);
854        assert_eq!(result.failure_count(), 0);
855    }
856
857    #[tokio::test]
858    async fn test_server_init_result_replace_server() {
859        let mut result = ServerInitResult::new();
860
861        let mock_child1 = tokio::process::Command::new("echo")
862            .stdin(Stdio::piped())
863            .stdout(Stdio::piped())
864            .kill_on_drop(true)
865            .spawn()
866            .unwrap();
867
868        let mock_stdin1 = tokio::process::Command::new("cat")
869            .stdin(Stdio::piped())
870            .spawn()
871            .unwrap()
872            .stdin
873            .take()
874            .unwrap();
875
876        let mock_stdout1 = tokio::process::Command::new("echo")
877            .stdout(Stdio::piped())
878            .spawn()
879            .unwrap()
880            .stdout
881            .take()
882            .unwrap();
883
884        let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
885        let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
886
887        let server1 = LspServer {
888            client: client1,
889            capabilities: lsp_types::ServerCapabilities::default(),
890            position_encoding: PositionEncodingKind::UTF8,
891            _child: mock_child1,
892        };
893
894        result.add_server("rust".to_string(), server1);
895        assert_eq!(result.server_count(), 1);
896
897        let mock_child2 = tokio::process::Command::new("echo")
898            .stdin(Stdio::piped())
899            .stdout(Stdio::piped())
900            .kill_on_drop(true)
901            .spawn()
902            .unwrap();
903
904        let mock_stdin2 = tokio::process::Command::new("cat")
905            .stdin(Stdio::piped())
906            .spawn()
907            .unwrap()
908            .stdin
909            .take()
910            .unwrap();
911
912        let mock_stdout2 = tokio::process::Command::new("echo")
913            .stdout(Stdio::piped())
914            .spawn()
915            .unwrap()
916            .stdout
917            .take()
918            .unwrap();
919
920        let transport2 = LspTransport::new(mock_stdin2, mock_stdout2);
921        let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
922
923        let server2 = LspServer {
924            client: client2,
925            capabilities: lsp_types::ServerCapabilities::default(),
926            position_encoding: PositionEncodingKind::UTF16,
927            _child: mock_child2,
928        };
929
930        result.add_server("rust".to_string(), server2);
931        assert_eq!(result.server_count(), 1);
932    }
933
934    #[test]
935    fn test_server_init_result_debug() {
936        let mut result = ServerInitResult::new();
937
938        result.add_failure(ServerSpawnFailure {
939            language_id: "rust".to_string(),
940            command: "rust-analyzer".to_string(),
941            message: "not found".to_string(),
942        });
943
944        let debug_str = format!("{result:?}");
945        assert!(debug_str.contains("ServerInitResult"));
946    }
947
948    #[test]
949    fn test_server_init_result_multiple_failures() {
950        let mut result = ServerInitResult::new();
951
952        result.add_failure(ServerSpawnFailure {
953            language_id: "python".to_string(),
954            command: "pyright".to_string(),
955            message: "not found".to_string(),
956        });
957
958        result.add_failure(ServerSpawnFailure {
959            language_id: "typescript".to_string(),
960            command: "tsserver".to_string(),
961            message: "command not found".to_string(),
962        });
963
964        assert_eq!(result.failure_count(), 2);
965        assert_eq!(result.server_count(), 0);
966        assert!(result.all_failed());
967        assert!(!result.partial_success());
968    }
969
970    #[tokio::test]
971    async fn test_spawn_batch_empty_configs() {
972        let configs: &[ServerInitConfig] = &[];
973        let result = LspServer::spawn_batch(configs).await;
974
975        assert!(!result.has_servers());
976        assert!(!result.all_failed());
977        assert!(!result.partial_success());
978        assert_eq!(result.server_count(), 0);
979        assert_eq!(result.failure_count(), 0);
980    }
981
982    #[tokio::test]
983    async fn test_spawn_batch_single_invalid_config() {
984        let configs = vec![ServerInitConfig {
985            server_config: LspServerConfig {
986                language_id: "rust".to_string(),
987                command: "nonexistent-command-12345".to_string(),
988                args: vec![],
989                env: std::collections::HashMap::new(),
990                file_patterns: vec!["**/*.rs".to_string()],
991                initialization_options: None,
992                timeout_seconds: 10,
993            },
994            workspace_roots: vec![],
995            initialization_options: None,
996        }];
997
998        let result = LspServer::spawn_batch(&configs).await;
999
1000        assert!(!result.has_servers());
1001        assert!(result.all_failed());
1002        assert!(!result.partial_success());
1003        assert_eq!(result.server_count(), 0);
1004        assert_eq!(result.failure_count(), 1);
1005
1006        let failure = &result.failures[0];
1007        assert_eq!(failure.language_id, "rust");
1008        assert_eq!(failure.command, "nonexistent-command-12345");
1009        assert!(failure.message.contains("spawn"));
1010    }
1011
1012    #[tokio::test]
1013    async fn test_spawn_batch_all_invalid_configs() {
1014        let configs = vec![
1015            ServerInitConfig {
1016                server_config: LspServerConfig {
1017                    language_id: "rust".to_string(),
1018                    command: "nonexistent-rust-analyzer".to_string(),
1019                    args: vec![],
1020                    env: std::collections::HashMap::new(),
1021                    file_patterns: vec!["**/*.rs".to_string()],
1022                    initialization_options: None,
1023                    timeout_seconds: 10,
1024                },
1025                workspace_roots: vec![],
1026                initialization_options: None,
1027            },
1028            ServerInitConfig {
1029                server_config: LspServerConfig {
1030                    language_id: "python".to_string(),
1031                    command: "nonexistent-pyright".to_string(),
1032                    args: vec![],
1033                    env: std::collections::HashMap::new(),
1034                    file_patterns: vec!["**/*.py".to_string()],
1035                    initialization_options: None,
1036                    timeout_seconds: 10,
1037                },
1038                workspace_roots: vec![],
1039                initialization_options: None,
1040            },
1041            ServerInitConfig {
1042                server_config: LspServerConfig {
1043                    language_id: "typescript".to_string(),
1044                    command: "nonexistent-tsserver".to_string(),
1045                    args: vec![],
1046                    env: std::collections::HashMap::new(),
1047                    file_patterns: vec!["**/*.ts".to_string()],
1048                    initialization_options: None,
1049                    timeout_seconds: 10,
1050                },
1051                workspace_roots: vec![],
1052                initialization_options: None,
1053            },
1054        ];
1055
1056        let result = LspServer::spawn_batch(&configs).await;
1057
1058        assert!(!result.has_servers());
1059        assert!(result.all_failed());
1060        assert!(!result.partial_success());
1061        assert_eq!(result.server_count(), 0);
1062        assert_eq!(result.failure_count(), 3);
1063
1064        let failure_languages: Vec<_> = result
1065            .failures
1066            .iter()
1067            .map(|f| f.language_id.as_str())
1068            .collect();
1069        assert!(failure_languages.contains(&"rust"));
1070        assert!(failure_languages.contains(&"python"));
1071        assert!(failure_languages.contains(&"typescript"));
1072    }
1073
1074    #[tokio::test]
1075    async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1076        let configs = vec![
1077            ServerInitConfig {
1078                server_config: LspServerConfig {
1079                    language_id: "lang1".to_string(),
1080                    command: "cmd1-nonexistent".to_string(),
1081                    args: vec![],
1082                    env: std::collections::HashMap::new(),
1083                    file_patterns: vec![],
1084                    initialization_options: None,
1085                    timeout_seconds: 10,
1086                },
1087                workspace_roots: vec![],
1088                initialization_options: None,
1089            },
1090            ServerInitConfig {
1091                server_config: LspServerConfig {
1092                    language_id: "lang2".to_string(),
1093                    command: "cmd2-nonexistent".to_string(),
1094                    args: vec![],
1095                    env: std::collections::HashMap::new(),
1096                    file_patterns: vec![],
1097                    initialization_options: None,
1098                    timeout_seconds: 10,
1099                },
1100                workspace_roots: vec![],
1101                initialization_options: None,
1102            },
1103        ];
1104
1105        let result = LspServer::spawn_batch(&configs).await;
1106
1107        assert_eq!(result.failure_count(), 2);
1108
1109        assert_eq!(result.failures[0].language_id, "lang1");
1110        assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1111
1112        assert_eq!(result.failures[1].language_id, "lang2");
1113        assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1114    }
1115
1116    #[tokio::test]
1117    async fn test_spawn_batch_logs_each_failure() {
1118        let configs = vec![
1119            ServerInitConfig {
1120                server_config: LspServerConfig {
1121                    language_id: "test1".to_string(),
1122                    command: "nonexistent-test1".to_string(),
1123                    args: vec![],
1124                    env: std::collections::HashMap::new(),
1125                    file_patterns: vec![],
1126                    initialization_options: None,
1127                    timeout_seconds: 10,
1128                },
1129                workspace_roots: vec![],
1130                initialization_options: None,
1131            },
1132            ServerInitConfig {
1133                server_config: LspServerConfig {
1134                    language_id: "test2".to_string(),
1135                    command: "nonexistent-test2".to_string(),
1136                    args: vec![],
1137                    env: std::collections::HashMap::new(),
1138                    file_patterns: vec![],
1139                    initialization_options: None,
1140                    timeout_seconds: 10,
1141                },
1142                workspace_roots: vec![],
1143                initialization_options: None,
1144            },
1145        ];
1146
1147        let result = LspServer::spawn_batch(&configs).await;
1148
1149        assert_eq!(result.failure_count(), 2);
1150        assert_eq!(result.failures[0].language_id, "test1");
1151        assert_eq!(result.failures[1].language_id, "test2");
1152    }
1153}