1pub fn hook_binary_override() -> Option<String> {
12 std::env::var("LEAN_CTX_HOOK_BINARY")
13 .ok()
14 .or_else(|| crate::core::config::Config::load().hook_binary.clone())
15 .map(|s| s.trim().to_string())
16 .filter(|s| !s.is_empty())
17}
18
19pub fn is_lean_ctx_hook_binary(binary: &str) -> bool {
21 let binary = binary.trim().trim_matches(['\'', '"']);
22 let name = binary.rsplit(['/', '\\']).next().unwrap_or(binary);
23 matches!(
24 name,
25 "lean-ctx" | "lean-ctx.exe" | "lean-ctx.cmd" | "lean-ctx.bat"
26 ) || hook_binary_override()
27 .is_some_and(|configured| configured.trim_matches(['\'', '"']) == binary)
28}
29
30pub fn is_lean_ctx_hook_command(command: &str, operation: &str) -> bool {
34 let Some((binary, action)) = command.trim().rsplit_once(" hook ") else {
35 return false;
36 };
37 let binary = binary.trim();
38 let quoted = binary.len() >= 2
39 && ((binary.starts_with('"') && binary.ends_with('"'))
40 || (binary.starts_with('\'') && binary.ends_with('\'')));
41 let binary_body = if quoted {
42 &binary[1..binary.len() - 1]
43 } else {
44 binary
45 };
46 action == operation
47 && !binary_body.contains(['\'', '"', '`'])
48 && (quoted || !binary_body.chars().any(char::is_whitespace))
49 && is_lean_ctx_hook_binary(binary)
50}
51
52pub fn resolve_portable_binary() -> String {
53 let current = std::env::current_exe()
54 .ok()
55 .map(|p| p.to_string_lossy().into_owned());
56
57 let which_raw = crate::core::pathutil::find_all_on_path("lean-ctx")
60 .into_iter()
61 .map(|p| p.to_string_lossy().into_owned())
62 .find(|s| !s.is_empty() && !is_transient_build_binary(s));
63
64 choose_binary_path(current.as_deref(), which_raw.as_deref())
65}
66
67const TRANSIENT_CARGO_DIRS: &[&str] = &["deps", "build", "examples", "incremental"];
70
71pub fn is_transient_build_binary(path: &str) -> bool {
83 let comps: Vec<&str> = path.split(['/', '\\']).collect();
84 (1..comps.len().saturating_sub(1)).any(|i| {
85 if !TRANSIENT_CARGO_DIRS.contains(&comps[i]) {
86 return false;
87 }
88 let profile = comps[i - 1];
89 let named_profile = matches!(profile, "debug" | "release")
90 && ((i >= 2 && comps[i - 2].ends_with("target"))
91 || (i >= 3 && comps[i - 3].ends_with("target")));
92 named_profile || is_cargo_profile_dir(std::path::Path::new(&comps[..i].join("/")))
93 })
94}
95
96pub fn refuses_transient_binary_for_home(binary: &str, home: &std::path::Path) -> bool {
105 let real_home = crate::core::config::shell_policy::real_user_home();
106 let refused = transient_binary_for_real_home(binary, home, real_home.as_deref());
107 if refused {
108 tracing::warn!(
109 "not writing transient build binary {binary} into the hook configuration of {}; \
110 install lean-ctx (or put it on PATH) and run `lean-ctx setup`",
111 home.display()
112 );
113 }
114 refused
115}
116
117fn transient_binary_for_real_home(
118 binary: &str,
119 home: &std::path::Path,
120 real_home: Option<&std::path::Path>,
121) -> bool {
122 if !is_transient_build_binary(binary.trim().trim_matches(['\'', '"'])) {
123 return false;
124 }
125 let Some(real_home) = real_home else {
127 return false;
128 };
129 let canonical = |p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.into());
130 canonical(home) == canonical(real_home)
131}
132
133const LAUNCHER_NAMES: &[&str] = if cfg!(windows) {
135 &["lean-ctx.exe", "lean-ctx.cmd"]
136} else {
137 &["lean-ctx"]
138};
139
140pub fn path_dirs() -> Vec<std::path::PathBuf> {
142 std::env::var_os("PATH")
143 .map(|p| {
144 std::env::split_paths(&p)
145 .filter(|d| !d.as_os_str().is_empty())
146 .collect()
147 })
148 .unwrap_or_default()
149}
150
151pub fn dir_on_path(dir: &std::path::Path, path_dirs: &[std::path::PathBuf]) -> bool {
153 let canonical = std::fs::canonicalize(dir).ok();
154 path_dirs
155 .iter()
156 .any(|d| d == dir || (canonical.is_some() && std::fs::canonicalize(d).ok() == canonical))
157}
158
159pub fn launchers_on_path(path_dirs: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
162 path_dirs
163 .iter()
164 .filter_map(|d| {
165 LAUNCHER_NAMES
166 .iter()
167 .map(|name| d.join(name))
168 .find(|p| p.is_file())
169 })
170 .collect()
171}
172
173pub fn stable_shell_binary(binary: &str) -> String {
184 stable_shell_binary_in(binary, &path_dirs())
185}
186
187pub fn resolve_agent_binary() -> String {
196 stable_shell_binary(&resolve_portable_binary())
197}
198
199fn stable_shell_binary_in(binary: &str, path_dirs: &[std::path::PathBuf]) -> String {
200 let path = std::path::Path::new(binary);
201 if !path.is_absolute() || path.parent().is_some_and(|d| dir_on_path(d, path_dirs)) {
202 return binary.to_string();
203 }
204 let Ok(canonical) = std::fs::canonicalize(path) else {
205 return binary.to_string();
206 };
207 launchers_on_path(path_dirs)
208 .iter()
209 .find(|launcher| launcher_runs(launcher, &canonical))
210 .map_or_else(
211 || binary.to_string(),
212 |p| sanitize_exe_path(&p.to_string_lossy()),
213 )
214}
215
216fn launcher_runs(launcher: &std::path::Path, target: &std::path::Path) -> bool {
219 std::fs::canonicalize(launcher).is_ok_and(|l| l == target)
220 || scoop_shim_target(launcher)
221 .is_some_and(|t| std::fs::canonicalize(t).is_ok_and(|t| t == target))
222 || version_manager_shim_owns(launcher, target)
223}
224
225const MAX_SHIM_BYTES: u64 = 4096;
227
228fn scoop_shim_target(launcher: &std::path::Path) -> Option<std::path::PathBuf> {
233 use std::io::Read;
234 let mut text = String::new();
235 std::fs::File::open(launcher.with_extension("shim"))
236 .ok()?
237 .take(MAX_SHIM_BYTES + 1)
238 .read_to_string(&mut text)
239 .ok()?;
240 if text.len() as u64 > MAX_SHIM_BYTES {
241 return None;
242 }
243 text.lines()
244 .find_map(|line| {
245 let (key, value) = line.split_once('=')?;
246 (key.trim() == "path").then(|| std::path::PathBuf::from(value.trim().trim_matches('"')))
247 })
248 .filter(|p| p.is_absolute())
249}
250
251fn version_manager_shim_owns(launcher: &std::path::Path, target: &std::path::Path) -> bool {
256 let Some(shims) = launcher.parent() else {
257 return false;
258 };
259 if shims.file_name() != Some(std::ffi::OsStr::new("shims")) {
260 return false;
261 }
262 shims.parent().is_some_and(|root| {
263 std::fs::canonicalize(root.join("installs"))
264 .is_ok_and(|installs| target.starts_with(installs))
265 })
266}
267
268fn is_cargo_profile_dir(dir: &std::path::Path) -> bool {
273 dir.join(".cargo-lock").is_file()
274 || (dir.join(".fingerprint").is_dir() && dir.join("deps").is_dir())
275}
276
277pub fn in_cargo_output_dir(path: &std::path::Path) -> bool {
287 let Some(dir) = path.parent() else {
288 return false;
289 };
290 if is_cargo_profile_dir(dir) {
291 return true;
292 }
293 matches!(
294 dir.file_name().and_then(|name| name.to_str()),
295 Some("deps" | "examples")
296 ) && dir.parent().is_some_and(is_cargo_profile_dir)
297}
298
299fn choose_binary_path(current_exe: Option<&str>, which_raw: Option<&str>) -> String {
321 let is_build_artifact = |p: &str| {
326 p.split(['/', '\\'])
327 .collect::<Vec<_>>()
328 .windows(2)
329 .any(|w| w[0].ends_with("target") && matches!(w[1], "debug" | "release"))
330 || in_cargo_output_dir(std::path::Path::new(p))
331 };
332
333 if let Some(exe) = current_exe
335 && std::path::Path::new(exe).is_absolute()
336 && !is_build_artifact(exe)
337 {
338 return sanitize_exe_path(exe);
339 }
340
341 if let Some(raw) = which_raw {
344 let path = pick_best_binary_line(raw);
345 if std::path::Path::new(&path).is_absolute() && !is_transient_build_binary(&path) {
346 return sanitize_exe_path(&path);
347 }
348 }
349
350 if let Some(exe) = current_exe
352 && std::path::Path::new(exe).is_absolute()
353 {
354 return sanitize_exe_path(exe);
355 }
356
357 "lean-ctx".to_string()
359}
360
361fn pick_best_binary_line(raw: &str) -> String {
365 let lines: Vec<&str> = raw
366 .lines()
367 .map(str::trim)
368 .filter(|l| !l.is_empty())
369 .collect();
370 if lines.len() <= 1 {
371 return lines.first().unwrap_or(&"lean-ctx").to_string();
372 }
373 if cfg!(windows)
374 && let Some(cmd) = lines.iter().find(|l| {
375 std::path::Path::new(*l).extension().is_some_and(|ext| {
376 ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("exe")
377 })
378 })
379 {
380 return cmd.to_string();
381 }
382 lines[0].to_string()
383}
384
385fn sanitize_exe_path(path: &str) -> String {
386 let cleaned = path.trim_end_matches(" (deleted)");
387 if cfg!(windows) {
388 super::pathutil::normalize_tool_path(cleaned)
389 } else {
390 cleaned.to_string()
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn single_line_returns_as_is() {
400 assert_eq!(
401 pick_best_binary_line("/usr/bin/lean-ctx"),
402 "/usr/bin/lean-ctx"
403 );
404 }
405
406 #[test]
407 fn multiline_returns_first_line() {
408 let raw = "/usr/bin/lean-ctx\n/usr/local/bin/lean-ctx";
409 let result = pick_best_binary_line(raw);
410 assert_eq!(result, "/usr/bin/lean-ctx");
411 }
412
413 #[test]
414 fn empty_returns_fallback() {
415 assert_eq!(pick_best_binary_line(""), "lean-ctx");
416 }
417
418 #[test]
419 fn sanitize_removes_deleted_suffix() {
420 assert_eq!(
421 sanitize_exe_path("/usr/bin/lean-ctx (deleted)"),
422 "/usr/bin/lean-ctx"
423 );
424 }
425
426 #[test]
427 fn whitespace_lines_are_filtered() {
428 let raw = " /usr/bin/lean-ctx \n \n /usr/local/bin/lean-ctx ";
429 assert_eq!(pick_best_binary_line(raw), "/usr/bin/lean-ctx");
430 }
431
432 #[cfg(windows)]
433 #[test]
434 fn sanitize_normalizes_msys_path_on_windows() {
435 assert_eq!(
436 sanitize_exe_path("/c/Users/ABC/.local/bin/lean-ctx"),
437 "C:/Users/ABC/.local/bin/lean-ctx"
438 );
439 }
440
441 #[cfg(windows)]
442 #[test]
443 fn sanitize_keeps_native_windows_path() {
444 assert_eq!(
445 sanitize_exe_path(r"C:\Users\ABC\lean-ctx.exe"),
446 "C:/Users/ABC/lean-ctx.exe"
447 );
448 }
449
450 #[cfg(not(windows))]
451 #[test]
452 fn sanitize_unix_path_unchanged() {
453 assert_eq!(
454 sanitize_exe_path("/usr/local/bin/lean-ctx"),
455 "/usr/local/bin/lean-ctx"
456 );
457 }
458
459 #[test]
463 fn hook_binary_override_env_is_verbatim_and_blank_is_none() {
464 let _lock = crate::core::data_dir::test_env_lock();
465 crate::test_env::set_var("LEAN_CTX_HOOK_BINARY", "$HOME/.local/bin/lean-ctx");
466 assert_eq!(
467 hook_binary_override().as_deref(),
468 Some("$HOME/.local/bin/lean-ctx")
469 );
470 crate::test_env::set_var("LEAN_CTX_HOOK_BINARY", " ");
471 assert_eq!(hook_binary_override(), None);
472 crate::test_env::remove_var("LEAN_CTX_HOOK_BINARY");
473 }
474
475 #[test]
476 fn resolve_portable_binary_is_absolute() {
477 let resolved = resolve_portable_binary();
482 assert!(
483 std::path::Path::new(&resolved).is_absolute(),
484 "resolve_portable_binary must return an absolute path, got: {resolved}"
485 );
486 }
487
488 #[test]
489 fn nothing_resolvable_returns_bare_name() {
490 assert_eq!(choose_binary_path(None, None), "lean-ctx");
492 assert_eq!(choose_binary_path(Some("lean-ctx"), None), "lean-ctx");
494 }
495
496 #[cfg(not(windows))]
498 mod unix_paths {
499 use super::*;
500
501 #[test]
502 fn current_exe_beats_path_lookup() {
503 let chosen = choose_binary_path(
506 Some("/Users/dev/.local/bin/lean-ctx"),
507 Some("/opt/homebrew/bin/lean-ctx"),
508 );
509 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
510 }
511
512 #[test]
513 fn release_build_artifact_falls_back_to_path() {
514 let chosen = choose_binary_path(
517 Some("/work/lean-ctx/rust/target/release/lean-ctx"),
518 Some("/Users/dev/.local/bin/lean-ctx"),
519 );
520 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
521 }
522
523 #[test]
524 fn debug_build_artifact_falls_back_to_path() {
525 let chosen = choose_binary_path(
526 Some("/work/lean-ctx/rust/target/debug/deps/lean_ctx-abc123"),
527 Some("/usr/local/bin/lean-ctx"),
528 );
529 assert_eq!(chosen, "/usr/local/bin/lean-ctx");
530 }
531
532 #[test]
533 fn relocated_target_dir_falls_back_to_path() {
534 let chosen = choose_binary_path(
537 Some("/Users/dev/.cargo/shared-target/release/lean-ctx"),
538 Some("/Users/dev/.local/bin/lean-ctx"),
539 );
540 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
541 let chosen = choose_binary_path(
543 Some("/opt/lean-ctx/release/lean-ctx"),
544 Some("/Users/dev/.local/bin/lean-ctx"),
545 );
546 assert_eq!(chosen, "/opt/lean-ctx/release/lean-ctx");
547 }
548
549 #[test]
550 fn build_artifact_without_path_keeps_absolute_current_exe() {
551 let chosen =
554 choose_binary_path(Some("/work/lean-ctx/rust/target/release/lean-ctx"), None);
555 assert_eq!(chosen, "/work/lean-ctx/rust/target/release/lean-ctx");
556 }
557
558 #[test]
559 fn relative_current_exe_falls_back_to_path() {
560 let chosen = choose_binary_path(Some("lean-ctx"), Some("/usr/bin/lean-ctx"));
561 assert_eq!(chosen, "/usr/bin/lean-ctx");
562 }
563
564 #[test]
567 fn transient_build_binaries_are_recognised() {
568 for transient in [
569 "/work/lean-ctx/rust/target/debug/deps/lean_ctx_server-1a2b3c4d",
570 "/work/lean-ctx/rust/target/release/deps/lean_ctx-abc123",
571 "/work/rust/target/debug/examples/demo",
572 "/work/rust/target/debug/build/lean-ctx-0f/build-script-build",
573 "/work/rust/target/debug/incremental/x/lean-ctx",
574 "/work/rust/target/x86_64-unknown-linux-gnu/debug/deps/lean_ctx-1",
575 "/Users/dev/.cargo/shared-target/debug/deps/lean_ctx-1",
576 r"C:\work\rust\target\debug\deps\lean_ctx-1.exe",
577 ] {
578 assert!(is_transient_build_binary(transient), "{transient}");
579 }
580 for stable in [
581 "/work/lean-ctx/rust/target/release/lean-ctx",
582 "/work/lean-ctx/rust/target/debug/lean-ctx",
583 "/Users/dev/.local/bin/lean-ctx",
584 "/opt/build/deps/lean-ctx",
585 "/usr/local/bin/lean-ctx",
586 "lean-ctx",
587 ] {
588 assert!(!is_transient_build_binary(stable), "{stable}");
589 }
590 }
591
592 #[test]
594 fn transient_binary_in_unnamed_target_dir_is_recognised_on_disk() {
595 let tmp = tempfile::tempdir().unwrap();
596 let profile = tmp.path().join("target-agent-shared/debug");
597 std::fs::create_dir_all(profile.join("deps")).unwrap();
598 std::fs::write(profile.join(".cargo-lock"), "").unwrap();
599 let test_bin = profile.join("deps/lean_ctx_server-1a2b");
600 assert!(is_transient_build_binary(&test_bin.to_string_lossy()));
601 assert!(!is_transient_build_binary(
602 &profile.join("lean-ctx").to_string_lossy()
603 ));
604 }
605
606 #[test]
609 fn transient_path_entry_is_not_chosen() {
610 let chosen = choose_binary_path(
611 Some("/work/rust/target/debug/deps/lean_ctx_server-1a2b"),
612 Some("/work/rust/target/debug/deps/lean-ctx"),
613 );
614 assert_eq!(chosen, "/work/rust/target/debug/deps/lean_ctx_server-1a2b");
615 }
616
617 #[test]
620 fn transient_binary_refused_only_for_real_home() {
621 let tmp = tempfile::tempdir().unwrap();
622 let real = tmp.path().join("real");
623 let other = tmp.path().join("other");
624 std::fs::create_dir_all(&real).unwrap();
625 std::fs::create_dir_all(&other).unwrap();
626 let test_bin = "/work/rust/target/debug/deps/lean_ctx_server-1a2b3c4d";
627 assert!(transient_binary_for_real_home(test_bin, &real, Some(&real)));
628 assert!(transient_binary_for_real_home(
629 &format!("\"{test_bin}\""),
630 &real,
631 Some(&real)
632 ));
633 assert!(!transient_binary_for_real_home(
634 test_bin,
635 &other,
636 Some(&real)
637 ));
638 assert!(!transient_binary_for_real_home(test_bin, &real, None));
639 assert!(!transient_binary_for_real_home(
640 "/work/rust/target/release/lean-ctx",
641 &real,
642 Some(&real)
643 ));
644 assert!(!transient_binary_for_real_home(
645 "/Users/dev/.local/bin/lean-ctx",
646 &real,
647 Some(&real)
648 ));
649 }
650
651 #[test]
652 fn path_lookup_multiline_picks_first() {
653 let chosen = choose_binary_path(
654 None,
655 Some("/Users/dev/.local/bin/lean-ctx\n/opt/homebrew/bin/lean-ctx"),
656 );
657 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
658 }
659 }
660
661 #[cfg(unix)]
664 mod stable_shell_binary {
665 use super::super::*;
666 use std::path::{Path, PathBuf};
667
668 fn launcher(dir: &Path) -> PathBuf {
669 std::fs::create_dir_all(dir).unwrap();
670 let path = dir.join("lean-ctx");
671 std::fs::write(&path, "#!/bin/sh\n").unwrap();
672 path
673 }
674
675 #[test]
676 fn binary_in_a_path_dir_is_kept() {
677 let tmp = tempfile::tempdir().unwrap();
678 let bin = launcher(&tmp.path().join("bin"));
679 let other = launcher(&tmp.path().join("other"));
680 let dirs: Vec<PathBuf> =
681 vec![other.parent().unwrap().into(), bin.parent().unwrap().into()];
682 let bin = bin.to_string_lossy();
683 assert_eq!(stable_shell_binary_in(&bin, &dirs), bin);
684 }
685
686 #[test]
687 fn versioned_install_dir_resolves_to_the_path_launcher() {
688 let tmp = tempfile::tempdir().unwrap();
689 let cellar = launcher(&tmp.path().join("Cellar/lean-ctx/3.10.2/bin"));
690 let unrelated = launcher(&tmp.path().join("other"));
691 let shims = tmp.path().join("shims/lean-ctx");
692 std::fs::create_dir_all(shims.parent().unwrap()).unwrap();
693 std::os::unix::fs::symlink(&cellar, &shims).unwrap();
694 let dirs = vec![
695 unrelated.parent().unwrap().into(),
696 shims.parent().unwrap().into(),
697 ];
698 assert_eq!(
699 stable_shell_binary_in(&cellar.to_string_lossy(), &dirs),
700 shims.to_string_lossy()
701 );
702 }
703
704 #[test]
705 fn symlinked_path_dir_counts_as_on_path() {
706 let tmp = tempfile::tempdir().unwrap();
707 let bin = launcher(&tmp.path().join("real"));
708 let link = tmp.path().join("link");
709 std::os::unix::fs::symlink(bin.parent().unwrap(), &link).unwrap();
710 let bin = bin.to_string_lossy();
711 assert_eq!(stable_shell_binary_in(&bin, &[link]), bin);
712 }
713
714 #[test]
715 fn no_launcher_on_path_keeps_the_binary() {
716 let tmp = tempfile::tempdir().unwrap();
717 let bin = launcher(&tmp.path().join("apps/3.10.2"));
718 let bin = bin.to_string_lossy();
719 assert_eq!(stable_shell_binary_in(&bin, &[tmp.path().join("x")]), bin);
720 assert_eq!(stable_shell_binary_in("lean-ctx", &[]), "lean-ctx");
721 }
722
723 #[test]
724 fn unrelated_public_launcher_keeps_the_selected_binary() {
725 let tmp = tempfile::tempdir().unwrap();
726 let selected = launcher(&tmp.path().join("private-install"));
727 let public = launcher(&tmp.path().join("public-bin"));
728 let selected = selected.to_string_lossy();
729 assert_eq!(
730 stable_shell_binary_in(&selected, &[public.parent().unwrap().into()]),
731 selected
732 );
733 }
734
735 fn shim_file(launcher: &Path, target: &Path) {
736 let body = format!("path = \"{}\"\nargs = \n", target.display());
737 std::fs::write(launcher.with_extension("shim"), body).unwrap();
738 }
739
740 #[test]
741 fn shim_file_naming_the_binary_verifies_the_launcher() {
742 let tmp = tempfile::tempdir().unwrap();
743 let exe = launcher(&tmp.path().join("apps/lean-ctx/3.10.4"));
744 let shim = launcher(&tmp.path().join("shims"));
745 shim_file(&shim, &exe);
746 assert_eq!(
747 stable_shell_binary_in(&exe.to_string_lossy(), &[shim.parent().unwrap().into()]),
748 shim.to_string_lossy()
749 );
750 }
751
752 #[test]
753 fn shim_file_naming_another_binary_is_ignored() {
754 let tmp = tempfile::tempdir().unwrap();
755 let exe = launcher(&tmp.path().join("private-install"));
756 let public = launcher(&tmp.path().join("apps/lean-ctx/3.10.4"));
757 let shim = launcher(&tmp.path().join("shims"));
758 shim_file(&shim, &public);
759 let exe = exe.to_string_lossy();
760 assert_eq!(
761 stable_shell_binary_in(&exe, &[shim.parent().unwrap().into()]),
762 exe
763 );
764 }
765
766 #[test]
767 fn oversized_shim_file_is_ignored() {
768 let tmp = tempfile::tempdir().unwrap();
769 let exe = launcher(&tmp.path().join("apps/lean-ctx/3.10.4"));
770 let shim = launcher(&tmp.path().join("shims"));
771 let padding = " ".repeat(MAX_SHIM_BYTES as usize);
772 let body = format!("{padding}\npath = \"{}\"\n", exe.display());
773 std::fs::write(shim.with_extension("shim"), body).unwrap();
774 let exe = exe.to_string_lossy();
775 assert_eq!(
776 stable_shell_binary_in(&exe, &[shim.parent().unwrap().into()]),
777 exe
778 );
779 }
780
781 #[test]
782 fn version_manager_shim_owning_the_install_tree_is_used() {
783 let tmp = tempfile::tempdir().unwrap();
784 let data = tmp.path().join("mise");
785 let exe = launcher(&data.join("installs/lean-ctx/3.10.4/bin"));
786 let shim = launcher(&data.join("shims"));
787 assert_eq!(
788 stable_shell_binary_in(&exe.to_string_lossy(), &[shim.parent().unwrap().into()]),
789 shim.to_string_lossy()
790 );
791 }
792
793 #[test]
794 fn version_manager_shim_of_another_tree_is_ignored() {
795 let tmp = tempfile::tempdir().unwrap();
796 let exe = launcher(&tmp.path().join("private/installs/lean-ctx/3.10.4/bin"));
797 let data = tmp.path().join("mise");
798 launcher(&data.join("installs/lean-ctx/3.10.4/bin"));
799 let shim = launcher(&data.join("shims"));
800 let exe = exe.to_string_lossy();
801 assert_eq!(
802 stable_shell_binary_in(&exe, &[shim.parent().unwrap().into()]),
803 exe
804 );
805 }
806 }
807
808 #[cfg(windows)]
811 #[test]
812 fn scoop_versioned_exe_resolves_to_the_shim() {
813 let tmp = tempfile::tempdir().unwrap();
814 let apps = tmp.path().join("apps").join("lean-ctx").join("3.10.4");
815 let shims = tmp.path().join("shims");
816 for dir in [&apps, &shims] {
817 std::fs::create_dir_all(dir).unwrap();
818 std::fs::write(dir.join("lean-ctx.exe"), b"").unwrap();
819 }
820 let exe = apps.join("lean-ctx.exe");
821 std::fs::write(
822 shims.join("lean-ctx.shim"),
823 format!("path = \"{}\"\r\n", exe.display()),
824 )
825 .unwrap();
826 assert_eq!(
827 stable_shell_binary_in(&exe.to_string_lossy(), &[shims.clone()]),
828 sanitize_exe_path(&shims.join("lean-ctx.exe").to_string_lossy())
829 );
830 }
831
832 #[cfg(windows)]
835 #[test]
836 fn unverified_scoop_shim_keeps_the_selected_executable() {
837 let tmp = tempfile::tempdir().unwrap();
838 let apps = tmp.path().join("apps").join("lean-ctx").join("3.10.4");
839 let shims = tmp.path().join("shims");
840 for dir in [&apps, &shims] {
841 std::fs::create_dir_all(dir).unwrap();
842 std::fs::write(dir.join("lean-ctx.exe"), b"").unwrap();
843 }
844 let exe = apps.join("lean-ctx.exe");
845 assert_eq!(
846 stable_shell_binary_in(&exe.to_string_lossy(), &[shims.clone()]),
847 exe.to_string_lossy()
848 );
849 }
850
851 #[test]
852 fn agent_binary_is_absolute() {
853 let resolved = resolve_agent_binary();
856 assert!(
857 std::path::Path::new(&resolved).is_absolute(),
858 "resolve_agent_binary must return an absolute path, got: {resolved}"
859 );
860 }
861}