1use anyhow::{Context, Result};
4use colored::Colorize;
5use std::io::{self, IsTerminal, Write};
6use std::path::{Path, PathBuf};
7use tokio::io::{AsyncBufReadExt, BufReader};
8
9use crate::manifest::{Manifest, find_manifest};
10
11pub trait CommandExecutor: Sized {
13 fn execute(self) -> impl std::future::Future<Output = Result<()>> + Send
15 where
16 Self: Send,
17 {
18 async move {
19 let manifest_path = if let Ok(path) = find_manifest() {
20 path
21 } else {
22 match handle_legacy_ccpm_migration().await {
24 Ok(Some(path)) => path,
25 Ok(None) => {
26 return Err(anyhow::anyhow!(
27 "No agpm.toml found in current directory or any parent directory. \
28 Run 'agpm init' to create a new project."
29 ));
30 }
31 Err(e) => return Err(e),
32 }
33 };
34 self.execute_from_path(manifest_path).await
35 }
36 }
37
38 fn execute_from_path(
40 self,
41 manifest_path: PathBuf,
42 ) -> impl std::future::Future<Output = Result<()>> + Send;
43}
44
45#[derive(Debug)]
47pub struct CommandContext {
48 pub manifest: Manifest,
50 pub manifest_path: PathBuf,
52 pub project_dir: PathBuf,
54 pub lockfile_path: PathBuf,
56}
57
58impl CommandContext {
59 pub fn new(manifest: Manifest, project_dir: PathBuf) -> Result<Self> {
61 let lockfile_path = project_dir.join("agpm.lock");
62 Ok(Self {
63 manifest,
64 manifest_path: project_dir.join("agpm.toml"),
65 project_dir,
66 lockfile_path,
67 })
68 }
69
70 pub fn from_manifest_path(manifest_path: impl AsRef<Path>) -> Result<Self> {
75 let manifest_path = manifest_path.as_ref();
76
77 if !manifest_path.exists() {
78 return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
79 }
80
81 let project_dir = manifest_path
82 .parent()
83 .ok_or_else(|| anyhow::anyhow!("Invalid manifest path"))?
84 .to_path_buf();
85
86 let manifest = Manifest::load(manifest_path).with_context(|| {
87 format!("Failed to parse manifest file: {}", manifest_path.display())
88 })?;
89
90 let lockfile_path = project_dir.join("agpm.lock");
91
92 Ok(Self {
93 manifest,
94 manifest_path: manifest_path.to_path_buf(),
95 project_dir,
96 lockfile_path,
97 })
98 }
99
100 pub fn load_lockfile(&self) -> Result<Option<crate::lockfile::LockFile>> {
105 if self.lockfile_path.exists() {
106 let lockfile =
107 crate::lockfile::LockFile::load(&self.lockfile_path).with_context(|| {
108 format!("Failed to load lockfile: {}", self.lockfile_path.display())
109 })?;
110 Ok(Some(lockfile))
111 } else {
112 Ok(None)
113 }
114 }
115
116 pub fn load_lockfile_with_regeneration(
159 &self,
160 can_regenerate: bool,
161 operation_name: &str,
162 ) -> Result<Option<crate::lockfile::LockFile>> {
163 if !self.lockfile_path.exists() {
165 return Ok(None);
166 }
167
168 match crate::lockfile::LockFile::load(&self.lockfile_path) {
170 Ok(lockfile) => Ok(Some(lockfile)),
171 Err(e) => {
172 let error_msg = e.to_string();
174 let can_auto_recover = can_regenerate
175 && (error_msg.contains("Invalid TOML syntax")
176 || error_msg.contains("Lockfile version")
177 || error_msg.contains("missing field")
178 || error_msg.contains("invalid type")
179 || error_msg.contains("expected"));
180
181 if !can_auto_recover {
182 return Err(e);
184 }
185
186 let backup_path = self.lockfile_path.with_extension("lock.invalid");
188
189 let regenerate_message = format!(
191 "The lockfile appears to be invalid or corrupted.\n\n\
192 Error: {}\n\n\
193 Note: The lockfile format is not yet stable as this is beta software.\n\n\
194 The invalid lockfile will be backed up to: {}",
195 error_msg,
196 backup_path.display()
197 );
198
199 if io::stdin().is_terminal() {
201 println!("{}", regenerate_message);
203 print!("Would you like to regenerate the lockfile automatically? [Y/n] ");
204 io::stdout().flush().unwrap();
205
206 let mut input = String::new();
207 match io::stdin().read_line(&mut input) {
208 Ok(_) => {
209 let response = input.trim().to_lowercase();
210 if response.is_empty() || response == "y" || response == "yes" {
211 self.backup_and_regenerate_lockfile(&backup_path, operation_name)?;
213 Ok(None) } else {
215 Err(crate::core::AgpmError::InvalidLockfileError {
217 file: self.lockfile_path.display().to_string(),
218 reason: format!(
219 "{} (User declined automatic regeneration)",
220 error_msg
221 ),
222 can_regenerate: true,
223 }
224 .into())
225 }
226 }
227 Err(_) => {
228 Err(self.create_non_interactive_error(&error_msg, operation_name))
230 }
231 }
232 } else {
233 Err(self.create_non_interactive_error(&error_msg, operation_name))
235 }
236 }
237 }
238 }
239
240 fn backup_and_regenerate_lockfile(
242 &self,
243 backup_path: &Path,
244 operation_name: &str,
245 ) -> Result<()> {
246 if let Err(e) = std::fs::copy(&self.lockfile_path, backup_path) {
248 eprintln!("Warning: Failed to backup invalid lockfile: {}", e);
249 } else {
250 println!("✓ Backed up invalid lockfile to: {}", backup_path.display());
251 }
252
253 if let Err(e) = std::fs::remove_file(&self.lockfile_path) {
255 return Err(anyhow::anyhow!("Failed to remove invalid lockfile: {}", e));
256 }
257
258 println!("✓ Removed invalid lockfile");
259 println!("Note: Run 'agpm install' to regenerate the lockfile");
260
261 if operation_name != "install" {
263 println!("Alternatively, run 'agpm {} --regenerate' if available", operation_name);
264 }
265
266 Ok(())
267 }
268
269 fn create_non_interactive_error(
271 &self,
272 error_msg: &str,
273 _operation_name: &str,
274 ) -> anyhow::Error {
275 let backup_path = self.lockfile_path.with_extension("lock.invalid");
276
277 crate::core::AgpmError::InvalidLockfileError {
278 file: self.lockfile_path.display().to_string(),
279 reason: format!(
280 "{}\n\n\
281 To fix this issue:\n\
282 1. Backup the invalid lockfile: cp agpm.lock {}\n\
283 2. Remove the invalid lockfile: rm agpm.lock\n\
284 3. Regenerate it: agpm install\n\n\
285 Note: The lockfile format is not yet stable as this is beta software.",
286 error_msg,
287 backup_path.display()
288 ),
289 can_regenerate: true,
290 }
291 .into()
292 }
293
294 pub fn save_lockfile(&self, lockfile: &crate::lockfile::LockFile) -> Result<()> {
299 lockfile
300 .save(&self.lockfile_path)
301 .with_context(|| format!("Failed to save lockfile: {}", self.lockfile_path.display()))
302 }
303}
304
305pub async fn handle_legacy_ccpm_migration() -> Result<Option<PathBuf>> {
344 let current_dir = std::env::current_dir()?;
345 let legacy_dir = find_legacy_ccpm_directory(¤t_dir);
346
347 let Some(dir) = legacy_dir else {
348 return Ok(None);
349 };
350
351 if !std::io::stdin().is_terminal() {
353 eprintln!("{}", "Legacy CCPM files detected (non-interactive mode).".yellow());
355 eprintln!(
356 "Run {} to migrate manually.",
357 format!("agpm migrate --path {}", dir.display()).cyan()
358 );
359 return Ok(None);
360 }
361
362 let ccpm_toml = dir.join("ccpm.toml");
364 let ccpm_lock = dir.join("ccpm.lock");
365
366 let mut files = Vec::new();
367 if ccpm_toml.exists() {
368 files.push("ccpm.toml");
369 }
370 if ccpm_lock.exists() {
371 files.push("ccpm.lock");
372 }
373
374 let files_str = files.join(" and ");
375
376 println!("{}", "Legacy CCPM files detected!".yellow().bold());
377 println!("{} {} found in {}", "→".cyan(), files_str, dir.display());
378 println!();
379
380 print!("{} ", "Would you like to migrate to AGPM now? [Y/n]:".green());
382 io::stdout().flush()?;
383
384 let mut reader = BufReader::new(tokio::io::stdin());
386 let mut response = String::new();
387 reader.read_line(&mut response).await?;
388 let response = response.trim().to_lowercase();
389
390 if response.is_empty() || response == "y" || response == "yes" {
391 println!();
392 println!("{}", "🚀 Starting migration...".cyan());
393
394 let migrate_cmd = super::migrate::MigrateCommand::new(Some(dir.clone()), false, false);
396
397 migrate_cmd.execute().await?;
398
399 Ok(Some(dir.join("agpm.toml")))
401 } else {
402 println!();
403 println!("{}", "Migration cancelled.".yellow());
404 println!(
405 "Run {} to migrate manually.",
406 format!("agpm migrate --path {}", dir.display()).cyan()
407 );
408 Ok(None)
409 }
410}
411
412#[must_use]
424pub fn check_for_legacy_ccpm_files() -> Option<String> {
425 check_for_legacy_ccpm_files_from(std::env::current_dir().ok()?)
426}
427
428fn find_legacy_ccpm_directory(start_dir: &Path) -> Option<PathBuf> {
438 let mut dir = start_dir;
439
440 loop {
441 let ccpm_toml = dir.join("ccpm.toml");
442 let ccpm_lock = dir.join("ccpm.lock");
443
444 if ccpm_toml.exists() || ccpm_lock.exists() {
445 return Some(dir.to_path_buf());
446 }
447
448 dir = dir.parent()?;
449 }
450}
451
452fn check_for_legacy_ccpm_files_from(start_dir: PathBuf) -> Option<String> {
457 let current = start_dir;
458 let mut dir = current.as_path();
459
460 loop {
461 let ccpm_toml = dir.join("ccpm.toml");
462 let ccpm_lock = dir.join("ccpm.lock");
463
464 if ccpm_toml.exists() || ccpm_lock.exists() {
465 let mut files = Vec::new();
466 if ccpm_toml.exists() {
467 files.push("ccpm.toml");
468 }
469 if ccpm_lock.exists() {
470 files.push("ccpm.lock");
471 }
472
473 let files_str = files.join(" and ");
474 let location = if dir == current {
475 "current directory".to_string()
476 } else {
477 format!("parent directory: {}", dir.display())
478 };
479
480 return Some(format!(
481 "{}\n\n{} {} found in {}.\n{}\n {}\n\n{}",
482 "Legacy CCPM files detected!".yellow().bold(),
483 "→".cyan(),
484 files_str,
485 location,
486 "Run the migration command to upgrade:".yellow(),
487 format!("agpm migrate --path {}", dir.display()).cyan().bold(),
488 "Or run 'agpm init' to create a new AGPM project.".dimmed()
489 ));
490 }
491
492 dir = dir.parent()?;
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use tempfile::TempDir;
500
501 #[test]
502 fn test_command_context_from_manifest_path() {
503 let temp_dir = TempDir::new().unwrap();
504 let manifest_path = temp_dir.path().join("agpm.toml");
505
506 std::fs::write(
508 &manifest_path,
509 r#"
510[sources]
511test = "https://github.com/test/repo.git"
512
513[agents]
514"#,
515 )
516 .unwrap();
517
518 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
519
520 assert_eq!(context.manifest_path, manifest_path);
521 assert_eq!(context.project_dir, temp_dir.path());
522 assert_eq!(context.lockfile_path, temp_dir.path().join("agpm.lock"));
523 assert!(context.manifest.sources.contains_key("test"));
524 }
525
526 #[test]
527 fn test_command_context_missing_manifest() {
528 let result = CommandContext::from_manifest_path("/nonexistent/agpm.toml");
529 assert!(result.is_err());
530 assert!(result.unwrap_err().to_string().contains("not found"));
531 }
532
533 #[test]
534 fn test_command_context_invalid_manifest() {
535 let temp_dir = TempDir::new().unwrap();
536 let manifest_path = temp_dir.path().join("agpm.toml");
537
538 std::fs::write(&manifest_path, "invalid toml {{").unwrap();
540
541 let result = CommandContext::from_manifest_path(&manifest_path);
542 assert!(result.is_err());
543 assert!(result.unwrap_err().to_string().contains("Failed to parse manifest"));
544 }
545
546 #[test]
547 fn test_load_lockfile_exists() {
548 let temp_dir = TempDir::new().unwrap();
549 let manifest_path = temp_dir.path().join("agpm.toml");
550 let lockfile_path = temp_dir.path().join("agpm.lock");
551
552 std::fs::write(&manifest_path, "[sources]\n").unwrap();
554 std::fs::write(
555 &lockfile_path,
556 r#"
557version = 1
558
559[[sources]]
560name = "test"
561url = "https://github.com/test/repo.git"
562commit = "abc123"
563fetched_at = "2024-01-01T00:00:00Z"
564"#,
565 )
566 .unwrap();
567
568 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
569 let lockfile = context.load_lockfile().unwrap();
570
571 assert!(lockfile.is_some());
572 let lockfile = lockfile.unwrap();
573 assert_eq!(lockfile.sources.len(), 1);
574 assert_eq!(lockfile.sources[0].name, "test");
575 }
576
577 #[test]
578 fn test_load_lockfile_not_exists() {
579 let temp_dir = TempDir::new().unwrap();
580 let manifest_path = temp_dir.path().join("agpm.toml");
581
582 std::fs::write(&manifest_path, "[sources]\n").unwrap();
583
584 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
585 let lockfile = context.load_lockfile().unwrap();
586
587 assert!(lockfile.is_none());
588 }
589
590 #[test]
591 fn test_save_lockfile() {
592 let temp_dir = TempDir::new().unwrap();
593 let manifest_path = temp_dir.path().join("agpm.toml");
594
595 std::fs::write(&manifest_path, "[sources]\n").unwrap();
596
597 let context = CommandContext::from_manifest_path(&manifest_path).unwrap();
598
599 let lockfile = crate::lockfile::LockFile {
600 version: 1,
601 sources: vec![],
602 agents: vec![],
603 snippets: vec![],
604 commands: vec![],
605 scripts: vec![],
606 hooks: vec![],
607 mcp_servers: vec![],
608 };
609
610 context.save_lockfile(&lockfile).unwrap();
611
612 assert!(context.lockfile_path.exists());
613 let saved_content = std::fs::read_to_string(&context.lockfile_path).unwrap();
614 assert!(saved_content.contains("version = 1"));
615 }
616
617 #[test]
618 fn test_check_for_legacy_ccpm_no_files() {
619 let temp_dir = TempDir::new().unwrap();
620 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
621 assert!(result.is_none());
622 }
623
624 #[test]
625 fn test_check_for_legacy_ccpm_toml_only() {
626 let temp_dir = TempDir::new().unwrap();
627 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
628
629 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
630 assert!(result.is_some());
631 let msg = result.unwrap();
632 assert!(msg.contains("Legacy CCPM files detected"));
633 assert!(msg.contains("ccpm.toml"));
634 assert!(msg.contains("agpm migrate"));
635 }
636
637 #[test]
638 fn test_check_for_legacy_ccpm_lock_only() {
639 let temp_dir = TempDir::new().unwrap();
640 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
641
642 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
643 assert!(result.is_some());
644 let msg = result.unwrap();
645 assert!(msg.contains("ccpm.lock"));
646 }
647
648 #[test]
649 fn test_check_for_legacy_ccpm_both_files() {
650 let temp_dir = TempDir::new().unwrap();
651 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
652 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
653
654 let result = check_for_legacy_ccpm_files_from(temp_dir.path().to_path_buf());
655 assert!(result.is_some());
656 let msg = result.unwrap();
657 assert!(msg.contains("ccpm.toml and ccpm.lock"));
658 }
659
660 #[test]
661 fn test_find_legacy_ccpm_directory_no_files() {
662 let temp_dir = TempDir::new().unwrap();
663 let result = find_legacy_ccpm_directory(temp_dir.path());
664 assert!(result.is_none());
665 }
666
667 #[test]
668 fn test_find_legacy_ccpm_directory_in_current_dir() {
669 let temp_dir = TempDir::new().unwrap();
670 std::fs::write(temp_dir.path().join("ccpm.toml"), "[sources]\n").unwrap();
671
672 let result = find_legacy_ccpm_directory(temp_dir.path());
673 assert!(result.is_some());
674 assert_eq!(result.unwrap(), temp_dir.path());
675 }
676
677 #[test]
678 fn test_find_legacy_ccpm_directory_in_parent() {
679 let temp_dir = TempDir::new().unwrap();
680 let parent = temp_dir.path();
681 let child = parent.join("subdir");
682 std::fs::create_dir(&child).unwrap();
683
684 std::fs::write(parent.join("ccpm.toml"), "[sources]\n").unwrap();
686
687 let result = find_legacy_ccpm_directory(&child);
689 assert!(result.is_some());
690 assert_eq!(result.unwrap(), parent);
691 }
692
693 #[test]
694 fn test_find_legacy_ccpm_directory_finds_lock_file() {
695 let temp_dir = TempDir::new().unwrap();
696 std::fs::write(temp_dir.path().join("ccpm.lock"), "# lock\n").unwrap();
697
698 let result = find_legacy_ccpm_directory(temp_dir.path());
699 assert!(result.is_some());
700 assert_eq!(result.unwrap(), temp_dir.path());
701 }
702
703 #[tokio::test]
704 async fn test_handle_legacy_ccpm_migration_no_files() {
705 let temp_dir = TempDir::new().unwrap();
706 let original_dir = std::env::current_dir().unwrap();
707
708 std::env::set_current_dir(temp_dir.path()).unwrap();
710
711 let result = handle_legacy_ccpm_migration().await;
712
713 std::env::set_current_dir(original_dir).unwrap();
715
716 assert!(result.is_ok());
717 assert!(result.unwrap().is_none());
718 }
719
720 #[cfg(test)]
721 mod lockfile_regeneration_tests {
722 use super::*;
723 use crate::manifest::Manifest;
724 use tempfile::TempDir;
725
726 #[test]
727 fn test_load_lockfile_with_regeneration_valid_lockfile() {
728 let temp_dir = TempDir::new().unwrap();
729 let project_dir = temp_dir.path();
730 let manifest_path = project_dir.join("agpm.toml");
731 let lockfile_path = project_dir.join("agpm.lock");
732
733 let manifest_content = r#"[sources]
735example = "https://github.com/example/repo.git"
736
737[agents]
738test = { source = "example", path = "test.md", version = "v1.0.0" }
739"#;
740 std::fs::write(&manifest_path, manifest_content).unwrap();
741
742 let lockfile_content = r#"version = 1
744
745[[sources]]
746name = "example"
747url = "https://github.com/example/repo.git"
748commit = "abc123def456789012345678901234567890abcd"
749fetched_at = "2024-01-01T00:00:00Z"
750
751[[agents]]
752name = "test"
753source = "example"
754path = "test.md"
755version = "v1.0.0"
756resolved_commit = "abc123def456789012345678901234567890abcd"
757checksum = "sha256:examplechecksum"
758installed_at = ".claude/agents/test.md"
759"#;
760 std::fs::write(&lockfile_path, lockfile_content).unwrap();
761
762 let manifest = Manifest::load(&manifest_path).unwrap();
764 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
765
766 let result = ctx.load_lockfile_with_regeneration(true, "test").unwrap();
767 assert!(result.is_some());
768 }
769
770 #[test]
771 fn test_load_lockfile_with_regeneration_invalid_toml() {
772 let temp_dir = TempDir::new().unwrap();
773 let project_dir = temp_dir.path();
774 let manifest_path = project_dir.join("agpm.toml");
775 let lockfile_path = project_dir.join("agpm.lock");
776
777 let manifest_content = r#"[sources]
779example = "https://github.com/example/repo.git"
780"#;
781 std::fs::write(&manifest_path, manifest_content).unwrap();
782
783 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
785
786 let manifest = Manifest::load(&manifest_path).unwrap();
788 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
789
790 let result = ctx.load_lockfile_with_regeneration(true, "test");
792 assert!(result.is_err());
793
794 let error_msg = result.unwrap_err().to_string();
795 assert!(error_msg.contains("Invalid or corrupted lockfile detected"));
796 assert!(error_msg.contains("beta software"));
797 assert!(error_msg.contains("cp agpm.lock"));
798 }
799
800 #[test]
801 fn test_load_lockfile_with_regeneration_missing_lockfile() {
802 let temp_dir = TempDir::new().unwrap();
803 let project_dir = temp_dir.path();
804 let manifest_path = project_dir.join("agpm.toml");
805
806 let manifest_content = r#"[sources]
808example = "https://github.com/example/repo.git"
809"#;
810 std::fs::write(&manifest_path, manifest_content).unwrap();
811
812 let manifest = Manifest::load(&manifest_path).unwrap();
814 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
815
816 let result = ctx.load_lockfile_with_regeneration(true, "test").unwrap();
817 assert!(result.is_none()); }
819
820 #[test]
821 fn test_load_lockfile_with_regeneration_version_incompatibility() {
822 let temp_dir = TempDir::new().unwrap();
823 let project_dir = temp_dir.path();
824 let manifest_path = project_dir.join("agpm.toml");
825 let lockfile_path = project_dir.join("agpm.lock");
826
827 let manifest_content = r#"[sources]
829example = "https://github.com/example/repo.git"
830"#;
831 std::fs::write(&manifest_path, manifest_content).unwrap();
832
833 let lockfile_content = r#"version = 999
835
836[[sources]]
837name = "example"
838url = "https://github.com/example/repo.git"
839commit = "abc123def456789012345678901234567890abcd"
840fetched_at = "2024-01-01T00:00:00Z"
841"#;
842 std::fs::write(&lockfile_path, lockfile_content).unwrap();
843
844 let manifest = Manifest::load(&manifest_path).unwrap();
846 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
847
848 let result = ctx.load_lockfile_with_regeneration(true, "test");
849 assert!(result.is_err());
850
851 let error_msg = result.unwrap_err().to_string();
852 assert!(error_msg.contains("version") || error_msg.contains("newer"));
853 }
854
855 #[test]
856 fn test_load_lockfile_with_regeneration_cannot_regenerate() {
857 let temp_dir = TempDir::new().unwrap();
858 let project_dir = temp_dir.path();
859 let manifest_path = project_dir.join("agpm.toml");
860 let lockfile_path = project_dir.join("agpm.lock");
861
862 let manifest_content = r#"[sources]
864example = "https://github.com/example/repo.git"
865"#;
866 std::fs::write(&manifest_path, manifest_content).unwrap();
867
868 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
870
871 let manifest = Manifest::load(&manifest_path).unwrap();
873 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
874
875 let result = ctx.load_lockfile_with_regeneration(false, "test");
876 assert!(result.is_err());
877
878 let error_msg = result.unwrap_err().to_string();
880 assert!(!error_msg.contains("Invalid or corrupted lockfile detected"));
881 assert!(
882 error_msg.contains("Failed to load lockfile")
883 || error_msg.contains("Invalid TOML syntax")
884 );
885 }
886
887 #[test]
888 fn test_backup_and_regenerate_lockfile() {
889 let temp_dir = TempDir::new().unwrap();
890 let project_dir = temp_dir.path();
891 let manifest_path = project_dir.join("agpm.toml");
892 let lockfile_path = project_dir.join("agpm.lock");
893
894 let manifest_content = r#"[sources]
896example = "https://github.com/example/repo.git"
897"#;
898 std::fs::write(&manifest_path, manifest_content).unwrap();
899
900 std::fs::write(&lockfile_path, "invalid content").unwrap();
902
903 let manifest = Manifest::load(&manifest_path).unwrap();
905 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
906
907 let backup_path = lockfile_path.with_extension("lock.invalid");
908
909 ctx.backup_and_regenerate_lockfile(&backup_path, "test").unwrap();
911
912 assert!(backup_path.exists());
914 assert_eq!(std::fs::read_to_string(&backup_path).unwrap(), "invalid content");
915
916 assert!(!lockfile_path.exists());
918 }
919
920 #[test]
921 fn test_create_non_interactive_error() {
922 let temp_dir = TempDir::new().unwrap();
923 let project_dir = temp_dir.path();
924 let manifest_path = project_dir.join("agpm.toml");
925
926 let manifest_content = r#"[sources]
928example = "https://github.com/example/repo.git"
929"#;
930 std::fs::write(&manifest_path, manifest_content).unwrap();
931
932 let manifest = Manifest::load(&manifest_path).unwrap();
934 let ctx = CommandContext::new(manifest, project_dir.to_path_buf()).unwrap();
935
936 let error = ctx.create_non_interactive_error("Invalid TOML syntax", "test");
937 let error_msg = error.to_string();
938
939 assert!(error_msg.contains("Invalid TOML syntax"));
940 assert!(error_msg.contains("beta software"));
941 assert!(error_msg.contains("cp agpm.lock"));
942 assert!(error_msg.contains("rm agpm.lock"));
943 assert!(error_msg.contains("agpm install"));
944 }
945 }
946
947 }