1mod checks;
4mod common;
5mod deprecations;
6mod fix;
7mod integrations;
8mod migrate;
9mod overhead;
10mod workspace_scope;
11
12#[allow(clippy::wildcard_imports)]
13use checks::*;
14#[allow(clippy::wildcard_imports)]
15use common::*;
16
17pub(super) const GREEN: &str = "\x1b[32m";
18
19pub(super) const RED: &str = "\x1b[31m";
20
21pub(super) const BOLD: &str = "\x1b[1m";
22
23pub(super) const RST: &str = "\x1b[0m";
24
25pub(super) const DIM: &str = "\x1b[2m";
26
27pub(super) const WHITE: &str = "\x1b[97m";
28
29pub(super) const YELLOW: &str = "\x1b[33m";
30
31pub(super) struct Outcome {
32 pub ok: bool,
33 pub line: String,
34}
35
36pub fn run() {
38 let mut passed = 0u32;
39 let total = 10u32;
40
41 println!("{BOLD}{WHITE}lean-ctx doctor{RST} {DIM}diagnostics{RST}\n");
42
43 let path_bin = resolve_lean_ctx_binary();
45 let also_in_path_dirs = path_in_path_env();
46 let bin_ok = path_bin.is_some() || also_in_path_dirs;
47 if bin_ok {
48 passed += 1;
49 }
50 let bin_line = if let Some(p) = path_bin {
51 format!("{BOLD}lean-ctx in PATH{RST} {WHITE}{}{RST}", p.display())
52 } else if also_in_path_dirs {
53 format!(
54 "{BOLD}lean-ctx in PATH{RST} {YELLOW}found via PATH walk (not resolved by `command -v`){RST}"
55 )
56 } else {
57 format!("{BOLD}lean-ctx in PATH{RST} {RED}not found{RST}")
58 };
59 print_check(&Outcome {
60 ok: bin_ok,
61 line: bin_line,
62 });
63
64 let ver = if bin_ok {
66 lean_ctx_version_from_path()
67 } else {
68 Outcome {
69 ok: false,
70 line: format!("{BOLD}lean-ctx version{RST} {RED}skipped (binary not in PATH){RST}"),
71 }
72 };
73 if ver.ok {
74 passed += 1;
75 }
76 print_check(&ver);
77
78 let lean_dir = crate::core::data_dir::lean_ctx_data_dir().ok();
80 let dir_outcome = match &lean_dir {
81 Some(p) if p.is_dir() => {
82 passed += 1;
83 Outcome {
84 ok: true,
85 line: format!(
86 "{BOLD}data dir{RST} {GREEN}exists{RST} {DIM}{}{RST}",
87 p.display()
88 ),
89 }
90 }
91 Some(p) => Outcome {
92 ok: false,
93 line: format!(
94 "{BOLD}data dir{RST} {RED}missing or not a directory{RST} {DIM}{}{RST}",
95 p.display()
96 ),
97 },
98 None => Outcome {
99 ok: false,
100 line: format!("{BOLD}data dir{RST} {RED}could not resolve data directory{RST}"),
101 },
102 };
103 print_check(&dir_outcome);
104
105 let stats_path = lean_dir.as_ref().map(|d| d.join("stats.json"));
107 let stats_outcome = match stats_path.as_ref().and_then(|p| std::fs::metadata(p).ok()) {
108 Some(m) if m.is_file() => {
109 passed += 1;
110 let size = m.len();
111 let path_display = if let Some(p) = stats_path.as_ref() {
112 p.display().to_string()
113 } else {
114 String::new()
115 };
116 Outcome {
117 ok: true,
118 line: format!(
119 "{BOLD}stats.json{RST} {GREEN}exists{RST} {WHITE}{size} bytes{RST} {DIM}{path_display}{RST}",
120 ),
121 }
122 }
123 Some(_m) => {
124 let path_display = if let Some(p) = stats_path.as_ref() {
125 p.display().to_string()
126 } else {
127 String::new()
128 };
129 Outcome {
130 ok: false,
131 line: format!(
132 "{BOLD}stats.json{RST} {RED}not a file{RST} {DIM}{path_display}{RST}",
133 ),
134 }
135 }
136 None => {
137 passed += 1;
138 Outcome {
139 ok: true,
140 line: match &stats_path {
141 Some(p) => format!(
142 "{BOLD}stats.json{RST} {YELLOW}not yet created{RST} {DIM}(will appear after first use) {}{RST}",
143 p.display()
144 ),
145 None => format!("{BOLD}stats.json{RST} {RED}could not resolve path{RST}"),
146 },
147 }
148 }
149 };
150 print_check(&stats_outcome);
151
152 let split_dirs = crate::core::data_dir::all_data_dirs_with_stats();
153 if split_dirs.len() >= 2 {
154 let dirs_str = split_dirs
155 .iter()
156 .map(|d| d.display().to_string())
157 .collect::<Vec<_>>()
158 .join(", ");
159 print_check(&Outcome {
160 ok: false,
161 line: format!(
162 "{BOLD}data dir split{RST} {RED}stats.json found in {count} locations{RST}: {dirs_str} {DIM}(run: lean-ctx doctor --fix to merge){RST}",
163 count = split_dirs.len(),
164 ),
165 });
166 }
167
168 if let Some((src, n)) = crate::core::xdg_migrate::pending() {
172 print_check(&Outcome {
173 ok: false,
174 line: format!(
175 "{BOLD}XDG layout{RST} {YELLOW}{n} item(s) in single dir{RST} {DIM}{}{RST} {DIM}(run: lean-ctx doctor --fix to split into config/data/state/cache){RST}",
176 src.display()
177 ),
178 });
179 }
180
181 let config_path = lean_dir.as_ref().map(|d| d.join("config.toml"));
183 let config_outcome = match &config_path {
184 Some(p) => match std::fs::metadata(p) {
185 Ok(m) if m.is_file() => {
186 passed += 1;
187 Outcome {
188 ok: true,
189 line: format!(
190 "{BOLD}config.toml{RST} {GREEN}exists{RST} {DIM}{}{RST}",
191 p.display()
192 ),
193 }
194 }
195 Ok(_) => Outcome {
196 ok: false,
197 line: format!(
198 "{BOLD}config.toml{RST} {RED}exists but is not a regular file{RST} {DIM}{}{RST}",
199 p.display()
200 ),
201 },
202 Err(_) => {
203 passed += 1;
204 Outcome {
205 ok: true,
206 line: format!(
207 "{BOLD}config.toml{RST} {YELLOW}not found, using defaults{RST} {DIM}(expected at {}){RST}",
208 p.display()
209 ),
210 }
211 }
212 },
213 None => Outcome {
214 ok: false,
215 line: format!("{BOLD}config.toml{RST} {RED}could not resolve path{RST}"),
216 },
217 };
218 print_check(&config_outcome);
219
220 let allowlist_outcome = shell_allowlist_outcome();
222 if allowlist_outcome.ok {
223 passed += 1;
224 }
225 print_check(&allowlist_outcome);
226
227 let path_jail = path_jail_outcome();
229 if path_jail.ok {
230 passed += 1;
231 }
232 print_check(&path_jail);
233
234 let passthrough_outcome = compact_format_passthrough_outcome();
236 if passthrough_outcome.ok {
237 passed += 1;
238 }
239 print_check(&passthrough_outcome);
240
241 let perm_inherit_outcome = permission_inheritance_outcome();
243 if perm_inherit_outcome.ok {
244 passed += 1;
245 }
246 print_check(&perm_inherit_outcome);
247
248 let proxy_outcome = proxy_upstream_outcome();
250 if proxy_outcome.ok {
251 passed += 1;
252 }
253 print_check(&proxy_outcome);
254
255 let aliases = shell_aliases_outcome();
257 if aliases.ok {
258 passed += 1;
259 }
260 print_check(&aliases);
261
262 let mcp = mcp_config_outcome();
264 if mcp.ok {
265 passed += 1;
266 }
267 print_check(&mcp);
268
269 let workspace_scope = workspace_scope::workspace_scope_outcome(mcp.ok);
271 if let Some(ref ws) = workspace_scope {
272 if ws.ok {
273 passed += 1;
274 }
275 print_check(ws);
276 }
277
278 let skill = skill_files_outcome();
280 if skill.ok {
281 passed += 1;
282 }
283 print_check(&skill);
284
285 let port = port_3333_outcome();
287 if port.ok {
288 passed += 1;
289 }
290 print_check(&port);
291
292 #[cfg(unix)]
294 let daemon_outcome = {
295 let autostart = crate::daemon_autostart::is_installed();
296 let autostart_tag = if autostart {
299 match crate::daemon_autostart::service_file_path() {
300 Some(p) => format!(" {DIM}[autostart: on — {}]{RST}", p.display()),
301 None => format!(" {DIM}[autostart: on]{RST}"),
302 }
303 } else {
304 String::new()
305 };
306 if crate::daemon::is_daemon_running() {
307 let pid_path = crate::daemon::daemon_pid_path();
308 let pid_str = std::fs::read_to_string(&pid_path).unwrap_or_default();
309 Outcome {
310 ok: true,
311 line: format!(
312 "{BOLD}Daemon{RST} {GREEN}running (PID {}){RST}{autostart_tag}",
313 pid_str.trim()
314 ),
315 }
316 } else {
317 let hint = if autostart {
318 format!("{DIM}(autostart enabled, will restart){RST}")
319 } else {
320 format!("{DIM}(run: lean-ctx daemon start or: lean-ctx daemon enable){RST}")
321 };
322 Outcome {
323 ok: true,
324 line: format!("{BOLD}Daemon{RST} {YELLOW}not running{RST} {hint}"),
325 }
326 }
327 };
328 #[cfg(not(unix))]
329 let daemon_outcome = Outcome {
330 ok: true,
331 line: format!("{BOLD}Daemon{RST} {DIM}not supported on this platform{RST}"),
332 };
333 if daemon_outcome.ok {
334 passed += 1;
335 }
336 print_check(&daemon_outcome);
337
338 #[cfg(target_os = "linux")]
340 {
341 if let Ok(o) = std::process::Command::new("systemctl")
342 .args(["--user", "is-active", "lean-ctx-daemon.service"])
343 .output()
344 {
345 let state = String::from_utf8_lossy(&o.stdout).trim().to_string();
346 if state != "active" {
347 println!(
348 " {DIM} systemd unit state: {YELLOW}{state}{RST}{DIM} (expected: active){RST}"
349 );
350 }
351 }
352 let username = std::env::var("USER")
353 .or_else(|_| std::env::var("LOGNAME"))
354 .unwrap_or_else(|_| "$(whoami)".to_string());
355 if let Ok(o) = std::process::Command::new("loginctl")
356 .args(["show-user", &username, "-p", "Linger", "--value"])
357 .output()
358 {
359 let val = String::from_utf8_lossy(&o.stdout).trim().to_string();
360 if val != "yes" {
361 println!(
362 " {YELLOW}⚠{RST} Linger not enabled — daemon won't start at boot without login"
363 );
364 println!(" {DIM}Fix: loginctl enable-linger {username}{RST}");
365 }
366 }
367 }
368 if let Some(log_path) = crate::core::startup_guard::crash_loop_log_path(
369 crate::core::startup_guard::MCP_PROCESS_NAME,
370 ) {
371 if log_path.exists() {
372 if let Ok(contents) = std::fs::read_to_string(&log_path) {
373 let lines: Vec<&str> = contents.lines().collect();
374 if lines.len() >= 5 {
375 println!(
376 " {YELLOW}⚠{RST} Crash-loop log: {} recent restarts {DIM}({}){RST}",
377 lines.len(),
378 log_path.display()
379 );
380 }
381 }
382 }
383 }
384
385 let provider_outcome = provider_outcome();
387 print_check(&provider_outcome);
388
389 let bridge_outcomes = mcp_bridge_outcomes();
391 for bridge_check in &bridge_outcomes {
392 print_check(bridge_check);
393 }
394
395 let plan_outcomes = plan_mode_outcomes();
397 for plan_check in &plan_outcomes {
398 print_check(plan_check);
399 }
400
401 let session_outcome = session_state_outcome();
403 if session_outcome.ok {
404 passed += 1;
405 }
406 print_check(&session_outcome);
407
408 let docker_outcomes = docker_env_outcomes();
410 for docker_check in &docker_outcomes {
411 if docker_check.ok {
412 passed += 1;
413 }
414 print_check(docker_check);
415 }
416
417 let pi = pi_outcome();
419 if let Some(ref pi_check) = pi {
420 if pi_check.ok {
421 passed += 1;
422 }
423 print_check(pi_check);
424 }
425
426 let integrity = crate::core::integrity::check();
428 let integrity_ok = integrity.seed_ok && integrity.origin_ok;
429 if integrity_ok {
430 passed += 1;
431 }
432 let integrity_line = if integrity_ok {
433 format!(
434 "{BOLD}Build origin{RST} {GREEN}official{RST} {DIM}{}{RST}",
435 integrity.repo
436 )
437 } else {
438 format!(
439 "{BOLD}Build origin{RST} {RED}MODIFIED REDISTRIBUTION{RST} {YELLOW}pkg={}, repo={}{RST}",
440 integrity.pkg_name, integrity.repo
441 )
442 };
443 print_check(&Outcome {
444 ok: integrity_ok,
445 line: integrity_line,
446 });
447
448 let cache_safety = cache_safety_outcome();
450 if cache_safety.ok {
451 passed += 1;
452 }
453 print_check(&cache_safety);
454
455 let claude_truncation = claude_truncation_outcome();
457 if let Some(ref ct) = claude_truncation {
458 if ct.ok {
459 passed += 1;
460 }
461 print_check(ct);
462 }
463
464 let codebuddy_truncation = codebuddy_truncation_outcome();
466 if let Some(ref cbt) = codebuddy_truncation {
467 if cbt.ok {
468 passed += 1;
469 }
470 print_check(cbt);
471 }
472
473 let bm25_health = bm25_cache_health_outcome();
475 if bm25_health.ok {
476 passed += 1;
477 }
478 print_check(&bm25_health);
479
480 let semantic_index = semantic_index_outcome();
483 if let Some(ref check) = semantic_index {
484 if check.ok {
485 passed += 1;
486 }
487 print_check(check);
488 }
489
490 let archive_footprint = archive_footprint_outcome();
492 if archive_footprint.ok {
493 passed += 1;
494 }
495 print_check(&archive_footprint);
496
497 let mem_profile = memory_profile_outcome();
499 passed += 1;
500 print_check(&mem_profile);
501
502 let mem_cleanup = memory_cleanup_outcome();
504 passed += 1;
505 print_check(&mem_cleanup);
506
507 let ram_outcome = ram_guardian_outcome();
509 if ram_outcome.ok {
510 passed += 1;
511 }
512 print_check(&ram_outcome);
513
514 let cap_warnings = capacity_warnings();
516 for cw in &cap_warnings {
517 if cw.ok {
518 passed += 1;
519 }
520 print_check(cw);
521 }
522
523 let orphan_outcome = orphaned_knowledge_outcome();
525 if orphan_outcome.ok {
526 passed += 1;
527 }
528 print_check(&orphan_outcome);
529
530 let proxy_health = proxy_health_outcome();
532 if proxy_health.ok {
533 passed += 1;
534 }
535 print_check(&proxy_health);
536
537 let stale_env = stale_proxy_env_outcome();
539 if let Some(ref check) = stale_env {
540 if check.ok {
541 passed += 1;
542 }
543 print_check(check);
544 }
545
546 let subscription_conflict = proxy_subscription_conflict_outcome();
548 if let Some(ref check) = subscription_conflict {
549 if check.ok {
550 passed += 1;
551 }
552 print_check(check);
553 }
554
555 let deprecation_check = deprecations::deprecations_outcome();
558 if deprecation_check.ok {
559 passed += 1;
560 }
561 print_check(&deprecation_check);
562
563 println!("\n {BOLD}{WHITE}LSP (optional — for ctx_refactor):{RST}");
565 let lsp_outcomes = lsp_server_outcomes();
566 for lsp_check in &lsp_outcomes {
567 print_check(lsp_check);
568 }
569
570 let mut effective_total = total + 10; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += cap_warnings.len() as u32;
578 effective_total += docker_outcomes.len() as u32;
579 if pi.is_some() {
580 effective_total += 1;
581 }
582 if claude_truncation.is_some() {
583 effective_total += 1;
584 }
585 if stale_env.is_some() {
586 effective_total += 1;
587 }
588 if subscription_conflict.is_some() {
589 effective_total += 1;
590 }
591 if workspace_scope.is_some() {
592 effective_total += 1;
593 }
594 if semantic_index.is_some() {
595 effective_total += 1;
596 }
597 let cfg = crate::core::config::Config::load();
599 let shadow_line = if cfg.shadow_mode {
600 format!("{BOLD}Shadow mode{RST} {GREEN}active{RST} {DIM}(native tools intercepted → ctx_*){RST}")
601 } else {
602 format!("{BOLD}Shadow mode{RST} {DIM}disabled{RST} {DIM}(enable: lean-ctx config set shadow_mode true){RST}")
603 };
604 println!(" {shadow_line}");
605
606 let tool_profile_line = if crate::server::tool_visibility::explicit_profile(&cfg) {
612 let profile = crate::core::tool_profiles::ToolProfile::from_config(&cfg);
613 format!(
614 "{BOLD}Tool profile{RST} {WHITE}{profile}{RST} {DIM}{} + ctx_call gateway{RST}",
615 profile.description()
616 )
617 } else {
618 let lazy_count = crate::tool_defs::core_tool_names().len();
619 format!(
620 "{BOLD}Tool profile{RST} {WHITE}lean (default){RST} {DIM}{lazy_count} lazy-core tools advertised + ctx_call gateway{RST}"
621 )
622 };
623 println!(" {tool_profile_line}");
624
625 let cep = &crate::core::stats::load().cep;
629 let hit_ratio = if cep.total_cache_reads > 0 {
630 (cep.total_cache_hits as f64 / cep.total_cache_reads as f64) * 100.0
631 } else {
632 0.0
633 };
634 println!(
635 " {BOLD}Session cache{RST} {WHITE}{} sessions{RST} {DIM}{}/{} reads cached ({hit_ratio:.0}% hit) · prove: lean-ctx verify-cache{RST}",
636 cep.sessions, cep.total_cache_hits, cep.total_cache_reads
637 );
638
639 let needs_attention = effective_total.saturating_sub(passed);
640 println!();
641 println!(" {BOLD}{WHITE}Summary:{RST} {GREEN}{passed}{RST}{DIM}/{effective_total}{RST} checks passed");
642 if needs_attention > 0 {
643 println!(
644 " {YELLOW}{needs_attention} check(s) need attention.{RST} Auto-repair what's fixable: {BOLD}lean-ctx doctor --fix{RST}"
645 );
646 } else {
647 println!(" {GREEN}Everything looks good.{RST}");
648 }
649 println!(" {DIM}LSP servers are optional enhancements (not counted in score){RST}");
650 println!(" {DIM}{}{RST}", crate::core::integrity::origin_line());
651}
652
653pub fn run_compact() {
654 let (passed, total) = compact_score();
655 print_compact_status(passed, total);
656}
657
658pub fn run_cli(args: &[String]) -> i32 {
659 let (sub, rest) = match args.first().map(String::as_str) {
660 Some("integrations") => ("integrations", &args[1..]),
661 Some("overhead") => ("overhead", &args[1..]),
662 _ => ("", args),
663 };
664
665 let fix = rest.iter().any(|a| a == "--fix");
666 let json = rest.iter().any(|a| a == "--json");
667 let migrate_check = rest.iter().any(|a| a == "--migrate-check");
668 let help = rest.iter().any(|a| a == "--help" || a == "-h");
669
670 if help {
671 println!("Usage:");
672 println!(" lean-ctx doctor");
673 println!(" lean-ctx doctor overhead [--json] Fixed context cost per session");
674 println!(" lean-ctx doctor integrations [--json]");
675 println!(" lean-ctx doctor --fix [--json]");
676 println!(" lean-ctx doctor --migrate-check [--json]");
677 return 0;
678 }
679
680 if sub == "overhead" {
681 return overhead::run_overhead(json);
682 }
683
684 if migrate_check {
685 return migrate::run_migrate_check(json);
686 }
687
688 if sub == "integrations" {
689 if fix {
690 let _ = fix::run_fix(&fix::DoctorFixOptions { json: false });
691 }
692 return integrations::run_integrations(&integrations::IntegrationsOptions { json });
693 }
694
695 if !fix {
696 run();
697 return 0;
698 }
699
700 match fix::run_fix(&fix::DoctorFixOptions { json }) {
701 Ok(code) => code,
702 Err(e) => {
703 tracing::error!("doctor --fix failed: {e}");
704 2
705 }
706 }
707}
708
709pub fn compact_score() -> (u32, u32) {
710 let mut passed = 0u32;
711 let total = 6u32;
712
713 if resolve_lean_ctx_binary().is_some() || path_in_path_env() {
714 passed += 1;
715 }
716 let lean_dir = crate::core::data_dir::lean_ctx_data_dir().ok();
717 if lean_dir.as_ref().is_some_and(|p| p.is_dir()) {
718 passed += 1;
719 }
720 if lean_dir
721 .as_ref()
722 .map(|d| d.join("stats.json"))
723 .and_then(|p| std::fs::metadata(p).ok())
724 .is_some_and(|m| m.is_file())
725 {
726 passed += 1;
727 }
728 if shell_aliases_outcome().ok {
729 passed += 1;
730 }
731 if mcp_config_outcome().ok {
732 passed += 1;
733 }
734 if skill_files_outcome().ok {
735 passed += 1;
736 }
737
738 (passed, total)
739}
740
741pub(super) fn print_compact_status(passed: u32, total: u32) {
742 let status = if passed == total {
743 format!("{GREEN}✓ All {total} checks passed{RST}")
744 } else {
745 format!("{YELLOW}{passed}/{total} passed{RST} — run {BOLD}lean-ctx doctor{RST} for details")
746 };
747 println!(" {status}");
748}
749
750#[cfg(test)]
751mod tests {
752 use super::is_active_shell_impl;
753
754 fn make_capacity_check(name: &str, current: usize, limit: usize) -> Option<(bool, String)> {
758 if limit == 0 {
759 return None;
760 }
761 let pct = (current as f64 / limit as f64 * 100.0) as u32;
762 if pct > 100 {
763 Some((true, format!("{name}: {current}/{limit} ({pct}%)")))
764 } else if pct >= 80 {
765 Some((false, format!("{name}: {current}/{limit} ({pct}%)")))
766 } else {
767 None
768 }
769 }
770
771 #[test]
772 fn capacity_below_80_no_warning() {
773 assert!(make_capacity_check("facts", 100, 200).is_none());
774 assert!(make_capacity_check("facts", 159, 200).is_none());
775 }
776
777 #[test]
778 fn capacity_at_80_yellow_warning() {
779 let result = make_capacity_check("facts", 160, 200);
780 assert!(result.is_some());
781 let (critical, msg) = result.unwrap();
782 assert!(!critical);
783 assert!(msg.contains("160/200"));
784 assert!(msg.contains("80%"));
785 }
786
787 #[test]
788 fn capacity_at_92_yellow_warning() {
789 let result = make_capacity_check("facts", 185, 200);
790 assert!(result.is_some());
791 let (critical, msg) = result.unwrap();
792 assert!(!critical);
793 assert!(msg.contains("185/200"));
794 assert!(msg.contains("92%"));
795 }
796
797 #[test]
798 fn capacity_at_95_is_warning_not_critical() {
799 let result = make_capacity_check("facts", 190, 200);
800 assert!(result.is_some());
801 let (critical, msg) = result.unwrap();
802 assert!(!critical, "95% is full-but-healthy, not over cap");
803 assert!(msg.contains("190/200"));
804 assert!(msg.contains("95%"));
805 }
806
807 #[test]
808 fn capacity_at_100_is_warning_not_critical() {
809 let result = make_capacity_check("facts", 200, 200);
811 assert!(result.is_some());
812 let (critical, _) = result.unwrap();
813 assert!(!critical);
814 }
815
816 #[test]
817 fn capacity_over_100_is_critical() {
818 let result = make_capacity_check("facts", 206, 200);
821 assert!(result.is_some());
822 let (critical, msg) = result.unwrap();
823 assert!(critical);
824 assert!(msg.contains("206/200"));
825 assert!(msg.contains("103%"));
826 }
827
828 #[test]
829 fn capacity_zero_limit_skipped() {
830 assert!(make_capacity_check("facts", 50, 0).is_none());
831 }
832
833 #[test]
834 fn bashrc_active_on_non_windows_when_shell_empty() {
835 assert!(is_active_shell_impl("~/.bashrc", "", false, false));
836 }
837
838 #[test]
839 fn bashrc_not_active_on_windows_when_shell_empty() {
840 assert!(!is_active_shell_impl("~/.bashrc", "", true, false));
841 }
842
843 #[test]
844 fn bashrc_active_when_shell_contains_bash_on_linux() {
845 assert!(is_active_shell_impl(
846 "~/.bashrc",
847 "/usr/bin/bash",
848 false,
849 false
850 ));
851 }
852
853 #[test]
854 fn bashrc_not_active_on_windows_even_with_bash_in_shell_env() {
855 std::env::remove_var("BASH_VERSION");
858 assert!(!is_active_shell_impl(
859 "~/.bashrc",
860 "C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe",
861 true,
862 false,
863 ));
864 }
865
866 #[test]
867 fn bashrc_not_active_on_windows_powershell_even_with_bash_in_shell() {
868 assert!(!is_active_shell_impl(
869 "~/.bashrc",
870 "C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe",
871 true,
872 true,
873 ));
874 }
875
876 #[test]
877 fn bashrc_not_active_on_windows_powershell_with_empty_shell() {
878 assert!(!is_active_shell_impl("~/.bashrc", "", true, true));
879 }
880
881 #[test]
882 fn zshrc_unaffected_by_powershell_flag() {
883 assert!(is_active_shell_impl("~/.zshrc", "/bin/zsh", false, false));
884 assert!(is_active_shell_impl("~/.zshrc", "/bin/zsh", true, true));
885 }
886
887 #[test]
888 fn bashrc_not_active_on_windows_without_powershell_detection() {
889 std::env::remove_var("BASH_VERSION");
892 assert!(!is_active_shell_impl(
893 "~/.bashrc",
894 "/usr/bin/bash",
895 true,
896 false,
897 ));
898 }
899
900 #[test]
901 fn bashrc_active_on_linux() {
902 assert!(is_active_shell_impl("~/.bashrc", "/bin/bash", false, false));
903 assert!(is_active_shell_impl("~/.bashrc", "", false, false));
904 }
905}