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