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