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 setup to auto-merge){RST}",
163 count = split_dirs.len(),
164 ),
165 });
166 }
167
168 let config_path = lean_dir.as_ref().map(|d| d.join("config.toml"));
170 let config_outcome = match &config_path {
171 Some(p) => match std::fs::metadata(p) {
172 Ok(m) if m.is_file() => {
173 passed += 1;
174 Outcome {
175 ok: true,
176 line: format!(
177 "{BOLD}config.toml{RST} {GREEN}exists{RST} {DIM}{}{RST}",
178 p.display()
179 ),
180 }
181 }
182 Ok(_) => Outcome {
183 ok: false,
184 line: format!(
185 "{BOLD}config.toml{RST} {RED}exists but is not a regular file{RST} {DIM}{}{RST}",
186 p.display()
187 ),
188 },
189 Err(_) => {
190 passed += 1;
191 Outcome {
192 ok: true,
193 line: format!(
194 "{BOLD}config.toml{RST} {YELLOW}not found, using defaults{RST} {DIM}(expected at {}){RST}",
195 p.display()
196 ),
197 }
198 }
199 },
200 None => Outcome {
201 ok: false,
202 line: format!("{BOLD}config.toml{RST} {RED}could not resolve path{RST}"),
203 },
204 };
205 print_check(&config_outcome);
206
207 let allowlist_outcome = shell_allowlist_outcome();
209 if allowlist_outcome.ok {
210 passed += 1;
211 }
212 print_check(&allowlist_outcome);
213
214 let path_jail = path_jail_outcome();
216 if path_jail.ok {
217 passed += 1;
218 }
219 print_check(&path_jail);
220
221 let passthrough_outcome = compact_format_passthrough_outcome();
223 if passthrough_outcome.ok {
224 passed += 1;
225 }
226 print_check(&passthrough_outcome);
227
228 let perm_inherit_outcome = permission_inheritance_outcome();
230 if perm_inherit_outcome.ok {
231 passed += 1;
232 }
233 print_check(&perm_inherit_outcome);
234
235 let proxy_outcome = proxy_upstream_outcome();
237 if proxy_outcome.ok {
238 passed += 1;
239 }
240 print_check(&proxy_outcome);
241
242 let aliases = shell_aliases_outcome();
244 if aliases.ok {
245 passed += 1;
246 }
247 print_check(&aliases);
248
249 let mcp = mcp_config_outcome();
251 if mcp.ok {
252 passed += 1;
253 }
254 print_check(&mcp);
255
256 let workspace_scope = workspace_scope::workspace_scope_outcome(mcp.ok);
258 if let Some(ref ws) = workspace_scope {
259 if ws.ok {
260 passed += 1;
261 }
262 print_check(ws);
263 }
264
265 let skill = skill_files_outcome();
267 if skill.ok {
268 passed += 1;
269 }
270 print_check(&skill);
271
272 let port = port_3333_outcome();
274 if port.ok {
275 passed += 1;
276 }
277 print_check(&port);
278
279 #[cfg(unix)]
281 let daemon_outcome = {
282 let autostart = crate::daemon_autostart::is_installed();
283 let autostart_tag = if autostart {
286 match crate::daemon_autostart::service_file_path() {
287 Some(p) => format!(" {DIM}[autostart: on — {}]{RST}", p.display()),
288 None => format!(" {DIM}[autostart: on]{RST}"),
289 }
290 } else {
291 String::new()
292 };
293 if crate::daemon::is_daemon_running() {
294 let pid_path = crate::daemon::daemon_pid_path();
295 let pid_str = std::fs::read_to_string(&pid_path).unwrap_or_default();
296 Outcome {
297 ok: true,
298 line: format!(
299 "{BOLD}Daemon{RST} {GREEN}running (PID {}){RST}{autostart_tag}",
300 pid_str.trim()
301 ),
302 }
303 } else {
304 let hint = if autostart {
305 format!("{DIM}(autostart enabled, will restart){RST}")
306 } else {
307 format!("{DIM}(run: lean-ctx daemon start or: lean-ctx daemon enable){RST}")
308 };
309 Outcome {
310 ok: true,
311 line: format!("{BOLD}Daemon{RST} {YELLOW}not running{RST} {hint}"),
312 }
313 }
314 };
315 #[cfg(not(unix))]
316 let daemon_outcome = Outcome {
317 ok: true,
318 line: format!("{BOLD}Daemon{RST} {DIM}not supported on this platform{RST}"),
319 };
320 if daemon_outcome.ok {
321 passed += 1;
322 }
323 print_check(&daemon_outcome);
324
325 #[cfg(target_os = "linux")]
327 {
328 if let Ok(o) = std::process::Command::new("systemctl")
329 .args(["--user", "is-active", "lean-ctx-daemon.service"])
330 .output()
331 {
332 let state = String::from_utf8_lossy(&o.stdout).trim().to_string();
333 if state != "active" {
334 println!(
335 " {DIM} systemd unit state: {YELLOW}{state}{RST}{DIM} (expected: active){RST}"
336 );
337 }
338 }
339 let username = std::env::var("USER")
340 .or_else(|_| std::env::var("LOGNAME"))
341 .unwrap_or_else(|_| "$(whoami)".to_string());
342 if let Ok(o) = std::process::Command::new("loginctl")
343 .args(["show-user", &username, "-p", "Linger", "--value"])
344 .output()
345 {
346 let val = String::from_utf8_lossy(&o.stdout).trim().to_string();
347 if val != "yes" {
348 println!(
349 " {YELLOW}⚠{RST} Linger not enabled — daemon won't start at boot without login"
350 );
351 println!(" {DIM}Fix: loginctl enable-linger {username}{RST}");
352 }
353 }
354 }
355 if let Some(log_path) = crate::core::startup_guard::crash_loop_log_path(
356 crate::core::startup_guard::MCP_PROCESS_NAME,
357 ) {
358 if log_path.exists() {
359 if let Ok(contents) = std::fs::read_to_string(&log_path) {
360 let lines: Vec<&str> = contents.lines().collect();
361 if lines.len() >= 5 {
362 println!(
363 " {YELLOW}⚠{RST} Crash-loop log: {} recent restarts {DIM}({}){RST}",
364 lines.len(),
365 log_path.display()
366 );
367 }
368 }
369 }
370 }
371
372 let provider_outcome = provider_outcome();
374 print_check(&provider_outcome);
375
376 let bridge_outcomes = mcp_bridge_outcomes();
378 for bridge_check in &bridge_outcomes {
379 print_check(bridge_check);
380 }
381
382 let plan_outcomes = plan_mode_outcomes();
384 for plan_check in &plan_outcomes {
385 print_check(plan_check);
386 }
387
388 let session_outcome = session_state_outcome();
390 if session_outcome.ok {
391 passed += 1;
392 }
393 print_check(&session_outcome);
394
395 let docker_outcomes = docker_env_outcomes();
397 for docker_check in &docker_outcomes {
398 if docker_check.ok {
399 passed += 1;
400 }
401 print_check(docker_check);
402 }
403
404 let pi = pi_outcome();
406 if let Some(ref pi_check) = pi {
407 if pi_check.ok {
408 passed += 1;
409 }
410 print_check(pi_check);
411 }
412
413 let integrity = crate::core::integrity::check();
415 let integrity_ok = integrity.seed_ok && integrity.origin_ok;
416 if integrity_ok {
417 passed += 1;
418 }
419 let integrity_line = if integrity_ok {
420 format!(
421 "{BOLD}Build origin{RST} {GREEN}official{RST} {DIM}{}{RST}",
422 integrity.repo
423 )
424 } else {
425 format!(
426 "{BOLD}Build origin{RST} {RED}MODIFIED REDISTRIBUTION{RST} {YELLOW}pkg={}, repo={}{RST}",
427 integrity.pkg_name, integrity.repo
428 )
429 };
430 print_check(&Outcome {
431 ok: integrity_ok,
432 line: integrity_line,
433 });
434
435 let cache_safety = cache_safety_outcome();
437 if cache_safety.ok {
438 passed += 1;
439 }
440 print_check(&cache_safety);
441
442 let claude_truncation = claude_truncation_outcome();
444 if let Some(ref ct) = claude_truncation {
445 if ct.ok {
446 passed += 1;
447 }
448 print_check(ct);
449 }
450
451 let bm25_health = bm25_cache_health_outcome();
453 if bm25_health.ok {
454 passed += 1;
455 }
456 print_check(&bm25_health);
457
458 let semantic_index = semantic_index_outcome();
461 if let Some(ref check) = semantic_index {
462 if check.ok {
463 passed += 1;
464 }
465 print_check(check);
466 }
467
468 let archive_footprint = archive_footprint_outcome();
470 if archive_footprint.ok {
471 passed += 1;
472 }
473 print_check(&archive_footprint);
474
475 let mem_profile = memory_profile_outcome();
477 passed += 1;
478 print_check(&mem_profile);
479
480 let mem_cleanup = memory_cleanup_outcome();
482 passed += 1;
483 print_check(&mem_cleanup);
484
485 let ram_outcome = ram_guardian_outcome();
487 if ram_outcome.ok {
488 passed += 1;
489 }
490 print_check(&ram_outcome);
491
492 let cap_warnings = capacity_warnings();
494 for cw in &cap_warnings {
495 if cw.ok {
496 passed += 1;
497 }
498 print_check(cw);
499 }
500
501 let proxy_health = proxy_health_outcome();
503 if proxy_health.ok {
504 passed += 1;
505 }
506 print_check(&proxy_health);
507
508 let stale_env = stale_proxy_env_outcome();
510 if let Some(ref check) = stale_env {
511 if check.ok {
512 passed += 1;
513 }
514 print_check(check);
515 }
516
517 let subscription_conflict = proxy_subscription_conflict_outcome();
519 if let Some(ref check) = subscription_conflict {
520 if check.ok {
521 passed += 1;
522 }
523 print_check(check);
524 }
525
526 let deprecation_check = deprecations::deprecations_outcome();
529 if deprecation_check.ok {
530 passed += 1;
531 }
532 print_check(&deprecation_check);
533
534 println!("\n {BOLD}{WHITE}LSP (optional — for ctx_refactor):{RST}");
536 let lsp_outcomes = lsp_server_outcomes();
537 for lsp_check in &lsp_outcomes {
538 print_check(lsp_check);
539 }
540
541 let mut effective_total = total + 10; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += 1; effective_total += cap_warnings.len() as u32;
548 effective_total += docker_outcomes.len() as u32;
549 if pi.is_some() {
550 effective_total += 1;
551 }
552 if claude_truncation.is_some() {
553 effective_total += 1;
554 }
555 if stale_env.is_some() {
556 effective_total += 1;
557 }
558 if subscription_conflict.is_some() {
559 effective_total += 1;
560 }
561 if workspace_scope.is_some() {
562 effective_total += 1;
563 }
564 if semantic_index.is_some() {
565 effective_total += 1;
566 }
567 let cfg = crate::core::config::Config::load();
569 let shadow_line = if cfg.shadow_mode {
570 format!("{BOLD}Shadow mode{RST} {GREEN}active{RST} {DIM}(native tools intercepted → ctx_*){RST}")
571 } else {
572 format!("{BOLD}Shadow mode{RST} {DIM}disabled{RST} {DIM}(enable: lean-ctx config set shadow_mode true){RST}")
573 };
574 println!(" {shadow_line}");
575
576 let tool_profile = crate::core::tool_profiles::ToolProfile::from_config(&cfg);
580 println!(
581 " {BOLD}Tool profile{RST} {WHITE}{tool_profile}{RST} {DIM}{} + ctx_call gateway{RST}",
582 tool_profile.description()
583 );
584
585 let cep = &crate::core::stats::load().cep;
589 let hit_ratio = if cep.total_cache_reads > 0 {
590 (cep.total_cache_hits as f64 / cep.total_cache_reads as f64) * 100.0
591 } else {
592 0.0
593 };
594 println!(
595 " {BOLD}Session cache{RST} {WHITE}{} sessions{RST} {DIM}{}/{} reads cached ({hit_ratio:.0}% hit) · prove: lean-ctx verify-cache{RST}",
596 cep.sessions, cep.total_cache_hits, cep.total_cache_reads
597 );
598
599 let needs_attention = effective_total.saturating_sub(passed);
600 println!();
601 println!(" {BOLD}{WHITE}Summary:{RST} {GREEN}{passed}{RST}{DIM}/{effective_total}{RST} checks passed");
602 if needs_attention > 0 {
603 println!(
604 " {YELLOW}{needs_attention} check(s) need attention.{RST} Auto-repair what's fixable: {BOLD}lean-ctx doctor --fix{RST}"
605 );
606 } else {
607 println!(" {GREEN}Everything looks good.{RST}");
608 }
609 println!(" {DIM}LSP servers are optional enhancements (not counted in score){RST}");
610 println!(" {DIM}{}{RST}", crate::core::integrity::origin_line());
611}
612
613pub fn run_compact() {
614 let (passed, total) = compact_score();
615 print_compact_status(passed, total);
616}
617
618pub fn run_cli(args: &[String]) -> i32 {
619 let (sub, rest) = match args.first().map(String::as_str) {
620 Some("integrations") => ("integrations", &args[1..]),
621 Some("overhead") => ("overhead", &args[1..]),
622 _ => ("", args),
623 };
624
625 let fix = rest.iter().any(|a| a == "--fix");
626 let json = rest.iter().any(|a| a == "--json");
627 let migrate_check = rest.iter().any(|a| a == "--migrate-check");
628 let help = rest.iter().any(|a| a == "--help" || a == "-h");
629
630 if help {
631 println!("Usage:");
632 println!(" lean-ctx doctor");
633 println!(" lean-ctx doctor overhead [--json] Fixed context cost per session");
634 println!(" lean-ctx doctor integrations [--json]");
635 println!(" lean-ctx doctor --fix [--json]");
636 println!(" lean-ctx doctor --migrate-check [--json]");
637 return 0;
638 }
639
640 if sub == "overhead" {
641 return overhead::run_overhead(json);
642 }
643
644 if migrate_check {
645 return migrate::run_migrate_check(json);
646 }
647
648 if sub == "integrations" {
649 if fix {
650 let _ = fix::run_fix(&fix::DoctorFixOptions { json: false });
651 }
652 return integrations::run_integrations(&integrations::IntegrationsOptions { json });
653 }
654
655 if !fix {
656 run();
657 return 0;
658 }
659
660 match fix::run_fix(&fix::DoctorFixOptions { json }) {
661 Ok(code) => code,
662 Err(e) => {
663 tracing::error!("doctor --fix failed: {e}");
664 2
665 }
666 }
667}
668
669pub fn compact_score() -> (u32, u32) {
670 let mut passed = 0u32;
671 let total = 6u32;
672
673 if resolve_lean_ctx_binary().is_some() || path_in_path_env() {
674 passed += 1;
675 }
676 let lean_dir = crate::core::data_dir::lean_ctx_data_dir().ok();
677 if lean_dir.as_ref().is_some_and(|p| p.is_dir()) {
678 passed += 1;
679 }
680 if lean_dir
681 .as_ref()
682 .map(|d| d.join("stats.json"))
683 .and_then(|p| std::fs::metadata(p).ok())
684 .is_some_and(|m| m.is_file())
685 {
686 passed += 1;
687 }
688 if shell_aliases_outcome().ok {
689 passed += 1;
690 }
691 if mcp_config_outcome().ok {
692 passed += 1;
693 }
694 if skill_files_outcome().ok {
695 passed += 1;
696 }
697
698 (passed, total)
699}
700
701pub(super) fn print_compact_status(passed: u32, total: u32) {
702 let status = if passed == total {
703 format!("{GREEN}✓ All {total} checks passed{RST}")
704 } else {
705 format!("{YELLOW}{passed}/{total} passed{RST} — run {BOLD}lean-ctx doctor{RST} for details")
706 };
707 println!(" {status}");
708}
709
710#[cfg(test)]
711mod tests {
712 use super::is_active_shell_impl;
713
714 fn make_capacity_check(name: &str, current: usize, limit: usize) -> Option<(bool, String)> {
718 if limit == 0 {
719 return None;
720 }
721 let pct = (current as f64 / limit as f64 * 100.0) as u32;
722 if pct > 100 {
723 Some((true, format!("{name}: {current}/{limit} ({pct}%)")))
724 } else if pct >= 80 {
725 Some((false, format!("{name}: {current}/{limit} ({pct}%)")))
726 } else {
727 None
728 }
729 }
730
731 #[test]
732 fn capacity_below_80_no_warning() {
733 assert!(make_capacity_check("facts", 100, 200).is_none());
734 assert!(make_capacity_check("facts", 159, 200).is_none());
735 }
736
737 #[test]
738 fn capacity_at_80_yellow_warning() {
739 let result = make_capacity_check("facts", 160, 200);
740 assert!(result.is_some());
741 let (critical, msg) = result.unwrap();
742 assert!(!critical);
743 assert!(msg.contains("160/200"));
744 assert!(msg.contains("80%"));
745 }
746
747 #[test]
748 fn capacity_at_92_yellow_warning() {
749 let result = make_capacity_check("facts", 185, 200);
750 assert!(result.is_some());
751 let (critical, msg) = result.unwrap();
752 assert!(!critical);
753 assert!(msg.contains("185/200"));
754 assert!(msg.contains("92%"));
755 }
756
757 #[test]
758 fn capacity_at_95_is_warning_not_critical() {
759 let result = make_capacity_check("facts", 190, 200);
760 assert!(result.is_some());
761 let (critical, msg) = result.unwrap();
762 assert!(!critical, "95% is full-but-healthy, not over cap");
763 assert!(msg.contains("190/200"));
764 assert!(msg.contains("95%"));
765 }
766
767 #[test]
768 fn capacity_at_100_is_warning_not_critical() {
769 let result = make_capacity_check("facts", 200, 200);
771 assert!(result.is_some());
772 let (critical, _) = result.unwrap();
773 assert!(!critical);
774 }
775
776 #[test]
777 fn capacity_over_100_is_critical() {
778 let result = make_capacity_check("facts", 206, 200);
781 assert!(result.is_some());
782 let (critical, msg) = result.unwrap();
783 assert!(critical);
784 assert!(msg.contains("206/200"));
785 assert!(msg.contains("103%"));
786 }
787
788 #[test]
789 fn capacity_zero_limit_skipped() {
790 assert!(make_capacity_check("facts", 50, 0).is_none());
791 }
792
793 #[test]
794 fn bashrc_active_on_non_windows_when_shell_empty() {
795 assert!(is_active_shell_impl("~/.bashrc", "", false, false));
796 }
797
798 #[test]
799 fn bashrc_not_active_on_windows_when_shell_empty() {
800 assert!(!is_active_shell_impl("~/.bashrc", "", true, false));
801 }
802
803 #[test]
804 fn bashrc_active_when_shell_contains_bash_on_linux() {
805 assert!(is_active_shell_impl(
806 "~/.bashrc",
807 "/usr/bin/bash",
808 false,
809 false
810 ));
811 }
812
813 #[test]
814 fn bashrc_not_active_on_windows_even_with_bash_in_shell_env() {
815 std::env::remove_var("BASH_VERSION");
818 assert!(!is_active_shell_impl(
819 "~/.bashrc",
820 "C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe",
821 true,
822 false,
823 ));
824 }
825
826 #[test]
827 fn bashrc_not_active_on_windows_powershell_even_with_bash_in_shell() {
828 assert!(!is_active_shell_impl(
829 "~/.bashrc",
830 "C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe",
831 true,
832 true,
833 ));
834 }
835
836 #[test]
837 fn bashrc_not_active_on_windows_powershell_with_empty_shell() {
838 assert!(!is_active_shell_impl("~/.bashrc", "", true, true));
839 }
840
841 #[test]
842 fn zshrc_unaffected_by_powershell_flag() {
843 assert!(is_active_shell_impl("~/.zshrc", "/bin/zsh", false, false));
844 assert!(is_active_shell_impl("~/.zshrc", "/bin/zsh", true, true));
845 }
846
847 #[test]
848 fn bashrc_not_active_on_windows_without_powershell_detection() {
849 std::env::remove_var("BASH_VERSION");
852 assert!(!is_active_shell_impl(
853 "~/.bashrc",
854 "/usr/bin/bash",
855 true,
856 false,
857 ));
858 }
859
860 #[test]
861 fn bashrc_active_on_linux() {
862 assert!(is_active_shell_impl("~/.bashrc", "/bin/bash", false, false));
863 assert!(is_active_shell_impl("~/.bashrc", "", false, false));
864 }
865}