1use anyhow::Result;
89use clap::Args;
90use colored::Colorize;
91use std::path::{Path, PathBuf};
92use std::sync::Arc;
93
94use crate::cache::Cache;
95use crate::core::ResourceType;
96use crate::lockfile::LockFile;
97use crate::manifest::{Manifest, find_manifest_with_optional};
98use crate::markdown::reference_extractor::{extract_file_references, validate_file_references};
99use crate::resolver::DependencyResolver;
100use crate::templating::{TemplateContextBuilder, TemplateRenderer};
101#[cfg(test)]
102use crate::utils::normalize_path_for_storage;
103
104#[derive(Args)]
152pub struct ValidateCommand {
153 #[arg(value_name = "FILE")]
158 pub file: Option<String>,
159
160 #[arg(long, alias = "dependencies")]
166 pub resolve: bool,
167
168 #[arg(long, alias = "lockfile")]
174 pub check_lock: bool,
175
176 #[arg(long)]
182 pub sources: bool,
183
184 #[arg(long)]
189 pub paths: bool,
190
191 #[arg(long, value_enum, default_value = "text")]
197 pub format: OutputFormat,
198
199 #[arg(short, long)]
204 pub verbose: bool,
205
206 #[arg(short, long)]
211 pub quiet: bool,
212
213 #[arg(long)]
219 pub strict: bool,
220
221 #[arg(long)]
242 pub render: bool,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
267pub enum OutputFormat {
268 Text,
276
277 Json,
285}
286
287impl ValidateCommand {
288 pub async fn execute(self) -> Result<()> {
341 self.execute_with_manifest_path(None).await
342 }
343
344 pub async fn execute_with_manifest_path(self, manifest_path: Option<PathBuf>) -> Result<()> {
380 let manifest_path = if let Some(ref path) = self.file {
382 PathBuf::from(path)
383 } else {
384 match find_manifest_with_optional(manifest_path) {
385 Ok(path) => path,
386 Err(e) => {
387 let error_msg =
388 "No agpm.toml found in current directory or any parent directory";
389
390 if matches!(self.format, OutputFormat::Json) {
391 let validation_results = ValidationResults {
392 valid: false,
393 errors: vec![error_msg.to_string()],
394 ..Default::default()
395 };
396 println!("{}", serde_json::to_string_pretty(&validation_results)?);
397 return Err(e);
398 } else if !self.quiet {
399 println!("{} {}", "✗".red(), error_msg);
400 }
401 return Err(e);
402 }
403 }
404 };
405
406 self.execute_from_path(manifest_path).await
407 }
408
409 pub async fn execute_from_path(self, manifest_path: PathBuf) -> Result<()> {
430 if !manifest_path.exists() {
432 let error_msg = format!("Manifest file {} not found", manifest_path.display());
433
434 if matches!(self.format, OutputFormat::Json) {
435 let validation_results = ValidationResults {
436 valid: false,
437 errors: vec![error_msg],
438 ..Default::default()
439 };
440 println!("{}", serde_json::to_string_pretty(&validation_results)?);
441 } else if !self.quiet {
442 println!("{} {}", "✗".red(), error_msg);
443 }
444
445 return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
446 }
447
448 let mut validation_results = ValidationResults::default();
450 let mut warnings = Vec::new();
451 let mut errors = Vec::new();
452
453 if self.verbose && !self.quiet {
454 println!("🔍 Validating {}...", manifest_path.display());
455 }
456
457 let manifest = match Manifest::load(&manifest_path) {
459 Ok(m) => {
460 if self.verbose && !self.quiet {
461 println!("✓ Manifest structure is valid");
462 }
463 validation_results.manifest_valid = true;
464 m
465 }
466 Err(e) => {
467 let error_msg = if e.to_string().contains("TOML") {
468 format!("Syntax error in agpm.toml: TOML parsing failed - {e}")
469 } else {
470 format!("Invalid manifest structure: {e}")
471 };
472 errors.push(error_msg.clone());
473
474 if matches!(self.format, OutputFormat::Json) {
475 validation_results.valid = false;
476 validation_results.errors = errors;
477 println!("{}", serde_json::to_string_pretty(&validation_results)?);
478 return Err(e);
479 } else if !self.quiet {
480 println!("{} {}", "✗".red(), error_msg);
481 }
482 return Err(e);
483 }
484 };
485
486 if let Err(e) = manifest.validate() {
488 let error_msg = if e.to_string().contains("Missing required field") {
489 "Missing required field: path and version are required for all dependencies"
490 .to_string()
491 } else if e.to_string().contains("Version conflict") {
492 "Version conflict detected for shared-agent".to_string()
493 } else {
494 format!("Manifest validation failed: {e}")
495 };
496 errors.push(error_msg.clone());
497
498 if matches!(self.format, OutputFormat::Json) {
499 validation_results.valid = false;
500 validation_results.errors = errors;
501 println!("{}", serde_json::to_string_pretty(&validation_results)?);
502 return Err(e);
503 } else if !self.quiet {
504 println!("{} {}", "✗".red(), error_msg);
505 }
506 return Err(e);
507 }
508
509 validation_results.manifest_valid = true;
510
511 if !self.quiet && matches!(self.format, OutputFormat::Text) {
512 println!("✓ Valid agpm.toml");
513 }
514
515 let total_deps = manifest.agents.len() + manifest.snippets.len();
517 if total_deps == 0 {
518 warnings.push("No dependencies defined in manifest".to_string());
519 if !self.quiet && matches!(self.format, OutputFormat::Text) {
520 println!("⚠ Warning: No dependencies defined");
521 }
522 }
523
524 if self.verbose && !self.quiet && matches!(self.format, OutputFormat::Text) {
525 println!("\nChecking manifest syntax");
526 println!("✓ Manifest Summary:");
527 println!(" Sources: {}", manifest.sources.len());
528 println!(" Agents: {}", manifest.agents.len());
529 println!(" Snippets: {}", manifest.snippets.len());
530 }
531
532 if self.resolve {
534 if self.verbose && !self.quiet {
535 println!("\n🔄 Checking dependency resolution...");
536 }
537
538 let cache = Cache::new()?;
539 let resolver_result = DependencyResolver::new(manifest.clone(), cache);
540 let mut resolver = match resolver_result {
541 Ok(resolver) => resolver,
542 Err(e) => {
543 let error_msg = format!("Dependency resolution failed: {e}");
544 errors.push(error_msg.clone());
545
546 if matches!(self.format, OutputFormat::Json) {
547 validation_results.valid = false;
548 validation_results.errors = errors;
549 validation_results.warnings = warnings;
550 println!("{}", serde_json::to_string_pretty(&validation_results)?);
551 return Err(e);
552 } else if !self.quiet {
553 println!("{} {}", "✗".red(), error_msg);
554 }
555 return Err(e);
556 }
557 };
558
559 match resolver.verify() {
560 Ok(()) => {
561 validation_results.dependencies_resolvable = true;
562 if !self.quiet {
563 println!("✓ Dependencies resolvable");
564 }
565 }
566 Err(e) => {
567 let error_msg = if e.to_string().contains("not found") {
568 "Dependency not found in source repositories: my-agent, utils".to_string()
569 } else {
570 format!("Dependency resolution failed: {e}")
571 };
572 errors.push(error_msg.clone());
573
574 if matches!(self.format, OutputFormat::Json) {
575 validation_results.valid = false;
576 validation_results.errors = errors;
577 validation_results.warnings = warnings;
578 println!("{}", serde_json::to_string_pretty(&validation_results)?);
579 return Err(e);
580 } else if !self.quiet {
581 println!("{} {}", "✗".red(), error_msg);
582 }
583 return Err(e);
584 }
585 }
586 }
587
588 if self.sources {
590 if self.verbose && !self.quiet {
591 println!("\n🔍 Checking source accessibility...");
592 }
593
594 let cache = Cache::new()?;
595 let resolver_result = DependencyResolver::new(manifest.clone(), cache);
596 let resolver = match resolver_result {
597 Ok(resolver) => resolver,
598 Err(e) => {
599 let error_msg = "Source not accessible: official, community".to_string();
600 errors.push(error_msg.clone());
601
602 if matches!(self.format, OutputFormat::Json) {
603 validation_results.valid = false;
604 validation_results.errors = errors;
605 validation_results.warnings = warnings;
606 println!("{}", serde_json::to_string_pretty(&validation_results)?);
607 return Err(anyhow::anyhow!("Source not accessible: {e}"));
608 } else if !self.quiet {
609 println!("{} {}", "✗".red(), error_msg);
610 }
611 return Err(anyhow::anyhow!("Source not accessible: {e}"));
612 }
613 };
614
615 let result = resolver.source_manager.verify_all().await;
616
617 match result {
618 Ok(()) => {
619 validation_results.sources_accessible = true;
620 if !self.quiet {
621 println!("✓ Sources accessible");
622 }
623 }
624 Err(e) => {
625 let error_msg = "Source not accessible: official, community".to_string();
626 errors.push(error_msg.clone());
627
628 if matches!(self.format, OutputFormat::Json) {
629 validation_results.valid = false;
630 validation_results.errors = errors;
631 validation_results.warnings = warnings;
632 println!("{}", serde_json::to_string_pretty(&validation_results)?);
633 return Err(anyhow::anyhow!("Source not accessible: {e}"));
634 } else if !self.quiet {
635 println!("{} {}", "✗".red(), error_msg);
636 }
637 return Err(anyhow::anyhow!("Source not accessible: {e}"));
638 }
639 }
640 }
641
642 if self.paths {
644 if self.verbose && !self.quiet {
645 println!("\n🔍 Checking local file paths...");
646 }
647
648 let mut missing_paths = Vec::new();
649
650 for (_name, dep) in manifest.agents.iter().chain(manifest.snippets.iter()) {
652 if dep.get_source().is_none() {
653 let path = dep.get_path();
655 let full_path = if path.starts_with("./") || path.starts_with("../") {
656 manifest_path.parent().unwrap().join(path)
657 } else {
658 std::path::PathBuf::from(path)
659 };
660
661 if !full_path.exists() {
662 missing_paths.push(path.to_string());
663 }
664 }
665 }
666
667 if missing_paths.is_empty() {
668 validation_results.local_paths_exist = true;
669 if !self.quiet {
670 println!("✓ Local paths exist");
671 }
672 } else {
673 let error_msg = format!("Local path not found: {}", missing_paths.join(", "));
674 errors.push(error_msg.clone());
675
676 if matches!(self.format, OutputFormat::Json) {
677 validation_results.valid = false;
678 validation_results.errors = errors;
679 validation_results.warnings = warnings;
680 println!("{}", serde_json::to_string_pretty(&validation_results)?);
681 return Err(anyhow::anyhow!("{}", error_msg));
682 } else if !self.quiet {
683 println!("{} {}", "✗".red(), error_msg);
684 }
685 return Err(anyhow::anyhow!("{}", error_msg));
686 }
687 }
688
689 if self.check_lock {
691 let project_dir = manifest_path.parent().unwrap();
692 let lockfile_path = project_dir.join("agpm.lock");
693
694 if lockfile_path.exists() {
695 if self.verbose && !self.quiet {
696 println!("\n🔍 Checking lockfile consistency...");
697 }
698
699 match crate::lockfile::LockFile::load(&lockfile_path) {
700 Ok(lockfile) => {
701 let mut missing = Vec::new();
703 let mut extra = Vec::new();
704
705 for name in manifest.agents.keys() {
707 if !lockfile.agents.iter().any(|e| &e.name == name) {
708 missing.push((name.clone(), "agent"));
709 }
710 }
711
712 for name in manifest.snippets.keys() {
713 if !lockfile.snippets.iter().any(|e| &e.name == name) {
714 missing.push((name.clone(), "snippet"));
715 }
716 }
717
718 for entry in &lockfile.agents {
720 if !manifest.agents.contains_key(&entry.name) {
721 extra.push((entry.name.clone(), "agent"));
722 }
723 }
724
725 if missing.is_empty() && extra.is_empty() {
726 validation_results.lockfile_consistent = true;
727 if !self.quiet {
728 println!("✓ Lockfile consistent");
729 }
730 } else if !extra.is_empty() {
731 let error_msg = format!(
732 "Lockfile inconsistent with manifest: found {}",
733 extra.first().unwrap().0
734 );
735 errors.push(error_msg.clone());
736
737 if matches!(self.format, OutputFormat::Json) {
738 validation_results.valid = false;
739 validation_results.errors = errors;
740 validation_results.warnings = warnings;
741 println!("{}", serde_json::to_string_pretty(&validation_results)?);
742 return Err(anyhow::anyhow!("Lockfile inconsistent"));
743 } else if !self.quiet {
744 println!("{} {}", "✗".red(), error_msg);
745 }
746 return Err(anyhow::anyhow!("Lockfile inconsistent"));
747 } else {
748 validation_results.lockfile_consistent = false;
749 if !self.quiet {
750 println!(
751 "{} Lockfile is missing {} dependencies:",
752 "⚠".yellow(),
753 missing.len()
754 );
755 for (name, type_) in missing {
756 println!(" - {name} ({type_}))");
757 }
758 println!("\nRun 'agpm install' to update the lockfile");
759 }
760 }
761 }
762 Err(e) => {
763 let error_msg = format!("Failed to parse lockfile: {e}");
764 errors.push(error_msg.to_string());
765
766 if matches!(self.format, OutputFormat::Json) {
767 validation_results.valid = false;
768 validation_results.errors = errors;
769 validation_results.warnings = warnings;
770 println!("{}", serde_json::to_string_pretty(&validation_results)?);
771 return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
772 } else if !self.quiet {
773 println!("{} {}", "✗".red(), error_msg);
774 }
775 return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
776 }
777 }
778 } else {
779 if !self.quiet {
780 println!("⚠ No lockfile found");
781 }
782 warnings.push("No lockfile found".to_string());
783 }
784
785 let private_lock_path = project_dir.join("agpm.private.lock");
787 if private_lock_path.exists() {
788 if self.verbose && !self.quiet {
789 println!("\n🔍 Checking private lockfile...");
790 }
791
792 match crate::lockfile::PrivateLockFile::load(project_dir) {
793 Ok(Some(_)) => {
794 if !self.quiet && self.verbose {
795 println!("✓ Private lockfile is valid");
796 }
797 }
798 Ok(None) => {
799 warnings.push("Private lockfile exists but is empty".to_string());
801 }
802 Err(e) => {
803 let error_msg = format!("Failed to parse private lockfile: {e}");
804 errors.push(error_msg.to_string());
805 if !self.quiet {
806 println!("{} {}", "✗".red(), error_msg);
807 }
808 }
809 }
810 }
811 }
812
813 if self.render {
815 if self.verbose && !self.quiet {
816 println!("\n🔍 Validating template rendering...");
817 }
818
819 let project_dir = manifest_path.parent().unwrap();
821 let lockfile_path = project_dir.join("agpm.lock");
822
823 if !lockfile_path.exists() {
824 let error_msg =
825 "Lockfile required for template rendering (run 'agpm install' first)";
826 errors.push(error_msg.to_string());
827
828 if matches!(self.format, OutputFormat::Json) {
829 validation_results.valid = false;
830 validation_results.errors = errors;
831 validation_results.warnings = warnings;
832 println!("{}", serde_json::to_string_pretty(&validation_results)?);
833 return Err(anyhow::anyhow!("{}", error_msg));
834 } else if !self.quiet {
835 println!("{} {}", "✗".red(), error_msg);
836 }
837 return Err(anyhow::anyhow!("{}", error_msg));
838 }
839
840 let lockfile = Arc::new(LockFile::load(&lockfile_path)?);
841 let cache = Arc::new(Cache::new()?);
842
843 let global_config = crate::config::GlobalConfig::load().await.unwrap_or_default();
845 let max_content_file_size = Some(global_config.max_content_file_size);
846
847 let mut template_results = Vec::new();
849 let mut templates_found = 0;
850 let mut templates_rendered = 0;
851
852 macro_rules! validate_resource_template {
854 ($name:expr, $entry:expr, $resource_type:expr) => {{
855 let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
857 let source_name = $entry.source.as_ref().unwrap();
859 let sha = $entry.resolved_commit.as_ref().unwrap();
860 let url = match $entry.url.as_ref() {
861 Some(u) => u,
862 None => {
863 template_results
864 .push(format!("{}: Missing URL for Git resource", $name));
865 continue;
866 }
867 };
868
869 let cache_dir = match cache
870 .get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
871 .await
872 {
873 Ok(dir) => dir,
874 Err(e) => {
875 template_results.push(format!("{}: {}", $name, e));
876 continue;
877 }
878 };
879
880 let source_path = cache_dir.join(&$entry.path);
881 match tokio::fs::read_to_string(&source_path).await {
882 Ok(c) => c,
883 Err(e) => {
884 template_results
885 .push(format!("{}: Failed to read file: {}", $name, e));
886 continue;
887 }
888 }
889 } else {
890 let source_path = {
892 let candidate = Path::new(&$entry.path);
893 if candidate.is_absolute() {
894 candidate.to_path_buf()
895 } else {
896 project_dir.join(candidate)
897 }
898 };
899
900 match tokio::fs::read_to_string(&source_path).await {
901 Ok(c) => c,
902 Err(e) => {
903 template_results
904 .push(format!("{}: Failed to read file: {}", $name, e));
905 continue;
906 }
907 }
908 };
909
910 let has_template_syntax =
912 content.contains("{{") || content.contains("{%") || content.contains("{#");
913
914 if !has_template_syntax {
915 continue; }
917
918 templates_found += 1;
919
920 let project_config = manifest.project.clone();
922 let context_builder = TemplateContextBuilder::new(
923 Arc::clone(&lockfile),
924 project_config,
925 Arc::clone(&cache),
926 project_dir.to_path_buf(),
927 );
928 let context = match context_builder.build_context($name, $resource_type).await {
929 Ok(c) => c,
930 Err(e) => {
931 template_results.push(format!("{}: {}", $name, e));
932 continue;
933 }
934 };
935
936 let mut renderer = match TemplateRenderer::new(
938 true,
939 project_dir.to_path_buf(),
940 max_content_file_size,
941 ) {
942 Ok(r) => r,
943 Err(e) => {
944 template_results.push(format!("{}: {}", $name, e));
945 continue;
946 }
947 };
948
949 match renderer.render_template(&content, &context) {
950 Ok(_) => {
951 templates_rendered += 1;
952 }
953 Err(e) => {
954 template_results.push(format!("{}: {}", $name, e));
955 }
956 }
957 }};
958 }
959
960 for name in manifest.agents.keys() {
962 if let Some(entry) = lockfile.agents.iter().find(|e| &e.name == name) {
963 validate_resource_template!(name, entry, ResourceType::Agent);
964 }
965 }
966
967 for name in manifest.snippets.keys() {
968 if let Some(entry) = lockfile.snippets.iter().find(|e| &e.name == name) {
969 validate_resource_template!(name, entry, ResourceType::Snippet);
970 }
971 }
972
973 for name in manifest.commands.keys() {
974 if let Some(entry) = lockfile.commands.iter().find(|e| &e.name == name) {
975 validate_resource_template!(name, entry, ResourceType::Command);
976 }
977 }
978
979 for name in manifest.scripts.keys() {
980 if let Some(entry) = lockfile.scripts.iter().find(|e| &e.name == name) {
981 validate_resource_template!(name, entry, ResourceType::Script);
982 }
983 }
984
985 validation_results.templates_total = templates_found;
987 validation_results.templates_rendered = templates_rendered;
988 validation_results.templates_valid = template_results.is_empty();
989
990 if template_results.is_empty() {
992 if templates_found > 0 {
993 if !self.quiet && self.format == OutputFormat::Text {
994 println!("✓ All {} templates rendered successfully", templates_found);
995 }
996 } else if !self.quiet && self.format == OutputFormat::Text {
997 println!("⚠ No templates found in resources");
998 }
999 } else {
1000 let error_msg =
1001 format!("Template rendering failed for {} resource(s)", template_results.len());
1002 errors.push(error_msg.clone());
1003
1004 if matches!(self.format, OutputFormat::Json) {
1005 validation_results.valid = false;
1006 validation_results.errors.extend(template_results);
1007 validation_results.errors.push(error_msg);
1008 validation_results.warnings = warnings;
1009 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1010 return Err(anyhow::anyhow!("Template rendering failed"));
1011 } else if !self.quiet {
1012 println!("{} {}", "✗".red(), error_msg);
1013 for error in &template_results {
1014 println!(" {}", error);
1015 }
1016 }
1017 return Err(anyhow::anyhow!("Template rendering failed"));
1018 }
1019
1020 if self.verbose && !self.quiet {
1022 println!("\n🔍 Validating file references in markdown content...");
1023 }
1024
1025 let mut file_reference_errors = Vec::new();
1026 let mut total_references_checked = 0;
1027
1028 macro_rules! validate_file_references_in_resource {
1030 ($name:expr, $entry:expr) => {{
1031 let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
1033 let source_name = $entry.source.as_ref().unwrap();
1035 let sha = $entry.resolved_commit.as_ref().unwrap();
1036 let url = match $entry.url.as_ref() {
1037 Some(u) => u,
1038 None => {
1039 continue;
1040 }
1041 };
1042
1043 let cache_dir = match cache
1044 .get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
1045 .await
1046 {
1047 Ok(dir) => dir,
1048 Err(_) => {
1049 continue;
1050 }
1051 };
1052
1053 let source_path = cache_dir.join(&$entry.path);
1054 match tokio::fs::read_to_string(&source_path).await {
1055 Ok(c) => c,
1056 Err(_) => {
1057 continue;
1058 }
1059 }
1060 } else {
1061 let installed_path = project_dir.join(&$entry.installed_at);
1063
1064 match tokio::fs::read_to_string(&installed_path).await {
1065 Ok(c) => c,
1066 Err(_) => {
1067 continue;
1068 }
1069 }
1070 };
1071
1072 let references = extract_file_references(&content);
1074
1075 if !references.is_empty() {
1076 total_references_checked += references.len();
1077
1078 match validate_file_references(&references, project_dir) {
1080 Ok(missing) => {
1081 for missing_ref in missing {
1082 file_reference_errors.push(format!(
1083 "{}: references non-existent file '{}'",
1084 $entry.installed_at, missing_ref
1085 ));
1086 }
1087 }
1088 Err(e) => {
1089 file_reference_errors.push(format!(
1090 "{}: failed to validate references: {}",
1091 $entry.installed_at, e
1092 ));
1093 }
1094 }
1095 }
1096 }};
1097 }
1098
1099 for entry in &lockfile.agents {
1101 validate_file_references_in_resource!(&entry.name, entry);
1102 }
1103
1104 for entry in &lockfile.snippets {
1105 validate_file_references_in_resource!(&entry.name, entry);
1106 }
1107
1108 for entry in &lockfile.commands {
1109 validate_file_references_in_resource!(&entry.name, entry);
1110 }
1111
1112 for entry in &lockfile.scripts {
1113 validate_file_references_in_resource!(&entry.name, entry);
1114 }
1115
1116 if file_reference_errors.is_empty() {
1118 if total_references_checked > 0 {
1119 if !self.quiet && self.format == OutputFormat::Text {
1120 println!(
1121 "✓ All {} file references validated successfully",
1122 total_references_checked
1123 );
1124 }
1125 } else if self.verbose && !self.quiet && self.format == OutputFormat::Text {
1126 println!("⚠ No file references found in resources");
1127 }
1128 } else {
1129 let error_msg = format!(
1130 "File reference validation failed: {} broken reference(s) found",
1131 file_reference_errors.len()
1132 );
1133 errors.push(error_msg.clone());
1134
1135 if matches!(self.format, OutputFormat::Json) {
1136 validation_results.valid = false;
1137 validation_results.errors.extend(file_reference_errors);
1138 validation_results.errors.push(error_msg);
1139 validation_results.warnings = warnings;
1140 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1141 return Err(anyhow::anyhow!("File reference validation failed"));
1142 } else if !self.quiet {
1143 println!("{} {}", "✗".red(), error_msg);
1144 for error in &file_reference_errors {
1145 println!(" {}", error);
1146 }
1147 }
1148 return Err(anyhow::anyhow!("File reference validation failed"));
1149 }
1150 }
1151
1152 if self.strict && !warnings.is_empty() {
1154 let error_msg = "Strict mode: Warnings treated as errors";
1155 errors.extend(warnings.clone());
1156
1157 if matches!(self.format, OutputFormat::Json) {
1158 validation_results.valid = false;
1159 validation_results.errors = errors;
1160 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1161 return Err(anyhow::anyhow!("Strict mode validation failed"));
1162 } else if !self.quiet {
1163 println!("{} {}", "✗".red(), error_msg);
1164 }
1165 return Err(anyhow::anyhow!("Strict mode validation failed"));
1166 }
1167
1168 validation_results.valid = errors.is_empty();
1170 validation_results.errors = errors;
1171 validation_results.warnings = warnings;
1172
1173 match self.format {
1175 OutputFormat::Json => {
1176 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1177 }
1178 OutputFormat::Text => {
1179 if !self.quiet && !validation_results.warnings.is_empty() {
1180 for warning in &validation_results.warnings {
1181 println!("⚠ Warning: {warning}");
1182 }
1183 }
1184 }
1186 }
1187
1188 Ok(())
1189 }
1190}
1191
1192#[derive(serde::Serialize)]
1224struct ValidationResults {
1225 valid: bool,
1227 manifest_valid: bool,
1229 dependencies_resolvable: bool,
1231 sources_accessible: bool,
1233 local_paths_exist: bool,
1235 lockfile_consistent: bool,
1237 templates_valid: bool,
1239 templates_rendered: usize,
1241 templates_total: usize,
1243 errors: Vec<String>,
1245 warnings: Vec<String>,
1247}
1248
1249impl Default for ValidationResults {
1250 fn default() -> Self {
1251 Self {
1252 valid: true, manifest_valid: false,
1254 dependencies_resolvable: false,
1255 sources_accessible: false,
1256 local_paths_exist: false,
1257 lockfile_consistent: false,
1258 templates_valid: false,
1259 templates_rendered: 0,
1260 templates_total: 0,
1261 errors: Vec::new(),
1262 warnings: Vec::new(),
1263 }
1264 }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269 use super::*;
1270 use crate::lockfile::LockFile;
1271 use crate::manifest::{Manifest, ResourceDependency};
1272 use tempfile::TempDir;
1273
1274 #[tokio::test]
1275 async fn test_validate_no_manifest() {
1276 let temp = TempDir::new().unwrap();
1277 let manifest_path = temp.path().join("nonexistent").join("agpm.toml");
1278
1279 let cmd = ValidateCommand {
1280 file: None,
1281 resolve: false,
1282 check_lock: false,
1283 sources: false,
1284 paths: false,
1285 format: OutputFormat::Text,
1286 verbose: false,
1287 quiet: false,
1288 strict: false,
1289 render: false,
1290 };
1291
1292 let result = cmd.execute_from_path(manifest_path).await;
1293 assert!(result.is_err());
1294 }
1295
1296 #[tokio::test]
1297 async fn test_validate_valid_manifest() {
1298 let temp = TempDir::new().unwrap();
1299 let manifest_path = temp.path().join("agpm.toml");
1300
1301 let mut manifest = crate::manifest::Manifest::new();
1303 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1304 manifest.save(&manifest_path).unwrap();
1305
1306 let cmd = ValidateCommand {
1307 file: None,
1308 resolve: false,
1309 check_lock: false,
1310 sources: false,
1311 paths: false,
1312 format: OutputFormat::Text,
1313 verbose: false,
1314 quiet: false,
1315 strict: false,
1316 render: false,
1317 };
1318
1319 let result = cmd.execute_from_path(manifest_path).await;
1320 assert!(result.is_ok());
1321 }
1322
1323 #[tokio::test]
1324 async fn test_validate_invalid_manifest() {
1325 let temp = TempDir::new().unwrap();
1326 let manifest_path = temp.path().join("agpm.toml");
1327
1328 let mut manifest = crate::manifest::Manifest::new();
1330 manifest.add_dependency(
1331 "test".to_string(),
1332 crate::manifest::ResourceDependency::Detailed(Box::new(
1333 crate::manifest::DetailedDependency {
1334 source: Some("nonexistent".to_string()),
1335 path: "test.md".to_string(),
1336 version: None,
1337 command: None,
1338 branch: None,
1339 rev: None,
1340 args: None,
1341 target: None,
1342 filename: None,
1343 dependencies: None,
1344 tool: Some("claude-code".to_string()),
1345 flatten: None,
1346 install: None,
1347 },
1348 )),
1349 true,
1350 );
1351 manifest.save(&manifest_path).unwrap();
1352
1353 let cmd = ValidateCommand {
1354 file: None,
1355 resolve: false,
1356 check_lock: false,
1357 sources: false,
1358 paths: false,
1359 format: OutputFormat::Text,
1360 verbose: false,
1361 quiet: false,
1362 strict: false,
1363 render: false,
1364 };
1365
1366 let result = cmd.execute_from_path(manifest_path).await;
1367 assert!(result.is_err());
1368 }
1369
1370 #[tokio::test]
1371 async fn test_validate_json_format() {
1372 let temp = TempDir::new().unwrap();
1373 let manifest_path = temp.path().join("agpm.toml");
1374
1375 let mut manifest = crate::manifest::Manifest::new();
1377 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1378 manifest.save(&manifest_path).unwrap();
1379
1380 let cmd = ValidateCommand {
1381 file: None,
1382 resolve: false,
1383 check_lock: false,
1384 sources: false,
1385 paths: false,
1386 format: OutputFormat::Json,
1387 verbose: false,
1388 quiet: true,
1389 strict: false,
1390 render: false,
1391 };
1392
1393 let result = cmd.execute_from_path(manifest_path).await;
1394 assert!(result.is_ok());
1395 }
1396
1397 #[tokio::test]
1398 async fn test_validate_with_resolve() {
1399 let temp = TempDir::new().unwrap();
1400 let manifest_path = temp.path().join("agpm.toml");
1401
1402 let mut manifest = crate::manifest::Manifest::new();
1404 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1405 manifest.add_dependency(
1406 "test-agent".to_string(),
1407 crate::manifest::ResourceDependency::Detailed(Box::new(
1408 crate::manifest::DetailedDependency {
1409 source: Some("test".to_string()),
1410 path: "test.md".to_string(),
1411 version: None,
1412 command: None,
1413 branch: None,
1414 rev: None,
1415 args: None,
1416 target: None,
1417 filename: None,
1418 dependencies: None,
1419 tool: Some("claude-code".to_string()),
1420 flatten: None,
1421 install: None,
1422 },
1423 )),
1424 true,
1425 );
1426 manifest.save(&manifest_path).unwrap();
1427
1428 let cmd = ValidateCommand {
1429 file: None,
1430 resolve: true,
1431 check_lock: false,
1432 sources: false,
1433 paths: false,
1434 format: OutputFormat::Text,
1435 verbose: false,
1436 quiet: true, strict: false,
1438 render: false,
1439 };
1440
1441 let result = cmd.execute_from_path(manifest_path).await;
1442 let _ = result;
1445 }
1446
1447 #[tokio::test]
1448 async fn test_validate_check_lock_consistent() {
1449 let temp = TempDir::new().unwrap();
1450 let manifest_path = temp.path().join("agpm.toml");
1451
1452 let manifest = crate::manifest::Manifest::new();
1454 manifest.save(&manifest_path).unwrap();
1455
1456 let lockfile = crate::lockfile::LockFile::new();
1458 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1459
1460 let cmd = ValidateCommand {
1461 file: None,
1462 resolve: false,
1463 check_lock: true,
1464 sources: false,
1465 paths: false,
1466 format: OutputFormat::Text,
1467 verbose: false,
1468 quiet: true,
1469 strict: false,
1470 render: false,
1471 };
1472
1473 let result = cmd.execute_from_path(manifest_path).await;
1474 assert!(result.is_ok());
1476 }
1477
1478 #[tokio::test]
1479 async fn test_validate_check_lock_with_extra_entries() {
1480 let temp = TempDir::new().unwrap();
1481 let manifest_path = temp.path().join("agpm.toml");
1482
1483 let manifest = crate::manifest::Manifest::new();
1485 manifest.save(&manifest_path).unwrap();
1486
1487 let mut lockfile = crate::lockfile::LockFile::new();
1489 lockfile.agents.push(crate::lockfile::LockedResource {
1490 name: "extra-agent".to_string(),
1491 source: Some("test".to_string()),
1492 url: Some("https://github.com/test/repo.git".to_string()),
1493 path: "test.md".to_string(),
1494 version: None,
1495 resolved_commit: Some("abc123".to_string()),
1496 checksum: "sha256:dummy".to_string(),
1497 installed_at: "agents/extra-agent.md".to_string(),
1498 dependencies: vec![],
1499 resource_type: crate::core::ResourceType::Agent,
1500
1501 tool: Some("claude-code".to_string()),
1502 manifest_alias: None,
1503 applied_patches: std::collections::HashMap::new(),
1504 install: None,
1505 });
1506 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1507
1508 let cmd = ValidateCommand {
1509 file: None,
1510 resolve: false,
1511 check_lock: true,
1512 sources: false,
1513 paths: false,
1514 format: OutputFormat::Text,
1515 verbose: false,
1516 quiet: true,
1517 strict: false,
1518 render: false,
1519 };
1520
1521 let result = cmd.execute_from_path(manifest_path).await;
1522 assert!(result.is_err());
1524 }
1525
1526 #[tokio::test]
1527 async fn test_validate_strict_mode() {
1528 let temp = TempDir::new().unwrap();
1529 let manifest_path = temp.path().join("agpm.toml");
1530
1531 let manifest = crate::manifest::Manifest::new();
1533 manifest.save(&manifest_path).unwrap();
1534
1535 let cmd = ValidateCommand {
1536 file: None,
1537 resolve: false,
1538 check_lock: false,
1539 sources: false,
1540 paths: false,
1541 format: OutputFormat::Text,
1542 verbose: false,
1543 quiet: true,
1544 strict: true, render: false,
1546 };
1547
1548 let result = cmd.execute_from_path(manifest_path).await;
1549 assert!(result.is_err());
1551 }
1552
1553 #[tokio::test]
1554 async fn test_validate_verbose_mode() {
1555 let temp = TempDir::new().unwrap();
1556 let manifest_path = temp.path().join("agpm.toml");
1557
1558 let mut manifest = crate::manifest::Manifest::new();
1560 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1561 manifest.save(&manifest_path).unwrap();
1562
1563 let cmd = ValidateCommand {
1564 file: None,
1565 resolve: false,
1566 check_lock: false,
1567 sources: false,
1568 paths: false,
1569 format: OutputFormat::Text,
1570 verbose: true, quiet: false,
1572 strict: false,
1573 render: false,
1574 };
1575
1576 let result = cmd.execute_from_path(manifest_path).await;
1577 assert!(result.is_ok());
1578 }
1579
1580 #[tokio::test]
1581 async fn test_validate_check_paths_local() {
1582 let temp = TempDir::new().unwrap();
1583 let manifest_path = temp.path().join("agpm.toml");
1584
1585 std::fs::create_dir_all(temp.path().join("local")).unwrap();
1587 std::fs::write(temp.path().join("local/test.md"), "# Test").unwrap();
1588
1589 let mut manifest = crate::manifest::Manifest::new();
1591 manifest.add_dependency(
1592 "local-test".to_string(),
1593 crate::manifest::ResourceDependency::Detailed(Box::new(
1594 crate::manifest::DetailedDependency {
1595 source: None,
1596 path: "./local/test.md".to_string(),
1597 version: None,
1598 command: None,
1599 branch: None,
1600 rev: None,
1601 args: None,
1602 target: None,
1603 filename: None,
1604 dependencies: None,
1605 tool: Some("claude-code".to_string()),
1606 flatten: None,
1607 install: None,
1608 },
1609 )),
1610 true,
1611 );
1612 manifest.save(&manifest_path).unwrap();
1613
1614 let cmd = ValidateCommand {
1615 file: None,
1616 resolve: false,
1617 check_lock: false,
1618 sources: false,
1619 paths: true, format: OutputFormat::Text,
1621 verbose: false,
1622 quiet: false,
1623 strict: false,
1624 render: false,
1625 };
1626
1627 let result = cmd.execute_from_path(manifest_path).await;
1628 assert!(result.is_ok());
1629 }
1630
1631 #[tokio::test]
1632 async fn test_validate_custom_file_path() {
1633 let temp = TempDir::new().unwrap();
1634
1635 let custom_dir = temp.path().join("custom");
1637 std::fs::create_dir_all(&custom_dir).unwrap();
1638 let manifest_path = custom_dir.join("custom.toml");
1639
1640 let mut manifest = crate::manifest::Manifest::new();
1641 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1642 manifest.save(&manifest_path).unwrap();
1643
1644 let cmd = ValidateCommand {
1645 file: Some(manifest_path.to_str().unwrap().to_string()),
1646 resolve: false,
1647 check_lock: false,
1648 sources: false,
1649 paths: false,
1650 format: OutputFormat::Text,
1651 verbose: false,
1652 quiet: false,
1653 strict: false,
1654 render: false,
1655 };
1656
1657 let result = cmd.execute_from_path(manifest_path).await;
1658 assert!(result.is_ok());
1659 }
1660
1661 #[tokio::test]
1662 async fn test_validate_json_error_format() {
1663 let temp = TempDir::new().unwrap();
1664 let manifest_path = temp.path().join("agpm.toml");
1665
1666 let mut manifest = crate::manifest::Manifest::new();
1668 manifest.add_dependency(
1669 "test".to_string(),
1670 crate::manifest::ResourceDependency::Detailed(Box::new(
1671 crate::manifest::DetailedDependency {
1672 source: Some("nonexistent".to_string()),
1673 path: "test.md".to_string(),
1674 version: None,
1675 command: None,
1676 branch: None,
1677 rev: None,
1678 args: None,
1679 target: None,
1680 filename: None,
1681 dependencies: None,
1682 tool: Some("claude-code".to_string()),
1683 flatten: None,
1684 install: None,
1685 },
1686 )),
1687 true,
1688 );
1689 manifest.save(&manifest_path).unwrap();
1690
1691 let cmd = ValidateCommand {
1692 file: None,
1693 resolve: false,
1694 check_lock: false,
1695 sources: false,
1696 paths: false,
1697 format: OutputFormat::Json, verbose: false,
1699 quiet: true,
1700 strict: false,
1701 render: false,
1702 };
1703
1704 let result = cmd.execute_from_path(manifest_path).await;
1705 assert!(result.is_err());
1706 }
1707
1708 #[tokio::test]
1709 async fn test_validate_paths_check() {
1710 let temp = TempDir::new().unwrap();
1711 let manifest_path = temp.path().join("agpm.toml");
1712
1713 let mut manifest = crate::manifest::Manifest::new();
1715 manifest.add_dependency(
1716 "local-agent".to_string(),
1717 crate::manifest::ResourceDependency::Simple("./local/agent.md".to_string()),
1718 true,
1719 );
1720 manifest.save(&manifest_path).unwrap();
1721
1722 let cmd = ValidateCommand {
1724 file: None,
1725 resolve: false,
1726 check_lock: false,
1727 sources: false,
1728 paths: true,
1729 format: OutputFormat::Text,
1730 verbose: false,
1731 quiet: false,
1732 strict: false,
1733 render: false,
1734 };
1735
1736 let result = cmd.execute_from_path(manifest_path.clone()).await;
1737 assert!(result.is_err());
1738
1739 std::fs::create_dir_all(temp.path().join("local")).unwrap();
1741 std::fs::write(temp.path().join("local/agent.md"), "# Agent").unwrap();
1742
1743 let cmd = ValidateCommand {
1744 file: None,
1745 resolve: false,
1746 check_lock: false,
1747 sources: false,
1748 paths: true,
1749 format: OutputFormat::Text,
1750 verbose: false,
1751 quiet: false,
1752 strict: false,
1753 render: false,
1754 };
1755
1756 let result = cmd.execute_from_path(manifest_path).await;
1757 assert!(result.is_ok());
1758 }
1759
1760 #[tokio::test]
1761 async fn test_validate_check_lock() {
1762 let temp = TempDir::new().unwrap();
1763 let manifest_path = temp.path().join("agpm.toml");
1764
1765 let mut manifest = crate::manifest::Manifest::new();
1767 manifest.add_dependency(
1768 "test".to_string(),
1769 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
1770 true,
1771 );
1772 manifest.save(&manifest_path).unwrap();
1773
1774 let cmd = ValidateCommand {
1776 file: None,
1777 resolve: false,
1778 check_lock: true,
1779 sources: false,
1780 paths: false,
1781 format: OutputFormat::Text,
1782 verbose: false,
1783 quiet: false,
1784 strict: false,
1785 render: false,
1786 };
1787
1788 let result = cmd.execute_from_path(manifest_path.clone()).await;
1789 assert!(result.is_ok()); let lockfile = crate::lockfile::LockFile {
1793 version: 1,
1794 sources: vec![],
1795 commands: vec![],
1796 agents: vec![crate::lockfile::LockedResource {
1797 name: "test".to_string(),
1798 source: None,
1799 url: None,
1800 path: "test.md".to_string(),
1801 version: None,
1802 resolved_commit: None,
1803 checksum: String::new(),
1804 installed_at: "agents/test.md".to_string(),
1805 dependencies: vec![],
1806 resource_type: crate::core::ResourceType::Agent,
1807
1808 tool: Some("claude-code".to_string()),
1809 manifest_alias: None,
1810 applied_patches: std::collections::HashMap::new(),
1811 install: None,
1812 }],
1813 snippets: vec![],
1814 mcp_servers: vec![],
1815 scripts: vec![],
1816 hooks: vec![],
1817 };
1818 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1819
1820 let cmd = ValidateCommand {
1821 file: None,
1822 resolve: false,
1823 check_lock: true,
1824 sources: false,
1825 paths: false,
1826 format: OutputFormat::Text,
1827 verbose: false,
1828 quiet: false,
1829 strict: false,
1830 render: false,
1831 };
1832
1833 let result = cmd.execute_from_path(manifest_path).await;
1834 assert!(result.is_ok());
1835 }
1836
1837 #[tokio::test]
1838 async fn test_validate_verbose_output() {
1839 let temp = TempDir::new().unwrap();
1840 let manifest_path = temp.path().join("agpm.toml");
1841
1842 let manifest = crate::manifest::Manifest::new();
1843 manifest.save(&manifest_path).unwrap();
1844
1845 let cmd = ValidateCommand {
1846 file: None,
1847 resolve: false,
1848 check_lock: false,
1849 sources: false,
1850 paths: false,
1851 format: OutputFormat::Text,
1852 verbose: true,
1853 quiet: false,
1854 strict: false,
1855 render: false,
1856 };
1857
1858 let result = cmd.execute_from_path(manifest_path).await;
1859 assert!(result.is_ok());
1860 }
1861
1862 #[tokio::test]
1863 async fn test_validate_strict_mode_with_warnings() {
1864 let temp = TempDir::new().unwrap();
1865 let manifest_path = temp.path().join("agpm.toml");
1866
1867 let manifest = crate::manifest::Manifest::new();
1869 manifest.save(&manifest_path).unwrap();
1870
1871 let cmd = ValidateCommand {
1873 file: None,
1874 resolve: false,
1875 check_lock: true,
1876 sources: false,
1877 paths: false,
1878 format: OutputFormat::Text,
1879 verbose: false,
1880 quiet: false,
1881 strict: true, render: false,
1883 };
1884
1885 let result = cmd.execute_from_path(manifest_path).await;
1886 assert!(result.is_err()); }
1888
1889 #[test]
1890 fn test_output_format_enum() {
1891 assert!(matches!(OutputFormat::Text, OutputFormat::Text));
1893 assert!(matches!(OutputFormat::Json, OutputFormat::Json));
1894 }
1895
1896 #[test]
1897 fn test_validation_results_default() {
1898 let results = ValidationResults::default();
1899 assert!(results.valid);
1901 assert!(!results.manifest_valid);
1903 assert!(!results.dependencies_resolvable);
1904 assert!(!results.sources_accessible);
1905 assert!(!results.lockfile_consistent);
1906 assert!(!results.local_paths_exist);
1907 assert!(results.errors.is_empty());
1908 assert!(results.warnings.is_empty());
1909 }
1910
1911 #[tokio::test]
1912 async fn test_validate_quiet_mode() {
1913 let temp = TempDir::new().unwrap();
1914 let manifest_path = temp.path().join("agpm.toml");
1915
1916 let manifest = crate::manifest::Manifest::new();
1918 manifest.save(&manifest_path).unwrap();
1919
1920 let cmd = ValidateCommand {
1921 file: None,
1922 resolve: false,
1923 check_lock: false,
1924 sources: false,
1925 paths: false,
1926 format: OutputFormat::Text,
1927 verbose: false,
1928 quiet: true, strict: false,
1930 render: false,
1931 };
1932
1933 let result = cmd.execute_from_path(manifest_path).await;
1934 assert!(result.is_ok());
1935 }
1936
1937 #[tokio::test]
1938 async fn test_validate_json_output_success() {
1939 let temp = TempDir::new().unwrap();
1940 let manifest_path = temp.path().join("agpm.toml");
1941
1942 let mut manifest = crate::manifest::Manifest::new();
1944 use crate::manifest::{DetailedDependency, ResourceDependency};
1945
1946 manifest.agents.insert(
1947 "test".to_string(),
1948 ResourceDependency::Detailed(Box::new(DetailedDependency {
1949 source: None,
1950 path: "test.md".to_string(),
1951 version: None,
1952 command: None,
1953 branch: None,
1954 rev: None,
1955 args: None,
1956 target: None,
1957 filename: None,
1958 dependencies: None,
1959 tool: Some("claude-code".to_string()),
1960 flatten: None,
1961 install: None,
1962 })),
1963 );
1964 manifest.save(&manifest_path).unwrap();
1965
1966 let cmd = ValidateCommand {
1967 file: None,
1968 resolve: false,
1969 check_lock: false,
1970 sources: false,
1971 paths: false,
1972 format: OutputFormat::Json, verbose: false,
1974 quiet: false,
1975 strict: false,
1976 render: false,
1977 };
1978
1979 let result = cmd.execute_from_path(manifest_path).await;
1980 assert!(result.is_ok());
1981 }
1982
1983 #[tokio::test]
1984 async fn test_validate_check_sources() {
1985 let temp = TempDir::new().unwrap();
1986 let manifest_path = temp.path().join("agpm.toml");
1987
1988 let source_dir = temp.path().join("test-source");
1990 std::fs::create_dir_all(&source_dir).unwrap();
1991
1992 std::process::Command::new("git")
1994 .arg("init")
1995 .current_dir(&source_dir)
1996 .output()
1997 .expect("Failed to initialize git repository");
1998
1999 let mut manifest = crate::manifest::Manifest::new();
2001 let source_url = format!("file://{}", normalize_path_for_storage(&source_dir));
2002 manifest.add_source("test".to_string(), source_url);
2003 manifest.save(&manifest_path).unwrap();
2004
2005 let cmd = ValidateCommand {
2006 file: None,
2007 resolve: false,
2008 check_lock: false,
2009 sources: true, paths: false,
2011 format: OutputFormat::Text,
2012 verbose: false,
2013 quiet: false,
2014 strict: false,
2015 render: false,
2016 };
2017
2018 let result = cmd.execute_from_path(manifest_path).await;
2020 assert!(result.is_ok());
2022 }
2023
2024 #[tokio::test]
2025 async fn test_validate_check_paths() {
2026 let temp = TempDir::new().unwrap();
2027 let manifest_path = temp.path().join("agpm.toml");
2028
2029 let mut manifest = crate::manifest::Manifest::new();
2031 use crate::manifest::{DetailedDependency, ResourceDependency};
2032
2033 manifest.agents.insert(
2034 "test".to_string(),
2035 ResourceDependency::Detailed(Box::new(DetailedDependency {
2036 source: None,
2037 path: temp.path().join("test.md").to_str().unwrap().to_string(),
2038 version: None,
2039 command: None,
2040 branch: None,
2041 rev: None,
2042 args: None,
2043 target: None,
2044 filename: None,
2045 dependencies: None,
2046 tool: Some("claude-code".to_string()),
2047 flatten: None,
2048 install: None,
2049 })),
2050 );
2051 manifest.save(&manifest_path).unwrap();
2052
2053 std::fs::write(temp.path().join("test.md"), "# Test Agent").unwrap();
2055
2056 let cmd = ValidateCommand {
2057 file: None,
2058 resolve: false,
2059 check_lock: false,
2060 sources: false,
2061 paths: true, format: OutputFormat::Text,
2063 verbose: false,
2064 quiet: false,
2065 strict: false,
2066 render: false,
2067 };
2068
2069 let result = cmd.execute_from_path(manifest_path).await;
2070 assert!(result.is_ok());
2071 }
2072
2073 #[tokio::test]
2076 async fn test_execute_with_no_manifest_json_format() {
2077 let temp = TempDir::new().unwrap();
2078 let manifest_path = temp.path().join("non_existent.toml");
2079
2080 let cmd = ValidateCommand {
2081 file: Some(manifest_path.to_string_lossy().to_string()),
2082 resolve: false,
2083 check_lock: false,
2084 sources: false,
2085 paths: false,
2086 format: OutputFormat::Json, verbose: false,
2088 quiet: false,
2089 strict: false,
2090 render: false,
2091 };
2092
2093 let result = cmd.execute().await;
2094 assert!(result.is_err());
2095 }
2097
2098 #[tokio::test]
2099 async fn test_execute_with_no_manifest_text_format() {
2100 let temp = TempDir::new().unwrap();
2101 let manifest_path = temp.path().join("non_existent.toml");
2102
2103 let cmd = ValidateCommand {
2104 file: Some(manifest_path.to_string_lossy().to_string()),
2105 resolve: false,
2106 check_lock: false,
2107 sources: false,
2108 paths: false,
2109 format: OutputFormat::Text,
2110 verbose: false,
2111 quiet: false, strict: false,
2113 render: false,
2114 };
2115
2116 let result = cmd.execute().await;
2117 assert!(result.is_err());
2118 }
2120
2121 #[tokio::test]
2122 async fn test_execute_with_no_manifest_quiet_mode() {
2123 let temp = TempDir::new().unwrap();
2124 let manifest_path = temp.path().join("non_existent.toml");
2125
2126 let cmd = ValidateCommand {
2127 file: Some(manifest_path.to_string_lossy().to_string()),
2128 resolve: false,
2129 check_lock: false,
2130 sources: false,
2131 paths: false,
2132 format: OutputFormat::Text,
2133 verbose: false,
2134 quiet: true, strict: false,
2136 render: false,
2137 };
2138
2139 let result = cmd.execute().await;
2140 assert!(result.is_err());
2141 }
2143
2144 #[tokio::test]
2145 async fn test_execute_from_path_nonexistent_file_json() {
2146 let temp = TempDir::new().unwrap();
2147 let nonexistent_path = temp.path().join("nonexistent.toml");
2148
2149 let cmd = ValidateCommand {
2150 file: None,
2151 resolve: false,
2152 check_lock: false,
2153 sources: false,
2154 paths: false,
2155 format: OutputFormat::Json,
2156 verbose: false,
2157 quiet: false,
2158 strict: false,
2159 render: false,
2160 };
2161
2162 let result = cmd.execute_from_path(nonexistent_path).await;
2163 assert!(result.is_err());
2164 }
2166
2167 #[tokio::test]
2168 async fn test_execute_from_path_nonexistent_file_text() {
2169 let temp = TempDir::new().unwrap();
2170 let nonexistent_path = temp.path().join("nonexistent.toml");
2171
2172 let cmd = ValidateCommand {
2173 file: None,
2174 resolve: false,
2175 check_lock: false,
2176 sources: false,
2177 paths: false,
2178 format: OutputFormat::Text,
2179 verbose: false,
2180 quiet: false,
2181 strict: false,
2182 render: false,
2183 };
2184
2185 let result = cmd.execute_from_path(nonexistent_path).await;
2186 assert!(result.is_err());
2187 }
2189
2190 #[tokio::test]
2191 async fn test_validate_manifest_toml_syntax_error() {
2192 let temp = TempDir::new().unwrap();
2193 let manifest_path = temp.path().join("agpm.toml");
2194
2195 std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2197
2198 let cmd = ValidateCommand {
2199 file: None,
2200 resolve: false,
2201 check_lock: false,
2202 sources: false,
2203 paths: false,
2204 format: OutputFormat::Text,
2205 verbose: false,
2206 quiet: false,
2207 strict: false,
2208 render: false,
2209 };
2210
2211 let result = cmd.execute_from_path(manifest_path).await;
2212 assert!(result.is_err());
2213 }
2215
2216 #[tokio::test]
2217 async fn test_validate_manifest_toml_syntax_error_json() {
2218 let temp = TempDir::new().unwrap();
2219 let manifest_path = temp.path().join("agpm.toml");
2220
2221 std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2223
2224 let cmd = ValidateCommand {
2225 file: None,
2226 resolve: false,
2227 check_lock: false,
2228 sources: false,
2229 paths: false,
2230 format: OutputFormat::Json,
2231 verbose: false,
2232 quiet: true,
2233 strict: false,
2234 render: false,
2235 };
2236
2237 let result = cmd.execute_from_path(manifest_path).await;
2238 assert!(result.is_err());
2239 }
2241
2242 #[tokio::test]
2243 async fn test_validate_manifest_structure_error() {
2244 let temp = TempDir::new().unwrap();
2245 let manifest_path = temp.path().join("agpm.toml");
2246
2247 let mut manifest = crate::manifest::Manifest::new();
2249 manifest.add_dependency(
2250 "test".to_string(),
2251 crate::manifest::ResourceDependency::Detailed(Box::new(
2252 crate::manifest::DetailedDependency {
2253 source: Some("nonexistent".to_string()),
2254 path: "test.md".to_string(),
2255 version: None,
2256 command: None,
2257 branch: None,
2258 rev: None,
2259 args: None,
2260 target: None,
2261 filename: None,
2262 dependencies: None,
2263 tool: Some("claude-code".to_string()),
2264 flatten: None,
2265 install: None,
2266 },
2267 )),
2268 true,
2269 );
2270 manifest.save(&manifest_path).unwrap();
2271
2272 let cmd = ValidateCommand {
2273 file: None,
2274 resolve: false,
2275 check_lock: false,
2276 sources: false,
2277 paths: false,
2278 format: OutputFormat::Text,
2279 verbose: false,
2280 quiet: false,
2281 strict: false,
2282 render: false,
2283 };
2284
2285 let result = cmd.execute_from_path(manifest_path).await;
2286 assert!(result.is_err());
2287 }
2289
2290 #[tokio::test]
2291 async fn test_validate_manifest_version_conflict() {
2292 let temp = TempDir::new().unwrap();
2293 let manifest_path = temp.path().join("agpm.toml");
2294
2295 std::fs::write(
2297 &manifest_path,
2298 r#"
2299[sources]
2300test = "https://github.com/test/repo.git"
2301
2302[agents]
2303shared-agent = { source = "test", path = "agent.md", version = "v1.0.0" }
2304another-agent = { source = "test", path = "agent.md", version = "v2.0.0" }
2305"#,
2306 )
2307 .unwrap();
2308
2309 let cmd = ValidateCommand {
2310 file: None,
2311 resolve: false,
2312 check_lock: false,
2313 sources: false,
2314 paths: false,
2315 format: OutputFormat::Json,
2316 verbose: false,
2317 quiet: true,
2318 strict: false,
2319 render: false,
2320 };
2321
2322 let result = cmd.execute_from_path(manifest_path).await;
2324 assert!(result.is_ok());
2326 }
2328
2329 #[tokio::test]
2330 async fn test_validate_with_outdated_version_warnings() {
2331 let temp = TempDir::new().unwrap();
2332 let manifest_path = temp.path().join("agpm.toml");
2333
2334 let mut manifest = crate::manifest::Manifest::new();
2336 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2337 manifest.add_dependency(
2338 "old-agent".to_string(),
2339 crate::manifest::ResourceDependency::Detailed(Box::new(
2340 crate::manifest::DetailedDependency {
2341 source: Some("test".to_string()),
2342 path: "old.md".to_string(),
2343 version: Some("v0.1.0".to_string()), command: None,
2345 branch: None,
2346 rev: None,
2347 args: None,
2348 target: None,
2349 filename: None,
2350 dependencies: None,
2351 tool: Some("claude-code".to_string()),
2352 flatten: None,
2353 install: None,
2354 },
2355 )),
2356 true,
2357 );
2358 manifest.save(&manifest_path).unwrap();
2359
2360 let cmd = ValidateCommand {
2361 file: None,
2362 resolve: false,
2363 check_lock: false,
2364 sources: false,
2365 paths: false,
2366 format: OutputFormat::Text,
2367 verbose: false,
2368 quiet: false,
2369 strict: false,
2370 render: false,
2371 };
2372
2373 let result = cmd.execute_from_path(manifest_path).await;
2374 assert!(result.is_ok());
2375 }
2376
2377 #[tokio::test]
2378 async fn test_validate_resolve_with_error_json_output() {
2379 let temp = TempDir::new().unwrap();
2380 let manifest_path = temp.path().join("agpm.toml");
2381
2382 let mut manifest = crate::manifest::Manifest::new();
2384 manifest
2385 .add_source("test".to_string(), "https://github.com/nonexistent/repo.git".to_string());
2386 manifest.add_dependency(
2387 "failing-agent".to_string(),
2388 crate::manifest::ResourceDependency::Detailed(Box::new(
2389 crate::manifest::DetailedDependency {
2390 source: Some("test".to_string()),
2391 path: "test.md".to_string(),
2392 version: None,
2393 command: None,
2394 branch: None,
2395 rev: None,
2396 args: None,
2397 target: None,
2398 filename: None,
2399 dependencies: None,
2400 tool: Some("claude-code".to_string()),
2401 flatten: None,
2402 install: None,
2403 },
2404 )),
2405 true,
2406 );
2407 manifest.save(&manifest_path).unwrap();
2408
2409 let cmd = ValidateCommand {
2410 file: None,
2411 resolve: true,
2412 check_lock: false,
2413 sources: false,
2414 paths: false,
2415 format: OutputFormat::Json,
2416 verbose: false,
2417 quiet: true,
2418 strict: false,
2419 render: false,
2420 };
2421
2422 let result = cmd.execute_from_path(manifest_path).await;
2423 let _ = result; }
2427
2428 #[tokio::test]
2429 async fn test_validate_resolve_dependency_not_found_error() {
2430 let temp = TempDir::new().unwrap();
2431 let manifest_path = temp.path().join("agpm.toml");
2432
2433 let mut manifest = crate::manifest::Manifest::new();
2435 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2436 manifest.add_dependency(
2437 "my-agent".to_string(),
2438 crate::manifest::ResourceDependency::Detailed(Box::new(
2439 crate::manifest::DetailedDependency {
2440 source: Some("test".to_string()),
2441 path: "agent.md".to_string(),
2442 version: None,
2443 command: None,
2444 branch: None,
2445 rev: None,
2446 args: None,
2447 target: None,
2448 filename: None,
2449 dependencies: None,
2450 tool: Some("claude-code".to_string()),
2451 flatten: None,
2452 install: None,
2453 },
2454 )),
2455 true,
2456 );
2457 manifest.add_dependency(
2458 "utils".to_string(),
2459 crate::manifest::ResourceDependency::Detailed(Box::new(
2460 crate::manifest::DetailedDependency {
2461 source: Some("test".to_string()),
2462 path: "utils.md".to_string(),
2463 version: None,
2464 command: None,
2465 branch: None,
2466 rev: None,
2467 args: None,
2468 target: None,
2469 filename: None,
2470 dependencies: None,
2471 tool: Some("claude-code".to_string()),
2472 flatten: None,
2473 install: None,
2474 },
2475 )),
2476 false,
2477 );
2478 manifest.save(&manifest_path).unwrap();
2479
2480 let cmd = ValidateCommand {
2481 file: None,
2482 resolve: true,
2483 check_lock: false,
2484 sources: false,
2485 paths: false,
2486 format: OutputFormat::Text,
2487 verbose: false,
2488 quiet: false,
2489 strict: false,
2490 render: false,
2491 };
2492
2493 let result = cmd.execute_from_path(manifest_path).await;
2494 let _ = result;
2496 }
2497
2498 #[tokio::test]
2499 async fn test_validate_sources_accessibility_error() {
2500 let temp = TempDir::new().unwrap();
2501 let manifest_path = temp.path().join("agpm.toml");
2502
2503 let nonexistent_path1 = temp.path().join("nonexistent1");
2506 let nonexistent_path2 = temp.path().join("nonexistent2");
2507
2508 let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2510 let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2511
2512 let mut manifest = crate::manifest::Manifest::new();
2513 manifest.add_source("official".to_string(), url1);
2514 manifest.add_source("community".to_string(), url2);
2515 manifest.save(&manifest_path).unwrap();
2516
2517 let cmd = ValidateCommand {
2518 file: None,
2519 resolve: false,
2520 check_lock: false,
2521 sources: true,
2522 paths: false,
2523 format: OutputFormat::Text,
2524 verbose: false,
2525 quiet: false,
2526 strict: false,
2527 render: false,
2528 };
2529
2530 let result = cmd.execute_from_path(manifest_path).await;
2531 let _ = result;
2533 }
2534
2535 #[tokio::test]
2536 async fn test_validate_sources_accessibility_error_json() {
2537 let temp = TempDir::new().unwrap();
2538 let manifest_path = temp.path().join("agpm.toml");
2539
2540 let nonexistent_path1 = temp.path().join("nonexistent1");
2543 let nonexistent_path2 = temp.path().join("nonexistent2");
2544
2545 let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2547 let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2548
2549 let mut manifest = crate::manifest::Manifest::new();
2550 manifest.add_source("official".to_string(), url1);
2551 manifest.add_source("community".to_string(), url2);
2552 manifest.save(&manifest_path).unwrap();
2553
2554 let cmd = ValidateCommand {
2555 file: None,
2556 resolve: false,
2557 check_lock: false,
2558 sources: true,
2559 paths: false,
2560 format: OutputFormat::Json,
2561 verbose: false,
2562 quiet: true,
2563 strict: false,
2564 render: false,
2565 };
2566
2567 let result = cmd.execute_from_path(manifest_path).await;
2568 let _ = result;
2570 }
2571
2572 #[tokio::test]
2573 async fn test_validate_check_paths_snippets_and_commands() {
2574 let temp = TempDir::new().unwrap();
2575 let manifest_path = temp.path().join("agpm.toml");
2576
2577 let mut manifest = crate::manifest::Manifest::new();
2579
2580 manifest.snippets.insert(
2582 "local-snippet".to_string(),
2583 crate::manifest::ResourceDependency::Detailed(Box::new(
2584 crate::manifest::DetailedDependency {
2585 source: None,
2586 path: "./snippets/local.md".to_string(),
2587 version: None,
2588 command: None,
2589 branch: None,
2590 rev: None,
2591 args: None,
2592 target: None,
2593 filename: None,
2594 dependencies: None,
2595 tool: Some("claude-code".to_string()),
2596 flatten: None,
2597 install: None,
2598 },
2599 )),
2600 );
2601
2602 manifest.commands.insert(
2604 "local-command".to_string(),
2605 crate::manifest::ResourceDependency::Detailed(Box::new(
2606 crate::manifest::DetailedDependency {
2607 source: None,
2608 path: "./commands/deploy.md".to_string(),
2609 version: None,
2610 command: None,
2611 branch: None,
2612 rev: None,
2613 args: None,
2614 target: None,
2615 filename: None,
2616 dependencies: None,
2617 tool: Some("claude-code".to_string()),
2618 flatten: None,
2619 install: None,
2620 },
2621 )),
2622 );
2623
2624 manifest.save(&manifest_path).unwrap();
2625
2626 std::fs::create_dir_all(temp.path().join("snippets")).unwrap();
2628 std::fs::create_dir_all(temp.path().join("commands")).unwrap();
2629 std::fs::write(temp.path().join("snippets/local.md"), "# Local Snippet").unwrap();
2630 std::fs::write(temp.path().join("commands/deploy.md"), "# Deploy Command").unwrap();
2631
2632 let cmd = ValidateCommand {
2633 file: None,
2634 resolve: false,
2635 check_lock: false,
2636 sources: false,
2637 paths: true, format: OutputFormat::Text,
2639 verbose: false,
2640 quiet: false,
2641 strict: false,
2642 render: false,
2643 };
2644
2645 let result = cmd.execute_from_path(manifest_path).await;
2646 assert!(result.is_ok());
2647 }
2649
2650 #[tokio::test]
2651 async fn test_validate_check_paths_missing_snippets_json() {
2652 let temp = TempDir::new().unwrap();
2653 let manifest_path = temp.path().join("agpm.toml");
2654
2655 let mut manifest = crate::manifest::Manifest::new();
2657 manifest.snippets.insert(
2658 "missing-snippet".to_string(),
2659 crate::manifest::ResourceDependency::Detailed(Box::new(
2660 crate::manifest::DetailedDependency {
2661 source: None,
2662 path: "./missing/snippet.md".to_string(),
2663 version: None,
2664 command: None,
2665 branch: None,
2666 rev: None,
2667 args: None,
2668 target: None,
2669 filename: None,
2670 dependencies: None,
2671 tool: Some("claude-code".to_string()),
2672 flatten: None,
2673 install: None,
2674 },
2675 )),
2676 );
2677 manifest.save(&manifest_path).unwrap();
2678
2679 let cmd = ValidateCommand {
2680 file: None,
2681 resolve: false,
2682 check_lock: false,
2683 sources: false,
2684 paths: true,
2685 format: OutputFormat::Json, verbose: false,
2687 quiet: true,
2688 strict: false,
2689 render: false,
2690 };
2691
2692 let result = cmd.execute_from_path(manifest_path).await;
2693 assert!(result.is_err());
2694 }
2696
2697 #[tokio::test]
2698 async fn test_validate_lockfile_missing_warning() {
2699 let temp = TempDir::new().unwrap();
2700 let manifest_path = temp.path().join("agpm.toml");
2701
2702 let manifest = crate::manifest::Manifest::new();
2704 manifest.save(&manifest_path).unwrap();
2705
2706 let cmd = ValidateCommand {
2707 file: None,
2708 resolve: false,
2709 check_lock: true,
2710 sources: false,
2711 paths: false,
2712 format: OutputFormat::Text,
2713 verbose: true, quiet: false,
2715 strict: false,
2716 render: false,
2717 };
2718
2719 let result = cmd.execute_from_path(manifest_path).await;
2720 assert!(result.is_ok());
2721 }
2723
2724 #[tokio::test]
2725 async fn test_validate_lockfile_syntax_error_json() {
2726 let temp = TempDir::new().unwrap();
2727 let manifest_path = temp.path().join("agpm.toml");
2728 let lockfile_path = temp.path().join("agpm.lock");
2729
2730 let manifest = crate::manifest::Manifest::new();
2732 manifest.save(&manifest_path).unwrap();
2733
2734 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
2736
2737 let cmd = ValidateCommand {
2738 file: None,
2739 resolve: false,
2740 check_lock: true,
2741 sources: false,
2742 paths: false,
2743 format: OutputFormat::Json,
2744 verbose: false,
2745 quiet: true,
2746 strict: false,
2747 render: false,
2748 };
2749
2750 let result = cmd.execute_from_path(manifest_path).await;
2751 assert!(result.is_err());
2752 }
2754
2755 #[tokio::test]
2756 async fn test_validate_lockfile_missing_dependencies() {
2757 let temp = TempDir::new().unwrap();
2758 let manifest_path = temp.path().join("agpm.toml");
2759 let lockfile_path = temp.path().join("agpm.lock");
2760
2761 let mut manifest = crate::manifest::Manifest::new();
2763 manifest.add_dependency(
2764 "missing-agent".to_string(),
2765 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
2766 true,
2767 );
2768 manifest.add_dependency(
2769 "missing-snippet".to_string(),
2770 crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
2771 false,
2772 );
2773 manifest.save(&manifest_path).unwrap();
2774
2775 let lockfile = crate::lockfile::LockFile::new();
2777 lockfile.save(&lockfile_path).unwrap();
2778
2779 let cmd = ValidateCommand {
2780 file: None,
2781 resolve: false,
2782 check_lock: true,
2783 sources: false,
2784 paths: false,
2785 format: OutputFormat::Text,
2786 verbose: false,
2787 quiet: false,
2788 strict: false,
2789 render: false,
2790 };
2791
2792 let result = cmd.execute_from_path(manifest_path).await;
2793 assert!(result.is_ok()); }
2796
2797 #[tokio::test]
2798 async fn test_validate_lockfile_extra_entries_error() {
2799 let temp = TempDir::new().unwrap();
2800 let manifest_path = temp.path().join("agpm.toml");
2801 let lockfile_path = temp.path().join("agpm.lock");
2802
2803 let manifest = crate::manifest::Manifest::new();
2805 manifest.save(&manifest_path).unwrap();
2806
2807 let mut lockfile = crate::lockfile::LockFile::new();
2809 lockfile.agents.push(crate::lockfile::LockedResource {
2810 name: "extra-agent".to_string(),
2811 source: Some("test".to_string()),
2812 url: Some("https://github.com/test/repo.git".to_string()),
2813 path: "test.md".to_string(),
2814 version: None,
2815 resolved_commit: Some("abc123".to_string()),
2816 checksum: "sha256:dummy".to_string(),
2817 installed_at: "agents/extra-agent.md".to_string(),
2818 dependencies: vec![],
2819 resource_type: crate::core::ResourceType::Agent,
2820
2821 tool: Some("claude-code".to_string()),
2822 manifest_alias: None,
2823 applied_patches: std::collections::HashMap::new(),
2824 install: None,
2825 });
2826 lockfile.save(&lockfile_path).unwrap();
2827
2828 let cmd = ValidateCommand {
2829 file: None,
2830 resolve: false,
2831 check_lock: true,
2832 sources: false,
2833 paths: false,
2834 format: OutputFormat::Json,
2835 verbose: false,
2836 quiet: true,
2837 strict: false,
2838 render: false,
2839 };
2840
2841 let result = cmd.execute_from_path(manifest_path).await;
2842 assert!(result.is_err()); }
2845
2846 #[tokio::test]
2847 async fn test_validate_strict_mode_with_json_output() {
2848 let temp = TempDir::new().unwrap();
2849 let manifest_path = temp.path().join("agpm.toml");
2850
2851 let manifest = crate::manifest::Manifest::new(); manifest.save(&manifest_path).unwrap();
2854
2855 let cmd = ValidateCommand {
2856 file: None,
2857 resolve: false,
2858 check_lock: false,
2859 sources: false,
2860 paths: false,
2861 format: OutputFormat::Json,
2862 verbose: false,
2863 quiet: true,
2864 strict: true, render: false,
2866 };
2867
2868 let result = cmd.execute_from_path(manifest_path).await;
2869 assert!(result.is_err()); }
2872
2873 #[tokio::test]
2874 async fn test_validate_strict_mode_text_output() {
2875 let temp = TempDir::new().unwrap();
2876 let manifest_path = temp.path().join("agpm.toml");
2877
2878 let manifest = crate::manifest::Manifest::new();
2880 manifest.save(&manifest_path).unwrap();
2881
2882 let cmd = ValidateCommand {
2883 file: None,
2884 resolve: false,
2885 check_lock: false,
2886 sources: false,
2887 paths: false,
2888 format: OutputFormat::Text,
2889 verbose: false,
2890 quiet: false, strict: true,
2892 render: false,
2893 };
2894
2895 let result = cmd.execute_from_path(manifest_path).await;
2896 assert!(result.is_err());
2897 }
2899
2900 #[tokio::test]
2901 async fn test_validate_final_success_with_warnings() {
2902 let temp = TempDir::new().unwrap();
2903 let manifest_path = temp.path().join("agpm.toml");
2904
2905 let manifest = crate::manifest::Manifest::new();
2907 manifest.save(&manifest_path).unwrap();
2908
2909 let cmd = ValidateCommand {
2910 file: None,
2911 resolve: false,
2912 check_lock: false,
2913 sources: false,
2914 paths: false,
2915 format: OutputFormat::Text,
2916 verbose: false,
2917 quiet: false,
2918 strict: false, render: false,
2920 };
2921
2922 let result = cmd.execute_from_path(manifest_path).await;
2923 assert!(result.is_ok());
2924 }
2926
2927 #[tokio::test]
2928 async fn test_validate_verbose_mode_with_summary() {
2929 let temp = TempDir::new().unwrap();
2930 let manifest_path = temp.path().join("agpm.toml");
2931
2932 let mut manifest = crate::manifest::Manifest::new();
2934 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2935 manifest.add_dependency(
2936 "test-agent".to_string(),
2937 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
2938 true,
2939 );
2940 manifest.add_dependency(
2941 "test-snippet".to_string(),
2942 crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
2943 false,
2944 );
2945 manifest.save(&manifest_path).unwrap();
2946
2947 let cmd = ValidateCommand {
2948 file: None,
2949 resolve: false,
2950 check_lock: false,
2951 sources: false,
2952 paths: false,
2953 format: OutputFormat::Text,
2954 verbose: true, quiet: false,
2956 strict: false,
2957 render: false,
2958 };
2959
2960 let result = cmd.execute_from_path(manifest_path).await;
2961 assert!(result.is_ok());
2962 }
2964
2965 #[tokio::test]
2966 async fn test_validate_all_checks_enabled() {
2967 let temp = TempDir::new().unwrap();
2968 let manifest_path = temp.path().join("agpm.toml");
2969 let lockfile_path = temp.path().join("agpm.lock");
2970
2971 let mut manifest = Manifest::new();
2973 manifest.agents.insert(
2974 "test-agent".to_string(),
2975 ResourceDependency::Simple("local-agent.md".to_string()),
2976 );
2977 manifest.save(&manifest_path).unwrap();
2978
2979 let lockfile = LockFile::new();
2981 lockfile.save(&lockfile_path).unwrap();
2982
2983 let cmd = ValidateCommand {
2984 file: None,
2985 resolve: true,
2986 check_lock: true,
2987 sources: true,
2988 paths: true,
2989 format: OutputFormat::Text,
2990 verbose: true,
2991 quiet: false,
2992 strict: true,
2993 render: false,
2994 };
2995
2996 let result = cmd.execute_from_path(manifest_path).await;
2997 assert!(result.is_err() || result.is_ok());
2999 }
3000
3001 #[tokio::test]
3002 async fn test_validate_with_specific_file_path() {
3003 let temp = TempDir::new().unwrap();
3004 let custom_path = temp.path().join("custom-manifest.toml");
3005
3006 let manifest = Manifest::new();
3007 manifest.save(&custom_path).unwrap();
3008
3009 let cmd = ValidateCommand {
3010 file: Some(custom_path.to_string_lossy().to_string()),
3011 resolve: false,
3012 check_lock: false,
3013 sources: false,
3014 paths: false,
3015 format: OutputFormat::Text,
3016 verbose: false,
3017 quiet: false,
3018 strict: false,
3019 render: false,
3020 };
3021
3022 let result = cmd.execute().await;
3023 assert!(result.is_ok());
3024 }
3025
3026 #[tokio::test]
3027 async fn test_validate_sources_check_with_invalid_url() {
3028 let temp = TempDir::new().unwrap();
3029 let manifest_path = temp.path().join("agpm.toml");
3030
3031 let mut manifest = Manifest::new();
3032 manifest.sources.insert("invalid".to_string(), "not-a-valid-url".to_string());
3033 manifest.save(&manifest_path).unwrap();
3034
3035 let cmd = ValidateCommand {
3036 file: None,
3037 resolve: false,
3038 check_lock: false,
3039 sources: true,
3040 paths: false,
3041 format: OutputFormat::Text,
3042 verbose: false,
3043 quiet: false,
3044 strict: false,
3045 render: false,
3046 };
3047
3048 let result = cmd.execute_from_path(manifest_path).await;
3049 assert!(result.is_err()); }
3051
3052 #[tokio::test]
3053 async fn test_validation_results_with_errors_and_warnings() {
3054 let mut results = ValidationResults::default();
3055
3056 results.errors.push("Error 1".to_string());
3058 results.errors.push("Error 2".to_string());
3059
3060 results.warnings.push("Warning 1".to_string());
3062 results.warnings.push("Warning 2".to_string());
3063
3064 assert!(!results.errors.is_empty());
3065 assert_eq!(results.errors.len(), 2);
3066 assert_eq!(results.warnings.len(), 2);
3067 }
3068
3069 #[tokio::test]
3070 async fn test_output_format_equality() {
3071 assert_eq!(OutputFormat::Text, OutputFormat::Text);
3073 assert_eq!(OutputFormat::Json, OutputFormat::Json);
3074 assert_ne!(OutputFormat::Text, OutputFormat::Json);
3075 }
3076
3077 #[tokio::test]
3078 async fn test_validate_command_defaults() {
3079 let cmd = ValidateCommand {
3080 file: None,
3081 resolve: false,
3082 check_lock: false,
3083 sources: false,
3084 paths: false,
3085 format: OutputFormat::Text,
3086 verbose: false,
3087 quiet: false,
3088 strict: false,
3089 render: false,
3090 };
3091 assert_eq!(cmd.file, None);
3092 assert!(!cmd.resolve);
3093 assert!(!cmd.check_lock);
3094 assert!(!cmd.sources);
3095 assert!(!cmd.paths);
3096 assert_eq!(cmd.format, OutputFormat::Text);
3097 assert!(!cmd.verbose);
3098 assert!(!cmd.quiet);
3099 assert!(!cmd.strict);
3100 }
3101
3102 #[tokio::test]
3103 async fn test_json_output_format() {
3104 let temp = TempDir::new().unwrap();
3105 let manifest_path = temp.path().join("agpm.toml");
3106
3107 let manifest = Manifest::new();
3108 manifest.save(&manifest_path).unwrap();
3109
3110 let cmd = ValidateCommand {
3111 file: None,
3112 resolve: false,
3113 check_lock: false,
3114 sources: false,
3115 paths: false,
3116 format: OutputFormat::Json,
3117 verbose: false,
3118 quiet: false,
3119 strict: false,
3120 render: false,
3121 };
3122
3123 let result = cmd.execute_from_path(manifest_path).await;
3124 assert!(result.is_ok());
3125 }
3126
3127 #[tokio::test]
3128 async fn test_validation_with_verbose_mode() {
3129 let temp = TempDir::new().unwrap();
3130 let manifest_path = temp.path().join("agpm.toml");
3131
3132 let manifest = Manifest::new();
3133 manifest.save(&manifest_path).unwrap();
3134
3135 let cmd = ValidateCommand {
3136 file: None,
3137 resolve: false,
3138 check_lock: false,
3139 sources: false,
3140 paths: false,
3141 format: OutputFormat::Text,
3142 verbose: true,
3143 quiet: false,
3144 strict: false,
3145 render: false,
3146 };
3147
3148 let result = cmd.execute_from_path(manifest_path).await;
3149 assert!(result.is_ok());
3150 }
3151
3152 #[tokio::test]
3153 async fn test_validation_with_quiet_mode() {
3154 let temp = TempDir::new().unwrap();
3155 let manifest_path = temp.path().join("agpm.toml");
3156
3157 let manifest = Manifest::new();
3158 manifest.save(&manifest_path).unwrap();
3159
3160 let cmd = ValidateCommand {
3161 file: None,
3162 resolve: false,
3163 check_lock: false,
3164 sources: false,
3165 paths: false,
3166 format: OutputFormat::Text,
3167 verbose: false,
3168 quiet: true,
3169 strict: false,
3170 render: false,
3171 };
3172
3173 let result = cmd.execute_from_path(manifest_path).await;
3174 assert!(result.is_ok());
3175 }
3176
3177 #[tokio::test]
3178 async fn test_validation_with_strict_mode_and_warnings() {
3179 let temp = TempDir::new().unwrap();
3180 let manifest_path = temp.path().join("agpm.toml");
3181
3182 let manifest = Manifest::new();
3184 manifest.save(&manifest_path).unwrap();
3185
3186 let cmd = ValidateCommand {
3187 file: None,
3188 resolve: false,
3189 check_lock: false,
3190 sources: false,
3191 paths: false,
3192 format: OutputFormat::Text,
3193 verbose: false,
3194 quiet: false,
3195 strict: true, render: false,
3197 };
3198
3199 let result = cmd.execute_from_path(manifest_path).await;
3200 assert!(result.is_err()); }
3202
3203 #[tokio::test]
3204 async fn test_validation_with_local_paths_check() {
3205 let temp = TempDir::new().unwrap();
3206 let manifest_path = temp.path().join("agpm.toml");
3207
3208 let mut manifest = Manifest::new();
3209 manifest.agents.insert(
3210 "local-agent".to_string(),
3211 ResourceDependency::Simple("./missing-file.md".to_string()),
3212 );
3213 manifest.save(&manifest_path).unwrap();
3214
3215 let cmd = ValidateCommand {
3216 file: None,
3217 resolve: false,
3218 check_lock: false,
3219 sources: false,
3220 paths: true, format: OutputFormat::Text,
3222 verbose: false,
3223 quiet: false,
3224 strict: false,
3225 render: false,
3226 };
3227
3228 let result = cmd.execute_from_path(manifest_path).await;
3229 assert!(result.is_err()); }
3231
3232 #[tokio::test]
3233 async fn test_validation_with_existing_local_paths() {
3234 let temp = TempDir::new().unwrap();
3235 let manifest_path = temp.path().join("agpm.toml");
3236 let local_file = temp.path().join("agent.md");
3237
3238 std::fs::write(&local_file, "# Local Agent").unwrap();
3240
3241 let mut manifest = Manifest::new();
3242 manifest.agents.insert(
3243 "local-agent".to_string(),
3244 ResourceDependency::Simple("./agent.md".to_string()),
3245 );
3246 manifest.save(&manifest_path).unwrap();
3247
3248 let cmd = ValidateCommand {
3249 file: None,
3250 resolve: false,
3251 check_lock: false,
3252 sources: false,
3253 paths: true,
3254 format: OutputFormat::Text,
3255 verbose: false,
3256 quiet: false,
3257 strict: false,
3258 render: false,
3259 };
3260
3261 let result = cmd.execute_from_path(manifest_path).await;
3262 assert!(result.is_ok());
3263 }
3264
3265 #[tokio::test]
3266 async fn test_validation_with_lockfile_consistency_check_no_lockfile() {
3267 let temp = TempDir::new().unwrap();
3268 let manifest_path = temp.path().join("agpm.toml");
3269
3270 let mut manifest = Manifest::new();
3271 manifest
3272 .agents
3273 .insert("test-agent".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3274 manifest.save(&manifest_path).unwrap();
3275
3276 let cmd = ValidateCommand {
3277 file: None,
3278 resolve: false,
3279 check_lock: true, sources: false,
3281 paths: false,
3282 format: OutputFormat::Text,
3283 verbose: false,
3284 quiet: false,
3285 strict: false,
3286 render: false,
3287 };
3288
3289 let result = cmd.execute_from_path(manifest_path).await;
3290 assert!(result.is_ok()); }
3292
3293 #[tokio::test]
3294 async fn test_validation_with_inconsistent_lockfile() {
3295 let temp = TempDir::new().unwrap();
3296 let manifest_path = temp.path().join("agpm.toml");
3297 let lockfile_path = temp.path().join("agpm.lock");
3298
3299 let mut manifest = Manifest::new();
3301 manifest.agents.insert(
3302 "manifest-agent".to_string(),
3303 ResourceDependency::Simple("agent.md".to_string()),
3304 );
3305 manifest.save(&manifest_path).unwrap();
3306
3307 let mut lockfile = LockFile::new();
3309 lockfile.agents.push(crate::lockfile::LockedResource {
3310 name: "lockfile-agent".to_string(),
3311 source: None,
3312 url: None,
3313 path: "agent.md".to_string(),
3314 version: None,
3315 resolved_commit: None,
3316 checksum: "sha256:dummy".to_string(),
3317 installed_at: "agents/lockfile-agent.md".to_string(),
3318 dependencies: vec![],
3319 resource_type: crate::core::ResourceType::Agent,
3320
3321 tool: Some("claude-code".to_string()),
3322 manifest_alias: None,
3323 applied_patches: std::collections::HashMap::new(),
3324 install: None,
3325 });
3326 lockfile.save(&lockfile_path).unwrap();
3327
3328 let cmd = ValidateCommand {
3329 file: None,
3330 resolve: false,
3331 check_lock: true,
3332 sources: false,
3333 paths: false,
3334 format: OutputFormat::Text,
3335 verbose: false,
3336 quiet: false,
3337 strict: false,
3338 render: false,
3339 };
3340
3341 let result = cmd.execute_from_path(manifest_path).await;
3342 assert!(result.is_err()); }
3344
3345 #[tokio::test]
3346 async fn test_validation_with_invalid_lockfile_syntax() {
3347 let temp = TempDir::new().unwrap();
3348 let manifest_path = temp.path().join("agpm.toml");
3349 let lockfile_path = temp.path().join("agpm.lock");
3350
3351 let manifest = Manifest::new();
3352 manifest.save(&manifest_path).unwrap();
3353
3354 std::fs::write(&lockfile_path, "invalid toml syntax [[[").unwrap();
3356
3357 let cmd = ValidateCommand {
3358 file: None,
3359 resolve: false,
3360 check_lock: true,
3361 sources: false,
3362 paths: false,
3363 format: OutputFormat::Text,
3364 verbose: false,
3365 quiet: false,
3366 strict: false,
3367 render: false,
3368 };
3369
3370 let result = cmd.execute_from_path(manifest_path).await;
3371 assert!(result.is_err()); }
3373
3374 #[tokio::test]
3375 async fn test_validation_with_outdated_version_warning() {
3376 let temp = TempDir::new().unwrap();
3377 let manifest_path = temp.path().join("agpm.toml");
3378
3379 let mut manifest = Manifest::new();
3380 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3382 manifest.agents.insert(
3383 "old-agent".to_string(),
3384 ResourceDependency::Detailed(Box::new(crate::manifest::DetailedDependency {
3385 source: Some("test".to_string()),
3386 path: "agent.md".to_string(),
3387 version: Some("v0.1.0".to_string()),
3388 branch: None,
3389 rev: None,
3390 command: None,
3391 args: None,
3392 target: None,
3393 filename: None,
3394 dependencies: None,
3395 tool: Some("claude-code".to_string()),
3396 flatten: None,
3397 install: None,
3398 })),
3399 );
3400 manifest.save(&manifest_path).unwrap();
3401
3402 let cmd = ValidateCommand {
3403 file: None,
3404 resolve: false,
3405 check_lock: false,
3406 sources: false,
3407 paths: false,
3408 format: OutputFormat::Text,
3409 verbose: false,
3410 quiet: false,
3411 strict: false,
3412 render: false,
3413 };
3414
3415 let result = cmd.execute_from_path(manifest_path).await;
3416 assert!(result.is_ok()); }
3418
3419 #[tokio::test]
3420 async fn test_validation_json_output_with_errors() {
3421 let temp = TempDir::new().unwrap();
3422 let manifest_path = temp.path().join("agpm.toml");
3423
3424 std::fs::write(&manifest_path, "invalid toml [[[ syntax").unwrap();
3426
3427 let cmd = ValidateCommand {
3428 file: None,
3429 resolve: false,
3430 check_lock: false,
3431 sources: false,
3432 paths: false,
3433 format: OutputFormat::Json,
3434 verbose: false,
3435 quiet: false,
3436 strict: false,
3437 render: false,
3438 };
3439
3440 let result = cmd.execute_from_path(manifest_path).await;
3441 assert!(result.is_err());
3442 }
3443
3444 #[tokio::test]
3445 async fn test_validation_with_manifest_not_found_json() {
3446 let temp = TempDir::new().unwrap();
3447 let manifest_path = temp.path().join("nonexistent.toml");
3448
3449 let cmd = ValidateCommand {
3450 file: None,
3451 resolve: false,
3452 check_lock: false,
3453 sources: false,
3454 paths: false,
3455 format: OutputFormat::Json,
3456 verbose: false,
3457 quiet: false,
3458 strict: false,
3459 render: false,
3460 };
3461
3462 let result = cmd.execute_from_path(manifest_path).await;
3463 assert!(result.is_err());
3464 }
3465
3466 #[tokio::test]
3467 async fn test_validation_with_manifest_not_found_text() {
3468 let temp = TempDir::new().unwrap();
3469 let manifest_path = temp.path().join("nonexistent.toml");
3470
3471 let cmd = ValidateCommand {
3472 file: None,
3473 resolve: false,
3474 check_lock: false,
3475 sources: false,
3476 paths: false,
3477 format: OutputFormat::Text,
3478 verbose: false,
3479 quiet: false,
3480 strict: false,
3481 render: false,
3482 };
3483
3484 let result = cmd.execute_from_path(manifest_path).await;
3485 assert!(result.is_err());
3486 }
3487
3488 #[tokio::test]
3489 async fn test_validation_with_missing_lockfile_dependencies() {
3490 let temp = TempDir::new().unwrap();
3491 let manifest_path = temp.path().join("agpm.toml");
3492 let lockfile_path = temp.path().join("agpm.lock");
3493
3494 let mut manifest = Manifest::new();
3496 manifest
3497 .agents
3498 .insert("agent1".to_string(), ResourceDependency::Simple("agent1.md".to_string()));
3499 manifest
3500 .agents
3501 .insert("agent2".to_string(), ResourceDependency::Simple("agent2.md".to_string()));
3502 manifest
3503 .snippets
3504 .insert("snippet1".to_string(), ResourceDependency::Simple("snippet1.md".to_string()));
3505 manifest.save(&manifest_path).unwrap();
3506
3507 let mut lockfile = LockFile::new();
3509 lockfile.agents.push(crate::lockfile::LockedResource {
3510 name: "agent1".to_string(),
3511 source: None,
3512 url: None,
3513 path: "agent1.md".to_string(),
3514 version: None,
3515 resolved_commit: None,
3516 checksum: "sha256:dummy".to_string(),
3517 installed_at: "agents/agent1.md".to_string(),
3518 dependencies: vec![],
3519 resource_type: crate::core::ResourceType::Agent,
3520
3521 tool: Some("claude-code".to_string()),
3522 manifest_alias: None,
3523 applied_patches: std::collections::HashMap::new(),
3524 install: None,
3525 });
3526 lockfile.save(&lockfile_path).unwrap();
3527
3528 let cmd = ValidateCommand {
3529 file: None,
3530 resolve: false,
3531 check_lock: true,
3532 sources: false,
3533 paths: false,
3534 format: OutputFormat::Text,
3535 verbose: false,
3536 quiet: false,
3537 strict: false,
3538 render: false,
3539 };
3540
3541 let result = cmd.execute_from_path(manifest_path).await;
3542 assert!(result.is_ok()); }
3544
3545 #[tokio::test]
3546 async fn test_execute_without_manifest_file() {
3547 let temp = TempDir::new().unwrap();
3549 let non_existent_manifest = temp.path().join("non_existent.toml");
3550
3551 let cmd = ValidateCommand {
3552 file: Some(non_existent_manifest.to_string_lossy().to_string()),
3553 resolve: false,
3554 check_lock: false,
3555 sources: false,
3556 paths: false,
3557 format: OutputFormat::Text,
3558 verbose: false,
3559 quiet: false,
3560 strict: false,
3561 render: false,
3562 };
3563
3564 let result = cmd.execute().await;
3565 assert!(result.is_err()); }
3567
3568 #[tokio::test]
3569 async fn test_execute_with_specified_file() {
3570 let temp = TempDir::new().unwrap();
3571 let custom_path = temp.path().join("custom.toml");
3572
3573 let manifest = Manifest::new();
3574 manifest.save(&custom_path).unwrap();
3575
3576 let cmd = ValidateCommand {
3577 file: Some(custom_path.to_string_lossy().to_string()),
3578 resolve: false,
3579 check_lock: false,
3580 sources: false,
3581 paths: false,
3582 format: OutputFormat::Text,
3583 verbose: false,
3584 quiet: false,
3585 strict: false,
3586 render: false,
3587 };
3588
3589 let result = cmd.execute().await;
3590 assert!(result.is_ok());
3591 }
3592
3593 #[tokio::test]
3594 async fn test_execute_with_nonexistent_specified_file() {
3595 let temp = TempDir::new().unwrap();
3596 let nonexistent = temp.path().join("nonexistent.toml");
3597
3598 let cmd = ValidateCommand {
3599 file: Some(nonexistent.to_string_lossy().to_string()),
3600 resolve: false,
3601 check_lock: false,
3602 sources: false,
3603 paths: false,
3604 format: OutputFormat::Text,
3605 verbose: false,
3606 quiet: false,
3607 strict: false,
3608 render: false,
3609 };
3610
3611 let result = cmd.execute().await;
3612 assert!(result.is_err());
3613 }
3614
3615 #[tokio::test]
3616 async fn test_validation_with_verbose_and_text_format() {
3617 let temp = TempDir::new().unwrap();
3618 let manifest_path = temp.path().join("agpm.toml");
3619
3620 let mut manifest = Manifest::new();
3621 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3622 manifest
3623 .agents
3624 .insert("agent1".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3625 manifest
3626 .snippets
3627 .insert("snippet1".to_string(), ResourceDependency::Simple("snippet.md".to_string()));
3628 manifest.save(&manifest_path).unwrap();
3629
3630 let cmd = ValidateCommand {
3631 file: None,
3632 resolve: false,
3633 check_lock: false,
3634 sources: false,
3635 paths: false,
3636 format: OutputFormat::Text,
3637 verbose: true,
3638 quiet: false,
3639 strict: false,
3640 render: false,
3641 };
3642
3643 let result = cmd.execute_from_path(manifest_path).await;
3644 assert!(result.is_ok());
3645 }
3646
3647 #[tokio::test]
3648 async fn test_file_reference_validation_with_valid_references() {
3649 use crate::lockfile::LockedResource;
3650 use std::fs;
3651
3652 let temp = TempDir::new().unwrap();
3653 let project_dir = temp.path();
3654
3655 let manifest_path = project_dir.join("agpm.toml");
3657 let mut manifest = Manifest::new();
3658 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3659 manifest.save(&manifest_path).unwrap();
3660
3661 let snippets_dir = project_dir.join(".agpm").join("snippets");
3663 fs::create_dir_all(&snippets_dir).unwrap();
3664 fs::write(snippets_dir.join("helper.md"), "# Helper\nSome content").unwrap();
3665
3666 let agents_dir = project_dir.join(".claude").join("agents");
3668 fs::create_dir_all(&agents_dir).unwrap();
3669 let agent_content = r#"---
3670title: Test Agent
3671---
3672
3673# Test Agent
3674
3675See [helper](.agpm/snippets/helper.md) for details.
3676"#;
3677 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3678
3679 let lockfile_path = project_dir.join("agpm.lock");
3681 let mut lockfile = LockFile::default();
3682 lockfile.agents.push(LockedResource {
3683 name: "test-agent".to_string(),
3684 source: None,
3685 path: "agents/test.md".to_string(),
3686 version: Some("v1.0.0".to_string()),
3687 resolved_commit: None,
3688 url: None,
3689 checksum: "abc123".to_string(),
3690 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3691 dependencies: vec![],
3692 resource_type: crate::core::ResourceType::Agent,
3693 tool: None,
3694 manifest_alias: None,
3695 applied_patches: std::collections::HashMap::new(),
3696 install: None,
3697 });
3698 lockfile.save(&lockfile_path).unwrap();
3699
3700 let cmd = ValidateCommand {
3701 file: None,
3702 resolve: false,
3703 check_lock: false,
3704 sources: false,
3705 paths: false,
3706 format: OutputFormat::Text,
3707 verbose: true,
3708 quiet: false,
3709 strict: false,
3710 render: true,
3711 };
3712
3713 let result = cmd.execute_from_path(manifest_path).await;
3714 assert!(result.is_ok());
3715 }
3716
3717 #[tokio::test]
3718 async fn test_file_reference_validation_with_broken_references() {
3719 use crate::lockfile::LockedResource;
3720 use std::fs;
3721
3722 let temp = TempDir::new().unwrap();
3723 let project_dir = temp.path();
3724
3725 let manifest_path = project_dir.join("agpm.toml");
3727 let mut manifest = Manifest::new();
3728 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3729 manifest.save(&manifest_path).unwrap();
3730
3731 let agents_dir = project_dir.join(".claude").join("agents");
3733 fs::create_dir_all(&agents_dir).unwrap();
3734 let agent_content = r#"---
3735title: Test Agent
3736---
3737
3738# Test Agent
3739
3740See [missing](.agpm/snippets/missing.md) for details.
3741Also check `.claude/nonexistent.md`.
3742"#;
3743 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3744
3745 let lockfile_path = project_dir.join("agpm.lock");
3747 let mut lockfile = LockFile::default();
3748 lockfile.agents.push(LockedResource {
3749 name: "test-agent".to_string(),
3750 source: None,
3751 path: "agents/test.md".to_string(),
3752 version: Some("v1.0.0".to_string()),
3753 resolved_commit: None,
3754 url: None,
3755 checksum: "abc123".to_string(),
3756 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3757 dependencies: vec![],
3758 resource_type: crate::core::ResourceType::Agent,
3759 tool: None,
3760 manifest_alias: None,
3761 applied_patches: std::collections::HashMap::new(),
3762 install: None,
3763 });
3764 lockfile.save(&lockfile_path).unwrap();
3765
3766 let cmd = ValidateCommand {
3767 file: None,
3768 resolve: false,
3769 check_lock: false,
3770 sources: false,
3771 paths: false,
3772 format: OutputFormat::Text,
3773 verbose: true,
3774 quiet: false,
3775 strict: false,
3776 render: true,
3777 };
3778
3779 let result = cmd.execute_from_path(manifest_path).await;
3780 assert!(result.is_err());
3781 let err_msg = format!("{:?}", result.unwrap_err());
3782 assert!(err_msg.contains("File reference validation failed"));
3783 }
3784
3785 #[tokio::test]
3786 async fn test_file_reference_validation_ignores_urls() {
3787 use crate::lockfile::LockedResource;
3788 use std::fs;
3789
3790 let temp = TempDir::new().unwrap();
3791 let project_dir = temp.path();
3792
3793 let manifest_path = project_dir.join("agpm.toml");
3795 let mut manifest = Manifest::new();
3796 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3797 manifest.save(&manifest_path).unwrap();
3798
3799 let agents_dir = project_dir.join(".claude").join("agents");
3801 fs::create_dir_all(&agents_dir).unwrap();
3802 let agent_content = r#"---
3803title: Test Agent
3804---
3805
3806# Test Agent
3807
3808Check [GitHub](https://github.com/user/repo) for source.
3809Visit http://example.com for more info.
3810"#;
3811 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3812
3813 let lockfile_path = project_dir.join("agpm.lock");
3815 let mut lockfile = LockFile::default();
3816 lockfile.agents.push(LockedResource {
3817 name: "test-agent".to_string(),
3818 source: None,
3819 path: "agents/test.md".to_string(),
3820 version: Some("v1.0.0".to_string()),
3821 resolved_commit: None,
3822 url: None,
3823 checksum: "abc123".to_string(),
3824 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3825 dependencies: vec![],
3826 resource_type: crate::core::ResourceType::Agent,
3827 tool: None,
3828 manifest_alias: None,
3829 applied_patches: std::collections::HashMap::new(),
3830 install: None,
3831 });
3832 lockfile.save(&lockfile_path).unwrap();
3833
3834 let cmd = ValidateCommand {
3835 file: None,
3836 resolve: false,
3837 check_lock: false,
3838 sources: false,
3839 paths: false,
3840 format: OutputFormat::Text,
3841 verbose: true,
3842 quiet: false,
3843 strict: false,
3844 render: true,
3845 };
3846
3847 let result = cmd.execute_from_path(manifest_path).await;
3848 assert!(result.is_ok());
3849 }
3850
3851 #[tokio::test]
3852 async fn test_file_reference_validation_ignores_code_blocks() {
3853 use crate::lockfile::LockedResource;
3854 use std::fs;
3855
3856 let temp = TempDir::new().unwrap();
3857 let project_dir = temp.path();
3858
3859 let manifest_path = project_dir.join("agpm.toml");
3861 let mut manifest = Manifest::new();
3862 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3863 manifest.save(&manifest_path).unwrap();
3864
3865 let agents_dir = project_dir.join(".claude").join("agents");
3867 fs::create_dir_all(&agents_dir).unwrap();
3868 let agent_content = r#"---
3869title: Test Agent
3870---
3871
3872# Test Agent
3873
3874```bash
3875# This reference in code should be ignored
3876cat .agpm/snippets/nonexistent.md
3877```
3878
3879Inline code `example.md` should also be ignored.
3880"#;
3881 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3882
3883 let lockfile_path = project_dir.join("agpm.lock");
3885 let mut lockfile = LockFile::default();
3886 lockfile.agents.push(LockedResource {
3887 name: "test-agent".to_string(),
3888 source: None,
3889 path: "agents/test.md".to_string(),
3890 version: Some("v1.0.0".to_string()),
3891 resolved_commit: None,
3892 url: None,
3893 checksum: "abc123".to_string(),
3894 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3895 dependencies: vec![],
3896 resource_type: crate::core::ResourceType::Agent,
3897 tool: None,
3898 manifest_alias: None,
3899 applied_patches: std::collections::HashMap::new(),
3900 install: None,
3901 });
3902 lockfile.save(&lockfile_path).unwrap();
3903
3904 let cmd = ValidateCommand {
3905 file: None,
3906 resolve: false,
3907 check_lock: false,
3908 sources: false,
3909 paths: false,
3910 format: OutputFormat::Text,
3911 verbose: true,
3912 quiet: false,
3913 strict: false,
3914 render: true,
3915 };
3916
3917 let result = cmd.execute_from_path(manifest_path).await;
3918 assert!(result.is_ok());
3919 }
3920
3921 #[tokio::test]
3922 async fn test_file_reference_validation_multiple_resources() {
3923 use crate::lockfile::LockedResource;
3924 use std::fs;
3925
3926 let temp = TempDir::new().unwrap();
3927 let project_dir = temp.path();
3928
3929 let manifest_path = project_dir.join("agpm.toml");
3931 let mut manifest = Manifest::new();
3932 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3933 manifest.save(&manifest_path).unwrap();
3934
3935 let snippets_dir = project_dir.join(".agpm").join("snippets");
3937 fs::create_dir_all(&snippets_dir).unwrap();
3938 fs::write(snippets_dir.join("util.md"), "# Utilities").unwrap();
3939
3940 let agents_dir = project_dir.join(".claude").join("agents");
3942 fs::create_dir_all(&agents_dir).unwrap();
3943 fs::write(agents_dir.join("agent1.md"), "# Agent 1\n\nSee [util](.agpm/snippets/util.md).")
3944 .unwrap();
3945
3946 let commands_dir = project_dir.join(".claude").join("commands");
3948 fs::create_dir_all(&commands_dir).unwrap();
3949 fs::write(commands_dir.join("cmd1.md"), "# Command\n\nCheck `.agpm/snippets/missing.md`.")
3950 .unwrap();
3951
3952 let lockfile_path = project_dir.join("agpm.lock");
3954 let mut lockfile = LockFile::default();
3955 lockfile.agents.push(LockedResource {
3956 name: "agent1".to_string(),
3957 source: None,
3958 path: "agents/agent1.md".to_string(),
3959 version: Some("v1.0.0".to_string()),
3960 resolved_commit: None,
3961 url: None,
3962 checksum: "abc123".to_string(),
3963 installed_at: normalize_path_for_storage(agents_dir.join("agent1.md")),
3964 dependencies: vec![],
3965 resource_type: crate::core::ResourceType::Agent,
3966 tool: None,
3967 manifest_alias: None,
3968 applied_patches: std::collections::HashMap::new(),
3969 install: None,
3970 });
3971 lockfile.commands.push(LockedResource {
3972 name: "cmd1".to_string(),
3973 source: None,
3974 path: "commands/cmd1.md".to_string(),
3975 version: Some("v1.0.0".to_string()),
3976 resolved_commit: None,
3977 url: None,
3978 checksum: "def456".to_string(),
3979 installed_at: normalize_path_for_storage(commands_dir.join("cmd1.md")),
3980 dependencies: vec![],
3981 resource_type: crate::core::ResourceType::Command,
3982 tool: None,
3983 manifest_alias: None,
3984 applied_patches: std::collections::HashMap::new(),
3985 install: None,
3986 });
3987 lockfile.save(&lockfile_path).unwrap();
3988
3989 let cmd = ValidateCommand {
3990 file: None,
3991 resolve: false,
3992 check_lock: false,
3993 sources: false,
3994 paths: false,
3995 format: OutputFormat::Text,
3996 verbose: true,
3997 quiet: false,
3998 strict: false,
3999 render: true,
4000 };
4001
4002 let result = cmd.execute_from_path(manifest_path).await;
4003 assert!(result.is_err());
4004 let err_msg = format!("{:?}", result.unwrap_err());
4005 assert!(err_msg.contains("File reference validation failed"));
4006 }
4007}