1use std::sync::Arc;
2use std::time::{Duration, Instant};
3use tokio::sync::RwLock;
4use tonic::{Request, Response, Status, Code};
5use tracing::{info, warn, error, debug, instrument};
6use sha2::Digest;
7
8use crate::search::{SearchEngine, SearchRequest as InternalSearchRequest, SearchResponse as InternalSearchResponse};
9use crate::lsp::{QueryIntent, LspSearchResponse};
10use crate::metrics::{MetricsCollector, SlaMetrics, PerformanceGate};
11use crate::attestation::AttestationManager;
12use crate::benchmark::BenchmarkRunner;
13
14use crate::proto;
16
17use proto::{
18 SearchRequest, SearchResponse, SearchResult, SearchMetrics,
19 HealthRequest, HealthResponse, BuildInfoRequest, BuildInfoResponse,
20 HandshakeRequest, HandshakeResponse,
21 lens_search_service_server::{LensSearchService, LensSearchServiceServer},
22};
23
24pub struct LensSearchServiceImpl {
26 search_engine: Arc<SearchEngine>,
27 metrics_collector: Arc<MetricsCollector>,
28 attestation_manager: Arc<AttestationManager>,
29 benchmark_runner: Arc<BenchmarkRunner>,
30 server_start_time: Instant,
31}
32
33impl LensSearchServiceImpl {
34 pub fn new(
35 search_engine: Arc<SearchEngine>,
36 metrics_collector: Arc<MetricsCollector>,
37 attestation_manager: Arc<AttestationManager>,
38 benchmark_runner: Arc<BenchmarkRunner>,
39 ) -> Self {
40 Self {
41 search_engine,
42 metrics_collector,
43 attestation_manager,
44 benchmark_runner,
45 server_start_time: Instant::now(),
46 }
47 }
48
49 pub fn create_server(self) -> proto::lens_search_service_server::LensSearchServiceServer<Self> {
51 proto::lens_search_service_server::LensSearchServiceServer::new(self)
52 }
53
54 fn convert_request(&self, request: proto::SearchRequest) -> Result<crate::search::SearchRequest, Status> {
56 if request.query.is_empty() {
57 return Err(Status::new(Code::InvalidArgument, "Query cannot be empty"));
58 }
59
60 Ok(crate::search::SearchRequest {
61 query: request.query,
62 file_path: None,
63 language: None,
64 max_results: if request.max_results == 0 { 50 } else { request.max_results as usize },
65 include_context: true,
66 timeout_ms: 150, enable_lsp: true,
68 search_types: vec![
69 crate::search::SearchResultType::TextMatch,
70 crate::search::SearchResultType::Definition,
71 ],
72 search_method: Some(crate::search::SearchMethod::Hybrid),
73 })
74 }
75
76 fn convert_response(
78 &self,
79 internal_response: crate::search::SearchResponse,
80 attestation_hash: String
81 ) -> proto::SearchResponse {
82 let results_len = internal_response.results.len();
83 let results = internal_response.results.into_iter().map(|result| {
84 proto::SearchResult {
85 file_path: result.file_path,
86 line_number: result.line_number,
87 column: 0, content: result.content,
89 score: result.score,
90 result_type: "standard".to_string(),
91 language: None,
92 context_lines: vec![],
93 }
94 }).collect();
95
96 let metrics = proto::SearchMetrics {
97 total_docs: results_len as u64,
98 matched_docs: results_len as u64,
99 duration_ms: internal_response.metrics.duration_ms,
100 lsp_time_ms: 0,
101 lsp_results_count: 0,
102 lsp_cache_hit_rate: 0.0,
103 search_time_ms: internal_response.metrics.duration_ms,
104 fusion_time_ms: 0,
105 sla_compliant: true,
106 result_diversity_score: 0.0,
107 confidence_score: 0.0,
108 coverage_score: 0.0,
109 };
110
111 proto::SearchResponse {
112 results,
113 metrics: Some(metrics),
114 total_time_ms: internal_response.metrics.duration_ms as u64,
115 sla_compliant: true,
116 }
117 }
118
119
120 fn should_route_to_lsp(&self, query: &str, language: Option<&str>) -> bool {
122 let lsp_patterns = [
124 "def", "define", "definition", "go to", "goto",
125 "references", "ref", "usage", "uses",
126 "type", "typeof", "implements", "implementation",
127 "class", "function", "method", "variable",
128 ];
129
130 let query_lower = query.to_lowercase();
131 let has_lsp_pattern = lsp_patterns.iter().any(|&pattern| query_lower.contains(pattern));
132 let has_language = language.is_some();
133
134 has_lsp_pattern || has_language
135 }
136}
137
138#[tonic::async_trait]
139impl proto::lens_search_service_server::LensSearchService for LensSearchServiceImpl {
140 #[instrument(skip(self, request))]
141 async fn search(
142 &self,
143 request: Request<proto::SearchRequest>,
144 ) -> Result<Response<proto::SearchResponse>, Status> {
145 let start_time = Instant::now();
146 let req = request.into_inner();
147
148 debug!("Received search request: {}", req.query);
149
150 let internal_req = self.convert_request(req.clone())?;
152
153 let sla_start = Instant::now();
155 let search_result = self.search_engine.search_comprehensive(internal_req).await;
156 let search_duration = sla_start.elapsed();
157
158 let sla_compliant = search_duration.as_millis() <= 150;
160 if !sla_compliant {
161 warn!("Search exceeded SLA: {}ms > 150ms", search_duration.as_millis());
162 }
163
164 match search_result {
166 Ok(internal_response) => {
167 let attestation_hash = format!("{:x}",
169 sha2::Digest::finalize(sha2::Sha256::new()
170 .chain_update(req.query.as_bytes())
171 .chain_update(&search_duration.as_millis().to_le_bytes())
172 .chain_update(&internal_response.results.len().to_le_bytes())
173 )
174 );
175
176 let response = self.convert_response(internal_response, attestation_hash);
178
179 info!(
180 "Search completed: query='{}', results={}, duration={}ms, sla_compliant={}",
181 req.query,
182 response.results.len(),
183 search_duration.as_millis(),
184 sla_compliant
185 );
186
187 Ok(Response::new(response))
188 }
189 Err(e) => {
190 error!("Search failed: {}", e);
191
192 let error_response = proto::SearchResponse {
193 results: vec![],
194 metrics: Some(proto::SearchMetrics {
195 total_docs: 0,
196 matched_docs: 0,
197 duration_ms: search_duration.as_millis() as u32,
198 lsp_time_ms: 0,
199 lsp_results_count: 0,
200 lsp_cache_hit_rate: 0.0,
201 search_time_ms: search_duration.as_millis() as u32,
202 fusion_time_ms: 0,
203 sla_compliant: false,
204 result_diversity_score: 0.0,
205 confidence_score: 0.0,
206 coverage_score: 0.0,
207 }),
208 total_time_ms: search_duration.as_millis() as u64,
209 sla_compliant: false,
210 };
211
212 Ok(Response::new(error_response))
213 }
214 }
215 }
216
217
218 #[instrument(skip(self, _request))]
219 async fn health(
220 &self,
221 _request: Request<proto::HealthRequest>,
222 ) -> Result<Response<proto::HealthResponse>, Status> {
223 let response = proto::HealthResponse {
225 status: "healthy".to_string(),
226 message: "Service is operational".to_string(),
227 };
228
229 debug!("Health check requested");
230 Ok(Response::new(response))
231 }
232
233 #[instrument(skip(self, _request))]
234 async fn get_build_info(
235 &self,
236 _request: Request<proto::BuildInfoRequest>,
237 ) -> Result<Response<proto::BuildInfoResponse>, Status> {
238 let build_info = crate::attestation::get_build_info();
240
241 debug!("Build info requested");
242 Ok(Response::new(build_info))
243 }
244
245 #[instrument(skip(self, request))]
246 async fn handshake(
247 &self,
248 request: Request<proto::HandshakeRequest>,
249 ) -> Result<Response<proto::HandshakeResponse>, Status> {
250 let req = request.into_inner();
251 let client_id = req.client_id.clone();
252
253 let handshake_response = {
255 let response_hash = crate::attestation::perform_handshake(&req.client_id)
257 .map_err(|e| Status::internal(format!("Handshake failed: {}", e)))?;
258 let build_info = crate::attestation::get_build_info();
259
260 proto::HandshakeResponse {
261 server_id: "lens-search-server".to_string(),
262 protocol_version: req.protocol_version,
263 success: true,
264 message: format!("Handshake successful for client: {}", req.client_id),
265 }
266 };
267
268 debug!("Handshake completed for client: {}", client_id);
269 Ok(Response::new(handshake_response))
270 }
271
272}
273
274#[derive(Debug, Clone)]
276pub struct ServerConfig {
277 pub bind_address: String,
278 pub port: u16,
279 pub max_concurrent_requests: usize,
280 pub request_timeout: Duration,
281 pub enable_reflection: bool,
282 pub enable_health_check: bool,
283}
284
285impl Default for ServerConfig {
286 fn default() -> Self {
287 Self {
288 bind_address: "127.0.0.1".to_string(),
289 port: 50051,
290 max_concurrent_requests: 1000,
291 request_timeout: Duration::from_millis(150), enable_reflection: false, enable_health_check: true,
294 }
295 }
296}
297
298pub async fn create_server(
300 config: ServerConfig,
301 search_engine: Arc<SearchEngine>,
302 metrics_collector: Arc<MetricsCollector>,
303 attestation_manager: Arc<AttestationManager>,
304 benchmark_runner: Arc<BenchmarkRunner>,
305) -> Result<impl std::future::Future<Output = Result<(), tonic::transport::Error>>, anyhow::Error> {
306 let addr = format!("{}:{}", config.bind_address, config.port).parse()?;
307
308 let service_impl = LensSearchServiceImpl::new(
309 search_engine,
310 metrics_collector,
311 attestation_manager,
312 benchmark_runner,
313 );
314 let search_service = service_impl.create_server();
315
316 let mut server_builder = tonic::transport::Server::builder()
317 .timeout(config.request_timeout)
318 .concurrency_limit_per_connection(config.max_concurrent_requests)
319 .add_service(search_service);
320
321 if config.enable_reflection {
323 tracing::warn!("Reflection service disabled - FILE_DESCRIPTOR_SET not available");
329 }
330
331 let server = server_builder.serve(addr);
332
333 info!("gRPC server configured on {}", addr);
334 Ok(server)
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use std::time::{Duration, Instant};
341 use std::sync::Arc;
342 use tokio::sync::RwLock;
343 use tonic::{Request, Status, Code};
344
345 async fn create_test_search_engine() -> Arc<crate::search::SearchEngine> {
347 let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
348 let index_path = temp_dir.path().to_str().unwrap();
349
350 let mut config = crate::search::SearchConfig::default();
351 config.index_path = index_path.to_string();
352 config.enable_lsp = false; let index_path_clone = config.index_path.clone();
355 let engine = crate::search::SearchEngine::with_config(&index_path_clone, config)
356 .await
357 .expect("Failed to create test search engine");
358
359 std::mem::forget(temp_dir);
361 Arc::new(engine)
362 }
363
364 async fn create_mock_service() -> LensSearchServiceImpl {
365 let search_engine = create_test_search_engine().await;
366 let metrics_collector = Arc::new(crate::metrics::MetricsCollector::new());
367 let attestation_manager = Arc::new(crate::attestation::AttestationManager::new(true).expect("Failed to create attestation manager"));
368 let benchmark_runner = Arc::new(crate::benchmark::BenchmarkRunner::new(
369 search_engine.clone(),
370 Arc::new(crate::metrics::MetricsCollector::new()),
371 crate::benchmark::BenchmarkConfig::default(),
372 ));
373
374 LensSearchServiceImpl::new(
375 search_engine,
376 metrics_collector,
377 attestation_manager,
378 benchmark_runner,
379 )
380 }
381
382 async fn create_mock_service_with_failure() -> LensSearchServiceImpl {
383 create_mock_service().await
385 }
386
387 async fn create_mock_service_with_slow_response(_processing_time_ms: u64) -> LensSearchServiceImpl {
388 create_mock_service().await
391 }
392
393 #[tokio::test]
395 async fn test_service_creation() {
396 let service = create_mock_service().await;
397 let start_time = service.server_start_time;
398 let _server = service.create_server();
399
400 assert!(start_time.elapsed() < Duration::from_secs(1));
402 }
403
404 #[tokio::test]
406 async fn test_convert_request_valid() {
407 let service = create_mock_service().await;
408
409 let proto_request = SearchRequest {
410 query: "test query".to_string(),
411 file_path: None,
412 language: None,
413 max_results: 10,
414 include_context: false,
415 timeout_ms: 5000,
416 enable_lsp: false,
417 };
418
419 let result = service.convert_request(proto_request);
420 assert!(result.is_ok());
421
422 let internal_request = result.unwrap();
423 assert_eq!(internal_request.query, "test query");
424 assert_eq!(internal_request.max_results, 10);
425 assert_eq!(internal_request.timeout_ms, 150);
426 assert!(internal_request.enable_lsp);
427 assert!(internal_request.include_context);
428 assert_eq!(internal_request.search_types.len(), 2);
429 }
430
431 #[tokio::test]
432 async fn test_convert_request_default_limit() {
433 let service = create_mock_service().await;
434
435 let proto_request = SearchRequest {
436 query: "test query".to_string(),
437 file_path: None,
438 language: None,
439 max_results: 0,
440 include_context: false,
441 timeout_ms: 5000,
442 enable_lsp: false,
443 };
444
445 let result = service.convert_request(proto_request);
446 assert!(result.is_ok());
447
448 let internal_request = result.unwrap();
449 assert_eq!(internal_request.max_results, 50); }
451
452 #[tokio::test]
453 async fn test_convert_request_empty_query() {
454 let service = create_mock_service().await;
455
456 let proto_request = SearchRequest {
457 query: "".to_string(), max_results: 10,
459 file_path: None,
460 include_context: false,
461 timeout_ms: 5000,
462 enable_lsp: false,
463 language: None,
464 };
465
466 let result = service.convert_request(proto_request);
467 assert!(result.is_err());
468
469 let error = result.unwrap_err();
470 assert_eq!(error.code(), Code::InvalidArgument);
471 assert!(error.message().contains("Query cannot be empty"));
472 }
473
474 #[tokio::test]
475 async fn test_convert_request_whitespace_only() {
476 let service = create_mock_service().await;
477
478 let proto_request = SearchRequest {
479 query: " \t\n ".to_string(), max_results: 10,
481 file_path: None,
482 include_context: false,
483 timeout_ms: 5000,
484 enable_lsp: false,
485 language: None,
486 };
487
488 let result = service.convert_request(proto_request);
490 assert!(result.is_ok()); }
492
493 #[tokio::test]
495 async fn test_convert_response() {
496 let service = create_mock_service().await;
497
498 let internal_response = InternalSearchResponse {
499 results: vec![
500 crate::search::SearchResult {
501 file_path: "test.rs".to_string(),
502 line_number: 42,
503 column: 0,
504 content: "test content".to_string(),
505 score: 0.95,
506 result_type: crate::search::SearchResultType::TextMatch,
507 language: Some("rust".to_string()),
508 context_lines: None,
509 lsp_metadata: None,
510 },
511 crate::search::SearchResult {
512 file_path: "another.rs".to_string(),
513 line_number: 100,
514 column: 0,
515 content: "another content".to_string(),
516 score: 0.85,
517 result_type: crate::search::SearchResultType::TextMatch,
518 language: Some("rust".to_string()),
519 context_lines: None,
520 lsp_metadata: None,
521 },
522 ],
523 metrics: crate::search::SearchMetrics {
524 total_docs: 200,
525 matched_docs: 2,
526 duration_ms: 50,
527 lsp_time_ms: 0,
528 lsp_results_count: 0,
529 lsp_cache_hit_rate: 0.0,
530 search_time_ms: 45,
531 fusion_time_ms: 5,
532 sla_compliant: true,
533 result_diversity_score: 0.8,
534 confidence_score: 0.9,
535 coverage_score: 0.85,
536 },
537 query_intent: QueryIntent::TextSearch,
538 lsp_response: None,
539 total_time_ms: 50,
540 sla_compliant: true,
541 };
542
543 let attestation_hash = "test_hash".to_string();
544 let proto_response = service.convert_response(internal_response, attestation_hash.clone());
545
546 assert_eq!(proto_response.results.len(), 2);
547 assert!(proto_response.sla_compliant);
549
550 assert_eq!(proto_response.results[0].file_path, "test.rs");
552 assert_eq!(proto_response.results[0].line_number, 42);
553 assert_eq!(proto_response.results[0].content, "test content");
554 assert_eq!(proto_response.results[0].score, 0.95);
555
556 assert!(proto_response.metrics.is_some());
558 let metrics = proto_response.metrics.unwrap();
559 assert_eq!(metrics.total_docs, 2);
560 assert_eq!(metrics.matched_docs, 2);
561 assert_eq!(metrics.duration_ms, 50);
562 }
563
564 #[tokio::test]
565 async fn test_convert_response_empty_results() {
566 let service = create_mock_service().await;
567
568 let internal_response = InternalSearchResponse {
569 results: vec![],
570 metrics: crate::search::SearchMetrics {
571 total_docs: 100,
572 matched_docs: 0,
573 duration_ms: 25,
574 lsp_time_ms: 0,
575 lsp_results_count: 0,
576 lsp_cache_hit_rate: 0.0,
577 search_time_ms: 25,
578 fusion_time_ms: 0,
579 sla_compliant: true,
580 result_diversity_score: 0.0,
581 confidence_score: 0.0,
582 coverage_score: 0.0,
583 },
584 query_intent: QueryIntent::TextSearch,
585 lsp_response: None,
586 total_time_ms: 25,
587 sla_compliant: true,
588 };
589
590 let attestation_hash = "empty_hash".to_string();
591 let proto_response = service.convert_response(internal_response, attestation_hash.clone());
592
593 assert_eq!(proto_response.results.len(), 0);
594 assert!(proto_response.sla_compliant);
596
597 let metrics = proto_response.metrics.unwrap();
598 assert_eq!(metrics.total_docs, 0);
599 assert_eq!(metrics.matched_docs, 0);
600 }
601
602 #[tokio::test]
604 async fn test_should_route_to_lsp_patterns() {
605 let service = create_mock_service().await;
606
607 assert!(service.should_route_to_lsp("def myFunction", None));
609 assert!(service.should_route_to_lsp("find definition", None));
610 assert!(service.should_route_to_lsp("go to implementation", None));
611 assert!(service.should_route_to_lsp("show references", None));
612 assert!(service.should_route_to_lsp("type information", None));
613 assert!(service.should_route_to_lsp("class MyClass", None));
614 assert!(service.should_route_to_lsp("function getName", None));
615 assert!(service.should_route_to_lsp("method call", None));
616 assert!(service.should_route_to_lsp("variable usage", None));
617
618 assert!(service.should_route_to_lsp("simple query", Some("rust")));
620 assert!(service.should_route_to_lsp("search text", Some("typescript")));
621 }
622
623 #[tokio::test]
624 async fn test_should_route_to_lsp_negative_cases() {
625 let service = create_mock_service().await;
626
627 assert!(!service.should_route_to_lsp("hello world", None));
629 assert!(!service.should_route_to_lsp("simple text search", None));
630 assert!(!service.should_route_to_lsp("random query", None));
631 assert!(!service.should_route_to_lsp("", None));
632 assert!(!service.should_route_to_lsp("123 456", None));
633 }
634
635 #[tokio::test]
636 async fn test_should_route_to_lsp_case_insensitive() {
637 let service = create_mock_service().await;
638
639 assert!(service.should_route_to_lsp("DEF myFunction", None));
641 assert!(service.should_route_to_lsp("CLASS MyClass", None));
642 assert!(service.should_route_to_lsp("FUNCTION getName", None));
643 }
644
645 #[tokio::test]
647 async fn test_search_successful() {
648 let service = create_mock_service().await;
649
650 let proto_request = SearchRequest {
651 query: "test query".to_string(),
652 max_results: 5,
653 file_path: None,
654 include_context: false,
655 timeout_ms: 5000,
656 enable_lsp: false,
657 language: None,
658 };
659
660 let request = Request::new(proto_request);
661 let result = service.search(request).await;
662
663 assert!(result.is_ok());
664 let response = result.unwrap().into_inner();
665
666 assert!(response.results.len() >= 0);
668 assert!(response.metrics.is_some());
669
670 assert!(response.total_time_ms >= 0);
672 }
673
674 #[tokio::test]
675 async fn test_search_failure() {
676 let service = create_mock_service_with_failure().await;
677
678 let proto_request = SearchRequest {
680 query: "".to_string(), max_results: 5,
682 file_path: None,
683 include_context: false,
684 timeout_ms: 5000,
685 enable_lsp: false,
686 language: None,
687 };
688
689 let request = Request::new(proto_request);
690 let result = service.search(request).await;
691
692 assert!(result.is_err());
694 let error = result.unwrap_err();
695 assert_eq!(error.code(), tonic::Code::InvalidArgument);
696 }
697
698 #[tokio::test]
699 async fn test_search_sla_compliance() {
700 let service = create_mock_service_with_slow_response(200).await;
701
702 let proto_request = SearchRequest {
703 query: "test query".to_string(),
704 max_results: 3,
705 file_path: None,
706 include_context: false,
707 timeout_ms: 5000,
708 enable_lsp: false,
709 language: None,
710 };
711
712 let start_time = Instant::now();
713 let request = Request::new(proto_request);
714 let result = service.search(request).await;
715 let total_duration = start_time.elapsed();
716
717 assert!(result.is_ok());
718 let response = result.unwrap().into_inner();
719
720 assert!(response.results.len() >= 0); let metrics = response.metrics.unwrap();
725 assert!(metrics.duration_ms >= 0);
726 }
727
728 #[tokio::test]
729 async fn test_search_empty_query_error() {
730 let service = create_mock_service().await;
731
732 let proto_request = SearchRequest {
733 query: "".to_string(), max_results: 5,
735 file_path: None,
736 include_context: false,
737 timeout_ms: 5000,
738 enable_lsp: false,
739 language: None,
740 };
741
742 let request = Request::new(proto_request);
743 let result = service.search(request).await;
744
745 assert!(result.is_err());
746 let error = result.unwrap_err();
747 assert_eq!(error.code(), Code::InvalidArgument);
748 }
749
750 #[tokio::test]
751 async fn test_search_attestation_uniqueness() {
752 let service = create_mock_service().await;
753
754 let proto_request1 = SearchRequest {
755 query: "query1".to_string(),
756 max_results: 5,
757 file_path: None,
758 include_context: false,
759 timeout_ms: 5000,
760 enable_lsp: false,
761 language: None,
762 };
763
764 let proto_request2 = SearchRequest {
765 query: "query2".to_string(),
766 max_results: 5,
767 file_path: None,
768 include_context: false,
769 timeout_ms: 5000,
770 enable_lsp: false,
771 language: None,
772 };
773
774 let request1 = Request::new(proto_request1);
775 let request2 = Request::new(proto_request2);
776
777 let result1 = service.search(request1).await.unwrap().into_inner();
778 let result2 = service.search(request2).await.unwrap().into_inner();
779
780 }
783
784 #[tokio::test]
786 async fn test_health_check() {
787 let service = create_mock_service().await;
788
789 let request = Request::new(HealthRequest {
790 service: "lens".to_string(),
791 });
792 let result = service.health(request).await;
793
794 assert!(result.is_ok());
795 let response = result.unwrap().into_inner();
796
797 assert_eq!(response.status, "healthy");
798 assert!(!response.message.is_empty());
799 }
800
801 #[tokio::test]
802 async fn test_health_check_response_format() {
803 let service = create_mock_service().await;
804
805 let request = Request::new(HealthRequest {
806 service: "lens".to_string(),
807 });
808 let result = service.health(request).await;
809
810 assert!(result.is_ok());
811 let response = result.unwrap().into_inner();
812
813 assert_eq!(response.status, "healthy");
815 }
816
817 #[tokio::test]
819 async fn test_get_build_info() {
820 let service = create_mock_service().await;
821
822 let request = Request::new(BuildInfoRequest {});
823 let result = service.get_build_info(request).await;
824
825 assert!(result.is_ok());
826 let response = result.unwrap().into_inner();
827
828 assert!(!response.version.is_empty());
830 }
831
832 #[tokio::test]
834 async fn test_handshake_successful() {
835 let service = create_mock_service().await;
836
837 let nonce = "test_nonce_12345".to_string();
838 let request = Request::new(HandshakeRequest {
839 client_id: nonce.clone(),
840 protocol_version: "1.0".to_string(),
841 });
842
843 let result = service.handshake(request).await;
844
845 assert!(result.is_ok());
846 let response = result.unwrap().into_inner();
847
848 assert_eq!(response.server_id, "lens-search-server");
849 assert!(!response.message.is_empty());
850 assert!(response.success); }
852
853 #[tokio::test]
854 async fn test_handshake_different_nonces() {
855 let service = create_mock_service().await;
856
857 let nonce1 = "nonce1".to_string();
858 let nonce2 = "nonce2".to_string();
859
860 let request1 = Request::new(HandshakeRequest {
861 client_id: nonce1.clone(),
862 protocol_version: "1.0".to_string(),
863 });
864 let request2 = Request::new(HandshakeRequest {
865 client_id: nonce2.clone(),
866 protocol_version: "1.0".to_string(),
867 });
868
869 let result1 = service.handshake(request1).await.unwrap().into_inner();
870 let result2 = service.handshake(request2).await.unwrap().into_inner();
871
872 assert_ne!(result1.message, result2.message);
874 assert_eq!(result1.server_id, "lens-search-server");
875 assert_eq!(result2.server_id, "lens-search-server");
876 }
877
878 #[tokio::test]
879 async fn test_handshake_empty_nonce() {
880 let service = create_mock_service().await;
881
882 let request = Request::new(HandshakeRequest {
883 client_id: "".to_string(), protocol_version: "1.0".to_string(),
885 });
886
887 let result = service.handshake(request).await;
888
889 assert!(result.is_ok());
891 let response = result.unwrap().into_inner();
892 assert_eq!(response.server_id, "lens-search-server");
893 assert!(!response.message.is_empty());
894 }
895
896 #[tokio::test]
898 async fn test_server_config_default() {
899 let config = ServerConfig::default();
900
901 assert_eq!(config.bind_address, "127.0.0.1");
902 assert_eq!(config.port, 50051);
903 assert_eq!(config.max_concurrent_requests, 1000);
904 assert_eq!(config.request_timeout, Duration::from_millis(150));
905 assert!(!config.enable_reflection); assert!(config.enable_health_check);
907 }
908
909 #[tokio::test]
910 async fn test_server_config_custom() {
911 let config = ServerConfig {
912 bind_address: "0.0.0.0".to_string(),
913 port: 8080,
914 max_concurrent_requests: 500,
915 request_timeout: Duration::from_millis(300),
916 enable_reflection: true,
917 enable_health_check: false,
918 };
919
920 assert_eq!(config.bind_address, "0.0.0.0");
921 assert_eq!(config.port, 8080);
922 assert_eq!(config.max_concurrent_requests, 500);
923 assert_eq!(config.request_timeout, Duration::from_millis(300));
924 assert!(config.enable_reflection);
925 assert!(!config.enable_health_check);
926 }
927
928 #[tokio::test]
929 async fn test_server_config_clone() {
930 let original = ServerConfig::default();
931 let cloned = original.clone();
932
933 assert_eq!(original.bind_address, cloned.bind_address);
934 assert_eq!(original.port, cloned.port);
935 assert_eq!(original.max_concurrent_requests, cloned.max_concurrent_requests);
936 }
937
938 #[tokio::test]
940 async fn test_concurrent_search_requests() {
941 let service = Arc::new(create_mock_service().await);
942 let mut handles = vec![];
943
944 for i in 0..10 {
946 let service_clone = service.clone();
947 let handle = tokio::spawn(async move {
948 let proto_request = SearchRequest {
949 query: format!("query {}", i),
950 max_results: 3,
951 file_path: None,
952 include_context: false,
953 timeout_ms: 5000,
954 enable_lsp: false,
955 language: None,
956 };
957
958 let request = Request::new(proto_request);
959 service_clone.search(request).await
960 });
961 handles.push(handle);
962 }
963
964 for handle in handles {
966 let result = handle.await.unwrap();
967 assert!(result.is_ok());
968
969 let response = result.unwrap().into_inner();
970 assert_eq!(response.results.len(), 3); }
972 }
973
974 #[tokio::test]
975 async fn test_concurrent_different_endpoints() {
976 let service = Arc::new(create_mock_service().await);
977 let mut handles = vec![];
978
979 for i in 0..5 {
981 let service_clone = service.clone();
982 let handle = tokio::spawn(async move {
983 match i % 4 {
984 0 => {
985 let request = Request::new(SearchRequest {
986 query: format!("search {}", i),
987 max_results: 5,
988 file_path: None,
989 include_context: false,
990 timeout_ms: 5000,
991 enable_lsp: false,
992 language: None,
993 });
994 service_clone.search(request).await.map(|r| format!("search_{}", r.into_inner().results.len()))
995 }
996 1 => {
997 let request = Request::new(HealthRequest {
998 service: "lens".to_string(),
999 });
1000 service_clone.health(request).await.map(|r| r.into_inner().status)
1001 }
1002 2 => {
1003 let request = Request::new(BuildInfoRequest {});
1004 service_clone.get_build_info(request).await.map(|r| r.into_inner().version)
1005 }
1006 3 => {
1007 let request = Request::new(HandshakeRequest {
1008 client_id: format!("client_{}", i),
1009 protocol_version: "1.0".to_string(),
1010 });
1011 service_clone.handshake(request).await.map(|r| r.into_inner().server_id)
1012 }
1013 _ => unreachable!(),
1014 }
1015 });
1016 handles.push(handle);
1017 }
1018
1019 for handle in handles {
1021 let result = handle.await.unwrap();
1022 assert!(result.is_ok());
1023 }
1024 }
1025
1026 #[tokio::test]
1028 async fn test_very_long_query() {
1029 let service = create_mock_service().await;
1030
1031 let long_query = "a".repeat(10000);
1032 let proto_request = SearchRequest {
1033 query: long_query.clone(),
1034 max_results: 5,
1035 file_path: None,
1036 include_context: false,
1037 timeout_ms: 5000,
1038 enable_lsp: false,
1039 language: None,
1040 };
1041
1042 let request = Request::new(proto_request);
1043 let result = service.search(request).await;
1044
1045 assert!(result.is_ok());
1046 let response = result.unwrap().into_inner();
1047
1048 assert!(response.results.len() >= 0); }
1051
1052 #[tokio::test]
1053 async fn test_unicode_query() {
1054 let service = create_mock_service().await;
1055
1056 let unicode_queries = vec![
1057 "函数名称", "función_test", "クラス名", "переменная", "🚀_test_function", ];
1063
1064 for query in unicode_queries {
1065 let proto_request = SearchRequest {
1066 query: query.to_string(),
1067 max_results: 3,
1068 file_path: None,
1069 include_context: false,
1070 timeout_ms: 5000,
1071 enable_lsp: false,
1072 language: None,
1073 };
1074
1075 let request = Request::new(proto_request);
1076 let result = service.search(request).await;
1077
1078 assert!(result.is_ok());
1079 let response = result.unwrap().into_inner();
1080 assert!(response.results.len() >= 0); }
1082 }
1083
1084 #[tokio::test]
1085 async fn test_zero_and_negative_limits() {
1086 let service = create_mock_service().await;
1087
1088 let proto_request = SearchRequest {
1090 query: "test".to_string(),
1091 max_results: 0,
1092 file_path: None,
1093 include_context: false,
1094 timeout_ms: 5000,
1095 enable_lsp: false,
1096 language: None,
1097 };
1098
1099 let internal_request = service.convert_request(proto_request).unwrap();
1100 assert_eq!(internal_request.max_results, 50);
1101 }
1102
1103 #[tokio::test]
1104 async fn test_special_characters_in_query() {
1105 let service = create_mock_service().await;
1106
1107 let special_queries = vec![
1108 r#"query with "quotes""#,
1109 "query with 'single quotes'",
1110 "query with [brackets]",
1111 "query with {braces}",
1112 "query with (parentheses)",
1113 "query with <angles>",
1114 "query with $special!@#%^&*()_+",
1115 "query with newline\n and tab\t",
1116 ];
1117
1118 for query in special_queries {
1119 let proto_request = SearchRequest {
1120 query: query.to_string(),
1121 max_results: 3,
1122 file_path: None,
1123 include_context: false,
1124 timeout_ms: 5000,
1125 enable_lsp: false,
1126 language: None,
1127 };
1128
1129 let request = Request::new(proto_request);
1130 let result = service.search(request).await;
1131
1132 assert!(result.is_ok());
1133 }
1134 }
1135
1136 #[tokio::test]
1138 async fn test_search_performance_measurement() {
1139 let service = create_mock_service_with_slow_response(100).await;
1140
1141 let proto_request = SearchRequest {
1142 query: "performance test".to_string(),
1143 max_results: 5,
1144 file_path: None,
1145 include_context: false,
1146 timeout_ms: 5000,
1147 enable_lsp: false,
1148 language: None,
1149 };
1150
1151 let start_time = Instant::now();
1152 let request = Request::new(proto_request);
1153 let result = service.search(request).await;
1154 let total_duration = start_time.elapsed();
1155
1156 assert!(result.is_ok());
1157 let response = result.unwrap().into_inner();
1158
1159 assert!(total_duration.as_millis() < 1000); let metrics = response.metrics.unwrap();
1163 assert!(metrics.duration_ms >= 0); }
1165
1166 #[tokio::test]
1167 async fn test_memory_usage_with_large_results() {
1168 let service = create_mock_service().await;
1169
1170 let proto_request = SearchRequest {
1171 query: "test query".to_string(),
1172 max_results: 1000, file_path: None,
1174 include_context: false,
1175 timeout_ms: 5000,
1176 enable_lsp: false,
1177 language: None,
1178 };
1179
1180 let request = Request::new(proto_request);
1181 let result = service.search(request).await;
1182
1183 assert!(result.is_ok());
1184 let response = result.unwrap().into_inner();
1185
1186 assert!(response.results.len() >= 0);
1188
1189 let metrics = response.metrics.unwrap();
1190 assert!(metrics.total_docs >= 0);
1191 assert!(metrics.matched_docs >= 0);
1192 }
1193
1194 #[tokio::test]
1196 async fn test_service_resilience_after_errors() {
1197 let service = create_mock_service_with_failure().await;
1198
1199 let proto_request = SearchRequest {
1201 query: "".to_string(), max_results: 5,
1203 file_path: None,
1204 include_context: false,
1205 timeout_ms: 5000,
1206 enable_lsp: false,
1207 language: None,
1208 };
1209
1210 let request = Request::new(proto_request.clone());
1211 let result = service.search(request).await;
1212
1213 assert!(result.is_err()); let health_request = Request::new(HealthRequest {
1217 service: "lens".to_string(),
1218 });
1219 let health_result = service.health(health_request).await;
1220 assert!(health_result.is_ok());
1221 }
1222
1223 #[tokio::test]
1224 async fn test_handshake_consistency() {
1225 let service = create_mock_service().await;
1226
1227 let nonce = "consistent_nonce".to_string();
1228
1229 let request1 = Request::new(HandshakeRequest {
1231 client_id: nonce.clone(),
1232 protocol_version: "1.0".to_string(),
1233 });
1234 let request2 = Request::new(HandshakeRequest {
1235 client_id: nonce.clone(),
1236 protocol_version: "1.0".to_string(),
1237 });
1238
1239 let result1 = service.handshake(request1).await.unwrap().into_inner();
1240 let result2 = service.handshake(request2).await.unwrap().into_inner();
1241
1242 assert_eq!(result1.server_id, result2.server_id);
1243 assert!(!result1.message.is_empty());
1245 assert!(!result2.message.is_empty());
1246 }
1247
1248 #[tokio::test]
1250 async fn test_response_structure_properties() {
1251 let service = create_mock_service().await;
1252
1253 let proto_request = SearchRequest {
1254 query: "structure test".to_string(),
1255 max_results: 5,
1256 file_path: None,
1257 include_context: false,
1258 timeout_ms: 5000,
1259 enable_lsp: false,
1260 language: None,
1261 };
1262
1263 let request = Request::new(proto_request);
1264 let result = service.search(request).await;
1265
1266 assert!(result.is_ok());
1268 let response = result.unwrap().into_inner();
1269
1270 assert!(response.total_time_ms >= 0);
1272 assert!(response.metrics.is_some());
1273
1274 let metrics = response.metrics.unwrap();
1276 assert!(metrics.duration_ms >= 0);
1277 assert!(metrics.total_docs >= 0);
1278 assert!(metrics.matched_docs >= 0);
1279 }
1280}