1use crate::{PawanError, Result};
33use std::path::PathBuf;
34use std::process::Command;
35
36#[derive(Debug, Clone, Default)]
38pub struct BootstrapOptions {
39 pub skip_mise: bool,
41 pub skip_native: bool,
43 pub include_deagle: bool,
48 pub force_reinstall: bool,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum BootstrapStepStatus {
56 AlreadyInstalled,
58 Installed,
60 Skipped(String),
62 Failed(String),
64}
65
66#[derive(Debug, Clone)]
68pub struct BootstrapStep {
69 pub name: String,
70 pub status: BootstrapStepStatus,
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct BootstrapReport {
76 pub steps: Vec<BootstrapStep>,
77}
78
79impl BootstrapReport {
80 pub fn all_ok(&self) -> bool {
83 !self
84 .steps
85 .iter()
86 .any(|s| matches!(s.status, BootstrapStepStatus::Failed(_)))
87 }
88
89 pub fn installed_count(&self) -> usize {
92 self.steps
93 .iter()
94 .filter(|s| matches!(s.status, BootstrapStepStatus::Installed))
95 .count()
96 }
97
98 pub fn already_installed_count(&self) -> usize {
100 self.steps
101 .iter()
102 .filter(|s| matches!(s.status, BootstrapStepStatus::AlreadyInstalled))
103 .count()
104 }
105
106 pub fn summary(&self) -> String {
108 let installed = self.installed_count();
109 let existing = self.already_installed_count();
110 let failed = self
111 .steps
112 .iter()
113 .filter(|s| matches!(s.status, BootstrapStepStatus::Failed(_)))
114 .count();
115 if failed > 0 {
116 format!(
117 "{} installed, {} already present, {} failed",
118 installed, existing, failed
119 )
120 } else if installed == 0 {
121 format!("all {} deps already present", existing)
122 } else {
123 format!("{} installed, {} already present", installed, existing)
124 }
125 }
126}
127
128pub const NATIVE_TOOLS: &[&str] = &["rg", "fd", "sd", "ast-grep", "erd"];
131
132fn mise_package_name(binary: &str) -> &str {
135 match binary {
136 "erd" => "erdtree",
137 "rg" => "ripgrep",
138 "ast-grep" | "sg" => "ast-grep",
139 other => other,
140 }
141}
142
143pub fn binary_exists(name: &str) -> bool {
145 which::which(name).is_ok()
146}
147
148pub fn is_bootstrapped() -> bool {
152 binary_exists("mise") && NATIVE_TOOLS.iter().all(|t| binary_exists(t))
153}
154
155pub fn missing_deps() -> Vec<String> {
159 let mut missing = Vec::new();
160 if !binary_exists("mise") {
161 missing.push("mise".to_string());
162 }
163 for tool in NATIVE_TOOLS {
164 if !binary_exists(tool) {
165 missing.push((*tool).to_string());
166 }
167 }
168 missing
169}
170
171pub fn marker_path() -> PathBuf {
175 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
176 PathBuf::from(home).join(".pawan").join(".bootstrapped")
177}
178
179pub fn ensure_deagle(force: bool) -> BootstrapStep {
183 if !force && binary_exists("deagle") {
184 return BootstrapStep {
185 name: "deagle".into(),
186 status: BootstrapStepStatus::AlreadyInstalled,
187 };
188 }
189
190 let output = Command::new("cargo")
191 .args(["install", "--locked", "deagle"])
192 .output();
193
194 let status = match output {
195 Ok(o) if o.status.success() => BootstrapStepStatus::Installed,
196 Ok(o) => {
197 let stderr = String::from_utf8_lossy(&o.stderr);
198 let brief: String = stderr.chars().take(200).collect();
199 BootstrapStepStatus::Failed(format!("cargo install deagle failed: {}", brief))
200 }
201 Err(e) => {
202 BootstrapStepStatus::Failed(format!("cargo install deagle spawn failed: {}", e))
203 }
204 };
205
206 BootstrapStep {
207 name: "deagle".into(),
208 status,
209 }
210}
211
212pub fn ensure_mise() -> BootstrapStep {
225 if binary_exists("mise") {
226 return BootstrapStep {
227 name: "mise".into(),
228 status: BootstrapStepStatus::AlreadyInstalled,
229 };
230 }
231 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
234 let local = format!("{}/.local/bin/mise", home);
235 if std::path::Path::new(&local).exists() {
236 return BootstrapStep {
237 name: "mise".into(),
238 status: BootstrapStepStatus::AlreadyInstalled,
239 };
240 }
241
242 let output = Command::new("cargo")
243 .args(["install", "--locked", "mise"])
244 .output();
245
246 let status = match output {
247 Ok(o) if o.status.success() => BootstrapStepStatus::Installed,
248 Ok(o) => {
249 let stderr = String::from_utf8_lossy(&o.stderr);
250 let brief: String = stderr.chars().take(200).collect();
251 BootstrapStepStatus::Failed(format!("cargo install mise failed: {}", brief))
252 }
253 Err(e) => BootstrapStepStatus::Failed(format!("cargo install mise spawn failed: {}", e)),
254 };
255
256 BootstrapStep {
257 name: "mise".into(),
258 status,
259 }
260}
261
262pub fn ensure_native_tool(tool: &str) -> BootstrapStep {
266 if binary_exists(tool) {
267 return BootstrapStep {
268 name: tool.into(),
269 status: BootstrapStepStatus::AlreadyInstalled,
270 };
271 }
272
273 let mise_bin = if binary_exists("mise") {
274 "mise".to_string()
275 } else {
276 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
277 let local = format!("{}/.local/bin/mise", home);
278 if std::path::Path::new(&local).exists() {
279 local
280 } else {
281 return BootstrapStep {
282 name: tool.into(),
283 status: BootstrapStepStatus::Skipped("mise not present".into()),
284 };
285 }
286 };
287
288 let pkg = mise_package_name(tool);
289 let install = Command::new(&mise_bin)
290 .args(["install", pkg, "-y"])
291 .output();
292
293 let status = match install {
294 Ok(o) if o.status.success() => {
295 let _ = Command::new(&mise_bin)
298 .args(["use", "--global", pkg])
299 .output();
300 BootstrapStepStatus::Installed
301 }
302 Ok(o) => {
303 let stderr = String::from_utf8_lossy(&o.stderr);
304 let brief: String = stderr.chars().take(200).collect();
305 BootstrapStepStatus::Failed(format!("mise install {} failed: {}", tool, brief))
306 }
307 Err(e) => {
308 BootstrapStepStatus::Failed(format!("mise install {} spawn failed: {}", tool, e))
309 }
310 };
311
312 BootstrapStep {
313 name: tool.into(),
314 status,
315 }
316}
317
318pub fn ensure_deps(opts: BootstrapOptions) -> BootstrapReport {
327 let mut report = BootstrapReport::default();
328
329 if !opts.skip_mise {
330 report.steps.push(ensure_mise());
331 }
332 if !opts.skip_native {
333 for tool in NATIVE_TOOLS {
334 report.steps.push(ensure_native_tool(tool));
335 }
336 }
337 if opts.include_deagle {
338 report.steps.push(ensure_deagle(opts.force_reinstall));
339 }
340
341 if report.all_ok() {
344 let path = marker_path();
345 if let Some(parent) = path.parent() {
346 let _ = std::fs::create_dir_all(parent);
347 }
348 let _ = std::fs::write(&path, chrono::Utc::now().to_rfc3339());
349 }
350
351 report
352}
353
354pub fn uninstall(purge_deagle: bool) -> Result<()> {
358 let path = marker_path();
359 if path.exists() {
360 std::fs::remove_file(&path)
361 .map_err(|e| PawanError::Config(format!("remove marker: {}", e)))?;
362 }
363
364 if purge_deagle && binary_exists("deagle") {
365 let output = Command::new("cargo")
366 .args(["uninstall", "deagle"])
367 .output()
368 .map_err(|e| PawanError::Config(format!("cargo uninstall spawn: {}", e)))?;
369 if !output.status.success() {
370 return Err(PawanError::Config(format!(
371 "cargo uninstall deagle failed: {}",
372 String::from_utf8_lossy(&output.stderr)
373 )));
374 }
375 }
376
377 Ok(())
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn bootstrap_report_default_is_all_ok() {
386 let report = BootstrapReport::default();
389 assert!(report.all_ok());
390 assert_eq!(report.installed_count(), 0);
391 assert_eq!(report.already_installed_count(), 0);
392 }
393
394 #[test]
395 fn bootstrap_report_with_failed_step_is_not_ok() {
396 let report = BootstrapReport {
397 steps: vec![BootstrapStep {
398 name: "deagle".into(),
399 status: BootstrapStepStatus::Failed("network".into()),
400 }],
401 };
402 assert!(!report.all_ok());
403 assert_eq!(report.installed_count(), 0);
404 }
405
406 #[test]
407 fn bootstrap_report_skipped_step_is_not_a_failure() {
408 let report = BootstrapReport {
410 steps: vec![BootstrapStep {
411 name: "mise".into(),
412 status: BootstrapStepStatus::Skipped("caller skipped".into()),
413 }],
414 };
415 assert!(report.all_ok(), "skipped != failed");
416 }
417
418 #[test]
419 fn bootstrap_report_installed_count_excludes_already_installed() {
420 let report = BootstrapReport {
421 steps: vec![
422 BootstrapStep {
423 name: "a".into(),
424 status: BootstrapStepStatus::Installed,
425 },
426 BootstrapStep {
427 name: "b".into(),
428 status: BootstrapStepStatus::AlreadyInstalled,
429 },
430 BootstrapStep {
431 name: "c".into(),
432 status: BootstrapStepStatus::Installed,
433 },
434 ],
435 };
436 assert_eq!(report.installed_count(), 2);
437 assert_eq!(report.already_installed_count(), 1);
438 }
439
440 #[test]
441 fn bootstrap_report_summary_shows_counts() {
442 let report = BootstrapReport {
444 steps: vec![
445 BootstrapStep {
446 name: "mise".into(),
447 status: BootstrapStepStatus::AlreadyInstalled,
448 },
449 BootstrapStep {
450 name: "deagle".into(),
451 status: BootstrapStepStatus::Installed,
452 },
453 BootstrapStep {
454 name: "rg".into(),
455 status: BootstrapStepStatus::Failed("nope".into()),
456 },
457 ],
458 };
459 let s = report.summary();
460 assert!(s.contains("1 installed"));
461 assert!(s.contains("1 already present"));
462 assert!(s.contains("1 failed"));
463 }
464
465 #[test]
466 fn bootstrap_report_summary_all_present() {
467 let report = BootstrapReport {
468 steps: vec![
469 BootstrapStep {
470 name: "mise".into(),
471 status: BootstrapStepStatus::AlreadyInstalled,
472 },
473 BootstrapStep {
474 name: "deagle".into(),
475 status: BootstrapStepStatus::AlreadyInstalled,
476 },
477 ],
478 };
479 assert_eq!(report.summary(), "all 2 deps already present");
480 }
481
482 #[test]
483 fn native_tools_constant_is_5_well_known_tools() {
484 assert_eq!(NATIVE_TOOLS.len(), 5);
488 assert!(NATIVE_TOOLS.contains(&"rg"));
489 assert!(NATIVE_TOOLS.contains(&"fd"));
490 assert!(NATIVE_TOOLS.contains(&"sd"));
491 assert!(NATIVE_TOOLS.contains(&"ast-grep"));
492 assert!(NATIVE_TOOLS.contains(&"erd"));
493 }
494
495 #[test]
496 fn mise_package_name_handles_binary_name_mismatch() {
497 assert_eq!(mise_package_name("rg"), "ripgrep");
501 assert_eq!(mise_package_name("erd"), "erdtree");
502 assert_eq!(mise_package_name("fd"), "fd");
503 assert_eq!(mise_package_name("sd"), "sd");
504 assert_eq!(mise_package_name("ast-grep"), "ast-grep");
505 assert_eq!(mise_package_name("sg"), "ast-grep");
506 assert_eq!(mise_package_name("unknown-tool"), "unknown-tool");
508 }
509
510 #[test]
511 fn marker_path_is_under_home_dot_pawan() {
512 let path = marker_path();
515 let s = path.to_string_lossy();
516 assert!(s.ends_with(".pawan/.bootstrapped"));
517 }
518
519 #[test]
520 fn ensure_deagle_is_idempotent_when_already_on_path() {
521 if !binary_exists("deagle") {
524 return;
527 }
528 let step = ensure_deagle(false);
529 assert_eq!(step.name, "deagle");
530 assert_eq!(
531 step.status,
532 BootstrapStepStatus::AlreadyInstalled,
533 "second call must be a no-op when deagle is present"
534 );
535 }
536
537 #[test]
538 fn ensure_mise_is_idempotent_when_already_on_path() {
539 if !binary_exists("mise") {
540 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
543 if !std::path::Path::new(&format!("{}/.local/bin/mise", home)).exists() {
544 return; }
546 }
547 let step = ensure_mise();
548 assert_eq!(step.name, "mise");
549 assert_eq!(step.status, BootstrapStepStatus::AlreadyInstalled);
550 }
551
552 #[test]
553 fn ensure_native_tool_is_idempotent_when_already_on_path() {
554 for tool in NATIVE_TOOLS {
556 if binary_exists(tool) {
557 let step = ensure_native_tool(tool);
558 assert_eq!(step.name, *tool);
559 assert_eq!(
560 step.status,
561 BootstrapStepStatus::AlreadyInstalled,
562 "ensure_native_tool({}) must be idempotent",
563 tool
564 );
565 return;
566 }
567 }
568 }
570
571 #[serial_test::serial(pawan_session_tests)]
572 #[test]
573 fn missing_deps_is_empty_on_fully_bootstrapped_box() {
574 if !is_bootstrapped() {
575 return;
576 }
577 assert!(
578 missing_deps().is_empty(),
579 "is_bootstrapped() and missing_deps() must agree"
580 );
581 }
582
583 #[test]
584 fn ensure_deps_with_all_skips_writes_empty_report() {
585 let opts = BootstrapOptions {
589 skip_mise: true,
590 skip_native: true,
591 include_deagle: false,
592 force_reinstall: false,
593 };
594 let report = ensure_deps(opts);
595 assert_eq!(report.steps.len(), 0);
596 assert!(report.all_ok());
597 assert_eq!(report.installed_count(), 0);
598 }
599
600 #[test]
601 fn default_options_do_not_include_deagle() {
602 let opts = BootstrapOptions::default();
606 assert!(!opts.include_deagle, "default must exclude deagle install");
607 assert!(!opts.skip_mise, "default installs mise");
608 assert!(!opts.skip_native, "default installs native tools");
609 assert!(!opts.force_reinstall);
610 }
611
612 #[test]
613 fn is_bootstrapped_does_not_require_deagle() {
614 if binary_exists("mise") && NATIVE_TOOLS.iter().all(|t| binary_exists(t)) {
622 assert!(is_bootstrapped());
623 }
624 assert!(
626 !missing_deps().iter().any(|d| d == "deagle"),
627 "missing_deps must not mention deagle"
628 );
629 }
630
631 #[serial_test::serial(pawan_session_tests)]
632 #[test]
633 fn uninstall_without_marker_file_is_ok() {
634 use std::sync::Mutex;
637 static LOCK: Mutex<()> = Mutex::new(());
638 let _guard = LOCK.lock().unwrap();
639
640 let tmp = tempfile::TempDir::new().unwrap();
642 let prev_home = std::env::var("HOME").ok();
643 std::env::set_var("HOME", tmp.path());
644
645 let result = uninstall(false);
646
647 if let Some(h) = prev_home {
648 std::env::set_var("HOME", h);
649 }
650
651 assert!(result.is_ok());
652 }
653
654 #[test]
655 fn bootstrap_report_summary_with_installs_only() {
656 let report = BootstrapReport {
657 steps: vec![
658 BootstrapStep {
659 name: "mise".into(),
660 status: BootstrapStepStatus::Installed,
661 },
662 BootstrapStep {
663 name: "rg".into(),
664 status: BootstrapStepStatus::Installed,
665 },
666 ],
667 };
668 let s = report.summary();
669 assert!(s.contains("2 installed"));
670 assert!(s.contains("0 already present"));
671 }
672
673 #[test]
674 fn missing_deps_lists_missing_mise() {
675 use std::sync::Mutex;
676 static LOCK: Mutex<()> = Mutex::new(());
677 let _guard = LOCK.lock().unwrap();
678
679 let tmp = tempfile::TempDir::new().unwrap();
680 let prev_path = std::env::var("PATH").ok();
681 std::env::set_var("PATH", tmp.path());
682
683 let missing = missing_deps();
684
685 if let Some(p) = prev_path {
686 std::env::set_var("PATH", p);
687 }
688
689 assert!(missing.contains(&"mise".to_string()));
690 }
691
692 #[test]
693 fn missing_deps_lists_missing_native_tools() {
694 use std::sync::Mutex;
695 static LOCK: Mutex<()> = Mutex::new(());
696 let _guard = LOCK.lock().unwrap();
697
698 let tmp = tempfile::TempDir::new().unwrap();
699 let prev_path = std::env::var("PATH").ok();
700 std::env::set_var("PATH", tmp.path());
701
702 let missing = missing_deps();
703
704 if let Some(p) = prev_path {
705 std::env::set_var("PATH", p);
706 }
707
708 for tool in NATIVE_TOOLS {
709 assert!(missing.contains(&tool.to_string()));
710 }
711 }
712
713 #[test]
714 fn ensure_deagle_with_force_reinstall_attempts_install() {
715 use std::sync::Mutex;
716 static LOCK: Mutex<()> = Mutex::new(());
717 let _guard = LOCK.lock().unwrap();
718
719 let tmp = tempfile::TempDir::new().unwrap();
720 let prev_path = std::env::var("PATH").ok();
721 std::env::set_var("PATH", tmp.path());
722
723 let step = ensure_deagle(true);
724
725 if let Some(p) = prev_path {
726 std::env::set_var("PATH", p);
727 }
728
729 assert_eq!(step.name, "deagle");
730 assert!(matches!(step.status, BootstrapStepStatus::Installed | BootstrapStepStatus::Failed(_)));
732 }
733
734 #[test]
735 fn ensure_mise_falls_back_to_local_bin() {
736 use std::sync::Mutex;
737 static LOCK: Mutex<()> = Mutex::new(());
738 let _guard = LOCK.lock().unwrap();
739
740 let tmp = tempfile::TempDir::new().unwrap();
741 let prev_path = std::env::var("PATH").ok();
742 std::env::set_var("PATH", tmp.path());
743
744 let home = tmp.path();
745 let local_bin = home.join(".local/bin");
746 std::fs::create_dir_all(&local_bin).unwrap();
747 std::fs::write(local_bin.join("mise"), "#!/bin/sh\necho mise").unwrap();
748
749 let prev_home = std::env::var("HOME").ok();
750 std::env::set_var("HOME", home);
751
752 let step = ensure_mise();
753
754 if let Some(h) = prev_home {
755 std::env::set_var("HOME", h);
756 }
757 if let Some(p) = prev_path {
758 std::env::set_var("PATH", p);
759 }
760
761 assert_eq!(step.name, "mise");
762 assert_eq!(step.status, BootstrapStepStatus::AlreadyInstalled);
763 }
764
765 #[test]
766 fn ensure_native_tool_skips_when_mise_missing() {
767 use std::sync::Mutex;
768 static LOCK: Mutex<()> = Mutex::new(());
769 let _guard = LOCK.lock().unwrap();
770
771 let tmp = tempfile::TempDir::new().unwrap();
772 let prev_path = std::env::var("PATH").ok();
773 std::env::set_var("PATH", tmp.path());
774
775 let home = tmp.path();
776 let prev_home = std::env::var("HOME").ok();
777 std::env::set_var("HOME", home);
778
779 let step = ensure_native_tool("rg");
780
781 if let Some(h) = prev_home {
782 std::env::set_var("HOME", h);
783 }
784 if let Some(p) = prev_path {
785 std::env::set_var("PATH", p);
786 }
787
788 assert_eq!(step.name, "rg");
789 assert!(matches!(step.status, BootstrapStepStatus::Skipped(_)));
790 }
791
792 #[test]
793 fn ensure_native_tool_uses_local_mise() {
794 use std::sync::Mutex;
795 static LOCK: Mutex<()> = Mutex::new(());
796 let _guard = LOCK.lock().unwrap();
797
798 let tmp = tempfile::TempDir::new().unwrap();
799 let prev_path = std::env::var("PATH").ok();
800 std::env::set_var("PATH", tmp.path());
801
802 let home = tmp.path();
803 let local_bin = home.join(".local/bin");
804 std::fs::create_dir_all(&local_bin).unwrap();
805 std::fs::write(local_bin.join("mise"), "#!/bin/sh\necho mise").unwrap();
806
807 let prev_home = std::env::var("HOME").ok();
808 std::env::set_var("HOME", home);
809
810 let step = ensure_native_tool("rg");
811
812 if let Some(h) = prev_home {
813 std::env::set_var("HOME", h);
814 }
815 if let Some(p) = prev_path {
816 std::env::set_var("PATH", p);
817 }
818
819 assert_eq!(step.name, "rg");
820 assert!(matches!(step.status, BootstrapStepStatus::Installed | BootstrapStepStatus::Failed(_)));
822 }
823
824 #[serial_test::serial(pawan_session_tests)]
825 #[test]
826 fn ensure_deps_writes_marker_on_success() {
827 use std::sync::Mutex;
828 static LOCK: Mutex<()> = Mutex::new(());
829 let _guard = LOCK.lock().unwrap();
830
831 let tmp = tempfile::TempDir::new().unwrap();
832 let prev_home = std::env::var("HOME").ok();
833 std::env::set_var("HOME", tmp.path());
834
835 let opts = BootstrapOptions {
836 skip_mise: true,
837 skip_native: true,
838 include_deagle: false,
839 force_reinstall: false,
840 };
841 let report = ensure_deps(opts);
842
843 if let Some(h) = prev_home {
844 std::env::set_var("HOME", h);
845 }
846
847 assert!(report.all_ok());
848 let marker = tmp.path().join(".pawan/.bootstrapped");
849 assert!(marker.exists(), "marker must be written on success");
850 }
851
852 #[serial_test::serial(pawan_session_tests)]
853 #[test]
854 fn ensure_deps_includes_deagle_when_requested() {
855 use std::sync::Mutex;
856 static LOCK: Mutex<()> = Mutex::new(());
857 let _guard = LOCK.lock().unwrap();
858
859 let tmp = tempfile::TempDir::new().unwrap();
860 let prev_home = std::env::var("HOME").ok();
861 std::env::set_var("HOME", tmp.path());
862
863 let opts = BootstrapOptions {
864 skip_mise: true,
865 skip_native: true,
866 include_deagle: true,
867 force_reinstall: false,
868 };
869 let report = ensure_deps(opts);
870
871 if let Some(h) = prev_home {
872 std::env::set_var("HOME", h);
873 }
874
875 assert_eq!(report.steps.len(), 1);
876 assert_eq!(report.steps[0].name, "deagle");
877 }
878
879 #[serial_test::serial(pawan_session_tests)]
880 #[test]
881 fn ensure_deps_with_force_reinstall() {
882 use std::sync::Mutex;
883 static LOCK: Mutex<()> = Mutex::new(());
884 let _guard = LOCK.lock().unwrap();
885
886 let tmp = tempfile::TempDir::new().unwrap();
887 let prev_home = std::env::var("HOME").ok();
888 std::env::set_var("HOME", tmp.path());
889
890 let opts = BootstrapOptions {
891 skip_mise: true,
892 skip_native: true,
893 include_deagle: true,
894 force_reinstall: true,
895 };
896 let report = ensure_deps(opts);
897
898 if let Some(h) = prev_home {
899 std::env::set_var("HOME", h);
900 }
901
902 assert_eq!(report.steps.len(), 1);
903 assert_eq!(report.steps[0].name, "deagle");
904 }
905
906 #[serial_test::serial(pawan_session_tests)]
907 #[test]
908 fn uninstall_removes_marker_file() {
909 use std::sync::Mutex;
910 static LOCK: Mutex<()> = Mutex::new(());
911 let _guard = LOCK.lock().unwrap();
912
913 let tmp = tempfile::TempDir::new().unwrap();
914 let prev_home = std::env::var("HOME").ok();
915 std::env::set_var("HOME", tmp.path());
916
917 let marker = tmp.path().join(".pawan/.bootstrapped");
918 std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
919 std::fs::write(&marker, "2024-01-01T00:00:00Z").unwrap();
920
921 let result = uninstall(false);
922
923 if let Some(h) = prev_home {
924 std::env::set_var("HOME", h);
925 }
926
927 assert!(result.is_ok());
928 assert!(!marker.exists(), "marker must be removed");
929 }
930
931 #[serial_test::serial(pawan_session_tests)]
932 #[test]
933 fn uninstall_with_purge_deagle_attempts_uninstall() {
934 use std::sync::Mutex;
935 static LOCK: Mutex<()> = Mutex::new(());
936 let _guard = LOCK.lock().unwrap();
937
938 let tmp = tempfile::TempDir::new().unwrap();
939 let prev_home = std::env::var("HOME").ok();
940 std::env::set_var("HOME", tmp.path());
941
942 let marker = tmp.path().join(".pawan/.bootstrapped");
943 std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
944 std::fs::write(&marker, "2024-01-01T00:00:00Z").unwrap();
945
946 let result = uninstall(true);
947
948 if let Some(h) = prev_home {
949 std::env::set_var("HOME", h);
950 }
951
952 assert!(result.is_ok() || result.is_err());
954 assert!(!marker.exists(), "marker must be removed regardless");
955 }}