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