1use crate::{
2 core, doctor, heatmap, hook_handlers, report, setup, shell, status, token_report, uninstall,
3};
4
5mod analytics;
6mod help;
7mod lifecycle;
8mod network;
9mod server;
10mod suggest;
11
12#[allow(clippy::wildcard_imports)]
13use analytics::*;
14#[allow(clippy::wildcard_imports)]
15use help::*;
16#[allow(clippy::wildcard_imports)]
17use lifecycle::*;
18#[allow(clippy::wildcard_imports)]
19use network::*;
20#[allow(clippy::wildcard_imports)]
21use server::*;
22
23pub fn run() {
24 let mut args: Vec<String> = std::env::args().collect();
25
26 if args.get(1).is_some_and(|a| a == "(deleted)") {
30 args.remove(1);
31 }
32
33 if !is_server_mode(&args) {
34 restore_sigpipe_default();
35 }
36
37 let enters_mcp = args.len() == 1 || args.get(1).is_some_and(|a| a == "mcp");
38 if !enters_mcp {
39 crate::core::logging::init_logging();
40 }
41
42 if args.len() > 1 {
43 let rest = args[2..].to_vec();
44
45 match args[1].as_str() {
46 "-c" | "exec" => {
47 let raw = rest.first().is_some_and(|a| a == "--raw");
48 let cmd_args = if raw { &args[3..] } else { &args[2..] };
49 let command = if cmd_args.len() == 1 {
50 cmd_args[0].clone()
51 } else {
52 shell::join_command(cmd_args)
53 };
54 core::agent_runtime_env::capture();
58 if crate::shell::reentry::should_pass_through() {
59 passthrough(&command);
60 }
61 if raw {
62 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
65 } else {
66 unsafe { std::env::set_var("LEAN_CTX_COMPRESS", "1") };
69 }
70 let code = shell::exec(&command);
71 core::tool_lifecycle::flush_all();
72 std::process::exit(code);
73 }
74 "-t" | "--track" => {
75 let cmd_args = &args[2..];
76 let code = if cmd_args.len() > 1 {
77 shell::exec_argv(cmd_args)
78 } else {
79 let command = cmd_args[0].clone();
80 if crate::shell::reentry::should_pass_through() {
81 passthrough(&command);
82 }
83 shell::exec(&command)
84 };
85 core::tool_lifecycle::flush_all();
86 std::process::exit(code);
87 }
88 "shell" | "--shell" => {
89 shell::interactive();
90 return;
91 }
92 "gain" => {
93 cmd_gain(&rest);
94 return;
95 }
96 "spend" => {
97 cmd_spend(&rest);
98 return;
99 }
100 "savings" => {
101 cmd_savings(&rest);
102 return;
103 }
104 "learning" => {
105 cmd_learning(&rest);
106 return;
107 }
108 "conformance" | "selftest" => {
109 cmd_conformance(&rest);
110 return;
111 }
112 "health" => {
113 let code = crate::cli::health_cmd::cmd_health(&rest);
114 if code != 0 {
115 std::process::exit(code);
116 }
117 return;
118 }
119 "billing" => {
120 cmd_billing(&rest);
121 return;
122 }
123 "finops" => {
124 cmd_finops(&rest);
125 return;
126 }
127 "roi" => {
128 super::cmd_roi(&rest);
131 return;
132 }
133 "output-savings" | "output_savings" => {
134 super::cmd_output_savings(&rest);
137 return;
138 }
139 "token-report" | "report-tokens" => {
140 let code = token_report::run_cli(&rest);
141 if code != 0 {
142 std::process::exit(code);
143 }
144 return;
145 }
146 "pack" => {
147 crate::cli::cmd_pack(&rest);
148 return;
149 }
150 "policy" => {
151 crate::cli::cmd_policy(&rest);
152 return;
153 }
154 "plugin" | "plugins" => {
155 crate::cli::plugin_cmd::cmd_plugin(&rest);
156 return;
157 }
158 "addon" | "addons" => {
159 crate::cli::addon_cmd::cmd_addon(&rest);
160 return;
161 }
162 "rules" => {
163 crate::cli::rules_cmd::cmd_rules(&rest);
164 return;
165 }
166 "proof" => {
167 crate::cli::cmd_proof(&rest);
168 return;
169 }
170 "snapshot" => {
171 crate::cli::cmd_snapshot(&rest);
172 return;
173 }
174 "verify" => {
175 crate::cli::cmd_verify(&rest);
176 return;
177 }
178 "eval" => {
179 crate::cli::eval_cmd::cmd_eval(&rest);
180 return;
181 }
182 "verify-cache" | "cache-selftest" => {
183 let code = crate::cli::verify_cache_cmd::cmd_verify_cache(&rest);
184 if code != 0 {
185 std::process::exit(code);
186 }
187 return;
188 }
189 "visualize" => {
190 super::cmd_visualize(&rest);
191 return;
192 }
193 "audit" => {
194 if rest.first().map(String::as_str) == Some("evidence") {
195 crate::cli::audit_report::cmd_evidence(&rest[1..]);
196 } else {
197 println!("{}", crate::cli::audit_report::generate_report());
198 }
199 return;
200 }
201 "compliance" => {
202 crate::cli::cmd_compliance(&rest);
203 return;
204 }
205 "agent" => {
206 crate::cli::cmd_agent(&rest);
207 return;
208 }
209 "instructions" => {
210 crate::cli::cmd_instructions(&rest);
211 return;
212 }
213 "index" => {
214 crate::cli::cmd_index(&rest);
215 return;
216 }
217 "semantic-search" | "search-code" => {
218 crate::cli::cmd_semantic_search(&rest);
219 core::stats::flush();
220 return;
221 }
222 "explore" => {
223 crate::cli::explore_cmd::cmd_explore(&rest);
224 core::stats::flush();
225 return;
226 }
227 "repomap" | "repo-map" => {
228 crate::cli::cmd_repomap(&rest);
229 core::stats::flush();
230 return;
231 }
232 "cep" => {
233 println!("{}", core::stats::format_cep_report());
234 return;
235 }
236 "dashboard" => {
237 cmd_dashboard(&rest);
238 return;
239 }
240 "team" => {
241 cmd_team(&rest);
242 return;
243 }
244 "provider" => {
245 cmd_provider(&rest);
246 return;
247 }
248 "serve" => {
249 cmd_serve(&rest);
250 return;
251 }
252 "watch" => {
253 cmd_watch(&rest);
254 return;
255 }
256 "proxy" => {
257 cmd_proxy(&rest);
258 return;
259 }
260 "daemon" => {
261 cmd_daemon(&rest);
262 return;
263 }
264 "init" => {
265 super::cmd_init(&rest);
266 return;
267 }
268 "setup" => {
269 let non_interactive = rest.iter().any(|a| a == "--non-interactive");
270 let yes = rest.iter().any(|a| a == "--yes" || a == "-y");
271 let fix = rest.iter().any(|a| a == "--fix");
272 let json = rest.iter().any(|a| a == "--json");
273 let no_auto_approve = rest.iter().any(|a| a == "--no-auto-approve");
274 let skip_rules = rest.iter().any(|a| a == "--skip-rules");
275
276 if non_interactive || fix || json || yes {
277 let opts = setup::SetupOptions {
278 non_interactive,
279 yes,
280 fix,
281 json,
282 no_auto_approve,
283 skip_rules,
284 ..Default::default()
285 };
286 match setup::run_setup_with_options(opts) {
287 Ok(report) => {
288 if json {
289 println!(
290 "{}",
291 serde_json::to_string_pretty(&report)
292 .unwrap_or_else(|_| "{}".to_string())
293 );
294 }
295 if !report.success {
296 std::process::exit(1);
297 }
298 }
299 Err(e) => {
300 eprintln!("{e}");
301 std::process::exit(1);
302 }
303 }
304 } else {
305 setup::run_setup();
306 }
307 return;
308 }
309 "onboard" => {
310 setup::run_onboard();
311 return;
312 }
313 "install" => {
314 let repair = rest.iter().any(|a| a == "--repair" || a == "--fix");
319 let json = rest.iter().any(|a| a == "--json");
320 if !repair {
321 setup::run_setup();
322 return;
323 }
324 let opts = setup::SetupOptions {
325 non_interactive: true,
326 yes: true,
327 fix: true,
328 json,
329 ..Default::default()
330 };
331 match setup::run_setup_with_options(opts) {
332 Ok(report) => {
333 if json {
334 println!(
335 "{}",
336 serde_json::to_string_pretty(&report)
337 .unwrap_or_else(|_| "{}".to_string())
338 );
339 }
340 if !report.success {
341 std::process::exit(1);
342 }
343 }
344 Err(e) => {
345 eprintln!("{e}");
346 std::process::exit(1);
347 }
348 }
349 return;
350 }
351 "bootstrap" => {
352 let json = rest.iter().any(|a| a == "--json");
353 let opts = setup::SetupOptions {
354 non_interactive: true,
355 yes: true,
356 fix: true,
357 json,
358 ..Default::default()
359 };
360 match setup::run_setup_with_options(opts) {
361 Ok(report) => {
362 if json {
363 println!(
364 "{}",
365 serde_json::to_string_pretty(&report)
366 .unwrap_or_else(|_| "{}".to_string())
367 );
368 }
369 if !report.success {
370 std::process::exit(1);
371 }
372 }
373 Err(e) => {
374 eprintln!("{e}");
375 std::process::exit(1);
376 }
377 }
378 return;
379 }
380 "status" => {
381 let code = status::run_cli(&rest);
382 if code != 0 {
383 std::process::exit(code);
384 }
385 return;
386 }
387 "read" => {
388 super::cmd_read(&rest);
389 core::tool_lifecycle::flush_all();
390 return;
391 }
392 "call" => {
393 super::cmd_call(&rest);
394 return;
395 }
396 "diff" => {
397 super::cmd_diff(&rest);
398 core::tool_lifecycle::flush_all();
399 return;
400 }
401 "grep" => {
402 super::cmd_grep(&rest);
403 core::tool_lifecycle::flush_all();
404 return;
405 }
406 "glob" => {
407 super::cmd_glob(&rest);
408 core::stats::flush();
409 return;
410 }
411 "find" => {
412 super::cmd_find(&rest);
413 core::tool_lifecycle::flush_all();
414 return;
415 }
416 "ls" => {
417 super::cmd_ls(&rest);
418 core::tool_lifecycle::flush_all();
419 return;
420 }
421 "deps" => {
422 super::cmd_deps(&rest);
423 core::tool_lifecycle::flush_all();
424 return;
425 }
426 "discover" => {
427 super::cmd_discover(&rest);
428 return;
429 }
430 "ghost" => {
431 super::cmd_ghost(&rest);
432 return;
433 }
434 "filter" => {
435 super::cmd_filter(&rest);
436 return;
437 }
438 "heatmap" => {
439 heatmap::cmd_heatmap(&rest);
440 return;
441 }
442 "graph" => {
443 cmd_graph(&rest);
444 return;
445 }
446 "smells" => {
447 cmd_smells(&rest);
448 return;
449 }
450 "session" => {
451 super::cmd_session_action(&rest);
452 return;
453 }
454 "ledger" => {
455 super::cmd_ledger(&rest);
456 return;
457 }
458 "control" | "context-control" => {
459 super::cmd_control(&rest);
460 return;
461 }
462 "plan" | "context-plan" => {
463 super::cmd_plan(&rest);
464 return;
465 }
466 "compile" | "context-compile" => {
467 super::cmd_compile(&rest);
468 return;
469 }
470 "knowledge" => {
471 super::cmd_knowledge(&rest);
472 return;
473 }
474 "skillify" => {
475 super::cmd_skillify(&rest);
476 return;
477 }
478 "summary" => {
479 super::cmd_summary(&rest);
480 return;
481 }
482 "overview" => {
483 super::cmd_overview(&rest);
484 return;
485 }
486 "compress" => {
487 super::cmd_compress(&rest);
488 return;
489 }
490 "wrapped" => {
491 eprintln!("'lean-ctx wrapped' has been removed. Use: lean-ctx gain --wrapped");
492 std::process::exit(1);
493 }
494 "sessions" | "session-store" => {
495 super::cmd_sessions(&rest);
496 return;
497 }
498 "benchmark" => {
499 super::cmd_benchmark(&rest);
500 return;
501 }
502 "compact" => {
503 cmd_compact(&rest);
504 return;
505 }
506 "profile" => {
507 super::cmd_profile(&rest);
508 return;
509 }
510 "tools" => {
511 if rest.first().map(String::as_str) == Some("health") {
514 super::cmd_tools_health(&rest[1..]);
515 return;
516 }
517 let mut forwarded = vec!["tools".to_string()];
521 forwarded.extend(rest.iter().cloned());
522 super::cmd_profile(&forwarded);
523 return;
524 }
525 "config" => {
526 super::cmd_config(&rest);
527 return;
528 }
529 "allow" => {
530 super::cmd_allow(&rest);
531 return;
532 }
533 "security" => {
534 super::cmd_security(&rest);
535 return;
536 }
537 "yolo" => {
538 super::cmd_yolo(&rest);
539 return;
540 }
541 "secure" | "lockdown" => {
542 super::cmd_secure(&rest);
543 return;
544 }
545 "trust" => {
546 super::cmd_trust(&rest);
547 return;
548 }
549 "untrust" => {
550 super::cmd_untrust(&rest);
551 return;
552 }
553 "stats" => {
554 super::cmd_stats(&rest);
555 return;
556 }
557 "introspect" => {
558 super::cmd_introspect(&rest);
559 return;
560 }
561 "cache" => {
562 super::cmd_cache(&rest);
563 return;
564 }
565 "theme" => {
566 super::cmd_theme(&rest);
567 return;
568 }
569 "tee" => {
570 super::cmd_tee(&rest);
571 return;
572 }
573 "terse" | "compression" => {
574 super::cmd_compression(&rest);
575 return;
576 }
577 "slow-log" => {
578 super::cmd_slow_log(&rest);
579 return;
580 }
581 "debug-log" => {
582 super::cmd_debug_log(&rest);
583 return;
584 }
585 "editor-signal" => {
588 let file = rest
589 .iter()
590 .position(|a| a == "--file")
591 .and_then(|i| rest.get(i + 1));
592 if let Some(path) = file {
593 if let Err(e) = core::editor_signal::record_focus(path) {
594 eprintln!("editor-signal: {e}");
595 std::process::exit(1);
596 }
597 } else {
598 eprintln!("usage: lean-ctx editor-signal --file <path>");
599 std::process::exit(2);
600 }
601 return;
602 }
603 "update" | "--self-update" => {
604 core::updater::run(&rest);
605 return;
606 }
607 "restart" => {
608 cmd_restart();
609 return;
610 }
611 "stop" => {
612 cmd_stop();
613 return;
614 }
615 "dev-install" => {
616 cmd_dev_install();
617 return;
618 }
619 "codesign-setup" => {
620 cmd_codesign_setup();
621 return;
622 }
623 "doctor" => {
624 let code = doctor::run_cli(&rest);
625 if code != 0 {
626 std::process::exit(code);
627 }
628 return;
629 }
630 "harden" => {
631 super::harden::run(&rest);
632 return;
633 }
634 "export-rules" => {
635 super::export_rules::run(&rest);
636 return;
637 }
638 "gotchas" | "bugs" => {
639 super::cloud::cmd_gotchas(&rest);
640 return;
641 }
642 "learn" => {
643 super::cmd_learn(&rest);
644 return;
645 }
646 "buddy" | "pet" => {
647 super::cloud::cmd_buddy(&rest);
648 return;
649 }
650 "hook" => {
651 hook_handlers::mark_hook_environment();
652 core::agent_runtime_env::capture();
656 let action = rest.first().map_or("help", std::string::String::as_str);
657 if !matches!(action, "rewrite" | "redirect") {
662 hook_handlers::arm_watchdog(std::time::Duration::from_secs(5));
663 }
664 match action {
665 "rewrite" => hook_handlers::handle_rewrite(),
666 "redirect" => hook_handlers::handle_redirect(),
667 "observe" => hook_handlers::handle_observe(),
668 "copilot" => hook_handlers::handle_copilot(),
669 "codex-pretooluse" => hook_handlers::handle_codex_pretooluse(),
670 "codex-session-start" => hook_handlers::handle_codex_session_start(),
671 "rewrite-inline" => hook_handlers::handle_rewrite_inline(),
672 _ => {
673 eprintln!(
674 "Usage: lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>"
675 );
676 eprintln!(
677 " Internal commands used by agent hooks (Claude, Cursor, Copilot, etc.)"
678 );
679 std::process::exit(1);
680 }
681 }
682 return;
683 }
684 "report-issue" | "report" => {
685 report::run(&rest);
686 return;
687 }
688 "uninstall" => {
689 if rest.iter().any(|a| a == "--help" || a == "-h") {
692 uninstall::print_help();
693 return;
694 }
695 let dry_run = rest.iter().any(|a| a == "--dry-run");
696 let keep_config = rest.iter().any(|a| a == "--keep-config");
697 let keep_binary = rest.iter().any(|a| a == "--keep-binary");
698 uninstall::run(dry_run, keep_config, keep_binary);
699 return;
700 }
701 "raw" | "bypass" => {
706 if rest.is_empty() {
707 eprintln!("Usage: lean-ctx raw \"command\"");
708 eprintln!(
709 "Runs the command with output passed through unchanged (no \
710 compression). The shell allowlist still applies."
711 );
712 std::process::exit(1);
713 }
714 let command = if rest.len() == 1 {
715 rest[0].clone()
716 } else {
717 shell::join_command(&args[2..])
718 };
719 unsafe { std::env::set_var("LEAN_CTX_RAW", "1") };
722 let code = shell::exec(&command);
723 std::process::exit(code);
724 }
725 "safety-levels" | "safety" => {
726 println!("{}", core::compression_safety::format_safety_table());
727 return;
728 }
729 "cheat" | "cheatsheet" | "cheat-sheet" => {
730 super::cmd_cheatsheet();
731 return;
732 }
733 "login" => {
734 super::cloud::cmd_login(&rest);
735 return;
736 }
737 "register" => {
738 super::cloud::cmd_register(&rest);
739 return;
740 }
741 "forgot-password" => {
742 super::cloud::cmd_forgot_password(&rest);
743 return;
744 }
745 "sync" => {
746 super::cloud::cmd_sync(&rest);
747 return;
748 }
749 "contribute" => {
750 super::cloud::cmd_contribute();
751 return;
752 }
753 "cloud" => {
754 super::cloud::cmd_cloud(&rest);
755 return;
756 }
757 "upgrade" => {
758 super::cloud::cmd_upgrade();
759 return;
760 }
761 "--version" | "-V" => {
762 println!("{}", core::integrity::origin_line());
763 return;
764 }
765 "help" => {
766 let want_all = rest
767 .iter()
768 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"));
769 if want_all {
770 print_help();
771 } else {
772 print_help_concise();
773 }
774 return;
775 }
776 "--help" | "-h" => {
777 if rest
778 .iter()
779 .any(|a| matches!(a.as_str(), "all" | "full" | "--all" | "-a"))
780 {
781 print_help();
782 } else {
783 print_help_concise();
784 }
785 return;
786 }
787 "mcp" => {}
788 _ => {
789 let unknown = &args[1];
790 eprintln!("lean-ctx: unknown command '{unknown}'");
791 if let Some(suggestion) = suggest::closest_command(unknown) {
792 eprintln!(" did you mean '{suggestion}'?");
793 }
794 eprintln!(" run 'lean-ctx help' for the full command list");
795 std::process::exit(1);
796 }
797 }
798 }
799
800 if args.len() == 1 && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
806 print_quickstart();
807 return;
808 }
809
810 if let Err(e) = run_mcp_server() {
811 tracing::error!("lean-ctx: {e}");
812 std::process::exit(1);
813 }
814}
815
816fn is_server_mode(args: &[String]) -> bool {
820 args.len() == 1
821 || args.get(1).is_some_and(|a| {
822 matches!(
823 a.as_str(),
824 "mcp" | "daemon" | "proxy" | "serve" | "watch" | "dashboard"
825 )
826 })
827}
828
829#[cfg(unix)]
839fn restore_sigpipe_default() {
840 unsafe {
843 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
844 }
845}
846
847#[cfg(not(unix))]
848fn restore_sigpipe_default() {}
849
850fn passthrough(command: &str) -> ! {
851 let (shell, flag) = shell::shell_and_flag();
852 let mut cmd = std::process::Command::new(&shell);
853 cmd.arg(&flag).arg(command);
854 shell::reentry::mark_child(&mut cmd);
855 shell::platform::apply_utf8_locale(&mut cmd);
856 let status = cmd.status().map_or(127, |s| s.code().unwrap_or(1));
857 std::process::exit(status);
858}
859
860pub(super) fn run_async<F: std::future::Future>(future: F) -> F::Output {
861 match tokio::runtime::Runtime::new() {
864 Ok(rt) => rt.block_on(future),
865 Err(e) => {
866 eprintln!("lean-ctx: failed to create async runtime: {e}");
867 std::process::exit(1);
868 }
869 }
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875 use serial_test::serial;
876
877 fn args_of(parts: &[&str]) -> Vec<String> {
878 parts.iter().map(|s| (*s).to_string()).collect()
879 }
880
881 #[test]
882 fn server_modes_keep_ignored_sigpipe() {
883 for mode in ["mcp", "daemon", "proxy", "serve", "watch", "dashboard"] {
884 assert!(
885 is_server_mode(&args_of(&["lean-ctx", mode])),
886 "{mode} must count as server mode"
887 );
888 }
889 assert!(is_server_mode(&args_of(&["lean-ctx"])));
891 }
892
893 #[test]
894 fn cli_modes_restore_default_sigpipe() {
895 for mode in ["doctor", "-c", "status", "ls", "grep", "gain", "help"] {
896 assert!(
897 !is_server_mode(&args_of(&["lean-ctx", mode])),
898 "{mode} must count as CLI mode (SIGPIPE default)"
899 );
900 }
901 }
902
903 #[test]
904 fn quickstart_is_short_and_points_to_setup() {
905 let q = quickstart_text();
906 assert!(
907 q.contains("lean-ctx onboard"),
908 "quickstart must point to onboard"
909 );
910 assert!(q.contains("lean-ctx help"), "quickstart must point to help");
911 assert!(
913 q.lines().count() <= 16,
914 "quickstart should be short; got {} lines",
915 q.lines().count()
916 );
917 assert!(
918 !q.contains("COMMANDS:"),
919 "quickstart must not inline the full command reference"
920 );
921 }
922
923 #[test]
924 fn concise_help_is_short_and_points_to_full() {
925 let h = concise_help_text();
926 assert!(h.contains("lean-ctx onboard"), "must lead with onboard");
927 assert!(
928 h.contains("lean-ctx help all"),
929 "must point to full reference"
930 );
931 assert!(
932 h.contains("lean-ctx tools"),
933 "must surface the tools profile command"
934 );
935 assert!(
937 h.lines().count() <= 40,
938 "concise help should stay short; got {} lines",
939 h.lines().count()
940 );
941 assert!(
942 !h.contains("SHELL HOOK PATTERNS"),
943 "concise help must not inline the full pattern catalog"
944 );
945 }
946
947 #[test]
948 fn capability_banner_tool_count_matches_registry() {
949 let n = crate::server::registry::tool_count();
950 let banner = capability_banner();
951 assert!(
952 banner.contains(&format!("{n} MCP tools")),
953 "banner must show the live registry count ({n}); got: {banner}"
954 );
955 }
956
957 #[test]
958 #[serial]
959 fn worker_threads_default_clamps_low() {
960 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
961 assert_eq!(resolve_worker_threads(1), 1);
962 }
963
964 #[test]
965 #[serial]
966 fn worker_threads_default_clamps_high() {
967 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
968 assert_eq!(resolve_worker_threads(32), 4);
969 }
970
971 #[test]
972 #[serial]
973 fn worker_threads_default_passthrough() {
974 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
975 assert_eq!(resolve_worker_threads(3), 3);
976 }
977
978 #[test]
979 #[serial]
980 fn worker_threads_env_override() {
981 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "12");
982 assert_eq!(resolve_worker_threads(2), 12);
983 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
984 }
985
986 #[test]
987 #[serial]
988 fn worker_threads_env_invalid_falls_back() {
989 crate::test_env::set_var("LEAN_CTX_WORKER_THREADS", "not_a_number");
990 assert_eq!(resolve_worker_threads(3), 3);
991 crate::test_env::remove_var("LEAN_CTX_WORKER_THREADS");
992 }
993}