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) => BootstrapStepStatus::Failed(format!("cargo install deagle spawn failed: {}", e)),
202 };
203
204 BootstrapStep {
205 name: "deagle".into(),
206 status,
207 }
208}
209
210pub fn ensure_mise() -> BootstrapStep {
223 if binary_exists("mise") {
224 return BootstrapStep {
225 name: "mise".into(),
226 status: BootstrapStepStatus::AlreadyInstalled,
227 };
228 }
229 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
232 let local = format!("{}/.local/bin/mise", home);
233 if std::path::Path::new(&local).exists() {
234 return BootstrapStep {
235 name: "mise".into(),
236 status: BootstrapStepStatus::AlreadyInstalled,
237 };
238 }
239
240 let output = Command::new("cargo")
241 .args(["install", "--locked", "mise"])
242 .output();
243
244 let status = match output {
245 Ok(o) if o.status.success() => BootstrapStepStatus::Installed,
246 Ok(o) => {
247 let stderr = String::from_utf8_lossy(&o.stderr);
248 let brief: String = stderr.chars().take(200).collect();
249 BootstrapStepStatus::Failed(format!("cargo install mise failed: {}", brief))
250 }
251 Err(e) => BootstrapStepStatus::Failed(format!("cargo install mise spawn failed: {}", e)),
252 };
253
254 BootstrapStep {
255 name: "mise".into(),
256 status,
257 }
258}
259
260pub fn ensure_native_tool(tool: &str) -> BootstrapStep {
264 if binary_exists(tool) {
265 return BootstrapStep {
266 name: tool.into(),
267 status: BootstrapStepStatus::AlreadyInstalled,
268 };
269 }
270
271 let mise_bin = if binary_exists("mise") {
272 "mise".to_string()
273 } else {
274 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
275 let local = format!("{}/.local/bin/mise", home);
276 if std::path::Path::new(&local).exists() {
277 local
278 } else {
279 return BootstrapStep {
280 name: tool.into(),
281 status: BootstrapStepStatus::Skipped("mise not present".into()),
282 };
283 }
284 };
285
286 let pkg = mise_package_name(tool);
287 let install = Command::new(&mise_bin)
288 .args(["install", pkg, "-y"])
289 .output();
290
291 let status = match install {
292 Ok(o) if o.status.success() => {
293 let _ = Command::new(&mise_bin)
296 .args(["use", "--global", pkg])
297 .output();
298 BootstrapStepStatus::Installed
299 }
300 Ok(o) => {
301 let stderr = String::from_utf8_lossy(&o.stderr);
302 let brief: String = stderr.chars().take(200).collect();
303 BootstrapStepStatus::Failed(format!("mise install {} failed: {}", tool, brief))
304 }
305 Err(e) => BootstrapStepStatus::Failed(format!("mise install {} spawn failed: {}", tool, e)),
306 };
307
308 BootstrapStep {
309 name: tool.into(),
310 status,
311 }
312}
313
314pub fn ensure_deps(opts: BootstrapOptions) -> BootstrapReport {
323 let mut report = BootstrapReport::default();
324
325 if !opts.skip_mise {
326 report.steps.push(ensure_mise());
327 }
328 if !opts.skip_native {
329 for tool in NATIVE_TOOLS {
330 report.steps.push(ensure_native_tool(tool));
331 }
332 }
333 if opts.include_deagle {
334 report.steps.push(ensure_deagle(opts.force_reinstall));
335 }
336
337 if report.all_ok() {
340 let path = marker_path();
341 if let Some(parent) = path.parent() {
342 let _ = std::fs::create_dir_all(parent);
343 }
344 let _ = std::fs::write(&path, chrono::Utc::now().to_rfc3339());
345 }
346
347 report
348}
349
350pub fn uninstall(purge_deagle: bool) -> Result<()> {
354 let path = marker_path();
355 if path.exists() {
356 std::fs::remove_file(&path)
357 .map_err(|e| PawanError::Config(format!("remove marker: {}", e)))?;
358 }
359
360 if purge_deagle && binary_exists("deagle") {
361 let output = Command::new("cargo")
362 .args(["uninstall", "deagle"])
363 .output()
364 .map_err(|e| PawanError::Config(format!("cargo uninstall spawn: {}", e)))?;
365 if !output.status.success() {
366 return Err(PawanError::Config(format!(
367 "cargo uninstall deagle failed: {}",
368 String::from_utf8_lossy(&output.stderr)
369 )));
370 }
371 }
372
373 Ok(())
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn bootstrap_report_default_is_all_ok() {
382 let report = BootstrapReport::default();
385 assert!(report.all_ok());
386 assert_eq!(report.installed_count(), 0);
387 assert_eq!(report.already_installed_count(), 0);
388 }
389
390 #[test]
391 fn bootstrap_report_with_failed_step_is_not_ok() {
392 let report = BootstrapReport {
393 steps: vec![BootstrapStep {
394 name: "deagle".into(),
395 status: BootstrapStepStatus::Failed("network".into()),
396 }],
397 };
398 assert!(!report.all_ok());
399 assert_eq!(report.installed_count(), 0);
400 }
401
402 #[test]
403 fn bootstrap_report_skipped_step_is_not_a_failure() {
404 let report = BootstrapReport {
406 steps: vec![BootstrapStep {
407 name: "mise".into(),
408 status: BootstrapStepStatus::Skipped("caller skipped".into()),
409 }],
410 };
411 assert!(report.all_ok(), "skipped != failed");
412 }
413
414 #[test]
415 fn bootstrap_report_installed_count_excludes_already_installed() {
416 let report = BootstrapReport {
417 steps: vec![
418 BootstrapStep {
419 name: "a".into(),
420 status: BootstrapStepStatus::Installed,
421 },
422 BootstrapStep {
423 name: "b".into(),
424 status: BootstrapStepStatus::AlreadyInstalled,
425 },
426 BootstrapStep {
427 name: "c".into(),
428 status: BootstrapStepStatus::Installed,
429 },
430 ],
431 };
432 assert_eq!(report.installed_count(), 2);
433 assert_eq!(report.already_installed_count(), 1);
434 }
435
436 #[test]
437 fn bootstrap_report_summary_shows_counts() {
438 let report = BootstrapReport {
440 steps: vec![
441 BootstrapStep {
442 name: "mise".into(),
443 status: BootstrapStepStatus::AlreadyInstalled,
444 },
445 BootstrapStep {
446 name: "deagle".into(),
447 status: BootstrapStepStatus::Installed,
448 },
449 BootstrapStep {
450 name: "rg".into(),
451 status: BootstrapStepStatus::Failed("nope".into()),
452 },
453 ],
454 };
455 let s = report.summary();
456 assert!(s.contains("1 installed"));
457 assert!(s.contains("1 already present"));
458 assert!(s.contains("1 failed"));
459 }
460
461 #[test]
462 fn bootstrap_report_summary_all_present() {
463 let report = BootstrapReport {
464 steps: vec![
465 BootstrapStep {
466 name: "mise".into(),
467 status: BootstrapStepStatus::AlreadyInstalled,
468 },
469 BootstrapStep {
470 name: "deagle".into(),
471 status: BootstrapStepStatus::AlreadyInstalled,
472 },
473 ],
474 };
475 assert_eq!(report.summary(), "all 2 deps already present");
476 }
477
478 #[test]
479 fn native_tools_constant_is_5_well_known_tools() {
480 assert_eq!(NATIVE_TOOLS.len(), 5);
484 assert!(NATIVE_TOOLS.contains(&"rg"));
485 assert!(NATIVE_TOOLS.contains(&"fd"));
486 assert!(NATIVE_TOOLS.contains(&"sd"));
487 assert!(NATIVE_TOOLS.contains(&"ast-grep"));
488 assert!(NATIVE_TOOLS.contains(&"erd"));
489 }
490
491 #[test]
492 fn mise_package_name_handles_binary_name_mismatch() {
493 assert_eq!(mise_package_name("rg"), "ripgrep");
497 assert_eq!(mise_package_name("erd"), "erdtree");
498 assert_eq!(mise_package_name("fd"), "fd");
499 assert_eq!(mise_package_name("sd"), "sd");
500 assert_eq!(mise_package_name("ast-grep"), "ast-grep");
501 assert_eq!(mise_package_name("sg"), "ast-grep");
502 assert_eq!(mise_package_name("unknown-tool"), "unknown-tool");
504 }
505
506 #[test]
507 fn marker_path_is_under_home_dot_pawan() {
508 let path = marker_path();
511 let s = path.to_string_lossy();
512 assert!(s.ends_with(".pawan/.bootstrapped"));
513 }
514
515 #[test]
516 fn ensure_deagle_is_idempotent_when_already_on_path() {
517 if !binary_exists("deagle") {
520 return;
523 }
524 let step = ensure_deagle(false);
525 assert_eq!(step.name, "deagle");
526 assert_eq!(
527 step.status,
528 BootstrapStepStatus::AlreadyInstalled,
529 "second call must be a no-op when deagle is present"
530 );
531 }
532
533 #[test]
534 fn ensure_mise_is_idempotent_when_already_on_path() {
535 if !binary_exists("mise") {
536 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
539 if !std::path::Path::new(&format!("{}/.local/bin/mise", home)).exists() {
540 return; }
542 }
543 let step = ensure_mise();
544 assert_eq!(step.name, "mise");
545 assert_eq!(step.status, BootstrapStepStatus::AlreadyInstalled);
546 }
547
548 #[test]
549 fn ensure_native_tool_is_idempotent_when_already_on_path() {
550 for tool in NATIVE_TOOLS {
552 if binary_exists(tool) {
553 let step = ensure_native_tool(tool);
554 assert_eq!(step.name, *tool);
555 assert_eq!(
556 step.status,
557 BootstrapStepStatus::AlreadyInstalled,
558 "ensure_native_tool({}) must be idempotent",
559 tool
560 );
561 return;
562 }
563 }
564 }
566
567 #[serial_test::serial(pawan_session_tests)]
568 #[test]
569 fn missing_deps_is_empty_on_fully_bootstrapped_box() {
570 if !is_bootstrapped() {
571 return;
572 }
573 assert!(
574 missing_deps().is_empty(),
575 "is_bootstrapped() and missing_deps() must agree"
576 );
577 }
578
579 #[test]
580 fn ensure_deps_with_all_skips_writes_empty_report() {
581 let opts = BootstrapOptions {
585 skip_mise: true,
586 skip_native: true,
587 include_deagle: false,
588 force_reinstall: false,
589 };
590 let report = ensure_deps(opts);
591 assert_eq!(report.steps.len(), 0);
592 assert!(report.all_ok());
593 assert_eq!(report.installed_count(), 0);
594 }
595
596 #[test]
597 fn default_options_do_not_include_deagle() {
598 let opts = BootstrapOptions::default();
602 assert!(!opts.include_deagle, "default must exclude deagle install");
603 assert!(!opts.skip_mise, "default installs mise");
604 assert!(!opts.skip_native, "default installs native tools");
605 assert!(!opts.force_reinstall);
606 }
607
608 #[test]
609 fn is_bootstrapped_does_not_require_deagle() {
610 if binary_exists("mise") && NATIVE_TOOLS.iter().all(|t| binary_exists(t)) {
618 assert!(is_bootstrapped());
619 }
620 assert!(
622 !missing_deps().iter().any(|d| d == "deagle"),
623 "missing_deps must not mention deagle"
624 );
625 }
626
627 #[serial_test::serial(pawan_session_tests)]
628 #[test]
629 fn uninstall_without_marker_file_is_ok() {
630 use std::sync::Mutex;
633 static LOCK: Mutex<()> = Mutex::new(());
634 let _guard = LOCK.lock().unwrap();
635
636 let tmp = tempfile::TempDir::new().unwrap();
638 let prev_home = std::env::var("HOME").ok();
639 std::env::set_var("HOME", tmp.path());
640
641 let result = uninstall(false);
642
643 if let Some(h) = prev_home {
644 std::env::set_var("HOME", h);
645 }
646
647 assert!(result.is_ok());
648 }
649
650 #[test]
651 fn bootstrap_report_summary_with_installs_only() {
652 let report = BootstrapReport {
653 steps: vec![
654 BootstrapStep {
655 name: "mise".into(),
656 status: BootstrapStepStatus::Installed,
657 },
658 BootstrapStep {
659 name: "rg".into(),
660 status: BootstrapStepStatus::Installed,
661 },
662 ],
663 };
664 let s = report.summary();
665 assert!(s.contains("2 installed"));
666 assert!(s.contains("0 already present"));
667 }
668
669 #[test]
670 fn missing_deps_lists_missing_mise() {
671 use std::sync::Mutex;
672 static LOCK: Mutex<()> = Mutex::new(());
673 let _guard = LOCK.lock().unwrap();
674
675 let tmp = tempfile::TempDir::new().unwrap();
676 let prev_path = std::env::var("PATH").ok();
677 std::env::set_var("PATH", tmp.path());
678
679 let missing = missing_deps();
680
681 if let Some(p) = prev_path {
682 std::env::set_var("PATH", p);
683 }
684
685 assert!(missing.contains(&"mise".to_string()));
686 }
687
688 #[test]
689 fn missing_deps_lists_missing_native_tools() {
690 use std::sync::Mutex;
691 static LOCK: Mutex<()> = Mutex::new(());
692 let _guard = LOCK.lock().unwrap();
693
694 let tmp = tempfile::TempDir::new().unwrap();
695 let prev_path = std::env::var("PATH").ok();
696 std::env::set_var("PATH", tmp.path());
697
698 let missing = missing_deps();
699
700 if let Some(p) = prev_path {
701 std::env::set_var("PATH", p);
702 }
703
704 for tool in NATIVE_TOOLS {
705 assert!(missing.contains(&tool.to_string()));
706 }
707 }
708
709 #[test]
710 fn ensure_deagle_with_force_reinstall_attempts_install() {
711 use std::sync::Mutex;
712 static LOCK: Mutex<()> = Mutex::new(());
713 let _guard = LOCK.lock().unwrap();
714
715 let tmp = tempfile::TempDir::new().unwrap();
716 let prev_path = std::env::var("PATH").ok();
717 std::env::set_var("PATH", tmp.path());
718
719 let step = ensure_deagle(true);
720
721 if let Some(p) = prev_path {
722 std::env::set_var("PATH", p);
723 }
724
725 assert_eq!(step.name, "deagle");
726 assert!(matches!(
728 step.status,
729 BootstrapStepStatus::Installed | BootstrapStepStatus::Failed(_)
730 ));
731 }
732
733 #[test]
734 fn ensure_mise_falls_back_to_local_bin() {
735 use std::sync::Mutex;
736 static LOCK: Mutex<()> = Mutex::new(());
737 let _guard = LOCK.lock().unwrap();
738
739 let tmp = tempfile::TempDir::new().unwrap();
740 let prev_path = std::env::var("PATH").ok();
741 std::env::set_var("PATH", tmp.path());
742
743 let home = tmp.path();
744 let local_bin = home.join(".local/bin");
745 std::fs::create_dir_all(&local_bin).unwrap();
746 std::fs::write(local_bin.join("mise"), "#!/bin/sh\necho mise").unwrap();
747
748 let prev_home = std::env::var("HOME").ok();
749 std::env::set_var("HOME", home);
750
751 let step = ensure_mise();
752
753 if let Some(h) = prev_home {
754 std::env::set_var("HOME", h);
755 }
756 if let Some(p) = prev_path {
757 std::env::set_var("PATH", p);
758 }
759
760 assert_eq!(step.name, "mise");
761 assert_eq!(step.status, BootstrapStepStatus::AlreadyInstalled);
762 }
763
764 #[test]
765 fn ensure_native_tool_skips_when_mise_missing() {
766 use std::sync::Mutex;
767 static LOCK: Mutex<()> = Mutex::new(());
768 let _guard = LOCK.lock().unwrap();
769
770 let tmp = tempfile::TempDir::new().unwrap();
771 let prev_path = std::env::var("PATH").ok();
772 std::env::set_var("PATH", tmp.path());
773
774 let home = tmp.path();
775 let prev_home = std::env::var("HOME").ok();
776 std::env::set_var("HOME", home);
777
778 let step = ensure_native_tool("rg");
779
780 if let Some(h) = prev_home {
781 std::env::set_var("HOME", h);
782 }
783 if let Some(p) = prev_path {
784 std::env::set_var("PATH", p);
785 }
786
787 assert_eq!(step.name, "rg");
788 assert!(matches!(step.status, BootstrapStepStatus::Skipped(_)));
789 }
790
791 #[test]
792 fn ensure_native_tool_uses_local_mise() {
793 use std::sync::Mutex;
794 static LOCK: Mutex<()> = Mutex::new(());
795 let _guard = LOCK.lock().unwrap();
796
797 let tmp = tempfile::TempDir::new().unwrap();
798 let prev_path = std::env::var("PATH").ok();
799 std::env::set_var("PATH", tmp.path());
800
801 let home = tmp.path();
802 let local_bin = home.join(".local/bin");
803 std::fs::create_dir_all(&local_bin).unwrap();
804 std::fs::write(local_bin.join("mise"), "#!/bin/sh\necho mise").unwrap();
805
806 let prev_home = std::env::var("HOME").ok();
807 std::env::set_var("HOME", home);
808
809 let step = ensure_native_tool("rg");
810
811 if let Some(h) = prev_home {
812 std::env::set_var("HOME", h);
813 }
814 if let Some(p) = prev_path {
815 std::env::set_var("PATH", p);
816 }
817
818 assert_eq!(step.name, "rg");
819 assert!(matches!(
821 step.status,
822 BootstrapStepStatus::Installed | BootstrapStepStatus::Failed(_)
823 ));
824 }
825
826 #[serial_test::serial(pawan_session_tests)]
827 #[test]
828 fn ensure_deps_writes_marker_on_success() {
829 use std::sync::Mutex;
830 static LOCK: Mutex<()> = Mutex::new(());
831 let _guard = LOCK.lock().unwrap();
832
833 let tmp = tempfile::TempDir::new().unwrap();
834 let prev_home = std::env::var("HOME").ok();
835 std::env::set_var("HOME", tmp.path());
836
837 let opts = BootstrapOptions {
838 skip_mise: true,
839 skip_native: true,
840 include_deagle: false,
841 force_reinstall: false,
842 };
843 let report = ensure_deps(opts);
844
845 if let Some(h) = prev_home {
846 std::env::set_var("HOME", h);
847 }
848
849 assert!(report.all_ok());
850 let marker = tmp.path().join(".pawan/.bootstrapped");
851 assert!(marker.exists(), "marker must be written on success");
852 }
853
854 #[serial_test::serial(pawan_session_tests)]
855 #[test]
856 fn ensure_deps_includes_deagle_when_requested() {
857 use std::sync::Mutex;
858 static LOCK: Mutex<()> = Mutex::new(());
859 let _guard = LOCK.lock().unwrap();
860
861 let tmp = tempfile::TempDir::new().unwrap();
862 let prev_home = std::env::var("HOME").ok();
863 std::env::set_var("HOME", tmp.path());
864
865 let opts = BootstrapOptions {
866 skip_mise: true,
867 skip_native: true,
868 include_deagle: true,
869 force_reinstall: false,
870 };
871 let report = ensure_deps(opts);
872
873 if let Some(h) = prev_home {
874 std::env::set_var("HOME", h);
875 }
876
877 assert_eq!(report.steps.len(), 1);
878 assert_eq!(report.steps[0].name, "deagle");
879 }
880
881 #[serial_test::serial(pawan_session_tests)]
882 #[test]
883 fn ensure_deps_with_force_reinstall() {
884 use std::sync::Mutex;
885 static LOCK: Mutex<()> = Mutex::new(());
886 let _guard = LOCK.lock().unwrap();
887
888 let tmp = tempfile::TempDir::new().unwrap();
889 let prev_home = std::env::var("HOME").ok();
890 std::env::set_var("HOME", tmp.path());
891
892 let opts = BootstrapOptions {
893 skip_mise: true,
894 skip_native: true,
895 include_deagle: true,
896 force_reinstall: true,
897 };
898 let report = ensure_deps(opts);
899
900 if let Some(h) = prev_home {
901 std::env::set_var("HOME", h);
902 }
903
904 assert_eq!(report.steps.len(), 1);
905 assert_eq!(report.steps[0].name, "deagle");
906 }
907
908 #[serial_test::serial(pawan_session_tests)]
909 #[test]
910 fn uninstall_removes_marker_file() {
911 use std::sync::Mutex;
912 static LOCK: Mutex<()> = Mutex::new(());
913 let _guard = LOCK.lock().unwrap();
914
915 let tmp = tempfile::TempDir::new().unwrap();
916 let prev_home = std::env::var("HOME").ok();
917 std::env::set_var("HOME", tmp.path());
918
919 let marker = tmp.path().join(".pawan/.bootstrapped");
920 std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
921 std::fs::write(&marker, "2024-01-01T00:00:00Z").unwrap();
922
923 let result = uninstall(false);
924
925 if let Some(h) = prev_home {
926 std::env::set_var("HOME", h);
927 }
928
929 assert!(result.is_ok());
930 assert!(!marker.exists(), "marker must be removed");
931 }
932
933 #[serial_test::serial(pawan_session_tests)]
934 #[test]
935 fn uninstall_with_purge_deagle_attempts_uninstall() {
936 use std::sync::Mutex;
937 static LOCK: Mutex<()> = Mutex::new(());
938 let _guard = LOCK.lock().unwrap();
939
940 let tmp = tempfile::TempDir::new().unwrap();
941 let prev_home = std::env::var("HOME").ok();
942 std::env::set_var("HOME", tmp.path());
943
944 let marker = tmp.path().join(".pawan/.bootstrapped");
945 std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
946 std::fs::write(&marker, "2024-01-01T00:00:00Z").unwrap();
947
948 let result = uninstall(true);
949
950 if let Some(h) = prev_home {
951 std::env::set_var("HOME", h);
952 }
953
954 assert!(result.is_ok() || result.is_err());
956 assert!(!marker.exists(), "marker must be removed regardless");
957 }
958}