1use std::collections::HashMap;
2use std::path::PathBuf;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct LensConfig {
8 pub server: ServerConfig,
10
11 pub search: SearchConfig,
13
14 pub context_engine: ContextEngineConfig,
16
17 pub lsp: LspConfig,
19
20 pub pipeline: PipelineConfig,
22
23 pub cache: CacheConfig,
25
26 pub metrics: MetricsConfig,
28
29 pub benchmark: BenchmarkConfig,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ServerConfig {
36 pub host: String,
38
39 pub grpc_port: u16,
41
42 pub metrics_port: u16,
44
45 pub enable_tls: bool,
47
48 pub request_timeout_ms: u64,
50
51 pub max_concurrent_requests: usize,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SearchConfig {
58 pub index_path: PathBuf,
60
61 pub max_results: usize,
63
64 pub default_timeout_ms: u64,
66
67 pub vector: VectorConfig,
69
70 pub text: TextConfig,
72
73 pub ranking: RankingConfig,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct VectorConfig {
80 pub enabled: bool,
82
83 pub model_path: Option<String>,
85
86 pub dimensions: usize,
88
89 pub ef_search: usize,
91
92 pub max_candidates: usize,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct TextConfig {
99 pub enable_fuzzy: bool,
101
102 pub fuzzy_threshold: f64,
104
105 pub enable_phrase: bool,
107
108 pub enable_boolean: bool,
110
111 pub stemming: StemmingConfig,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct StemmingConfig {
118 pub enabled: bool,
120
121 pub language: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RankingConfig {
128 pub bm25_k1: f64,
130
131 pub bm25_b: f64,
133
134 pub tfidf_boost: f64,
136
137 pub recency_boost: f64,
139
140 pub language_boosts: HashMap<String, f64>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct LspConfig {
147 pub enabled: bool,
149
150 pub servers: HashMap<String, LspServerConfig>,
152
153 pub request_timeout_ms: u64,
155
156 pub startup_timeout_ms: u64,
158
159 pub max_concurrent_requests: usize,
161
162 pub bfs: BfsConfig,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct LspServerConfig {
169 pub command: String,
171
172 pub args: Vec<String>,
174
175 pub working_dir: Option<PathBuf>,
177
178 pub env: HashMap<String, String>,
180
181 pub init_options: serde_json::Value,
183
184 pub file_extensions: Vec<String>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct BfsConfig {
191 pub max_depth: u32,
193
194 pub max_nodes: u32,
196
197 pub enable_definitions: bool,
199
200 pub enable_references: bool,
202
203 pub enable_types: bool,
205
206 pub enable_implementations: bool,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct PipelineConfig {
213 pub zero_copy: bool,
215
216 pub buffer_sizes: HashMap<String, usize>,
218
219 pub async_overlap: bool,
221
222 pub memory_pool: MemoryPoolConfig,
224
225 pub fusion: FusionConfig,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct MemoryPoolConfig {
232 pub initial_size_mb: usize,
234
235 pub max_size_mb: usize,
237
238 pub buffer_size: usize,
240
241 pub enable_reuse: bool,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct FusionConfig {
248 pub enabled: bool,
250
251 pub window_size: usize,
253
254 pub enable_prediction: bool,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct CacheConfig {
261 pub enabled: bool,
263
264 pub max_entries: usize,
266
267 pub ttl_hours: u64,
269
270 pub enable_compression: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct MetricsConfig {
277 pub enabled: bool,
279
280 pub endpoint: String,
282
283 pub export_interval_s: u64,
285
286 pub detailed: bool,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct BenchmarkConfig {
293 pub enabled: bool,
295
296 pub golden_dataset_path: PathBuf,
298
299 pub results_path: PathBuf,
301
302 pub enable_attestation: bool,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct ContextEngineConfig {
309 pub enable_hero_defaults: bool,
311
312 pub hero_lock_path: PathBuf,
314
315 pub strategy: ContextStrategy,
317
318 pub hero_params: HeroParams,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub enum ContextStrategy {
325 Hero,
327 Adaptive,
329 Legacy,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct HeroParams {
336 pub fusion: String,
338
339 pub chunk_policy: String,
341
342 pub chunk_len: u32,
344
345 pub overlap: u32,
347
348 pub retrieval_k: u32,
350
351 pub rrf_k0: u32,
353
354 pub reranker: String,
356
357 pub router: String,
359
360 pub max_chunks_per_file: u32,
362
363 pub symbol_boost: f64,
365
366 pub graph_expand_hops: u32,
368
369 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, max_nodes: 64, 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, 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 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 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 fn default_lsp_servers() -> HashMap<String, LspServerConfig> {
563 let mut servers = HashMap::new();
564
565 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 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 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 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 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 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 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 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 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 let serialized = serde_json::to_string(&config).unwrap();
1061 assert!(!serialized.is_empty());
1062
1063 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 config.server.grpc_port = 0;
1075 config.server.metrics_port = 1;
1076
1077 config.lsp.enabled = false;
1079 config.lsp.servers.clear();
1080 assert!(config.validate().is_ok()); 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 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 let save_result = original_config.save_to_file(&config_path).await;
1106 assert!(save_result.is_ok());
1107
1108 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 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 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}