Skip to main content

lens_core/
config.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use serde::{Deserialize, Serialize};
4
5/// Main configuration for the Lens search engine
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct LensConfig {
8    /// Server configuration
9    pub server: ServerConfig,
10    
11    /// Search engine configuration
12    pub search: SearchConfig,
13    
14    /// Context selection engine configuration
15    pub context_engine: ContextEngineConfig,
16    
17    /// LSP manager configuration
18    pub lsp: LspConfig,
19    
20    /// Pipeline configuration
21    pub pipeline: PipelineConfig,
22    
23    /// Cache configuration
24    pub cache: CacheConfig,
25    
26    /// Metrics and monitoring
27    pub metrics: MetricsConfig,
28    
29    /// Benchmarking configuration
30    pub benchmark: BenchmarkConfig,
31}
32
33/// Server-specific configuration
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ServerConfig {
36    /// Server host
37    pub host: String,
38    
39    /// gRPC server port
40    pub grpc_port: u16,
41    
42    /// Metrics server port
43    pub metrics_port: u16,
44    
45    /// Enable TLS
46    pub enable_tls: bool,
47    
48    /// Request timeout in milliseconds
49    pub request_timeout_ms: u64,
50    
51    /// Maximum concurrent requests
52    pub max_concurrent_requests: usize,
53}
54
55/// Search engine configuration
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SearchConfig {
58    /// Tantivy index path
59    pub index_path: PathBuf,
60    
61    /// Maximum search results per query
62    pub max_results: usize,
63    
64    /// Default query timeout in milliseconds
65    pub default_timeout_ms: u64,
66    
67    /// Vector search configuration
68    pub vector: VectorConfig,
69    
70    /// Text search configuration
71    pub text: TextConfig,
72    
73    /// Result ranking configuration
74    pub ranking: RankingConfig,
75}
76
77/// Vector search configuration
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct VectorConfig {
80    /// Enable vector search
81    pub enabled: bool,
82    
83    /// Vector model path
84    pub model_path: Option<String>,
85    
86    /// Vector dimensions
87    pub dimensions: usize,
88    
89    /// HNSW ef_search parameter
90    pub ef_search: usize,
91    
92    /// Maximum candidates for vector search
93    pub max_candidates: usize,
94}
95
96/// Text search configuration
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TextConfig {
99    /// Enable fuzzy matching
100    pub enable_fuzzy: bool,
101    
102    /// Fuzzy match threshold (0.0-1.0)
103    pub fuzzy_threshold: f64,
104    
105    /// Enable phrase queries
106    pub enable_phrase: bool,
107    
108    /// Enable boolean queries
109    pub enable_boolean: bool,
110    
111    /// Stemming configuration
112    pub stemming: StemmingConfig,
113}
114
115/// Stemming configuration
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct StemmingConfig {
118    /// Enable stemming
119    pub enabled: bool,
120    
121    /// Language for stemming
122    pub language: String,
123}
124
125/// Result ranking configuration
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RankingConfig {
128    /// BM25 k1 parameter
129    pub bm25_k1: f64,
130    
131    /// BM25 b parameter
132    pub bm25_b: f64,
133    
134    /// TF-IDF boost factor
135    pub tfidf_boost: f64,
136    
137    /// Recency boost factor
138    pub recency_boost: f64,
139    
140    /// Language-specific boost factors
141    pub language_boosts: HashMap<String, f64>,
142}
143
144/// LSP manager configuration
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct LspConfig {
147    /// Enable LSP integration
148    pub enabled: bool,
149    
150    /// LSP server configurations
151    pub servers: HashMap<String, LspServerConfig>,
152    
153    /// LSP request timeout in milliseconds
154    pub request_timeout_ms: u64,
155    
156    /// LSP server startup timeout in milliseconds
157    pub startup_timeout_ms: u64,
158    
159    /// Maximum concurrent LSP requests
160    pub max_concurrent_requests: usize,
161    
162    /// BFS configuration for symbol traversal
163    pub bfs: BfsConfig,
164}
165
166/// Individual LSP server configuration
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct LspServerConfig {
169    /// Command to start the LSP server
170    pub command: String,
171    
172    /// Command arguments
173    pub args: Vec<String>,
174    
175    /// Working directory
176    pub working_dir: Option<PathBuf>,
177    
178    /// Environment variables
179    pub env: HashMap<String, String>,
180    
181    /// Server initialization options
182    pub init_options: serde_json::Value,
183    
184    /// File extensions handled by this server
185    pub file_extensions: Vec<String>,
186}
187
188/// BFS configuration for symbol traversal
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct BfsConfig {
191    /// Maximum BFS depth (≤2 per TODO.md)
192    pub max_depth: u32,
193    
194    /// Maximum nodes to explore (≤64 per TODO.md)
195    pub max_nodes: u32,
196    
197    /// Enable definition traversal
198    pub enable_definitions: bool,
199    
200    /// Enable reference traversal
201    pub enable_references: bool,
202    
203    /// Enable type traversal
204    pub enable_types: bool,
205    
206    /// Enable implementation traversal
207    pub enable_implementations: bool,
208}
209
210/// Pipeline configuration
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct PipelineConfig {
213    /// Enable zero-copy optimization
214    pub zero_copy: bool,
215    
216    /// Pipeline stage buffer sizes
217    pub buffer_sizes: HashMap<String, usize>,
218    
219    /// Enable async overlap between stages
220    pub async_overlap: bool,
221    
222    /// Memory pool configuration
223    pub memory_pool: MemoryPoolConfig,
224    
225    /// Stage fusion configuration
226    pub fusion: FusionConfig,
227}
228
229/// Memory pool configuration
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct MemoryPoolConfig {
232    /// Initial pool size in MB
233    pub initial_size_mb: usize,
234    
235    /// Maximum pool size in MB
236    pub max_size_mb: usize,
237    
238    /// Buffer size in bytes
239    pub buffer_size: usize,
240    
241    /// Enable buffer reuse
242    pub enable_reuse: bool,
243}
244
245/// Stage fusion configuration
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct FusionConfig {
248    /// Enable stage fusion
249    pub enabled: bool,
250    
251    /// Fusion window size
252    pub window_size: usize,
253    
254    /// Enable predictive termination
255    pub enable_prediction: bool,
256}
257
258/// Cache configuration
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct CacheConfig {
261    /// Enable hint caching
262    pub enabled: bool,
263    
264    /// Maximum cache entries
265    pub max_entries: usize,
266    
267    /// Cache TTL in hours (24h per TODO.md)
268    pub ttl_hours: u64,
269    
270    /// Enable cache compression
271    pub enable_compression: bool,
272}
273
274/// Metrics configuration
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct MetricsConfig {
277    /// Enable Prometheus metrics
278    pub enabled: bool,
279    
280    /// Metrics endpoint path
281    pub endpoint: String,
282    
283    /// Export interval in seconds
284    pub export_interval_s: u64,
285    
286    /// Enable detailed metrics
287    pub detailed: bool,
288}
289
290/// Benchmarking configuration
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct BenchmarkConfig {
293    /// Enable benchmarking endpoints
294    pub enabled: bool,
295    
296    /// Golden dataset path
297    pub golden_dataset_path: PathBuf,
298    
299    /// Benchmark results output path
300    pub results_path: PathBuf,
301    
302    /// Enable fraud-resistant attestation
303    pub enable_attestation: bool,
304}
305
306/// Context selection engine configuration
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ContextEngineConfig {
309    /// Enable hero defaults from promoted configurations
310    pub enable_hero_defaults: bool,
311    
312    /// Hero configuration lock file path
313    pub hero_lock_path: PathBuf,
314    
315    /// Context selection strategy
316    pub strategy: ContextStrategy,
317    
318    /// Hero parameters for optimized context selection
319    pub hero_params: HeroParams,
320}
321
322/// Context selection strategy
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub enum ContextStrategy {
325    /// Use hero defaults for optimal performance
326    Hero,
327    /// Use adaptive configuration based on query type
328    Adaptive,
329    /// Use legacy configuration for backwards compatibility
330    Legacy,
331}
332
333/// Hero parameters derived from promoted configuration
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct HeroParams {
336    /// Fusion strategy (e.g., "aggressive_milvus")
337    pub fusion: String,
338    
339    /// Chunk policy (e.g., "ce_large")
340    pub chunk_policy: String,
341    
342    /// Chunk length in tokens
343    pub chunk_len: u32,
344    
345    /// Overlap between chunks
346    pub overlap: u32,
347    
348    /// Retrieval K parameter 
349    pub retrieval_k: u32,
350    
351    /// RRF K0 parameter for ranking fusion
352    pub rrf_k0: u32,
353    
354    /// Reranker type (e.g., "cross_encoder")
355    pub reranker: String,
356    
357    /// Router type (e.g., "ml_v2")
358    pub router: String,
359    
360    /// Maximum chunks per file
361    pub max_chunks_per_file: u32,
362    
363    /// Symbol boost factor
364    pub symbol_boost: f64,
365    
366    /// Graph expansion hops
367    pub graph_expand_hops: u32,
368    
369    /// Graph added tokens cap
370    pub graph_added_tokens_cap: u32,
371}
372
373impl Default for LensConfig {
374    fn default() -> Self {
375        Self {
376            server: ServerConfig {
377                host: "127.0.0.1".to_string(),
378                grpc_port: 50051,
379                metrics_port: 9090,
380                enable_tls: false,
381                request_timeout_ms: 5000,
382                max_concurrent_requests: 1000,
383            },
384            search: SearchConfig {
385                index_path: PathBuf::from("./data/index"),
386                max_results: 50,
387                default_timeout_ms: 2000,
388                vector: VectorConfig {
389                    enabled: false,
390                    model_path: None,
391                    dimensions: 768,
392                    ef_search: 256,
393                    max_candidates: 1000,
394                },
395                text: TextConfig {
396                    enable_fuzzy: true,
397                    fuzzy_threshold: 0.8,
398                    enable_phrase: true,
399                    enable_boolean: true,
400                    stemming: StemmingConfig {
401                        enabled: true,
402                        language: "english".to_string(),
403                    },
404                },
405                ranking: RankingConfig {
406                    bm25_k1: 1.5,
407                    bm25_b: 0.75,
408                    tfidf_boost: 1.0,
409                    recency_boost: 0.1,
410                    language_boosts: {
411                        let mut boosts = HashMap::new();
412                        boosts.insert("rust".to_string(), 1.2);
413                        boosts.insert("typescript".to_string(), 1.1);
414                        boosts.insert("python".to_string(), 1.1);
415                        boosts.insert("javascript".to_string(), 1.0);
416                        boosts
417                    },
418                },
419            },
420            context_engine: ContextEngineConfig {
421                enable_hero_defaults: true,
422                hero_lock_path: PathBuf::from("./release/hero.lock.json"),
423                strategy: ContextStrategy::Hero,
424                hero_params: HeroParams {
425                    fusion: "aggressive_milvus".to_string(),
426                    chunk_policy: "ce_large".to_string(),
427                    chunk_len: 384,
428                    overlap: 128,
429                    retrieval_k: 20,
430                    rrf_k0: 60,
431                    reranker: "cross_encoder".to_string(),
432                    router: "ml_v2".to_string(),
433                    max_chunks_per_file: 50,
434                    symbol_boost: 1.2,
435                    graph_expand_hops: 2,
436                    graph_added_tokens_cap: 256,
437                },
438            },
439            lsp: LspConfig {
440                enabled: true,
441                servers: Self::default_lsp_servers(),
442                request_timeout_ms: 1000,
443                startup_timeout_ms: 10000,
444                max_concurrent_requests: 100,
445                bfs: BfsConfig {
446                    max_depth: 2,        // ≤2 per TODO.md
447                    max_nodes: 64,       // ≤64 per TODO.md
448                    enable_definitions: true,
449                    enable_references: true,
450                    enable_types: true,
451                    enable_implementations: true,
452                },
453            },
454            pipeline: PipelineConfig {
455                zero_copy: true,
456                buffer_sizes: {
457                    let mut sizes = HashMap::new();
458                    sizes.insert("query_parse".to_string(), 1024);
459                    sizes.insert("index_search".to_string(), 4096);
460                    sizes.insert("result_merge".to_string(), 2048);
461                    sizes
462                },
463                async_overlap: true,
464                memory_pool: MemoryPoolConfig {
465                    initial_size_mb: 64,
466                    max_size_mb: 512,
467                    buffer_size: 4096,
468                    enable_reuse: true,
469                },
470                fusion: FusionConfig {
471                    enabled: true,
472                    window_size: 4,
473                    enable_prediction: true,
474                },
475            },
476            cache: CacheConfig {
477                enabled: true,
478                max_entries: 10000,
479                ttl_hours: 24,       // 24h per TODO.md
480                enable_compression: true,
481            },
482            metrics: MetricsConfig {
483                enabled: true,
484                endpoint: "/metrics".to_string(),
485                export_interval_s: 60,
486                detailed: false,
487            },
488            benchmark: BenchmarkConfig {
489                enabled: true,
490                golden_dataset_path: PathBuf::from("./golden-dataset.json"),
491                results_path: PathBuf::from("./benchmark-results"),
492                enable_attestation: true,
493            },
494        }
495    }
496}
497
498impl ContextEngineConfig {
499    /// Load hero parameters from hero lock file
500    pub async fn load_hero_params(&mut self) -> Result<(), Box<dyn std::error::Error>> {
501        if !self.enable_hero_defaults {
502            return Ok(());
503        }
504
505        let content = tokio::fs::read_to_string(&self.hero_lock_path).await?;
506        let hero_data: serde_json::Value = serde_json::from_str(&content)?;
507        
508        if let Some(params) = hero_data.get("params") {
509            if let Some(fusion) = params.get("fusion").and_then(|v| v.as_str()) {
510                self.hero_params.fusion = fusion.to_string();
511            }
512            if let Some(chunk_policy) = params.get("chunk_policy").and_then(|v| v.as_str()) {
513                self.hero_params.chunk_policy = chunk_policy.to_string();
514            }
515            if let Some(chunk_len) = params.get("chunk_len").and_then(|v| v.as_u64()) {
516                self.hero_params.chunk_len = chunk_len as u32;
517            }
518            if let Some(overlap) = params.get("overlap").and_then(|v| v.as_u64()) {
519                self.hero_params.overlap = overlap as u32;
520            }
521            if let Some(retrieval_k) = params.get("retrieval_k").and_then(|v| v.as_u64()) {
522                self.hero_params.retrieval_k = retrieval_k as u32;
523            }
524            if let Some(rrf_k0) = params.get("rrf_k0").and_then(|v| v.as_u64()) {
525                self.hero_params.rrf_k0 = rrf_k0 as u32;
526            }
527            if let Some(reranker) = params.get("reranker").and_then(|v| v.as_str()) {
528                self.hero_params.reranker = reranker.to_string();
529            }
530            if let Some(router) = params.get("router").and_then(|v| v.as_str()) {
531                self.hero_params.router = router.to_string();
532            }
533            if let Some(max_chunks_per_file) = params.get("max_chunks_per_file").and_then(|v| v.as_u64()) {
534                self.hero_params.max_chunks_per_file = max_chunks_per_file as u32;
535            }
536            if let Some(symbol_boost) = params.get("symbol_boost").and_then(|v| v.as_f64()) {
537                self.hero_params.symbol_boost = symbol_boost;
538            }
539            if let Some(graph_expand_hops) = params.get("graph_expand_hops").and_then(|v| v.as_u64()) {
540                self.hero_params.graph_expand_hops = graph_expand_hops as u32;
541            }
542            if let Some(graph_added_tokens_cap) = params.get("graph_added_tokens_cap").and_then(|v| v.as_u64()) {
543                self.hero_params.graph_added_tokens_cap = graph_added_tokens_cap as u32;
544            }
545        }
546
547        Ok(())
548    }
549
550    /// Get the current configuration strategy label for metrics
551    pub fn strategy_label(&self) -> &str {
552        match self.strategy {
553            ContextStrategy::Hero => "hero",
554            ContextStrategy::Adaptive => "adaptive", 
555            ContextStrategy::Legacy => "legacy",
556        }
557    }
558}
559
560impl LensConfig {
561    /// Default LSP server configurations
562    fn default_lsp_servers() -> HashMap<String, LspServerConfig> {
563        let mut servers = HashMap::new();
564
565        // TypeScript/JavaScript server
566        servers.insert("tsserver".to_string(), LspServerConfig {
567            command: "typescript-language-server".to_string(),
568            args: vec!["--stdio".to_string()],
569            working_dir: None,
570            env: HashMap::new(),
571            init_options: serde_json::json!({
572                "preferences": {
573                    "includeInlayParameterNameHints": "all",
574                    "includeInlayVariableTypeHints": true
575                }
576            }),
577            file_extensions: vec![".ts".to_string(), ".tsx".to_string(), ".js".to_string(), ".jsx".to_string()],
578        });
579
580        // Python server
581        servers.insert("pylsp".to_string(), LspServerConfig {
582            command: "pylsp".to_string(),
583            args: vec![],
584            working_dir: None,
585            env: HashMap::new(),
586            init_options: serde_json::json!({
587                "settings": {
588                    "pylsp": {
589                        "plugins": {
590                            "pycodestyle": {"enabled": false},
591                            "mccabe": {"enabled": false}
592                        }
593                    }
594                }
595            }),
596            file_extensions: vec![".py".to_string()],
597        });
598
599        // Rust server
600        servers.insert("rust-analyzer".to_string(), LspServerConfig {
601            command: "rust-analyzer".to_string(),
602            args: vec![],
603            working_dir: None,
604            env: HashMap::new(),
605            init_options: serde_json::json!({
606                "cargo": {
607                    "buildScripts": {
608                        "enable": true
609                    }
610                }
611            }),
612            file_extensions: vec![".rs".to_string()],
613        });
614
615        // Go server
616        servers.insert("gopls".to_string(), LspServerConfig {
617            command: "gopls".to_string(),
618            args: vec![],
619            working_dir: None,
620            env: HashMap::new(),
621            init_options: serde_json::json!({}),
622            file_extensions: vec![".go".to_string()],
623        });
624
625        servers
626    }
627
628    /// Load configuration from file
629    pub async fn load_from_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
630        let content = tokio::fs::read_to_string(path).await?;
631        let config: Self = toml::from_str(&content)?;
632        Ok(config)
633    }
634
635    /// Save configuration to file
636    pub async fn save_to_file(&self, path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
637        let content = toml::to_string_pretty(self)?;
638        tokio::fs::write(path, content).await?;
639        Ok(())
640    }
641
642    /// Validate configuration
643    pub fn validate(&self) -> Result<(), String> {
644        if self.server.grpc_port == self.server.metrics_port {
645            return Err("gRPC and metrics ports cannot be the same".to_string());
646        }
647
648        if self.lsp.enabled && self.lsp.servers.is_empty() {
649            return Err("LSP is enabled but no servers are configured".to_string());
650        }
651
652        if self.lsp.bfs.max_depth > 2 {
653            return Err("BFS max depth must be ≤2 per TODO.md requirements".to_string());
654        }
655
656        if self.lsp.bfs.max_nodes > 64 {
657            return Err("BFS max nodes must be ≤64 per TODO.md requirements".to_string());
658        }
659
660        if self.cache.ttl_hours > 48 {
661            return Err("Cache TTL should not exceed 48 hours for memory management".to_string());
662        }
663
664        Ok(())
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn test_default_config_validation() {
674        let config = LensConfig::default();
675        assert!(config.validate().is_ok());
676    }
677
678    #[test]
679    fn test_bfs_limits() {
680        let mut config = LensConfig::default();
681        
682        // Test max depth limit
683        config.lsp.bfs.max_depth = 3;
684        assert!(config.validate().is_err());
685        
686        config.lsp.bfs.max_depth = 2;
687        config.lsp.bfs.max_nodes = 65;
688        assert!(config.validate().is_err());
689        
690        config.lsp.bfs.max_nodes = 64;
691        assert!(config.validate().is_ok());
692    }
693
694    #[test]
695    fn test_port_conflict() {
696        let mut config = LensConfig::default();
697        config.server.metrics_port = config.server.grpc_port;
698        assert!(config.validate().is_err());
699    }
700
701    #[test]
702    fn test_lsp_enabled_without_servers() {
703        let mut config = LensConfig::default();
704        config.lsp.enabled = true;
705        config.lsp.servers.clear();
706        assert!(config.validate().is_err());
707    }
708
709    #[test]
710    fn test_cache_ttl_limits() {
711        let mut config = LensConfig::default();
712        config.cache.ttl_hours = 49;
713        assert!(config.validate().is_err());
714        
715        config.cache.ttl_hours = 24;
716        assert!(config.validate().is_ok());
717    }
718
719    #[test]
720    fn test_server_config_defaults() {
721        let config = ServerConfig {
722            host: "127.0.0.1".to_string(),
723            grpc_port: 50051,
724            metrics_port: 9090,
725            enable_tls: false,
726            request_timeout_ms: 5000,
727            max_concurrent_requests: 1000,
728        };
729        
730        assert_eq!(config.host, "127.0.0.1");
731        assert_eq!(config.grpc_port, 50051);
732        assert_eq!(config.metrics_port, 9090);
733        assert!(!config.enable_tls);
734    }
735
736    #[test]
737    fn test_search_config_defaults() {
738        let config = SearchConfig {
739            index_path: PathBuf::from("./data/index"),
740            max_results: 50,
741            default_timeout_ms: 2000,
742            vector: VectorConfig {
743                enabled: false,
744                model_path: None,
745                dimensions: 768,
746                ef_search: 256,
747                max_candidates: 1000,
748            },
749            text: TextConfig {
750                enable_fuzzy: true,
751                fuzzy_threshold: 0.8,
752                enable_phrase: true,
753                enable_boolean: true,
754                stemming: StemmingConfig {
755                    enabled: true,
756                    language: "english".to_string(),
757                },
758            },
759            ranking: RankingConfig {
760                bm25_k1: 1.5,
761                bm25_b: 0.75,
762                tfidf_boost: 1.0,
763                recency_boost: 0.1,
764                language_boosts: HashMap::new(),
765            },
766        };
767        
768        assert_eq!(config.max_results, 50);
769        assert_eq!(config.default_timeout_ms, 2000);
770        assert!(!config.vector.enabled);
771        assert!(config.text.enable_fuzzy);
772        assert_eq!(config.text.fuzzy_threshold, 0.8);
773    }
774
775    #[test]
776    fn test_vector_config() {
777        let config = VectorConfig {
778            enabled: true,
779            model_path: Some("./model.bin".to_string()),
780            dimensions: 384,
781            ef_search: 128,
782            max_candidates: 500,
783        };
784        
785        assert!(config.enabled);
786        assert_eq!(config.model_path, Some("./model.bin".to_string()));
787        assert_eq!(config.dimensions, 384);
788        assert_eq!(config.ef_search, 128);
789    }
790
791    #[test]
792    fn test_text_config() {
793        let stemming = StemmingConfig {
794            enabled: false,
795            language: "german".to_string(),
796        };
797        
798        let config = TextConfig {
799            enable_fuzzy: false,
800            fuzzy_threshold: 0.9,
801            enable_phrase: false,
802            enable_boolean: false,
803            stemming,
804        };
805        
806        assert!(!config.enable_fuzzy);
807        assert_eq!(config.fuzzy_threshold, 0.9);
808        assert!(!config.enable_phrase);
809        assert!(!config.stemming.enabled);
810        assert_eq!(config.stemming.language, "german");
811    }
812
813    #[test]
814    fn test_ranking_config() {
815        let mut language_boosts = HashMap::new();
816        language_boosts.insert("rust".to_string(), 2.0);
817        language_boosts.insert("go".to_string(), 1.5);
818        
819        let config = RankingConfig {
820            bm25_k1: 1.2,
821            bm25_b: 0.8,
822            tfidf_boost: 1.5,
823            recency_boost: 0.2,
824            language_boosts,
825        };
826        
827        assert_eq!(config.bm25_k1, 1.2);
828        assert_eq!(config.bm25_b, 0.8);
829        assert_eq!(config.tfidf_boost, 1.5);
830        assert_eq!(config.language_boosts.get("rust"), Some(&2.0));
831    }
832
833    #[test]
834    fn test_lsp_config() {
835        let servers = HashMap::new();
836        let bfs = BfsConfig {
837            max_depth: 1,
838            max_nodes: 32,
839            enable_definitions: false,
840            enable_references: false,
841            enable_types: true,
842            enable_implementations: true,
843        };
844        
845        let config = LspConfig {
846            enabled: false,
847            servers,
848            request_timeout_ms: 500,
849            startup_timeout_ms: 5000,
850            max_concurrent_requests: 50,
851            bfs,
852        };
853        
854        assert!(!config.enabled);
855        assert_eq!(config.request_timeout_ms, 500);
856        assert_eq!(config.bfs.max_depth, 1);
857        assert_eq!(config.bfs.max_nodes, 32);
858        assert!(!config.bfs.enable_definitions);
859        assert!(config.bfs.enable_types);
860    }
861
862    #[test]
863    fn test_lsp_server_config() {
864        let mut env = HashMap::new();
865        env.insert("LANG".to_string(), "en_US.UTF-8".to_string());
866        
867        let config = LspServerConfig {
868            command: "node".to_string(),
869            args: vec!["--inspect".to_string(), "server.js".to_string()],
870            working_dir: Some(PathBuf::from("/opt/lsp")),
871            env,
872            init_options: serde_json::json!({"debug": true}),
873            file_extensions: vec![".js".to_string(), ".ts".to_string()],
874        };
875        
876        assert_eq!(config.command, "node");
877        assert_eq!(config.args.len(), 2);
878        assert_eq!(config.working_dir, Some(PathBuf::from("/opt/lsp")));
879        assert_eq!(config.file_extensions.len(), 2);
880    }
881
882    #[test]
883    fn test_bfs_config() {
884        let config = BfsConfig {
885            max_depth: 2,
886            max_nodes: 64,
887            enable_definitions: true,
888            enable_references: true,
889            enable_types: false,
890            enable_implementations: false,
891        };
892        
893        assert_eq!(config.max_depth, 2);
894        assert_eq!(config.max_nodes, 64);
895        assert!(config.enable_definitions);
896        assert!(config.enable_references);
897        assert!(!config.enable_types);
898        assert!(!config.enable_implementations);
899    }
900
901    #[test]
902    fn test_pipeline_config() {
903        let mut buffer_sizes = HashMap::new();
904        buffer_sizes.insert("stage1".to_string(), 2048);
905        buffer_sizes.insert("stage2".to_string(), 4096);
906        
907        let memory_pool = MemoryPoolConfig {
908            initial_size_mb: 32,
909            max_size_mb: 256,
910            buffer_size: 2048,
911            enable_reuse: false,
912        };
913        
914        let fusion = FusionConfig {
915            enabled: false,
916            window_size: 2,
917            enable_prediction: false,
918        };
919        
920        let config = PipelineConfig {
921            zero_copy: false,
922            buffer_sizes,
923            async_overlap: false,
924            memory_pool,
925            fusion,
926        };
927        
928        assert!(!config.zero_copy);
929        assert!(!config.async_overlap);
930        assert_eq!(config.buffer_sizes.len(), 2);
931        assert_eq!(config.memory_pool.initial_size_mb, 32);
932        assert!(!config.fusion.enabled);
933    }
934
935    #[test]
936    fn test_memory_pool_config() {
937        let config = MemoryPoolConfig {
938            initial_size_mb: 128,
939            max_size_mb: 1024,
940            buffer_size: 8192,
941            enable_reuse: true,
942        };
943        
944        assert_eq!(config.initial_size_mb, 128);
945        assert_eq!(config.max_size_mb, 1024);
946        assert_eq!(config.buffer_size, 8192);
947        assert!(config.enable_reuse);
948    }
949
950    #[test]
951    fn test_fusion_config() {
952        let config = FusionConfig {
953            enabled: true,
954            window_size: 8,
955            enable_prediction: true,
956        };
957        
958        assert!(config.enabled);
959        assert_eq!(config.window_size, 8);
960        assert!(config.enable_prediction);
961    }
962
963    #[test]
964    fn test_cache_config() {
965        let config = CacheConfig {
966            enabled: false,
967            max_entries: 5000,
968            ttl_hours: 12,
969            enable_compression: false,
970        };
971        
972        assert!(!config.enabled);
973        assert_eq!(config.max_entries, 5000);
974        assert_eq!(config.ttl_hours, 12);
975        assert!(!config.enable_compression);
976    }
977
978    #[test]
979    fn test_metrics_config() {
980        let config = MetricsConfig {
981            enabled: true,
982            endpoint: "/health".to_string(),
983            export_interval_s: 30,
984            detailed: true,
985        };
986        
987        assert!(config.enabled);
988        assert_eq!(config.endpoint, "/health");
989        assert_eq!(config.export_interval_s, 30);
990        assert!(config.detailed);
991    }
992
993    #[test]
994    fn test_benchmark_config() {
995        let config = BenchmarkConfig {
996            enabled: false,
997            golden_dataset_path: PathBuf::from("/data/golden.json"),
998            results_path: PathBuf::from("/output/results"),
999            enable_attestation: false,
1000        };
1001        
1002        assert!(!config.enabled);
1003        assert_eq!(config.golden_dataset_path, PathBuf::from("/data/golden.json"));
1004        assert_eq!(config.results_path, PathBuf::from("/output/results"));
1005        assert!(!config.enable_attestation);
1006    }
1007
1008    #[test]
1009    fn test_default_lsp_servers() {
1010        let servers = LensConfig::default_lsp_servers();
1011        
1012        assert!(servers.contains_key("tsserver"));
1013        assert!(servers.contains_key("pylsp"));
1014        assert!(servers.contains_key("rust-analyzer"));
1015        assert!(servers.contains_key("gopls"));
1016        
1017        let ts_server = servers.get("tsserver").unwrap();
1018        assert_eq!(ts_server.command, "typescript-language-server");
1019        assert!(ts_server.file_extensions.contains(&".ts".to_string()));
1020        assert!(ts_server.file_extensions.contains(&".js".to_string()));
1021        
1022        let py_server = servers.get("pylsp").unwrap();
1023        assert_eq!(py_server.command, "pylsp");
1024        assert!(py_server.file_extensions.contains(&".py".to_string()));
1025        
1026        let rust_server = servers.get("rust-analyzer").unwrap();
1027        assert_eq!(rust_server.command, "rust-analyzer");
1028        assert!(rust_server.file_extensions.contains(&".rs".to_string()));
1029        
1030        let go_server = servers.get("gopls").unwrap();
1031        assert_eq!(go_server.command, "gopls");
1032        assert!(go_server.file_extensions.contains(&".go".to_string()));
1033    }
1034
1035    #[test]
1036    fn test_lens_config_default() {
1037        let config = LensConfig::default();
1038        
1039        assert_eq!(config.server.host, "127.0.0.1");
1040        assert_eq!(config.server.grpc_port, 50051);
1041        assert_eq!(config.search.max_results, 50);
1042        assert!(config.lsp.enabled);
1043        assert!(config.cache.enabled);
1044        assert!(config.metrics.enabled);
1045        assert!(config.benchmark.enabled);
1046        
1047        // Test language boosts
1048        let rust_boost = config.search.ranking.language_boosts.get("rust");
1049        assert_eq!(rust_boost, Some(&1.2));
1050        
1051        let ts_boost = config.search.ranking.language_boosts.get("typescript");
1052        assert_eq!(ts_boost, Some(&1.1));
1053    }
1054
1055    #[test]
1056    fn test_serialization_deserialization() {
1057        let config = LensConfig::default();
1058        
1059        // Test serialization
1060        let serialized = serde_json::to_string(&config).unwrap();
1061        assert!(!serialized.is_empty());
1062        
1063        // Test deserialization
1064        let deserialized: LensConfig = serde_json::from_str(&serialized).unwrap();
1065        assert_eq!(deserialized.server.host, config.server.host);
1066        assert_eq!(deserialized.search.max_results, config.search.max_results);
1067    }
1068
1069    #[test]
1070    fn test_edge_cases() {
1071        let mut config = LensConfig::default();
1072        
1073        // Test with zero ports (different values to avoid conflict)
1074        config.server.grpc_port = 0;
1075        config.server.metrics_port = 1;
1076        
1077        // Test empty LSP servers when disabled
1078        config.lsp.enabled = false;
1079        config.lsp.servers.clear();
1080        assert!(config.validate().is_ok()); // Should be OK when LSP is disabled
1081        
1082        // Test maximum allowed values
1083        config.lsp.bfs.max_depth = 2;
1084        config.lsp.bfs.max_nodes = 64;
1085        config.cache.ttl_hours = 48;
1086        assert!(config.validate().is_ok());
1087        
1088        // Test minimum values
1089        config.lsp.bfs.max_depth = 0;
1090        config.lsp.bfs.max_nodes = 0;
1091        config.cache.ttl_hours = 1;
1092        assert!(config.validate().is_ok());
1093    }
1094
1095    #[tokio::test]
1096    async fn test_config_file_operations() {
1097        use tempfile::TempDir;
1098        
1099        let temp_dir = TempDir::new().unwrap();
1100        let config_path = temp_dir.path().join("lens.toml");
1101        
1102        let original_config = LensConfig::default();
1103        
1104        // Test saving config to file
1105        let save_result = original_config.save_to_file(&config_path).await;
1106        assert!(save_result.is_ok());
1107        
1108        // Test loading config from file
1109        let loaded_config = LensConfig::load_from_file(&config_path).await;
1110        assert!(loaded_config.is_ok());
1111        
1112        let loaded = loaded_config.unwrap();
1113        assert_eq!(loaded.server.host, original_config.server.host);
1114        assert_eq!(loaded.search.max_results, original_config.search.max_results);
1115    }
1116
1117    #[test]
1118    fn test_context_engine_config() {
1119        let hero_params = HeroParams {
1120            fusion: "aggressive_milvus".to_string(),
1121            chunk_policy: "ce_large".to_string(),
1122            chunk_len: 384,
1123            overlap: 128,
1124            retrieval_k: 20,
1125            rrf_k0: 60,
1126            reranker: "cross_encoder".to_string(),
1127            router: "ml_v2".to_string(),
1128            max_chunks_per_file: 50,
1129            symbol_boost: 1.2,
1130            graph_expand_hops: 2,
1131            graph_added_tokens_cap: 256,
1132        };
1133
1134        let config = ContextEngineConfig {
1135            enable_hero_defaults: true,
1136            hero_lock_path: PathBuf::from("./release/hero.lock.json"),
1137            strategy: ContextStrategy::Hero,
1138            hero_params,
1139        };
1140
1141        assert!(config.enable_hero_defaults);
1142        assert_eq!(config.hero_lock_path, PathBuf::from("./release/hero.lock.json"));
1143        assert_eq!(config.strategy_label(), "hero");
1144        assert_eq!(config.hero_params.fusion, "aggressive_milvus");
1145        assert_eq!(config.hero_params.chunk_len, 384);
1146        assert_eq!(config.hero_params.symbol_boost, 1.2);
1147    }
1148
1149    #[test]
1150    fn test_context_strategy_labels() {
1151        let mut config = ContextEngineConfig {
1152            enable_hero_defaults: true,
1153            hero_lock_path: PathBuf::from("./release/hero.lock.json"),
1154            strategy: ContextStrategy::Hero,
1155            hero_params: HeroParams {
1156                fusion: "aggressive_milvus".to_string(),
1157                chunk_policy: "ce_large".to_string(),
1158                chunk_len: 384,
1159                overlap: 128,
1160                retrieval_k: 20,
1161                rrf_k0: 60,
1162                reranker: "cross_encoder".to_string(),
1163                router: "ml_v2".to_string(),
1164                max_chunks_per_file: 50,
1165                symbol_boost: 1.2,
1166                graph_expand_hops: 2,
1167                graph_added_tokens_cap: 256,
1168            },
1169        };
1170
1171        assert_eq!(config.strategy_label(), "hero");
1172        
1173        config.strategy = ContextStrategy::Adaptive;
1174        assert_eq!(config.strategy_label(), "adaptive");
1175        
1176        config.strategy = ContextStrategy::Legacy;
1177        assert_eq!(config.strategy_label(), "legacy");
1178    }
1179
1180    #[test]
1181    fn test_hero_params_serialization() {
1182        let hero_params = HeroParams {
1183            fusion: "aggressive_milvus".to_string(),
1184            chunk_policy: "ce_large".to_string(),
1185            chunk_len: 384,
1186            overlap: 128,
1187            retrieval_k: 20,
1188            rrf_k0: 60,
1189            reranker: "cross_encoder".to_string(),
1190            router: "ml_v2".to_string(),
1191            max_chunks_per_file: 50,
1192            symbol_boost: 1.2,
1193            graph_expand_hops: 2,
1194            graph_added_tokens_cap: 256,
1195        };
1196
1197        // Test serialization
1198        let serialized = serde_json::to_string(&hero_params).unwrap();
1199        assert!(!serialized.is_empty());
1200        assert!(serialized.contains("aggressive_milvus"));
1201        assert!(serialized.contains("cross_encoder"));
1202
1203        // Test deserialization
1204        let deserialized: HeroParams = serde_json::from_str(&serialized).unwrap();
1205        assert_eq!(deserialized.fusion, hero_params.fusion);
1206        assert_eq!(deserialized.chunk_len, hero_params.chunk_len);
1207        assert_eq!(deserialized.symbol_boost, hero_params.symbol_boost);
1208    }
1209
1210    #[test]
1211    fn test_default_config_includes_context_engine() {
1212        let config = LensConfig::default();
1213        
1214        assert!(config.context_engine.enable_hero_defaults);
1215        assert_eq!(config.context_engine.hero_lock_path, PathBuf::from("./release/hero.lock.json"));
1216        assert_eq!(config.context_engine.strategy_label(), "hero");
1217        assert_eq!(config.context_engine.hero_params.fusion, "aggressive_milvus");
1218        assert_eq!(config.context_engine.hero_params.retrieval_k, 20);
1219        assert_eq!(config.context_engine.hero_params.rrf_k0, 60);
1220    }
1221}