1use super::{LspConfig, LspServerType, QueryIntent, LspSearchResponse, LspSearchResult, TraversalBounds};
4use crate::lsp::{LspClient, HintCache, LspRouter, LspServerProcess};
5use anyhow::{anyhow, Result};
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::time::Instant;
10use tokio::sync::RwLock;
11use tracing::{debug, error, info, warn};
12
13pub struct LspManager {
15 config: LspConfig,
16 servers: HashMap<LspServerType, Arc<LspServerProcess>>,
17 clients: HashMap<LspServerType, Arc<LspClient>>,
18 hint_cache: Arc<HintCache>,
19 router: LspRouter,
20 stats: Arc<RwLock<LspStats>>,
21}
22
23#[derive(Debug, Default, Clone)]
24pub struct LspStats {
25 pub total_requests: u64,
26 pub cache_hits: u64,
27 pub lsp_routed: u64,
28 pub fallback_used: u64,
29 pub avg_response_time_ms: u64,
30 pub server_errors: HashMap<LspServerType, u64>,
31}
32
33impl LspManager {
34 pub async fn new(config: LspConfig) -> Result<Self> {
35 info!("Initializing LSP Manager with routing target: {}%", config.routing_percentage * 100.0);
36
37 let hint_cache = Arc::new(HintCache::new(config.cache_ttl_hours).await?);
38 let router = LspRouter::new(config.routing_percentage);
39 let stats = Arc::new(RwLock::new(LspStats::default()));
40
41 let mut manager = Self {
42 config,
43 servers: HashMap::new(),
44 clients: HashMap::new(),
45 hint_cache,
46 router,
47 stats,
48 };
49
50 manager.initialize_servers().await?;
52
53 Ok(manager)
54 }
55
56 async fn initialize_servers(&mut self) -> Result<()> {
57 let server_types = vec![
58 LspServerType::TypeScript,
59 LspServerType::Python,
60 LspServerType::Rust,
61 LspServerType::Go,
62 ];
63
64 for server_type in server_types {
65 match self.start_server(server_type).await {
66 Ok((server_process, client)) => {
67 self.servers.insert(server_type, Arc::new(server_process));
68 self.clients.insert(server_type, Arc::new(client));
69 info!("Successfully started {:?} language server", server_type);
70 }
71 Err(e) => {
72 warn!("Failed to start {:?} language server: {:?}", server_type, e);
73 }
75 }
76 }
77
78 if self.clients.is_empty() {
79 warn!("No language servers started - LSP functionality will be limited");
80 }
83
84 info!("LSP Manager initialized with {} active servers", self.clients.len());
85 Ok(())
86 }
87
88 async fn start_server(&self, server_type: LspServerType) -> Result<(LspServerProcess, LspClient)> {
89 let (command, args) = server_type.server_command();
90
91 let mut server_process = LspServerProcess::new(command, &args, self.config.server_timeout_ms).await?;
93
94 let client = LspClient::new(server_process.stdin(), server_process.stdout()).await?;
96
97 client.initialize(server_type).await?;
99
100 Ok((server_process, client))
101 }
102
103 pub async fn search(&self, query: &str, file_path: Option<&str>) -> Result<LspSearchResponse> {
104 let start_time = Instant::now();
105 let intent = QueryIntent::classify(query);
106
107 {
109 let mut stats = self.stats.write().await;
110 stats.total_requests += 1;
111 }
112
113 let should_use_lsp = self.should_route_to_lsp(query, &intent, file_path).await;
115
116 if should_use_lsp && intent.is_lsp_eligible() {
117 match self.lsp_search_with_safety_floor(query, &intent, file_path).await {
118 Ok(mut response) => {
119 response.total_time_ms = start_time.elapsed().as_millis() as u64;
120
121 {
123 let mut stats = self.stats.write().await;
124 stats.lsp_routed += 1;
125 stats.avg_response_time_ms =
126 (stats.avg_response_time_ms * (stats.total_requests - 1) + response.total_time_ms)
127 / stats.total_requests;
128 }
129
130 self.router.report_lsp_result(&intent, true, response.total_time_ms).await;
132
133 return Ok(response);
134 }
135 Err(e) => {
136 warn!("LSP search failed, falling back to text search: {:?}", e);
137
138 let failure_time = start_time.elapsed().as_millis() as u64;
139
140 self.router.report_lsp_result(&intent, false, failure_time).await;
142
143 if let Some(file_path) = file_path {
145 if let Some(ext) = PathBuf::from(file_path).extension() {
146 if let Some(server_type) = LspServerType::from_file_extension(&ext.to_string_lossy()) {
147 let mut stats = self.stats.write().await;
148 *stats.server_errors.entry(server_type).or_insert(0) += 1;
149 }
150 }
151 }
152 }
153 }
154 }
155
156 {
158 let mut stats = self.stats.write().await;
159 stats.fallback_used += 1;
160 }
161
162 Ok(LspSearchResponse {
163 lsp_results: vec![],
164 fallback_results: vec![], total_time_ms: start_time.elapsed().as_millis() as u64,
166 lsp_time_ms: 0,
167 cache_hit_rate: 0.0,
168 server_types_used: vec![],
169 intent,
170 })
171 }
172
173 async fn should_route_to_lsp(&self, query: &str, intent: &QueryIntent, file_path: Option<&str>) -> bool {
174 if !self.config.enabled || !intent.is_lsp_eligible() {
176 return false;
177 }
178
179 if let Some(path) = file_path {
181 if let Some(ext) = PathBuf::from(path).extension() {
182 if let Some(server_type) = LspServerType::from_file_extension(&ext.to_string_lossy()) {
183 return self.clients.contains_key(&server_type);
184 }
185 }
186 }
187
188 self.router.should_route(query, intent).await
190 }
191
192 async fn lsp_search_with_safety_floor(&self, query: &str, intent: &QueryIntent, file_path: Option<&str>) -> Result<LspSearchResponse> {
197 if intent.requires_safety_floor() {
198 let lsp_response = self.lsp_search(query, intent, file_path).await;
200
201 match lsp_response {
202 Ok(mut lsp_result) => {
203 if lsp_result.lsp_results.is_empty() {
206 warn!("LSP returned empty results for safety floor query '{}', intent: {:?}", query, intent);
207 }
209
210 debug!(
211 "Safety floor query '{}' (intent: {:?}) returned {} results",
212 query, intent, lsp_result.lsp_results.len()
213 );
214
215 Ok(lsp_result)
216 }
217 Err(e) => {
218 warn!("Safety floor LSP search failed for query '{}': {:?}", query, e);
220 Err(e)
221 }
222 }
223 } else {
224 self.lsp_search(query, intent, file_path).await
226 }
227 }
228
229 async fn lsp_search(&self, query: &str, intent: &QueryIntent, file_path: Option<&str>) -> Result<LspSearchResponse> {
230 let lsp_start = Instant::now();
231
232 let cache_key = format!("{}:{}:{}", query, intent.to_string(), file_path.unwrap_or(""));
234 if let Some(cached_result) = self.hint_cache.get(&cache_key).await? {
235 debug!("Cache hit for query: {}", query);
236
237 {
238 let mut stats = self.stats.write().await;
239 stats.cache_hits += 1;
240 }
241
242 return Ok(LspSearchResponse {
243 lsp_results: cached_result,
244 fallback_results: vec![],
245 total_time_ms: 0, lsp_time_ms: lsp_start.elapsed().as_millis() as u64,
247 cache_hit_rate: 1.0,
248 server_types_used: self.get_available_server_types(),
249 intent: intent.clone(),
250 });
251 }
252
253 let target_servers = self.determine_target_servers(query, file_path);
255
256 if target_servers.is_empty() {
257 return Err(anyhow!("No suitable language servers available"));
258 }
259
260 let results = self.execute_bounded_search(query, intent, &target_servers).await?;
262
263 self.hint_cache.set(cache_key, results.clone(), self.config.cache_ttl_hours * 3600).await?;
265
266 let lsp_time = lsp_start.elapsed().as_millis() as u64;
267
268 Ok(LspSearchResponse {
269 lsp_results: results,
270 fallback_results: vec![],
271 total_time_ms: 0, lsp_time_ms: lsp_time,
273 cache_hit_rate: 0.0,
274 server_types_used: target_servers,
275 intent: intent.clone(),
276 })
277 }
278
279 fn determine_target_servers(&self, _query: &str, file_path: Option<&str>) -> Vec<LspServerType> {
280 if let Some(path) = file_path {
281 if let Some(ext) = PathBuf::from(path).extension() {
283 if let Some(server_type) = LspServerType::from_file_extension(&ext.to_string_lossy()) {
284 if self.clients.contains_key(&server_type) {
285 return vec![server_type];
286 }
287 }
288 }
289 }
290
291 self.clients.keys().copied().collect()
293 }
294
295 async fn execute_bounded_search(
296 &self,
297 query: &str,
298 intent: &QueryIntent,
299 servers: &[LspServerType],
300 ) -> Result<Vec<LspSearchResult>> {
301 let mut all_results = Vec::new();
302 let bounds = &self.config.traversal_bounds;
303
304 let mut tasks = vec![];
306
307 for &server_type in servers {
308 if let Some(client) = self.clients.get(&server_type) {
309 let client = client.clone();
310 let query = query.to_string();
311 let intent = intent.clone();
312 let bounds = bounds.clone();
313
314 let task = tokio::spawn(async move {
315 client.bounded_search(&query, &intent, &bounds).await
316 });
317
318 tasks.push((server_type, task));
319 }
320 }
321
322 for (server_type, task) in tasks {
324 match tokio::time::timeout(
325 tokio::time::Duration::from_millis(self.config.server_timeout_ms),
326 task
327 ).await {
328 Ok(Ok(Ok(mut results))) => {
329 for result in &mut results {
331 result.server_type = server_type;
332 }
333 all_results.extend(results);
334 }
335 Ok(Ok(Err(e))) => {
336 warn!("LSP search failed for {:?}: {:?}", server_type, e);
337 }
338 Ok(Err(e)) => {
339 warn!("LSP task failed for {:?}: {:?}", server_type, e);
340 }
341 Err(_) => {
342 warn!("LSP search timed out for {:?}", server_type);
343 }
344 }
345 }
346
347 if all_results.len() > bounds.max_results as usize {
349 all_results.truncate(bounds.max_results as usize);
350 }
351
352 all_results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));
354
355 Ok(all_results)
356 }
357
358 fn get_available_server_types(&self) -> Vec<LspServerType> {
360 self.clients.keys().cloned().collect()
361 }
362
363 pub async fn get_stats(&self) -> LspStats {
364 self.stats.read().await.clone()
365 }
366
367 pub async fn get_routing_stats(&self) -> crate::lsp::router::RoutingStats {
369 self.router.get_routing_stats().await
370 }
371
372 pub async fn check_server_health(&self) -> Result<HashMap<LspServerType, bool>> {
374 let mut health_status = HashMap::new();
375
376 for (server_type, client) in &self.clients {
377 match client.ping().await {
378 Ok(_) => {
379 health_status.insert(*server_type, true);
380 debug!("{:?} LSP server is healthy", server_type);
381 }
382 Err(e) => {
383 health_status.insert(*server_type, false);
384 warn!("{:?} LSP server health check failed: {:?}", server_type, e);
385 }
386 }
387 }
388
389 Ok(health_status)
390 }
391
392 pub async fn shutdown(&mut self) -> Result<()> {
393 info!("Shutting down LSP Manager");
394
395 for (server_type, client) in &self.clients {
397 if let Err(e) = client.shutdown().await {
398 warn!("Error shutting down {:?} client: {:?}", server_type, e);
399 }
400 }
401
402 let servers = std::mem::take(&mut self.servers);
404 for (server_type, server) in servers {
405 if let Ok(mut server) = Arc::try_unwrap(server) {
406 if let Err(e) = server.shutdown().await {
407 warn!("Error shutting down {:?} server: {:?}", server_type, e);
408 }
409 } else {
410 warn!("Could not get exclusive access to {:?} server for shutdown", server_type);
411 }
412 }
413
414 self.hint_cache.shutdown().await?;
416
417 info!("LSP Manager shutdown complete");
418 Ok(())
419 }
420}
421
422impl Drop for LspManager {
423 fn drop(&mut self) {
424 debug!("LSP Manager dropped");
426 }
427}
428
429impl ToString for QueryIntent {
431 fn to_string(&self) -> String {
432 match self {
433 QueryIntent::Definition => "definition",
434 QueryIntent::References => "references",
435 QueryIntent::TypeDefinition => "type_definition",
436 QueryIntent::Implementation => "implementation",
437 QueryIntent::Declaration => "declaration",
438 QueryIntent::Symbol => "symbol",
439 QueryIntent::Completion => "completion",
440 QueryIntent::Hover => "hover",
441 QueryIntent::TextSearch => "text_search",
442 }.to_string()
443 }
444}
445
446#[cfg(test)]
447#[cfg(feature = "integration-tests")] mod tests {
449 use super::*;
450 use crate::lsp::{LspConfig, LspServerType, QueryIntent, LspSearchResult, TraversalBounds, HintType};
451 use std::time::Duration;
452 use tokio::sync::mpsc;
453 use serial_test::serial;
454
455 fn create_test_config() -> LspConfig {
456 LspConfig {
457 enabled: true,
458 routing_percentage: 0.5,
459 cache_ttl_hours: 1,
460 server_timeout_ms: 1000,
461 max_concurrent_requests: 5,
462 traversal_bounds: TraversalBounds {
463 max_depth: 3,
464 max_results: 100,
465 timeout_ms: 500,
466 },
467 }
468 }
469
470 fn create_test_lsp_result(content: &str, server_type: LspServerType) -> LspSearchResult {
471 LspSearchResult {
472 content: content.to_string(),
473 file_path: format!("test_{}.rs", content),
474 line_number: 42,
475 column: 10,
476 confidence: 0.9,
477 server_type,
478 hint_type: HintType::Definition,
479 context_lines: None,
480 }
481 }
482
483 #[tokio::test]
484 async fn test_lsp_stats_default() {
485 let stats = LspStats::default();
486
487 assert_eq!(stats.total_requests, 0);
488 assert_eq!(stats.cache_hits, 0);
489 assert_eq!(stats.lsp_routed, 0);
490 assert_eq!(stats.fallback_used, 0);
491 assert_eq!(stats.avg_response_time_ms, 0);
492 assert!(stats.server_errors.is_empty());
493 }
494
495 #[tokio::test]
496 async fn test_lsp_stats_clone() {
497 let mut stats = LspStats::default();
498 stats.total_requests = 10;
499 stats.cache_hits = 5;
500
501 let cloned = stats.clone();
502 assert_eq!(cloned.total_requests, 10);
503 assert_eq!(cloned.cache_hits, 5);
504 }
505
506 #[tokio::test]
507 #[serial] async fn test_lsp_manager_new_with_test_config() {
509 let config = create_test_config();
510
511 let result = LspManager::new(config).await;
514
515 match result {
517 Ok(manager) => {
518 let stats = manager.get_stats().await;
520 assert_eq!(stats.total_requests, 0);
521 }
522 Err(_) => {
523 }
525 }
526 }
527
528 #[tokio::test]
529 async fn test_determine_target_servers_with_file_path() {
530 let config = create_test_config();
531 let mut manager = LspManager {
532 config,
533 servers: HashMap::new(),
534 clients: HashMap::new(),
535 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
536 router: LspRouter::new(0.5),
537 stats: Arc::new(RwLock::new(LspStats::default())),
538 };
539
540 manager.clients.insert(LspServerType::TypeScript, Arc::new(create_mock_client().await));
542
543 let servers = manager.determine_target_servers("test", Some("test.ts"));
545 assert_eq!(servers, vec![LspServerType::TypeScript]);
546
547 let servers = manager.determine_target_servers("test", Some("test.py"));
549 assert!(servers.is_empty() || servers.contains(&LspServerType::TypeScript));
550
551 let servers = manager.determine_target_servers("test", None);
553 assert_eq!(servers.len(), 1);
554 assert_eq!(servers[0], LspServerType::TypeScript);
555 }
556
557 #[tokio::test]
558 async fn test_should_route_to_lsp_disabled() {
559 let mut config = create_test_config();
560 config.enabled = false;
561
562 let manager = LspManager {
563 config,
564 servers: HashMap::new(),
565 clients: HashMap::new(),
566 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
567 router: LspRouter::new(0.5),
568 stats: Arc::new(RwLock::new(LspStats::default())),
569 };
570
571 let intent = QueryIntent::Definition;
572 let should_route = manager.should_route_to_lsp("test", &intent, None).await;
573
574 assert!(!should_route);
575 }
576
577 #[tokio::test]
578 async fn test_should_route_to_lsp_ineligible_intent() {
579 let config = create_test_config();
580 let manager = LspManager {
581 config,
582 servers: HashMap::new(),
583 clients: HashMap::new(),
584 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
585 router: LspRouter::new(0.5),
586 stats: Arc::new(RwLock::new(LspStats::default())),
587 };
588
589 let intent = QueryIntent::TextSearch; let should_route = manager.should_route_to_lsp("test", &intent, None).await;
591
592 assert!(!should_route);
593 }
594
595 #[tokio::test]
596 async fn test_should_route_to_lsp_with_supported_file() {
597 let config = create_test_config();
598 let mut manager = LspManager {
599 config,
600 servers: HashMap::new(),
601 clients: HashMap::new(),
602 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
603 router: LspRouter::new(0.5),
604 stats: Arc::new(RwLock::new(LspStats::default())),
605 };
606
607 manager.clients.insert(LspServerType::TypeScript, Arc::new(create_mock_client().await));
609
610 let intent = QueryIntent::Definition;
611 let should_route = manager.should_route_to_lsp("test", &intent, Some("test.ts")).await;
612
613 assert!(should_route);
615 }
616
617 #[tokio::test]
618 async fn test_should_route_to_lsp_with_unsupported_file() {
619 let config = create_test_config();
620 let manager = LspManager {
621 config,
622 servers: HashMap::new(),
623 clients: HashMap::new(),
624 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
625 router: LspRouter::new(0.5),
626 stats: Arc::new(RwLock::new(LspStats::default())),
627 };
628
629 let intent = QueryIntent::Definition;
630 let should_route = manager.should_route_to_lsp("test", &intent, Some("test.txt")).await;
631
632 assert!(!should_route);
634 }
635
636 #[tokio::test]
637 async fn test_get_stats() {
638 let config = create_test_config();
639 let stats = Arc::new(RwLock::new(LspStats::default()));
640
641 {
643 let mut s = stats.write().await;
644 s.total_requests = 10;
645 s.cache_hits = 5;
646 s.lsp_routed = 8;
647 }
648
649 let manager = LspManager {
650 config,
651 servers: HashMap::new(),
652 clients: HashMap::new(),
653 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
654 router: LspRouter::new(0.5),
655 stats: stats.clone(),
656 };
657
658 let retrieved_stats = manager.get_stats().await;
659 assert_eq!(retrieved_stats.total_requests, 10);
660 assert_eq!(retrieved_stats.cache_hits, 5);
661 assert_eq!(retrieved_stats.lsp_routed, 8);
662 }
663
664 #[tokio::test]
665 async fn test_search_fallback() {
666 let config = create_test_config();
667 let manager = LspManager {
668 config,
669 servers: HashMap::new(),
670 clients: HashMap::new(),
671 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
672 router: LspRouter::new(0.0), stats: Arc::new(RwLock::new(LspStats::default())),
674 };
675
676 let response = manager.search("test query", None).await.unwrap();
677
678 assert!(response.lsp_results.is_empty());
680 assert_eq!(response.lsp_time_ms, 0);
681 assert_eq!(response.cache_hit_rate, 0.0);
682 assert!(response.server_types_used.is_empty());
683
684 let stats = manager.get_stats().await;
686 assert_eq!(stats.total_requests, 1);
687 assert_eq!(stats.fallback_used, 1);
688 assert_eq!(stats.lsp_routed, 0);
689 }
690
691 #[tokio::test]
692 async fn test_search_with_cache_simulation() {
693 let config = create_test_config();
694 let cache = Arc::new(HintCache::new(1).await.unwrap());
695
696 let cache_key = "test query:definition:";
698 let cached_results = vec![create_test_lsp_result("cached", LspServerType::TypeScript)];
699 cache.set(cache_key.to_string(), cached_results.clone(), 3600).await.unwrap();
700
701 let mut manager = LspManager {
702 config,
703 servers: HashMap::new(),
704 clients: HashMap::new(),
705 hint_cache: cache,
706 router: LspRouter::new(1.0), stats: Arc::new(RwLock::new(LspStats::default())),
708 };
709
710 manager.clients.insert(LspServerType::TypeScript, Arc::new(create_mock_client().await));
712
713 let response = manager.search("test query", None).await;
714
715 match response {
716 Ok(resp) => {
717 if resp.cache_hit_rate > 0.0 {
719 assert_eq!(resp.lsp_results.len(), 1);
720 assert_eq!(resp.lsp_results[0].content, "cached");
721 }
722
723 let stats = manager.get_stats().await;
725 assert_eq!(stats.total_requests, 1);
726 }
727 Err(_) => {
728 }
730 }
731 }
732
733 #[tokio::test]
734 async fn test_execute_bounded_search_empty_servers() {
735 let config = create_test_config();
736 let manager = LspManager {
737 config,
738 servers: HashMap::new(),
739 clients: HashMap::new(),
740 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
741 router: LspRouter::new(0.5),
742 stats: Arc::new(RwLock::new(LspStats::default())),
743 };
744
745 let intent = QueryIntent::Definition;
746 let servers = vec![];
747
748 let result = manager.execute_bounded_search("test", &intent, &servers).await.unwrap();
749 assert!(result.is_empty());
750 }
751
752 #[tokio::test]
753 async fn test_execute_bounded_search_with_unavailable_servers() {
754 let config = create_test_config();
755 let manager = LspManager {
756 config,
757 servers: HashMap::new(),
758 clients: HashMap::new(),
759 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
760 router: LspRouter::new(0.5),
761 stats: Arc::new(RwLock::new(LspStats::default())),
762 };
763
764 let intent = QueryIntent::Definition;
765 let servers = vec![LspServerType::TypeScript]; let result = manager.execute_bounded_search("test", &intent, &servers).await.unwrap();
768 assert!(result.is_empty());
769 }
770
771 #[tokio::test]
772 async fn test_lsp_manager_shutdown() {
773 let config = create_test_config();
774 let mut manager = LspManager {
775 config,
776 servers: HashMap::new(),
777 clients: HashMap::new(),
778 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
779 router: LspRouter::new(0.5),
780 stats: Arc::new(RwLock::new(LspStats::default())),
781 };
782
783 manager.clients.insert(LspServerType::TypeScript, Arc::new(create_mock_client().await));
785 manager.servers.insert(LspServerType::TypeScript, Arc::new(create_mock_server().await));
786
787 let result = manager.shutdown().await;
788
789 assert!(result.is_ok());
791 assert!(manager.servers.is_empty());
792 }
793
794 #[tokio::test]
795 async fn test_query_intent_to_string() {
796 assert_eq!(QueryIntent::Definition.to_string(), "definition");
797 assert_eq!(QueryIntent::References.to_string(), "references");
798 assert_eq!(QueryIntent::TypeDefinition.to_string(), "type_definition");
799 assert_eq!(QueryIntent::Implementation.to_string(), "implementation");
800 assert_eq!(QueryIntent::Declaration.to_string(), "declaration");
801 assert_eq!(QueryIntent::Symbol.to_string(), "symbol");
802 assert_eq!(QueryIntent::Completion.to_string(), "completion");
803 assert_eq!(QueryIntent::Hover.to_string(), "hover");
804 assert_eq!(QueryIntent::TextSearch.to_string(), "text_search");
805 }
806
807 #[tokio::test]
808 async fn test_lsp_manager_drop() {
809 let config = create_test_config();
810 let manager = LspManager {
811 config,
812 servers: HashMap::new(),
813 clients: HashMap::new(),
814 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
815 router: LspRouter::new(0.5),
816 stats: Arc::new(RwLock::new(LspStats::default())),
817 };
818
819 drop(manager);
821 }
822
823 #[tokio::test]
824 async fn test_stats_calculation() {
825 let config = create_test_config();
826 let manager = LspManager {
827 config,
828 servers: HashMap::new(),
829 clients: HashMap::new(),
830 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
831 router: LspRouter::new(0.0), stats: Arc::new(RwLock::new(LspStats::default())),
833 };
834
835 for _ in 0..3 {
837 let _ = manager.search("test", None).await;
838 }
839
840 let stats = manager.get_stats().await;
841 assert_eq!(stats.total_requests, 3);
842 assert_eq!(stats.fallback_used, 3);
843 assert_eq!(stats.lsp_routed, 0);
844 assert_eq!(stats.cache_hits, 0);
845 }
846
847 #[tokio::test]
848 async fn test_error_stats_tracking() {
849 let config = create_test_config();
850 let manager = LspManager {
851 config,
852 servers: HashMap::new(),
853 clients: HashMap::new(),
854 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
855 router: LspRouter::new(1.0), stats: Arc::new(RwLock::new(LspStats::default())),
857 };
858
859 let _ = manager.search("test", Some("test.ts")).await;
861
862 let stats = manager.get_stats().await;
863
864 assert_eq!(stats.total_requests, 1);
866 assert_eq!(stats.fallback_used, 1);
867
868 }
871
872 #[tokio::test]
873 async fn test_concurrent_searches() {
874 let config = create_test_config();
875 let manager = Arc::new(LspManager {
876 config,
877 servers: HashMap::new(),
878 clients: HashMap::new(),
879 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
880 router: LspRouter::new(0.0), stats: Arc::new(RwLock::new(LspStats::default())),
882 });
883
884 let mut handles = vec![];
886 for i in 0..5 {
887 let manager = manager.clone();
888 let handle = tokio::spawn(async move {
889 manager.search(&format!("query{}", i), None).await
890 });
891 handles.push(handle);
892 }
893
894 for handle in handles {
896 let result = handle.await.unwrap();
897 assert!(result.is_ok());
898 }
899
900 let stats = manager.get_stats().await;
902 assert_eq!(stats.total_requests, 5);
903 assert_eq!(stats.fallback_used, 5);
904 }
905
906 #[tokio::test]
907 async fn test_cache_key_generation() {
908 let query = "test query";
909 let intent = QueryIntent::Definition;
910 let file_path = Some("test.rs");
911
912 let cache_key = format!("{}:{}:{}", query, intent.to_string(), file_path.unwrap_or(""));
913 let expected = "test query:definition:test.rs";
914
915 assert_eq!(cache_key, expected);
916
917 let cache_key_no_file = format!("{}:{}:{}", query, intent.to_string(), "");
919 let expected_no_file = "test query:definition:";
920
921 assert_eq!(cache_key_no_file, expected_no_file);
922 }
923
924 async fn create_mock_client() -> LspClient {
926 let (tx, _rx) = mpsc::channel::<String>(10);
930 let (_stdin_tx, stdin_rx) = mpsc::channel::<String>(10);
931 let (stdout_tx, _stdout_rx) = mpsc::channel::<String>(10);
932
933 std::panic!("Mock client creation not implemented - test infrastructure needs completion");
938 }
939
940 async fn create_mock_server() -> LspServerProcess {
941 std::panic!("Mock server creation not implemented - test infrastructure needs completion");
943 }
944
945 #[tokio::test]
947 #[ignore = "Requires full mock infrastructure"]
948 async fn test_full_lsp_search_flow() {
949 }
953
954 #[tokio::test]
955 #[ignore = "Requires actual LSP servers"]
956 async fn test_real_lsp_server_integration() {
957 }
960
961 #[tokio::test]
962 async fn test_traversal_bounds_application() {
963 let config = create_test_config();
964
965 assert_eq!(config.traversal_bounds.max_depth, 3);
967 assert_eq!(config.traversal_bounds.max_results, 100);
968 assert_eq!(config.traversal_bounds.timeout_ms, 500);
969
970 let manager = LspManager {
972 config,
973 servers: HashMap::new(),
974 clients: HashMap::new(),
975 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
976 router: LspRouter::new(0.5),
977 stats: Arc::new(RwLock::new(LspStats::default())),
978 };
979
980 assert_eq!(manager.config.traversal_bounds.max_results, 100);
983 }
984
985 #[tokio::test]
986 async fn test_safety_floor_query_types() {
987 use crate::lsp::QueryIntent;
988
989 assert!(QueryIntent::Definition.requires_safety_floor());
991 assert!(QueryIntent::Symbol.requires_safety_floor());
992 assert!(QueryIntent::Definition.is_exact_query());
993 assert!(QueryIntent::Symbol.is_exact_query());
994
995 assert!(QueryIntent::TypeDefinition.requires_safety_floor());
997 assert!(QueryIntent::Implementation.requires_safety_floor());
998 assert!(QueryIntent::TypeDefinition.is_structural_query());
999 assert!(QueryIntent::Implementation.is_structural_query());
1000
1001 assert!(!QueryIntent::References.requires_safety_floor());
1003 assert!(!QueryIntent::Completion.requires_safety_floor());
1004 assert!(!QueryIntent::Hover.requires_safety_floor());
1005 assert!(!QueryIntent::TextSearch.requires_safety_floor());
1006
1007 assert!(!QueryIntent::References.is_exact_query());
1009 assert!(!QueryIntent::References.is_structural_query());
1010 }
1011
1012 #[tokio::test]
1013 async fn test_safety_floor_search_behavior() {
1014 let config = create_test_config();
1015 let manager = LspManager {
1016 config,
1017 servers: HashMap::new(),
1018 clients: HashMap::new(),
1019 hint_cache: Arc::new(HintCache::new(1).await.unwrap()),
1020 router: LspRouter::new(0.5),
1021 stats: Arc::new(RwLock::new(LspStats::default())),
1022 };
1023
1024 let exact_intent = QueryIntent::Definition;
1026 let struct_intent = QueryIntent::TypeDefinition;
1027 let non_safety_intent = QueryIntent::References;
1028
1029 assert!(exact_intent.requires_safety_floor());
1031 assert!(struct_intent.requires_safety_floor());
1032 assert!(!non_safety_intent.requires_safety_floor());
1033
1034 assert_eq!(QueryIntent::classify("def myFunction"), QueryIntent::Definition);
1036 assert_eq!(QueryIntent::classify("function handleClick"), QueryIntent::Definition);
1037 assert_eq!(QueryIntent::classify("class MyClass"), QueryIntent::Definition);
1038 assert_eq!(QueryIntent::classify("type interface MyInterface"), QueryIntent::TypeDefinition);
1039 assert_eq!(QueryIntent::classify("impl MyTrait"), QueryIntent::Implementation);
1040 assert_eq!(QueryIntent::classify("@symbol"), QueryIntent::Symbol);
1041 }
1042
1043 #[tokio::test]
1044 async fn test_monotone_requirements() {
1045 let exact_queries = vec![
1049 QueryIntent::Definition,
1050 QueryIntent::Symbol,
1051 ];
1052
1053 let structural_queries = vec![
1054 QueryIntent::TypeDefinition,
1055 QueryIntent::Implementation,
1056 ];
1057
1058 let non_monotone_queries = vec![
1059 QueryIntent::References,
1060 QueryIntent::Completion,
1061 QueryIntent::Hover,
1062 QueryIntent::TextSearch,
1063 ];
1064
1065 for query in exact_queries {
1066 assert!(query.requires_safety_floor(), "Exact query {:?} should require safety floor", query);
1067 assert!(query.is_exact_query(), "Query {:?} should be classified as exact", query);
1068 }
1069
1070 for query in structural_queries {
1071 assert!(query.requires_safety_floor(), "Structural query {:?} should require safety floor", query);
1072 assert!(query.is_structural_query(), "Query {:?} should be classified as structural", query);
1073 }
1074
1075 for query in non_monotone_queries {
1076 assert!(!query.requires_safety_floor(), "Query {:?} should not require safety floor", query);
1077 assert!(!query.is_exact_query(), "Query {:?} should not be exact", query);
1078 assert!(!query.is_structural_query(), "Query {:?} should not be structural", query);
1079 }
1080 }
1081}