1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use std::sync::Mutex;
4use std::time::SystemTime;
5
6use super::memory_policy::MemoryPolicy;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
10#[serde(rename_all = "lowercase")]
11pub enum TeeMode {
12 Never,
13 #[default]
14 Failures,
15 Always,
16}
17
18#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
20#[serde(rename_all = "lowercase")]
21pub enum TerseAgent {
22 #[default]
23 Off,
24 Lite,
25 Full,
26 Ultra,
27}
28
29impl TerseAgent {
30 pub fn from_env() -> Self {
32 match std::env::var("LEAN_CTX_TERSE_AGENT")
33 .unwrap_or_default()
34 .to_lowercase()
35 .as_str()
36 {
37 "lite" => Self::Lite,
38 "full" => Self::Full,
39 "ultra" => Self::Ultra,
40 _ => Self::Off,
41 }
42 }
43
44 pub fn effective(config_val: &TerseAgent) -> Self {
46 match std::env::var("LEAN_CTX_TERSE_AGENT") {
47 Ok(val) if !val.is_empty() => match val.to_lowercase().as_str() {
48 "lite" => Self::Lite,
49 "full" => Self::Full,
50 "ultra" => Self::Ultra,
51 _ => Self::Off,
52 },
53 _ => config_val.clone(),
54 }
55 }
56
57 pub fn is_active(&self) -> bool {
59 !matches!(self, Self::Off)
60 }
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
65#[serde(rename_all = "lowercase")]
66pub enum OutputDensity {
67 #[default]
68 Normal,
69 Terse,
70 Ultra,
71}
72
73impl OutputDensity {
74 pub fn from_env() -> Self {
76 match std::env::var("LEAN_CTX_OUTPUT_DENSITY")
77 .unwrap_or_default()
78 .to_lowercase()
79 .as_str()
80 {
81 "terse" => Self::Terse,
82 "ultra" => Self::Ultra,
83 _ => Self::Normal,
84 }
85 }
86
87 pub fn effective(config_val: &OutputDensity) -> Self {
89 let env_val = Self::from_env();
90 if env_val != Self::Normal {
91 return env_val;
92 }
93 let profile_val = crate::core::profiles::active_profile()
94 .compression
95 .output_density_effective()
96 .to_lowercase();
97 let profile_density = match profile_val.as_str() {
98 "terse" => Self::Terse,
99 "ultra" => Self::Ultra,
100 _ => Self::Normal,
101 };
102 if profile_density != Self::Normal {
103 return profile_density;
104 }
105 config_val.clone()
106 }
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
114#[serde(rename_all = "lowercase")]
115pub enum CompressionLevel {
116 #[default]
117 Off,
118 Lite,
119 Standard,
120 Max,
121}
122
123impl CompressionLevel {
124 pub fn to_components(&self) -> (TerseAgent, OutputDensity, &'static str, bool) {
127 match self {
128 Self::Off => (TerseAgent::Off, OutputDensity::Normal, "off", false),
129 Self::Lite => (TerseAgent::Lite, OutputDensity::Terse, "off", true),
130 Self::Standard => (TerseAgent::Full, OutputDensity::Terse, "compact", true),
131 Self::Max => (TerseAgent::Ultra, OutputDensity::Ultra, "tdd", true),
132 }
133 }
134
135 pub fn from_legacy(terse_agent: &TerseAgent, output_density: &OutputDensity) -> Self {
138 match (terse_agent, output_density) {
139 (TerseAgent::Ultra, _) | (_, OutputDensity::Ultra) => Self::Max,
140 (TerseAgent::Full, _) => Self::Standard,
141 (TerseAgent::Lite, _) | (_, OutputDensity::Terse) => Self::Lite,
142 _ => Self::Off,
143 }
144 }
145
146 pub fn from_env() -> Option<Self> {
148 std::env::var("LEAN_CTX_COMPRESSION").ok().and_then(|v| {
149 match v.trim().to_lowercase().as_str() {
150 "off" => Some(Self::Off),
151 "lite" => Some(Self::Lite),
152 "standard" => Some(Self::Standard),
153 "max" => Some(Self::Max),
154 _ => None,
155 }
156 })
157 }
158
159 pub fn effective(config: &Config) -> Self {
165 if let Some(env_level) = Self::from_env() {
166 return env_level;
167 }
168 if config.compression_level != Self::Off {
169 return config.compression_level.clone();
170 }
171 if config.ultra_compact {
172 return Self::Max;
173 }
174 Self::from_legacy(&config.terse_agent, &config.output_density)
175 }
176
177 pub fn from_str_label(s: &str) -> Option<Self> {
178 match s.trim().to_lowercase().as_str() {
179 "off" => Some(Self::Off),
180 "lite" => Some(Self::Lite),
181 "standard" | "std" => Some(Self::Standard),
182 "max" => Some(Self::Max),
183 _ => None,
184 }
185 }
186
187 pub fn is_active(&self) -> bool {
188 !matches!(self, Self::Off)
189 }
190
191 pub fn label(&self) -> &'static str {
192 match self {
193 Self::Off => "off",
194 Self::Lite => "lite",
195 Self::Standard => "standard",
196 Self::Max => "max",
197 }
198 }
199
200 pub fn description(&self) -> &'static str {
201 match self {
202 Self::Off => "No compression — full verbose output",
203 Self::Lite => "Light compression — concise output, basic terse filtering",
204 Self::Standard => {
205 "Standard compression — dense output, compact protocol, pattern-aware"
206 }
207 Self::Max => "Maximum compression — expert mode, TDD protocol, all layers active",
208 }
209 }
210}
211
212#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
216#[serde(rename_all = "lowercase")]
217pub enum MemoryCleanup {
218 #[default]
219 Aggressive,
220 Shared,
221}
222
223impl MemoryCleanup {
224 pub fn from_env() -> Option<Self> {
225 std::env::var("LEAN_CTX_MEMORY_CLEANUP").ok().and_then(|v| {
226 match v.trim().to_lowercase().as_str() {
227 "aggressive" => Some(Self::Aggressive),
228 "shared" => Some(Self::Shared),
229 _ => None,
230 }
231 })
232 }
233
234 pub fn effective(config: &Config) -> Self {
235 if let Some(env_val) = Self::from_env() {
236 return env_val;
237 }
238 config.memory_cleanup.clone()
239 }
240
241 pub fn idle_ttl_secs(&self) -> u64 {
243 match self {
244 Self::Aggressive => 300,
245 Self::Shared => 1800,
246 }
247 }
248
249 pub fn index_retention_multiplier(&self) -> f64 {
251 match self {
252 Self::Aggressive => 1.0,
253 Self::Shared => 3.0,
254 }
255 }
256}
257
258#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
263#[serde(rename_all = "lowercase")]
264pub enum MemoryProfile {
265 Low,
266 #[default]
267 Balanced,
268 Performance,
269}
270
271impl MemoryProfile {
272 pub fn from_env() -> Option<Self> {
273 std::env::var("LEAN_CTX_MEMORY_PROFILE").ok().and_then(|v| {
274 match v.trim().to_lowercase().as_str() {
275 "low" => Some(Self::Low),
276 "balanced" => Some(Self::Balanced),
277 "performance" => Some(Self::Performance),
278 _ => None,
279 }
280 })
281 }
282
283 pub fn effective(config: &Config) -> Self {
284 if let Some(env_val) = Self::from_env() {
285 return env_val;
286 }
287 config.memory_profile.clone()
288 }
289
290 pub fn bm25_max_cache_mb(&self) -> u64 {
291 match self {
292 Self::Low => 64,
293 Self::Balanced => 128,
294 Self::Performance => 512,
295 }
296 }
297
298 pub fn semantic_cache_enabled(&self) -> bool {
299 !matches!(self, Self::Low)
300 }
301
302 pub fn embeddings_enabled(&self) -> bool {
303 !matches!(self, Self::Low)
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(default)]
310pub struct Config {
311 pub ultra_compact: bool,
312 #[serde(default, deserialize_with = "deserialize_tee_mode")]
313 pub tee_mode: TeeMode,
314 #[serde(default)]
315 pub output_density: OutputDensity,
316 pub checkpoint_interval: u32,
317 pub excluded_commands: Vec<String>,
318 pub passthrough_urls: Vec<String>,
319 pub custom_aliases: Vec<AliasEntry>,
320 pub slow_command_threshold_ms: u64,
323 #[serde(default = "default_theme")]
324 pub theme: String,
325 #[serde(default)]
326 pub cloud: CloudConfig,
327 #[serde(default)]
328 pub autonomy: AutonomyConfig,
329 #[serde(default)]
330 pub proxy: ProxyConfig,
331 #[serde(default = "default_buddy_enabled")]
332 pub buddy_enabled: bool,
333 #[serde(default)]
334 pub redirect_exclude: Vec<String>,
335 #[serde(default)]
339 pub disabled_tools: Vec<String>,
340 #[serde(default)]
341 pub loop_detection: LoopDetectionConfig,
342 #[serde(default)]
346 pub rules_scope: Option<String>,
347 #[serde(default)]
350 pub extra_ignore_patterns: Vec<String>,
351 #[serde(default)]
355 pub terse_agent: TerseAgent,
356 #[serde(default)]
360 pub compression_level: CompressionLevel,
361 #[serde(default)]
363 pub archive: ArchiveConfig,
364 #[serde(default)]
366 pub memory: MemoryPolicy,
367 #[serde(default)]
371 pub allow_paths: Vec<String>,
372 #[serde(default)]
375 pub content_defined_chunking: bool,
376 #[serde(default)]
379 pub minimal_overhead: bool,
380 #[serde(default)]
383 pub shell_hook_disabled: bool,
384 #[serde(default)]
387 pub update_check_disabled: bool,
388 #[serde(default = "default_bm25_max_cache_mb")]
391 pub bm25_max_cache_mb: u64,
392 #[serde(default)]
395 pub memory_profile: MemoryProfile,
396 #[serde(default)]
400 pub memory_cleanup: MemoryCleanup,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(default)]
406pub struct ArchiveConfig {
407 pub enabled: bool,
408 pub threshold_chars: usize,
409 pub max_age_hours: u64,
410 pub max_disk_mb: u64,
411}
412
413impl Default for ArchiveConfig {
414 fn default() -> Self {
415 Self {
416 enabled: true,
417 threshold_chars: 4096,
418 max_age_hours: 48,
419 max_disk_mb: 500,
420 }
421 }
422}
423
424#[derive(Debug, Clone, Default, Serialize, Deserialize)]
426#[serde(default)]
427pub struct ProxyConfig {
428 pub anthropic_upstream: Option<String>,
429 pub openai_upstream: Option<String>,
430 pub gemini_upstream: Option<String>,
431}
432
433impl ProxyConfig {
434 pub fn resolve_upstream(&self, provider: ProxyProvider) -> String {
435 let (env_var, config_val, default) = match provider {
436 ProxyProvider::Anthropic => (
437 "LEAN_CTX_ANTHROPIC_UPSTREAM",
438 self.anthropic_upstream.as_deref(),
439 "https://api.anthropic.com",
440 ),
441 ProxyProvider::OpenAi => (
442 "LEAN_CTX_OPENAI_UPSTREAM",
443 self.openai_upstream.as_deref(),
444 "https://api.openai.com",
445 ),
446 ProxyProvider::Gemini => (
447 "LEAN_CTX_GEMINI_UPSTREAM",
448 self.gemini_upstream.as_deref(),
449 "https://generativelanguage.googleapis.com",
450 ),
451 };
452 std::env::var(env_var)
453 .ok()
454 .and_then(|v| normalize_url_opt(&v))
455 .or_else(|| config_val.and_then(normalize_url_opt))
456 .unwrap_or_else(|| normalize_url(default))
457 }
458}
459
460#[derive(Debug, Clone, Copy)]
461pub enum ProxyProvider {
462 Anthropic,
463 OpenAi,
464 Gemini,
465}
466
467pub fn normalize_url(value: &str) -> String {
468 value.trim().trim_end_matches('/').to_string()
469}
470
471pub fn normalize_url_opt(value: &str) -> Option<String> {
472 let trimmed = normalize_url(value);
473 if trimmed.is_empty() {
474 None
475 } else {
476 Some(trimmed)
477 }
478}
479
480pub fn is_local_proxy_url(value: &str) -> bool {
481 let n = normalize_url(value);
482 n.starts_with("http://127.0.0.1:")
483 || n.starts_with("http://localhost:")
484 || n.starts_with("http://[::1]:")
485}
486
487fn default_buddy_enabled() -> bool {
488 true
489}
490
491pub fn default_bm25_max_cache_mb() -> u64 {
492 128
493}
494
495fn deserialize_tee_mode<'de, D>(deserializer: D) -> Result<TeeMode, D::Error>
496where
497 D: serde::Deserializer<'de>,
498{
499 use serde::de::Error;
500 let v = serde_json::Value::deserialize(deserializer)?;
501 match &v {
502 serde_json::Value::Bool(true) => Ok(TeeMode::Failures),
503 serde_json::Value::Bool(false) => Ok(TeeMode::Never),
504 serde_json::Value::String(s) => match s.as_str() {
505 "never" => Ok(TeeMode::Never),
506 "failures" => Ok(TeeMode::Failures),
507 "always" => Ok(TeeMode::Always),
508 other => Err(D::Error::custom(format!("unknown tee_mode: {other}"))),
509 },
510 _ => Err(D::Error::custom("tee_mode must be string or bool")),
511 }
512}
513
514fn default_theme() -> String {
515 "default".to_string()
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520#[serde(default)]
521pub struct AutonomyConfig {
522 pub enabled: bool,
523 pub auto_preload: bool,
524 pub auto_dedup: bool,
525 pub auto_related: bool,
526 pub auto_consolidate: bool,
527 pub silent_preload: bool,
528 pub dedup_threshold: usize,
529 pub consolidate_every_calls: u32,
530 pub consolidate_cooldown_secs: u64,
531}
532
533impl Default for AutonomyConfig {
534 fn default() -> Self {
535 Self {
536 enabled: true,
537 auto_preload: true,
538 auto_dedup: true,
539 auto_related: true,
540 auto_consolidate: true,
541 silent_preload: true,
542 dedup_threshold: 8,
543 consolidate_every_calls: 25,
544 consolidate_cooldown_secs: 120,
545 }
546 }
547}
548
549impl AutonomyConfig {
550 pub fn from_env() -> Self {
552 let mut cfg = Self::default();
553 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
554 if v == "false" || v == "0" {
555 cfg.enabled = false;
556 }
557 }
558 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
559 cfg.auto_preload = v != "false" && v != "0";
560 }
561 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
562 cfg.auto_dedup = v != "false" && v != "0";
563 }
564 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
565 cfg.auto_related = v != "false" && v != "0";
566 }
567 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_CONSOLIDATE") {
568 cfg.auto_consolidate = v != "false" && v != "0";
569 }
570 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
571 cfg.silent_preload = v != "false" && v != "0";
572 }
573 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
574 if let Ok(n) = v.parse() {
575 cfg.dedup_threshold = n;
576 }
577 }
578 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_EVERY_CALLS") {
579 if let Ok(n) = v.parse() {
580 cfg.consolidate_every_calls = n;
581 }
582 }
583 if let Ok(v) = std::env::var("LEAN_CTX_CONSOLIDATE_COOLDOWN_SECS") {
584 if let Ok(n) = v.parse() {
585 cfg.consolidate_cooldown_secs = n;
586 }
587 }
588 cfg
589 }
590
591 pub fn load() -> Self {
593 let file_cfg = Config::load().autonomy;
594 let mut cfg = file_cfg;
595 if let Ok(v) = std::env::var("LEAN_CTX_AUTONOMY") {
596 if v == "false" || v == "0" {
597 cfg.enabled = false;
598 }
599 }
600 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_PRELOAD") {
601 cfg.auto_preload = v != "false" && v != "0";
602 }
603 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_DEDUP") {
604 cfg.auto_dedup = v != "false" && v != "0";
605 }
606 if let Ok(v) = std::env::var("LEAN_CTX_AUTO_RELATED") {
607 cfg.auto_related = v != "false" && v != "0";
608 }
609 if let Ok(v) = std::env::var("LEAN_CTX_SILENT_PRELOAD") {
610 cfg.silent_preload = v != "false" && v != "0";
611 }
612 if let Ok(v) = std::env::var("LEAN_CTX_DEDUP_THRESHOLD") {
613 if let Ok(n) = v.parse() {
614 cfg.dedup_threshold = n;
615 }
616 }
617 cfg
618 }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, Default)]
623#[serde(default)]
624pub struct CloudConfig {
625 pub contribute_enabled: bool,
626 pub last_contribute: Option<String>,
627 pub last_sync: Option<String>,
628 pub last_gain_sync: Option<String>,
629 pub last_model_pull: Option<String>,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct AliasEntry {
635 pub command: String,
636 pub alias: String,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(default)]
642pub struct LoopDetectionConfig {
643 pub normal_threshold: u32,
644 pub reduced_threshold: u32,
645 pub blocked_threshold: u32,
646 pub window_secs: u64,
647 pub search_group_limit: u32,
648}
649
650impl Default for LoopDetectionConfig {
651 fn default() -> Self {
652 Self {
653 normal_threshold: 2,
654 reduced_threshold: 4,
655 blocked_threshold: 0,
658 window_secs: 300,
659 search_group_limit: 10,
660 }
661 }
662}
663
664impl Default for Config {
665 fn default() -> Self {
666 Self {
667 ultra_compact: false,
668 tee_mode: TeeMode::default(),
669 output_density: OutputDensity::default(),
670 checkpoint_interval: 15,
671 excluded_commands: Vec::new(),
672 passthrough_urls: Vec::new(),
673 custom_aliases: Vec::new(),
674 slow_command_threshold_ms: 5000,
675 theme: default_theme(),
676 cloud: CloudConfig::default(),
677 autonomy: AutonomyConfig::default(),
678 proxy: ProxyConfig::default(),
679 buddy_enabled: default_buddy_enabled(),
680 redirect_exclude: Vec::new(),
681 disabled_tools: Vec::new(),
682 loop_detection: LoopDetectionConfig::default(),
683 rules_scope: None,
684 extra_ignore_patterns: Vec::new(),
685 terse_agent: TerseAgent::default(),
686 compression_level: CompressionLevel::default(),
687 archive: ArchiveConfig::default(),
688 memory: MemoryPolicy::default(),
689 allow_paths: Vec::new(),
690 content_defined_chunking: false,
691 minimal_overhead: false,
692 shell_hook_disabled: false,
693 update_check_disabled: false,
694 bm25_max_cache_mb: default_bm25_max_cache_mb(),
695 memory_profile: MemoryProfile::default(),
696 memory_cleanup: MemoryCleanup::default(),
697 }
698 }
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
703pub enum RulesScope {
704 Both,
705 Global,
706 Project,
707}
708
709impl Config {
710 pub fn rules_scope_effective(&self) -> RulesScope {
712 let raw = std::env::var("LEAN_CTX_RULES_SCOPE")
713 .ok()
714 .or_else(|| self.rules_scope.clone())
715 .unwrap_or_default();
716 match raw.trim().to_lowercase().as_str() {
717 "global" => RulesScope::Global,
718 "project" => RulesScope::Project,
719 _ => RulesScope::Both,
720 }
721 }
722
723 fn parse_disabled_tools_env(val: &str) -> Vec<String> {
724 val.split(',')
725 .map(|s| s.trim().to_string())
726 .filter(|s| !s.is_empty())
727 .collect()
728 }
729
730 pub fn disabled_tools_effective(&self) -> Vec<String> {
732 if let Ok(val) = std::env::var("LEAN_CTX_DISABLED_TOOLS") {
733 Self::parse_disabled_tools_env(&val)
734 } else {
735 self.disabled_tools.clone()
736 }
737 }
738
739 pub fn minimal_overhead_effective(&self) -> bool {
741 std::env::var("LEAN_CTX_MINIMAL").is_ok() || self.minimal_overhead
742 }
743
744 pub fn minimal_overhead_effective_for_client(&self, client_name: &str) -> bool {
752 if let Ok(raw) = std::env::var("LEAN_CTX_OVERHEAD_MODE") {
753 match raw.trim().to_lowercase().as_str() {
754 "minimal" => return true,
755 "full" => return self.minimal_overhead_effective(),
756 _ => {}
757 }
758 }
759
760 if self.minimal_overhead_effective() {
761 return true;
762 }
763
764 let client_lower = client_name.trim().to_lowercase();
765 if !client_lower.is_empty() {
766 if let Ok(list) = std::env::var("LEAN_CTX_MINIMAL_CLIENTS") {
767 for needle in list.split(',').map(|s| s.trim().to_lowercase()) {
768 if !needle.is_empty() && client_lower.contains(&needle) {
769 return true;
770 }
771 }
772 } else if client_lower.contains("hermes") || client_lower.contains("minimax") {
773 return true;
774 }
775 }
776
777 let model = std::env::var("LEAN_CTX_MODEL")
778 .or_else(|_| std::env::var("LCTX_MODEL"))
779 .unwrap_or_default();
780 let model = model.trim().to_lowercase();
781 if !model.is_empty() {
782 let m = model.replace(['_', ' '], "-");
783 if m.contains("minimax")
784 || m.contains("mini-max")
785 || m.contains("m2.7")
786 || m.contains("m2-7")
787 {
788 return true;
789 }
790 }
791
792 false
793 }
794
795 pub fn shell_hook_disabled_effective(&self) -> bool {
797 std::env::var("LEAN_CTX_NO_HOOK").is_ok() || self.shell_hook_disabled
798 }
799
800 pub fn update_check_disabled_effective(&self) -> bool {
802 std::env::var("LEAN_CTX_NO_UPDATE_CHECK").is_ok() || self.update_check_disabled
803 }
804
805 pub fn memory_policy_effective(&self) -> Result<MemoryPolicy, String> {
806 let mut policy = self.memory.clone();
807 policy.apply_env_overrides();
808 policy.validate()?;
809 Ok(policy)
810 }
811}
812
813#[cfg(test)]
814mod disabled_tools_tests {
815 use super::*;
816
817 #[test]
818 fn config_field_default_is_empty() {
819 let cfg = Config::default();
820 assert!(cfg.disabled_tools.is_empty());
821 }
822
823 #[test]
824 fn effective_returns_config_field_when_no_env_var() {
825 if std::env::var("LEAN_CTX_DISABLED_TOOLS").is_ok() {
827 return;
828 }
829 let cfg = Config {
830 disabled_tools: vec!["ctx_graph".to_string(), "ctx_agent".to_string()],
831 ..Default::default()
832 };
833 assert_eq!(
834 cfg.disabled_tools_effective(),
835 vec!["ctx_graph", "ctx_agent"]
836 );
837 }
838
839 #[test]
840 fn parse_env_basic() {
841 let result = Config::parse_disabled_tools_env("ctx_graph,ctx_agent");
842 assert_eq!(result, vec!["ctx_graph", "ctx_agent"]);
843 }
844
845 #[test]
846 fn parse_env_trims_whitespace_and_skips_empty() {
847 let result = Config::parse_disabled_tools_env(" ctx_graph , , ctx_agent ");
848 assert_eq!(result, vec!["ctx_graph", "ctx_agent"]);
849 }
850
851 #[test]
852 fn parse_env_single_entry() {
853 let result = Config::parse_disabled_tools_env("ctx_graph");
854 assert_eq!(result, vec!["ctx_graph"]);
855 }
856
857 #[test]
858 fn parse_env_empty_string_returns_empty() {
859 let result = Config::parse_disabled_tools_env("");
860 assert!(result.is_empty());
861 }
862
863 #[test]
864 fn disabled_tools_deserialization_defaults_to_empty() {
865 let cfg: Config = toml::from_str("").unwrap();
866 assert!(cfg.disabled_tools.is_empty());
867 }
868
869 #[test]
870 fn disabled_tools_deserialization_from_toml() {
871 let cfg: Config = toml::from_str(r#"disabled_tools = ["ctx_graph", "ctx_agent"]"#).unwrap();
872 assert_eq!(cfg.disabled_tools, vec!["ctx_graph", "ctx_agent"]);
873 }
874}
875
876#[cfg(test)]
877mod rules_scope_tests {
878 use super::*;
879
880 #[test]
881 fn default_is_both() {
882 let cfg = Config::default();
883 assert_eq!(cfg.rules_scope_effective(), RulesScope::Both);
884 }
885
886 #[test]
887 fn config_global() {
888 let cfg = Config {
889 rules_scope: Some("global".to_string()),
890 ..Default::default()
891 };
892 assert_eq!(cfg.rules_scope_effective(), RulesScope::Global);
893 }
894
895 #[test]
896 fn config_project() {
897 let cfg = Config {
898 rules_scope: Some("project".to_string()),
899 ..Default::default()
900 };
901 assert_eq!(cfg.rules_scope_effective(), RulesScope::Project);
902 }
903
904 #[test]
905 fn unknown_value_falls_back_to_both() {
906 let cfg = Config {
907 rules_scope: Some("nonsense".to_string()),
908 ..Default::default()
909 };
910 assert_eq!(cfg.rules_scope_effective(), RulesScope::Both);
911 }
912
913 #[test]
914 fn deserialization_none_by_default() {
915 let cfg: Config = toml::from_str("").unwrap();
916 assert!(cfg.rules_scope.is_none());
917 assert_eq!(cfg.rules_scope_effective(), RulesScope::Both);
918 }
919
920 #[test]
921 fn deserialization_from_toml() {
922 let cfg: Config = toml::from_str(r#"rules_scope = "project""#).unwrap();
923 assert_eq!(cfg.rules_scope.as_deref(), Some("project"));
924 assert_eq!(cfg.rules_scope_effective(), RulesScope::Project);
925 }
926}
927
928#[cfg(test)]
929mod loop_detection_config_tests {
930 use super::*;
931
932 #[test]
933 fn defaults_are_reasonable() {
934 let cfg = LoopDetectionConfig::default();
935 assert_eq!(cfg.normal_threshold, 2);
936 assert_eq!(cfg.reduced_threshold, 4);
937 assert_eq!(cfg.blocked_threshold, 0);
939 assert_eq!(cfg.window_secs, 300);
940 assert_eq!(cfg.search_group_limit, 10);
941 }
942
943 #[test]
944 fn deserialization_defaults_when_missing() {
945 let cfg: Config = toml::from_str("").unwrap();
946 assert_eq!(cfg.loop_detection.blocked_threshold, 0);
948 assert_eq!(cfg.loop_detection.search_group_limit, 10);
949 }
950
951 #[test]
952 fn deserialization_from_toml() {
953 let cfg: Config = toml::from_str(
954 r"
955 [loop_detection]
956 normal_threshold = 1
957 reduced_threshold = 3
958 blocked_threshold = 5
959 window_secs = 120
960 search_group_limit = 8
961 ",
962 )
963 .unwrap();
964 assert_eq!(cfg.loop_detection.normal_threshold, 1);
965 assert_eq!(cfg.loop_detection.reduced_threshold, 3);
966 assert_eq!(cfg.loop_detection.blocked_threshold, 5);
967 assert_eq!(cfg.loop_detection.window_secs, 120);
968 assert_eq!(cfg.loop_detection.search_group_limit, 8);
969 }
970
971 #[test]
972 fn partial_override_keeps_defaults() {
973 let cfg: Config = toml::from_str(
974 r"
975 [loop_detection]
976 blocked_threshold = 10
977 ",
978 )
979 .unwrap();
980 assert_eq!(cfg.loop_detection.blocked_threshold, 10);
981 assert_eq!(cfg.loop_detection.normal_threshold, 2);
982 assert_eq!(cfg.loop_detection.search_group_limit, 10);
983 }
984}
985
986impl Config {
987 pub fn path() -> Option<PathBuf> {
989 crate::core::data_dir::lean_ctx_data_dir()
990 .ok()
991 .map(|d| d.join("config.toml"))
992 }
993
994 pub fn local_path(project_root: &str) -> PathBuf {
996 PathBuf::from(project_root).join(".lean-ctx.toml")
997 }
998
999 fn find_project_root() -> Option<String> {
1000 let cwd = std::env::current_dir().ok();
1001
1002 if let Some(root) =
1003 crate::core::session::SessionState::load_latest().and_then(|s| s.project_root)
1004 {
1005 let root_path = std::path::Path::new(&root);
1006 let cwd_is_under_root = cwd.as_ref().is_some_and(|c| c.starts_with(root_path));
1007 let has_marker = root_path.join(".git").exists()
1008 || root_path.join("Cargo.toml").exists()
1009 || root_path.join("package.json").exists()
1010 || root_path.join("go.mod").exists()
1011 || root_path.join("pyproject.toml").exists()
1012 || root_path.join(".lean-ctx.toml").exists();
1013
1014 if cwd_is_under_root || has_marker {
1015 return Some(root);
1016 }
1017 }
1018
1019 if let Some(ref cwd) = cwd {
1020 let git_root = std::process::Command::new("git")
1021 .args(["rev-parse", "--show-toplevel"])
1022 .current_dir(cwd)
1023 .stdout(std::process::Stdio::piped())
1024 .stderr(std::process::Stdio::null())
1025 .output()
1026 .ok()
1027 .and_then(|o| {
1028 if o.status.success() {
1029 String::from_utf8(o.stdout)
1030 .ok()
1031 .map(|s| s.trim().to_string())
1032 } else {
1033 None
1034 }
1035 });
1036 if let Some(root) = git_root {
1037 return Some(root);
1038 }
1039 return Some(cwd.to_string_lossy().to_string());
1040 }
1041 None
1042 }
1043
1044 pub fn load() -> Self {
1046 static CACHE: Mutex<Option<(Config, SystemTime, Option<SystemTime>)>> = Mutex::new(None);
1047
1048 let Some(path) = Self::path() else {
1049 return Self::default();
1050 };
1051
1052 let local_path = Self::find_project_root().map(|r| Self::local_path(&r));
1053
1054 let mtime = std::fs::metadata(&path)
1055 .and_then(|m| m.modified())
1056 .unwrap_or(SystemTime::UNIX_EPOCH);
1057
1058 let local_mtime = local_path
1059 .as_ref()
1060 .and_then(|p| std::fs::metadata(p).and_then(|m| m.modified()).ok());
1061
1062 if let Ok(guard) = CACHE.lock() {
1063 if let Some((ref cfg, ref cached_mtime, ref cached_local_mtime)) = *guard {
1064 if *cached_mtime == mtime && *cached_local_mtime == local_mtime {
1065 return cfg.clone();
1066 }
1067 }
1068 }
1069
1070 let mut cfg: Config = match std::fs::read_to_string(&path) {
1071 Ok(content) => toml::from_str(&content).unwrap_or_default(),
1072 Err(_) => Self::default(),
1073 };
1074
1075 if let Some(ref lp) = local_path {
1076 if let Ok(local_content) = std::fs::read_to_string(lp) {
1077 cfg.merge_local(&local_content);
1078 }
1079 }
1080
1081 if let Ok(mut guard) = CACHE.lock() {
1082 *guard = Some((cfg.clone(), mtime, local_mtime));
1083 }
1084
1085 cfg
1086 }
1087
1088 fn merge_local(&mut self, local_toml: &str) {
1089 let local: Config = match toml::from_str(local_toml) {
1090 Ok(c) => c,
1091 Err(_) => return,
1092 };
1093 if local.ultra_compact {
1094 self.ultra_compact = true;
1095 }
1096 if local.tee_mode != TeeMode::default() {
1097 self.tee_mode = local.tee_mode;
1098 }
1099 if local.output_density != OutputDensity::default() {
1100 self.output_density = local.output_density;
1101 }
1102 if local.checkpoint_interval != 15 {
1103 self.checkpoint_interval = local.checkpoint_interval;
1104 }
1105 if !local.excluded_commands.is_empty() {
1106 self.excluded_commands.extend(local.excluded_commands);
1107 }
1108 if !local.passthrough_urls.is_empty() {
1109 self.passthrough_urls.extend(local.passthrough_urls);
1110 }
1111 if !local.custom_aliases.is_empty() {
1112 self.custom_aliases.extend(local.custom_aliases);
1113 }
1114 if local.slow_command_threshold_ms != 5000 {
1115 self.slow_command_threshold_ms = local.slow_command_threshold_ms;
1116 }
1117 if local.theme != "default" {
1118 self.theme = local.theme;
1119 }
1120 if !local.buddy_enabled {
1121 self.buddy_enabled = false;
1122 }
1123 if !local.redirect_exclude.is_empty() {
1124 self.redirect_exclude.extend(local.redirect_exclude);
1125 }
1126 if !local.disabled_tools.is_empty() {
1127 self.disabled_tools.extend(local.disabled_tools);
1128 }
1129 if !local.extra_ignore_patterns.is_empty() {
1130 self.extra_ignore_patterns
1131 .extend(local.extra_ignore_patterns);
1132 }
1133 if local.rules_scope.is_some() {
1134 self.rules_scope = local.rules_scope;
1135 }
1136 if local.proxy.anthropic_upstream.is_some() {
1137 self.proxy.anthropic_upstream = local.proxy.anthropic_upstream;
1138 }
1139 if local.proxy.openai_upstream.is_some() {
1140 self.proxy.openai_upstream = local.proxy.openai_upstream;
1141 }
1142 if local.proxy.gemini_upstream.is_some() {
1143 self.proxy.gemini_upstream = local.proxy.gemini_upstream;
1144 }
1145 if !local.autonomy.enabled {
1146 self.autonomy.enabled = false;
1147 }
1148 if !local.autonomy.auto_preload {
1149 self.autonomy.auto_preload = false;
1150 }
1151 if !local.autonomy.auto_dedup {
1152 self.autonomy.auto_dedup = false;
1153 }
1154 if !local.autonomy.auto_related {
1155 self.autonomy.auto_related = false;
1156 }
1157 if !local.autonomy.auto_consolidate {
1158 self.autonomy.auto_consolidate = false;
1159 }
1160 if local.autonomy.silent_preload {
1161 self.autonomy.silent_preload = true;
1162 }
1163 if !local.autonomy.silent_preload && self.autonomy.silent_preload {
1164 self.autonomy.silent_preload = false;
1165 }
1166 if local.autonomy.dedup_threshold != AutonomyConfig::default().dedup_threshold {
1167 self.autonomy.dedup_threshold = local.autonomy.dedup_threshold;
1168 }
1169 if local.autonomy.consolidate_every_calls
1170 != AutonomyConfig::default().consolidate_every_calls
1171 {
1172 self.autonomy.consolidate_every_calls = local.autonomy.consolidate_every_calls;
1173 }
1174 if local.autonomy.consolidate_cooldown_secs
1175 != AutonomyConfig::default().consolidate_cooldown_secs
1176 {
1177 self.autonomy.consolidate_cooldown_secs = local.autonomy.consolidate_cooldown_secs;
1178 }
1179 if local.compression_level != CompressionLevel::default() {
1180 self.compression_level = local.compression_level;
1181 }
1182 if local.terse_agent != TerseAgent::default() {
1183 self.terse_agent = local.terse_agent;
1184 }
1185 if !local.archive.enabled {
1186 self.archive.enabled = false;
1187 }
1188 if local.archive.threshold_chars != ArchiveConfig::default().threshold_chars {
1189 self.archive.threshold_chars = local.archive.threshold_chars;
1190 }
1191 if local.archive.max_age_hours != ArchiveConfig::default().max_age_hours {
1192 self.archive.max_age_hours = local.archive.max_age_hours;
1193 }
1194 if local.archive.max_disk_mb != ArchiveConfig::default().max_disk_mb {
1195 self.archive.max_disk_mb = local.archive.max_disk_mb;
1196 }
1197 let mem_def = MemoryPolicy::default();
1198 if local.memory.knowledge.max_facts != mem_def.knowledge.max_facts {
1199 self.memory.knowledge.max_facts = local.memory.knowledge.max_facts;
1200 }
1201 if local.memory.knowledge.max_patterns != mem_def.knowledge.max_patterns {
1202 self.memory.knowledge.max_patterns = local.memory.knowledge.max_patterns;
1203 }
1204 if local.memory.knowledge.max_history != mem_def.knowledge.max_history {
1205 self.memory.knowledge.max_history = local.memory.knowledge.max_history;
1206 }
1207 if local.memory.knowledge.contradiction_threshold
1208 != mem_def.knowledge.contradiction_threshold
1209 {
1210 self.memory.knowledge.contradiction_threshold =
1211 local.memory.knowledge.contradiction_threshold;
1212 }
1213
1214 if local.memory.episodic.max_episodes != mem_def.episodic.max_episodes {
1215 self.memory.episodic.max_episodes = local.memory.episodic.max_episodes;
1216 }
1217 if local.memory.episodic.max_actions_per_episode != mem_def.episodic.max_actions_per_episode
1218 {
1219 self.memory.episodic.max_actions_per_episode =
1220 local.memory.episodic.max_actions_per_episode;
1221 }
1222 if local.memory.episodic.summary_max_chars != mem_def.episodic.summary_max_chars {
1223 self.memory.episodic.summary_max_chars = local.memory.episodic.summary_max_chars;
1224 }
1225
1226 if local.memory.procedural.min_repetitions != mem_def.procedural.min_repetitions {
1227 self.memory.procedural.min_repetitions = local.memory.procedural.min_repetitions;
1228 }
1229 if local.memory.procedural.min_sequence_len != mem_def.procedural.min_sequence_len {
1230 self.memory.procedural.min_sequence_len = local.memory.procedural.min_sequence_len;
1231 }
1232 if local.memory.procedural.max_procedures != mem_def.procedural.max_procedures {
1233 self.memory.procedural.max_procedures = local.memory.procedural.max_procedures;
1234 }
1235 if local.memory.procedural.max_window_size != mem_def.procedural.max_window_size {
1236 self.memory.procedural.max_window_size = local.memory.procedural.max_window_size;
1237 }
1238
1239 if local.memory.lifecycle.decay_rate != mem_def.lifecycle.decay_rate {
1240 self.memory.lifecycle.decay_rate = local.memory.lifecycle.decay_rate;
1241 }
1242 if local.memory.lifecycle.low_confidence_threshold
1243 != mem_def.lifecycle.low_confidence_threshold
1244 {
1245 self.memory.lifecycle.low_confidence_threshold =
1246 local.memory.lifecycle.low_confidence_threshold;
1247 }
1248 if local.memory.lifecycle.stale_days != mem_def.lifecycle.stale_days {
1249 self.memory.lifecycle.stale_days = local.memory.lifecycle.stale_days;
1250 }
1251 if local.memory.lifecycle.similarity_threshold != mem_def.lifecycle.similarity_threshold {
1252 self.memory.lifecycle.similarity_threshold =
1253 local.memory.lifecycle.similarity_threshold;
1254 }
1255
1256 if local.memory.embeddings.max_facts != mem_def.embeddings.max_facts {
1257 self.memory.embeddings.max_facts = local.memory.embeddings.max_facts;
1258 }
1259 if !local.allow_paths.is_empty() {
1260 self.allow_paths.extend(local.allow_paths);
1261 }
1262 if local.minimal_overhead {
1263 self.minimal_overhead = true;
1264 }
1265 if local.shell_hook_disabled {
1266 self.shell_hook_disabled = true;
1267 }
1268 if local.bm25_max_cache_mb != default_bm25_max_cache_mb() {
1269 self.bm25_max_cache_mb = local.bm25_max_cache_mb;
1270 }
1271 if local.memory_profile != MemoryProfile::default() {
1272 self.memory_profile = local.memory_profile;
1273 }
1274 if local.memory_cleanup != MemoryCleanup::default() {
1275 self.memory_cleanup = local.memory_cleanup;
1276 }
1277 }
1278
1279 pub fn save(&self) -> std::result::Result<(), super::error::LeanCtxError> {
1281 let path = Self::path().ok_or_else(|| {
1282 super::error::LeanCtxError::Config("cannot determine home directory".into())
1283 })?;
1284 if let Some(parent) = path.parent() {
1285 std::fs::create_dir_all(parent)?;
1286 }
1287 let content = toml::to_string_pretty(self)
1288 .map_err(|e| super::error::LeanCtxError::Config(e.to_string()))?;
1289 std::fs::write(&path, content)?;
1290 Ok(())
1291 }
1292
1293 pub fn show(&self) -> String {
1295 let global_path = Self::path().map_or_else(
1296 || "~/.lean-ctx/config.toml".to_string(),
1297 |p| p.to_string_lossy().to_string(),
1298 );
1299 let content = toml::to_string_pretty(self).unwrap_or_default();
1300 let mut out = format!("Global config: {global_path}\n\n{content}");
1301
1302 if let Some(root) = Self::find_project_root() {
1303 let local = Self::local_path(&root);
1304 if local.exists() {
1305 out.push_str(&format!("\n\nLocal config (merged): {}\n", local.display()));
1306 } else {
1307 out.push_str(&format!(
1308 "\n\nLocal config: not found (create {} to override per-project)\n",
1309 local.display()
1310 ));
1311 }
1312 }
1313 out
1314 }
1315}
1316
1317#[cfg(test)]
1318mod compression_level_tests {
1319 use super::*;
1320
1321 #[test]
1322 fn default_is_off() {
1323 assert_eq!(CompressionLevel::default(), CompressionLevel::Off);
1324 }
1325
1326 #[test]
1327 fn to_components_off() {
1328 let (ta, od, crp, tm) = CompressionLevel::Off.to_components();
1329 assert_eq!(ta, TerseAgent::Off);
1330 assert_eq!(od, OutputDensity::Normal);
1331 assert_eq!(crp, "off");
1332 assert!(!tm);
1333 }
1334
1335 #[test]
1336 fn to_components_lite() {
1337 let (ta, od, crp, tm) = CompressionLevel::Lite.to_components();
1338 assert_eq!(ta, TerseAgent::Lite);
1339 assert_eq!(od, OutputDensity::Terse);
1340 assert_eq!(crp, "off");
1341 assert!(tm);
1342 }
1343
1344 #[test]
1345 fn to_components_standard() {
1346 let (ta, od, crp, tm) = CompressionLevel::Standard.to_components();
1347 assert_eq!(ta, TerseAgent::Full);
1348 assert_eq!(od, OutputDensity::Terse);
1349 assert_eq!(crp, "compact");
1350 assert!(tm);
1351 }
1352
1353 #[test]
1354 fn to_components_max() {
1355 let (ta, od, crp, tm) = CompressionLevel::Max.to_components();
1356 assert_eq!(ta, TerseAgent::Ultra);
1357 assert_eq!(od, OutputDensity::Ultra);
1358 assert_eq!(crp, "tdd");
1359 assert!(tm);
1360 }
1361
1362 #[test]
1363 fn from_legacy_ultra_agent_maps_to_max() {
1364 assert_eq!(
1365 CompressionLevel::from_legacy(&TerseAgent::Ultra, &OutputDensity::Normal),
1366 CompressionLevel::Max
1367 );
1368 }
1369
1370 #[test]
1371 fn from_legacy_ultra_density_maps_to_max() {
1372 assert_eq!(
1373 CompressionLevel::from_legacy(&TerseAgent::Off, &OutputDensity::Ultra),
1374 CompressionLevel::Max
1375 );
1376 }
1377
1378 #[test]
1379 fn from_legacy_full_agent_maps_to_standard() {
1380 assert_eq!(
1381 CompressionLevel::from_legacy(&TerseAgent::Full, &OutputDensity::Normal),
1382 CompressionLevel::Standard
1383 );
1384 }
1385
1386 #[test]
1387 fn from_legacy_lite_agent_maps_to_lite() {
1388 assert_eq!(
1389 CompressionLevel::from_legacy(&TerseAgent::Lite, &OutputDensity::Normal),
1390 CompressionLevel::Lite
1391 );
1392 }
1393
1394 #[test]
1395 fn from_legacy_terse_density_maps_to_lite() {
1396 assert_eq!(
1397 CompressionLevel::from_legacy(&TerseAgent::Off, &OutputDensity::Terse),
1398 CompressionLevel::Lite
1399 );
1400 }
1401
1402 #[test]
1403 fn from_legacy_both_off_maps_to_off() {
1404 assert_eq!(
1405 CompressionLevel::from_legacy(&TerseAgent::Off, &OutputDensity::Normal),
1406 CompressionLevel::Off
1407 );
1408 }
1409
1410 #[test]
1411 fn labels_match() {
1412 assert_eq!(CompressionLevel::Off.label(), "off");
1413 assert_eq!(CompressionLevel::Lite.label(), "lite");
1414 assert_eq!(CompressionLevel::Standard.label(), "standard");
1415 assert_eq!(CompressionLevel::Max.label(), "max");
1416 }
1417
1418 #[test]
1419 fn is_active_false_for_off() {
1420 assert!(!CompressionLevel::Off.is_active());
1421 }
1422
1423 #[test]
1424 fn is_active_true_for_all_others() {
1425 assert!(CompressionLevel::Lite.is_active());
1426 assert!(CompressionLevel::Standard.is_active());
1427 assert!(CompressionLevel::Max.is_active());
1428 }
1429
1430 #[test]
1431 fn deserialization_defaults_to_off() {
1432 let cfg: Config = toml::from_str("").unwrap();
1433 assert_eq!(cfg.compression_level, CompressionLevel::Off);
1434 }
1435
1436 #[test]
1437 fn deserialization_from_toml() {
1438 let cfg: Config = toml::from_str(r#"compression_level = "standard""#).unwrap();
1439 assert_eq!(cfg.compression_level, CompressionLevel::Standard);
1440 }
1441
1442 #[test]
1443 fn roundtrip_all_levels() {
1444 for level in [
1445 CompressionLevel::Off,
1446 CompressionLevel::Lite,
1447 CompressionLevel::Standard,
1448 CompressionLevel::Max,
1449 ] {
1450 let (ta, od, crp, tm) = level.to_components();
1451 assert!(!crp.is_empty());
1452 if level == CompressionLevel::Off {
1453 assert!(!tm);
1454 assert_eq!(ta, TerseAgent::Off);
1455 assert_eq!(od, OutputDensity::Normal);
1456 } else {
1457 assert!(tm);
1458 }
1459 }
1460 }
1461}
1462
1463#[cfg(test)]
1464mod memory_cleanup_tests {
1465 use super::*;
1466
1467 #[test]
1468 fn default_is_aggressive() {
1469 assert_eq!(MemoryCleanup::default(), MemoryCleanup::Aggressive);
1470 }
1471
1472 #[test]
1473 fn aggressive_ttl_is_300() {
1474 assert_eq!(MemoryCleanup::Aggressive.idle_ttl_secs(), 300);
1475 }
1476
1477 #[test]
1478 fn shared_ttl_is_1800() {
1479 assert_eq!(MemoryCleanup::Shared.idle_ttl_secs(), 1800);
1480 }
1481
1482 #[test]
1483 fn index_retention_multiplier_values() {
1484 assert!(
1485 (MemoryCleanup::Aggressive.index_retention_multiplier() - 1.0).abs() < f64::EPSILON
1486 );
1487 assert!((MemoryCleanup::Shared.index_retention_multiplier() - 3.0).abs() < f64::EPSILON);
1488 }
1489
1490 #[test]
1491 fn deserialization_defaults_to_aggressive() {
1492 let cfg: Config = toml::from_str("").unwrap();
1493 assert_eq!(cfg.memory_cleanup, MemoryCleanup::Aggressive);
1494 }
1495
1496 #[test]
1497 fn deserialization_from_toml() {
1498 let cfg: Config = toml::from_str(r#"memory_cleanup = "shared""#).unwrap();
1499 assert_eq!(cfg.memory_cleanup, MemoryCleanup::Shared);
1500 }
1501
1502 #[test]
1503 fn effective_uses_config_when_no_env() {
1504 let cfg = Config {
1505 memory_cleanup: MemoryCleanup::Shared,
1506 ..Default::default()
1507 };
1508 let eff = MemoryCleanup::effective(&cfg);
1509 assert_eq!(eff, MemoryCleanup::Shared);
1510 }
1511}