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::{RenderingMetadata, 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.push(format!(
929 "{}: Failed to read file '{}': {}",
930 $name,
931 source_path.display(),
932 e
933 ));
934 continue;
935 }
936 }
937 } else {
938 let source_path = {
940 let candidate = Path::new(&$entry.path);
941 if candidate.is_absolute() {
942 candidate.to_path_buf()
943 } else {
944 project_dir.join(candidate)
945 }
946 };
947
948 match tokio::fs::read_to_string(&source_path).await {
949 Ok(c) => c,
950 Err(e) => {
951 template_results.push(format!(
952 "{}: Failed to read file '{}': {}",
953 $name,
954 source_path.display(),
955 e
956 ));
957 continue;
958 }
959 }
960 };
961
962 let has_template_syntax =
964 content.contains("{{") || content.contains("{%") || content.contains("{#");
965
966 if !has_template_syntax {
967 continue; }
969
970 templates_found += 1;
971
972 let project_config = manifest.project.clone();
974 let context_builder = TemplateContextBuilder::new(
975 Arc::clone(&lockfile),
976 project_config,
977 Arc::clone(&cache),
978 project_dir.to_path_buf(),
979 );
980 let resource_id = crate::lockfile::ResourceId::new(
982 $entry.name.clone(),
983 $entry.source.clone(),
984 $entry.tool.clone(),
985 $resource_type,
986 $entry.variant_inputs.hash().to_string(),
987 );
988 let context = match context_builder
989 .build_context(&resource_id, $entry.variant_inputs.json())
990 .await
991 {
992 Ok((c, _checksum)) => c,
993 Err(e) => {
994 template_results.push(format!("{}: {}", $name, e));
995 continue;
996 }
997 };
998
999 let mut renderer = match TemplateRenderer::new(
1001 true,
1002 project_dir.to_path_buf(),
1003 max_content_file_size,
1004 ) {
1005 Ok(r) => r,
1006 Err(e) => {
1007 template_results.push(format!("{}: {}", $name, e));
1008 continue;
1009 }
1010 };
1011
1012 let rendering_metadata = RenderingMetadata {
1014 resource_name: $entry.name.clone(),
1015 resource_type: $resource_type,
1016 dependency_chain: vec![], source_path: Some($entry.path.clone().into()),
1018 depth: 0,
1019 };
1020
1021 match renderer.render_template(&content, &context, Some(&rendering_metadata)) {
1022 Ok(_) => {
1023 templates_rendered += 1;
1024 }
1025 Err(e) => {
1026 template_results.push(format!("{}: {}", $name, e));
1027 }
1028 }
1029 }};
1030 }
1031
1032 for resource_type in &[
1035 ResourceType::Agent,
1036 ResourceType::Snippet,
1037 ResourceType::Command,
1038 ResourceType::Script,
1039 ] {
1040 let manifest_resources = manifest.get_resources(resource_type);
1041 let lockfile_resources = lockfile.get_resources(resource_type);
1042
1043 for name in manifest_resources.keys() {
1044 if let Some(entry) = lockfile_resources
1045 .iter()
1046 .find(|e| e.manifest_alias.as_ref().unwrap_or(&e.name) == name)
1047 {
1048 validate_resource_template!(name, entry, *resource_type);
1049 }
1050 }
1051 }
1052
1053 validation_results.templates_total = templates_found;
1055 validation_results.templates_rendered = templates_rendered;
1056 validation_results.templates_valid = template_results.is_empty();
1057
1058 if template_results.is_empty() {
1060 if templates_found > 0 {
1061 if !self.quiet && self.format == OutputFormat::Text {
1062 println!("✓ All {} templates rendered successfully", templates_found);
1063 }
1064 } else if !self.quiet && self.format == OutputFormat::Text {
1065 println!("⚠ No templates found in resources");
1066 }
1067 } else {
1068 let error_msg =
1069 format!("Template rendering failed for {} resource(s)", template_results.len());
1070 errors.push(error_msg.clone());
1071
1072 if matches!(self.format, OutputFormat::Json) {
1073 validation_results.valid = false;
1074 validation_results.errors.extend(template_results);
1075 validation_results.errors.push(error_msg);
1076 validation_results.warnings = warnings;
1077 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1078 return Err(anyhow::anyhow!("Template rendering failed"));
1079 } else if !self.quiet {
1080 println!("{} {}", "✗".red(), error_msg);
1081 for error in &template_results {
1082 println!(" {}", error);
1083 }
1084 }
1085 return Err(anyhow::anyhow!("Template rendering failed"));
1086 }
1087
1088 if self.verbose && !self.quiet {
1090 println!("\n🔍 Validating file references in markdown content...");
1091 }
1092
1093 let mut file_reference_errors = Vec::new();
1094 let mut total_references_checked = 0;
1095
1096 macro_rules! validate_file_references_in_resource {
1098 ($name:expr, $entry:expr) => {{
1099 let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
1101 let source_name = $entry.source.as_ref().unwrap();
1103 let sha = $entry.resolved_commit.as_ref().unwrap();
1104 let url = match $entry.url.as_ref() {
1105 Some(u) => u,
1106 None => {
1107 continue;
1108 }
1109 };
1110
1111 let cache_dir = match cache
1112 .get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
1113 .await
1114 {
1115 Ok(dir) => dir,
1116 Err(_) => {
1117 continue;
1118 }
1119 };
1120
1121 let source_path = cache_dir.join(&$entry.path);
1122 match tokio::fs::read_to_string(&source_path).await {
1123 Ok(c) => c,
1124 Err(e) => {
1125 tracing::debug!(
1126 "Failed to read source file '{}' for reference validation: {}",
1127 source_path.display(),
1128 e
1129 );
1130 continue;
1131 }
1132 }
1133 } else {
1134 let installed_path = project_dir.join(&$entry.installed_at);
1136
1137 match tokio::fs::read_to_string(&installed_path).await {
1138 Ok(c) => c,
1139 Err(e) => {
1140 tracing::debug!(
1141 "Failed to read installed file '{}' for reference validation: {}",
1142 installed_path.display(),
1143 e
1144 );
1145 continue;
1146 }
1147 }
1148 };
1149
1150 let references = extract_file_references(&content);
1152
1153 if !references.is_empty() {
1154 total_references_checked += references.len();
1155
1156 match validate_file_references(&references, project_dir) {
1158 Ok(missing) => {
1159 for missing_ref in missing {
1160 file_reference_errors.push(format!(
1161 "{}: references non-existent file '{}'",
1162 $entry.installed_at, missing_ref
1163 ));
1164 }
1165 }
1166 Err(e) => {
1167 file_reference_errors.push(format!(
1168 "{}: failed to validate references: {}",
1169 $entry.installed_at, e
1170 ));
1171 }
1172 }
1173 }
1174 }};
1175 }
1176
1177 for entry in &lockfile.agents {
1179 validate_file_references_in_resource!(&entry.name, entry);
1180 }
1181
1182 for entry in &lockfile.snippets {
1183 validate_file_references_in_resource!(&entry.name, entry);
1184 }
1185
1186 for entry in &lockfile.commands {
1187 validate_file_references_in_resource!(&entry.name, entry);
1188 }
1189
1190 for entry in &lockfile.scripts {
1191 validate_file_references_in_resource!(&entry.name, entry);
1192 }
1193
1194 if file_reference_errors.is_empty() {
1196 if total_references_checked > 0 {
1197 if !self.quiet && self.format == OutputFormat::Text {
1198 println!(
1199 "✓ All {} file references validated successfully",
1200 total_references_checked
1201 );
1202 }
1203 } else if self.verbose && !self.quiet && self.format == OutputFormat::Text {
1204 println!("⚠ No file references found in resources");
1205 }
1206 } else {
1207 let error_msg = format!(
1208 "File reference validation failed: {} broken reference(s) found",
1209 file_reference_errors.len()
1210 );
1211 errors.push(error_msg.clone());
1212
1213 if matches!(self.format, OutputFormat::Json) {
1214 validation_results.valid = false;
1215 validation_results.errors.extend(file_reference_errors);
1216 validation_results.errors.push(error_msg);
1217 validation_results.warnings = warnings;
1218 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1219 return Err(anyhow::anyhow!("File reference validation failed"));
1220 } else if !self.quiet {
1221 println!("{} {}", "✗".red(), error_msg);
1222 for error in &file_reference_errors {
1223 println!(" {}", error);
1224 }
1225 }
1226 return Err(anyhow::anyhow!("File reference validation failed"));
1227 }
1228 }
1229
1230 if self.strict && !warnings.is_empty() {
1232 let error_msg = "Strict mode: Warnings treated as errors";
1233 errors.extend(warnings.clone());
1234
1235 if matches!(self.format, OutputFormat::Json) {
1236 validation_results.valid = false;
1237 validation_results.errors = errors;
1238 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1239 return Err(anyhow::anyhow!("Strict mode validation failed"));
1240 } else if !self.quiet {
1241 println!("{} {}", "✗".red(), error_msg);
1242 }
1243 return Err(anyhow::anyhow!("Strict mode validation failed"));
1244 }
1245
1246 validation_results.valid = errors.is_empty();
1248 validation_results.errors = errors;
1249 validation_results.warnings = warnings;
1250
1251 match self.format {
1253 OutputFormat::Json => {
1254 println!("{}", serde_json::to_string_pretty(&validation_results)?);
1255 }
1256 OutputFormat::Text => {
1257 if !self.quiet && !validation_results.warnings.is_empty() {
1258 for warning in &validation_results.warnings {
1259 println!("⚠ Warning: {warning}");
1260 }
1261 }
1262 }
1264 }
1265
1266 Ok(())
1267 }
1268}
1269
1270#[derive(serde::Serialize)]
1302struct ValidationResults {
1303 valid: bool,
1305 manifest_valid: bool,
1307 dependencies_resolvable: bool,
1309 sources_accessible: bool,
1311 local_paths_exist: bool,
1313 lockfile_consistent: bool,
1315 templates_valid: bool,
1317 templates_rendered: usize,
1319 templates_total: usize,
1321 errors: Vec<String>,
1323 warnings: Vec<String>,
1325}
1326
1327impl Default for ValidationResults {
1328 fn default() -> Self {
1329 Self {
1330 valid: true, manifest_valid: false,
1332 dependencies_resolvable: false,
1333 sources_accessible: false,
1334 local_paths_exist: false,
1335 lockfile_consistent: false,
1336 templates_valid: false,
1337 templates_rendered: 0,
1338 templates_total: 0,
1339 errors: Vec::new(),
1340 warnings: Vec::new(),
1341 }
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::manifest::{Manifest, ResourceDependency};
1349
1350 use tempfile::TempDir;
1351
1352 #[tokio::test]
1353 async fn test_validate_no_manifest() {
1354 let temp = TempDir::new().unwrap();
1355 let manifest_path = temp.path().join("nonexistent").join("agpm.toml");
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_err());
1372 }
1373
1374 #[tokio::test]
1375 async fn test_validate_valid_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_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1382 manifest.save(&manifest_path).unwrap();
1383
1384 let cmd = ValidateCommand {
1385 file: None,
1386 resolve: false,
1387 check_lock: false,
1388 sources: false,
1389 paths: false,
1390 format: OutputFormat::Text,
1391 verbose: false,
1392 quiet: false,
1393 strict: false,
1394 render: false,
1395 };
1396
1397 let result = cmd.execute_from_path(manifest_path).await;
1398 assert!(result.is_ok());
1399 }
1400
1401 #[tokio::test]
1402 async fn test_validate_invalid_manifest() {
1403 let temp = TempDir::new().unwrap();
1404 let manifest_path = temp.path().join("agpm.toml");
1405
1406 let mut manifest = crate::manifest::Manifest::new();
1408 manifest.add_dependency(
1409 "test".to_string(),
1410 crate::manifest::ResourceDependency::Detailed(Box::new(
1411 crate::manifest::DetailedDependency {
1412 source: Some("nonexistent".to_string()),
1413 path: "test.md".to_string(),
1414 version: None,
1415 command: None,
1416 branch: None,
1417 rev: None,
1418 args: None,
1419 target: None,
1420 filename: None,
1421 dependencies: None,
1422 tool: Some("claude-code".to_string()),
1423 flatten: None,
1424 install: None,
1425
1426 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1427 },
1428 )),
1429 true,
1430 );
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::Text,
1440 verbose: false,
1441 quiet: false,
1442 strict: false,
1443 render: false,
1444 };
1445
1446 let result = cmd.execute_from_path(manifest_path).await;
1447 assert!(result.is_err());
1448 }
1449
1450 #[tokio::test]
1451 async fn test_validate_json_format() {
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.save(&manifest_path).unwrap();
1459
1460 let cmd = ValidateCommand {
1461 file: None,
1462 resolve: false,
1463 check_lock: false,
1464 sources: false,
1465 paths: false,
1466 format: OutputFormat::Json,
1467 verbose: false,
1468 quiet: true,
1469 strict: false,
1470 render: false,
1471 };
1472
1473 let result = cmd.execute_from_path(manifest_path).await;
1474 assert!(result.is_ok());
1475 }
1476
1477 #[tokio::test]
1478 async fn test_validate_with_resolve() {
1479 let temp = TempDir::new().unwrap();
1480 let manifest_path = temp.path().join("agpm.toml");
1481
1482 let mut manifest = crate::manifest::Manifest::new();
1484 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1485 manifest.add_dependency(
1486 "test-agent".to_string(),
1487 crate::manifest::ResourceDependency::Detailed(Box::new(
1488 crate::manifest::DetailedDependency {
1489 source: Some("test".to_string()),
1490 path: "test.md".to_string(),
1491 version: None,
1492 command: None,
1493 branch: None,
1494 rev: None,
1495 args: None,
1496 target: None,
1497 filename: None,
1498 dependencies: None,
1499 tool: Some("claude-code".to_string()),
1500 flatten: None,
1501 install: None,
1502
1503 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1504 },
1505 )),
1506 true,
1507 );
1508 manifest.save(&manifest_path).unwrap();
1509
1510 let cmd = ValidateCommand {
1511 file: None,
1512 resolve: true,
1513 check_lock: false,
1514 sources: false,
1515 paths: false,
1516 format: OutputFormat::Text,
1517 verbose: false,
1518 quiet: true, strict: false,
1520 render: false,
1521 };
1522
1523 let result = cmd.execute_from_path(manifest_path).await;
1524 let _ = result;
1527 }
1528
1529 #[tokio::test]
1530 async fn test_validate_check_lock_consistent() {
1531 let temp = TempDir::new().unwrap();
1532 let manifest_path = temp.path().join("agpm.toml");
1533
1534 let manifest = crate::manifest::Manifest::new();
1536 manifest.save(&manifest_path).unwrap();
1537
1538 let lockfile = crate::lockfile::LockFile::new();
1540 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1541
1542 let cmd = ValidateCommand {
1543 file: None,
1544 resolve: false,
1545 check_lock: true,
1546 sources: false,
1547 paths: false,
1548 format: OutputFormat::Text,
1549 verbose: false,
1550 quiet: true,
1551 strict: false,
1552 render: false,
1553 };
1554
1555 let result = cmd.execute_from_path(manifest_path).await;
1556 assert!(result.is_ok());
1558 }
1559
1560 #[tokio::test]
1561 async fn test_validate_check_lock_with_extra_entries() {
1562 let temp = TempDir::new().unwrap();
1563 let manifest_path = temp.path().join("agpm.toml");
1564
1565 let manifest = crate::manifest::Manifest::new();
1567 manifest.save(&manifest_path).unwrap();
1568
1569 let mut lockfile = crate::lockfile::LockFile::new();
1571 lockfile.agents.push(crate::lockfile::LockedResource {
1572 name: "extra-agent".to_string(),
1573 source: Some("test".to_string()),
1574 url: Some("https://github.com/test/repo.git".to_string()),
1575 path: "test.md".to_string(),
1576 version: None,
1577 resolved_commit: Some("abc123".to_string()),
1578 checksum: "sha256:dummy".to_string(),
1579 installed_at: "agents/extra-agent.md".to_string(),
1580 dependencies: vec![],
1581 resource_type: crate::core::ResourceType::Agent,
1582
1583 tool: Some("claude-code".to_string()),
1584 manifest_alias: None,
1585 context_checksum: None,
1586 applied_patches: std::collections::BTreeMap::new(),
1587 install: None,
1588 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1589 });
1590 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1591
1592 let cmd = ValidateCommand {
1593 file: None,
1594 resolve: false,
1595 check_lock: true,
1596 sources: false,
1597 paths: false,
1598 format: OutputFormat::Text,
1599 verbose: false,
1600 quiet: true,
1601 strict: false,
1602 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_strict_mode() {
1612 let temp = TempDir::new().unwrap();
1613 let manifest_path = temp.path().join("agpm.toml");
1614
1615 let manifest = crate::manifest::Manifest::new();
1617 manifest.save(&manifest_path).unwrap();
1618
1619 let cmd = ValidateCommand {
1620 file: None,
1621 resolve: false,
1622 check_lock: false,
1623 sources: false,
1624 paths: false,
1625 format: OutputFormat::Text,
1626 verbose: false,
1627 quiet: true,
1628 strict: true, render: false,
1630 };
1631
1632 let result = cmd.execute_from_path(manifest_path).await;
1633 assert!(result.is_err());
1635 }
1636
1637 #[tokio::test]
1638 async fn test_validate_verbose_mode() {
1639 let temp = TempDir::new().unwrap();
1640 let manifest_path = temp.path().join("agpm.toml");
1641
1642 let mut manifest = crate::manifest::Manifest::new();
1644 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1645 manifest.save(&manifest_path).unwrap();
1646
1647 let cmd = ValidateCommand {
1648 file: None,
1649 resolve: false,
1650 check_lock: false,
1651 sources: false,
1652 paths: false,
1653 format: OutputFormat::Text,
1654 verbose: true, quiet: false,
1656 strict: false,
1657 render: false,
1658 };
1659
1660 let result = cmd.execute_from_path(manifest_path).await;
1661 assert!(result.is_ok());
1662 }
1663
1664 #[tokio::test]
1665 async fn test_validate_check_paths_local() {
1666 let temp = TempDir::new().unwrap();
1667 let manifest_path = temp.path().join("agpm.toml");
1668
1669 std::fs::create_dir_all(temp.path().join("local")).unwrap();
1671 std::fs::write(temp.path().join("local/test.md"), "# Test").unwrap();
1672
1673 let mut manifest = crate::manifest::Manifest::new();
1675 manifest.add_dependency(
1676 "local-test".to_string(),
1677 crate::manifest::ResourceDependency::Detailed(Box::new(
1678 crate::manifest::DetailedDependency {
1679 source: None,
1680 path: "./local/test.md".to_string(),
1681 version: None,
1682 command: None,
1683 branch: None,
1684 rev: None,
1685 args: None,
1686 target: None,
1687 filename: None,
1688 dependencies: None,
1689 tool: Some("claude-code".to_string()),
1690 flatten: None,
1691 install: None,
1692
1693 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1694 },
1695 )),
1696 true,
1697 );
1698 manifest.save(&manifest_path).unwrap();
1699
1700 let cmd = ValidateCommand {
1701 file: None,
1702 resolve: false,
1703 check_lock: false,
1704 sources: false,
1705 paths: true, format: OutputFormat::Text,
1707 verbose: false,
1708 quiet: false,
1709 strict: false,
1710 render: false,
1711 };
1712
1713 let result = cmd.execute_from_path(manifest_path).await;
1714 assert!(result.is_ok());
1715 }
1716
1717 #[tokio::test]
1718 async fn test_validate_custom_file_path() {
1719 let temp = TempDir::new().unwrap();
1720
1721 let custom_dir = temp.path().join("custom");
1723 std::fs::create_dir_all(&custom_dir).unwrap();
1724 let manifest_path = custom_dir.join("custom.toml");
1725
1726 let mut manifest = crate::manifest::Manifest::new();
1727 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1728 manifest.save(&manifest_path).unwrap();
1729
1730 let cmd = ValidateCommand {
1731 file: Some(manifest_path.to_str().unwrap().to_string()),
1732 resolve: false,
1733 check_lock: false,
1734 sources: false,
1735 paths: false,
1736 format: OutputFormat::Text,
1737 verbose: false,
1738 quiet: false,
1739 strict: false,
1740 render: false,
1741 };
1742
1743 let result = cmd.execute_from_path(manifest_path).await;
1744 assert!(result.is_ok());
1745 }
1746
1747 #[tokio::test]
1748 async fn test_validate_json_error_format() {
1749 let temp = TempDir::new().unwrap();
1750 let manifest_path = temp.path().join("agpm.toml");
1751
1752 let mut manifest = crate::manifest::Manifest::new();
1754 manifest.add_dependency(
1755 "test".to_string(),
1756 crate::manifest::ResourceDependency::Detailed(Box::new(
1757 crate::manifest::DetailedDependency {
1758 source: Some("nonexistent".to_string()),
1759 path: "test.md".to_string(),
1760 version: None,
1761 command: None,
1762 branch: None,
1763 rev: None,
1764 args: None,
1765 target: None,
1766 filename: None,
1767 dependencies: None,
1768 tool: Some("claude-code".to_string()),
1769 flatten: None,
1770 install: None,
1771
1772 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1773 },
1774 )),
1775 true,
1776 );
1777 manifest.save(&manifest_path).unwrap();
1778
1779 let cmd = ValidateCommand {
1780 file: None,
1781 resolve: false,
1782 check_lock: false,
1783 sources: false,
1784 paths: false,
1785 format: OutputFormat::Json, verbose: false,
1787 quiet: true,
1788 strict: false,
1789 render: false,
1790 };
1791
1792 let result = cmd.execute_from_path(manifest_path).await;
1793 assert!(result.is_err());
1794 }
1795
1796 #[tokio::test]
1797 async fn test_validate_paths_check() {
1798 let temp = TempDir::new().unwrap();
1799 let manifest_path = temp.path().join("agpm.toml");
1800
1801 let mut manifest = crate::manifest::Manifest::new();
1803 manifest.add_dependency(
1804 "local-agent".to_string(),
1805 crate::manifest::ResourceDependency::Simple("./local/agent.md".to_string()),
1806 true,
1807 );
1808 manifest.save(&manifest_path).unwrap();
1809
1810 let cmd = ValidateCommand {
1812 file: None,
1813 resolve: false,
1814 check_lock: false,
1815 sources: false,
1816 paths: true,
1817 format: OutputFormat::Text,
1818 verbose: false,
1819 quiet: false,
1820 strict: false,
1821 render: false,
1822 };
1823
1824 let result = cmd.execute_from_path(manifest_path.clone()).await;
1825 assert!(result.is_err());
1826
1827 std::fs::create_dir_all(temp.path().join("local")).unwrap();
1829 std::fs::write(temp.path().join("local/agent.md"), "# Agent").unwrap();
1830
1831 let cmd = ValidateCommand {
1832 file: None,
1833 resolve: false,
1834 check_lock: false,
1835 sources: false,
1836 paths: true,
1837 format: OutputFormat::Text,
1838 verbose: false,
1839 quiet: false,
1840 strict: false,
1841 render: false,
1842 };
1843
1844 let result = cmd.execute_from_path(manifest_path).await;
1845 assert!(result.is_ok());
1846 }
1847
1848 #[tokio::test]
1849 async fn test_validate_check_lock() {
1850 let temp = TempDir::new().unwrap();
1851 let manifest_path = temp.path().join("agpm.toml");
1852
1853 let mut manifest = crate::manifest::Manifest::new();
1855 manifest.add_dependency(
1856 "test".to_string(),
1857 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
1858 true,
1859 );
1860 manifest.save(&manifest_path).unwrap();
1861
1862 let cmd = ValidateCommand {
1864 file: None,
1865 resolve: false,
1866 check_lock: true,
1867 sources: false,
1868 paths: false,
1869 format: OutputFormat::Text,
1870 verbose: false,
1871 quiet: false,
1872 strict: false,
1873 render: false,
1874 };
1875
1876 let result = cmd.execute_from_path(manifest_path.clone()).await;
1877 assert!(result.is_ok()); let lockfile = crate::lockfile::LockFile {
1881 version: 1,
1882 sources: vec![],
1883 commands: vec![],
1884 agents: vec![crate::lockfile::LockedResource {
1885 name: "test".to_string(),
1886 source: None,
1887 url: None,
1888 path: "test.md".to_string(),
1889 version: None,
1890 resolved_commit: None,
1891 checksum: String::new(),
1892 installed_at: "agents/test.md".to_string(),
1893 dependencies: vec![],
1894 resource_type: crate::core::ResourceType::Agent,
1895
1896 tool: Some("claude-code".to_string()),
1897 manifest_alias: None,
1898 context_checksum: None,
1899 applied_patches: std::collections::BTreeMap::new(),
1900 install: None,
1901 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1902 }],
1903 snippets: vec![],
1904 mcp_servers: vec![],
1905 scripts: vec![],
1906 hooks: vec![],
1907 };
1908 lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1909
1910 let cmd = ValidateCommand {
1911 file: None,
1912 resolve: false,
1913 check_lock: true,
1914 sources: false,
1915 paths: false,
1916 format: OutputFormat::Text,
1917 verbose: false,
1918 quiet: false,
1919 strict: false,
1920 render: false,
1921 };
1922
1923 let result = cmd.execute_from_path(manifest_path).await;
1924 assert!(result.is_ok());
1925 }
1926
1927 #[tokio::test]
1928 async fn test_validate_verbose_output() {
1929 let temp = TempDir::new().unwrap();
1930 let manifest_path = temp.path().join("agpm.toml");
1931
1932 let manifest = crate::manifest::Manifest::new();
1933 manifest.save(&manifest_path).unwrap();
1934
1935 let cmd = ValidateCommand {
1936 file: None,
1937 resolve: false,
1938 check_lock: false,
1939 sources: false,
1940 paths: false,
1941 format: OutputFormat::Text,
1942 verbose: true,
1943 quiet: false,
1944 strict: false,
1945 render: false,
1946 };
1947
1948 let result = cmd.execute_from_path(manifest_path).await;
1949 assert!(result.is_ok());
1950 }
1951
1952 #[tokio::test]
1953 async fn test_validate_strict_mode_with_warnings() {
1954 let temp = TempDir::new().unwrap();
1955 let manifest_path = temp.path().join("agpm.toml");
1956
1957 let manifest = crate::manifest::Manifest::new();
1959 manifest.save(&manifest_path).unwrap();
1960
1961 let cmd = ValidateCommand {
1963 file: None,
1964 resolve: false,
1965 check_lock: true,
1966 sources: false,
1967 paths: false,
1968 format: OutputFormat::Text,
1969 verbose: false,
1970 quiet: false,
1971 strict: true, render: false,
1973 };
1974
1975 let result = cmd.execute_from_path(manifest_path).await;
1976 assert!(result.is_err()); }
1978
1979 #[test]
1980 fn test_output_format_enum() {
1981 assert!(matches!(OutputFormat::Text, OutputFormat::Text));
1983 assert!(matches!(OutputFormat::Json, OutputFormat::Json));
1984 }
1985
1986 #[test]
1987 fn test_validation_results_default() {
1988 let results = ValidationResults::default();
1989 assert!(results.valid);
1991 assert!(!results.manifest_valid);
1993 assert!(!results.dependencies_resolvable);
1994 assert!(!results.sources_accessible);
1995 assert!(!results.lockfile_consistent);
1996 assert!(!results.local_paths_exist);
1997 assert!(results.errors.is_empty());
1998 assert!(results.warnings.is_empty());
1999 }
2000
2001 #[tokio::test]
2002 async fn test_validate_quiet_mode() {
2003 let temp = TempDir::new().unwrap();
2004 let manifest_path = temp.path().join("agpm.toml");
2005
2006 let manifest = crate::manifest::Manifest::new();
2008 manifest.save(&manifest_path).unwrap();
2009
2010 let cmd = ValidateCommand {
2011 file: None,
2012 resolve: false,
2013 check_lock: false,
2014 sources: false,
2015 paths: false,
2016 format: OutputFormat::Text,
2017 verbose: false,
2018 quiet: true, strict: false,
2020 render: false,
2021 };
2022
2023 let result = cmd.execute_from_path(manifest_path).await;
2024 assert!(result.is_ok());
2025 }
2026
2027 #[tokio::test]
2028 async fn test_validate_json_output_success() {
2029 let temp = TempDir::new().unwrap();
2030 let manifest_path = temp.path().join("agpm.toml");
2031
2032 let mut manifest = crate::manifest::Manifest::new();
2034 use crate::manifest::{DetailedDependency, ResourceDependency};
2035
2036 manifest.agents.insert(
2037 "test".to_string(),
2038 ResourceDependency::Detailed(Box::new(DetailedDependency {
2039 source: None,
2040 path: "test.md".to_string(),
2041 version: None,
2042 command: None,
2043 branch: None,
2044 rev: None,
2045 args: None,
2046 target: None,
2047 filename: None,
2048 dependencies: None,
2049 tool: Some("claude-code".to_string()),
2050 flatten: None,
2051 install: None,
2052
2053 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2054 })),
2055 );
2056 manifest.save(&manifest_path).unwrap();
2057
2058 let cmd = ValidateCommand {
2059 file: None,
2060 resolve: false,
2061 check_lock: false,
2062 sources: false,
2063 paths: false,
2064 format: OutputFormat::Json, verbose: false,
2066 quiet: false,
2067 strict: false,
2068 render: false,
2069 };
2070
2071 let result = cmd.execute_from_path(manifest_path).await;
2072 assert!(result.is_ok());
2073 }
2074
2075 #[tokio::test]
2076 async fn test_validate_check_sources() {
2077 let temp = TempDir::new().unwrap();
2078 let manifest_path = temp.path().join("agpm.toml");
2079
2080 let source_dir = temp.path().join("test-source");
2082 std::fs::create_dir_all(&source_dir).unwrap();
2083
2084 std::process::Command::new("git")
2086 .arg("init")
2087 .current_dir(&source_dir)
2088 .output()
2089 .expect("Failed to initialize git repository");
2090
2091 let mut manifest = crate::manifest::Manifest::new();
2093 let source_url = format!("file://{}", normalize_path_for_storage(&source_dir));
2094 manifest.add_source("test".to_string(), source_url);
2095 manifest.save(&manifest_path).unwrap();
2096
2097 let cmd = ValidateCommand {
2098 file: None,
2099 resolve: false,
2100 check_lock: false,
2101 sources: true, paths: false,
2103 format: OutputFormat::Text,
2104 verbose: false,
2105 quiet: false,
2106 strict: false,
2107 render: false,
2108 };
2109
2110 let result = cmd.execute_from_path(manifest_path).await;
2112 assert!(result.is_ok());
2114 }
2115
2116 #[tokio::test]
2117 async fn test_validate_check_paths() {
2118 let temp = TempDir::new().unwrap();
2119 let manifest_path = temp.path().join("agpm.toml");
2120
2121 let mut manifest = crate::manifest::Manifest::new();
2123 use crate::manifest::{DetailedDependency, ResourceDependency};
2124
2125 manifest.agents.insert(
2126 "test".to_string(),
2127 ResourceDependency::Detailed(Box::new(DetailedDependency {
2128 source: None,
2129 path: temp.path().join("test.md").to_str().unwrap().to_string(),
2130 version: None,
2131 command: None,
2132 branch: None,
2133 rev: None,
2134 args: None,
2135 target: None,
2136 filename: None,
2137 dependencies: None,
2138 tool: Some("claude-code".to_string()),
2139 flatten: None,
2140 install: None,
2141
2142 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2143 })),
2144 );
2145 manifest.save(&manifest_path).unwrap();
2146
2147 std::fs::write(temp.path().join("test.md"), "# Test Agent").unwrap();
2149
2150 let cmd = ValidateCommand {
2151 file: None,
2152 resolve: false,
2153 check_lock: false,
2154 sources: false,
2155 paths: true, format: OutputFormat::Text,
2157 verbose: false,
2158 quiet: false,
2159 strict: false,
2160 render: false,
2161 };
2162
2163 let result = cmd.execute_from_path(manifest_path).await;
2164 assert!(result.is_ok());
2165 }
2166
2167 #[tokio::test]
2170 async fn test_execute_with_no_manifest_json_format() {
2171 let temp = TempDir::new().unwrap();
2172 let manifest_path = temp.path().join("non_existent.toml");
2173
2174 let cmd = ValidateCommand {
2175 file: Some(manifest_path.to_string_lossy().to_string()),
2176 resolve: false,
2177 check_lock: false,
2178 sources: false,
2179 paths: false,
2180 format: OutputFormat::Json, verbose: false,
2182 quiet: false,
2183 strict: false,
2184 render: false,
2185 };
2186
2187 let result = cmd.execute().await;
2188 assert!(result.is_err());
2189 }
2191
2192 #[tokio::test]
2193 async fn test_execute_with_no_manifest_text_format() {
2194 let temp = TempDir::new().unwrap();
2195 let manifest_path = temp.path().join("non_existent.toml");
2196
2197 let cmd = ValidateCommand {
2198 file: Some(manifest_path.to_string_lossy().to_string()),
2199 resolve: false,
2200 check_lock: false,
2201 sources: false,
2202 paths: false,
2203 format: OutputFormat::Text,
2204 verbose: false,
2205 quiet: false, strict: false,
2207 render: false,
2208 };
2209
2210 let result = cmd.execute().await;
2211 assert!(result.is_err());
2212 }
2214
2215 #[tokio::test]
2216 async fn test_execute_with_no_manifest_quiet_mode() {
2217 let temp = TempDir::new().unwrap();
2218 let manifest_path = temp.path().join("non_existent.toml");
2219
2220 let cmd = ValidateCommand {
2221 file: Some(manifest_path.to_string_lossy().to_string()),
2222 resolve: false,
2223 check_lock: false,
2224 sources: false,
2225 paths: false,
2226 format: OutputFormat::Text,
2227 verbose: false,
2228 quiet: true, strict: false,
2230 render: false,
2231 };
2232
2233 let result = cmd.execute().await;
2234 assert!(result.is_err());
2235 }
2237
2238 #[tokio::test]
2239 async fn test_execute_from_path_nonexistent_file_json() {
2240 let temp = TempDir::new().unwrap();
2241 let nonexistent_path = temp.path().join("nonexistent.toml");
2242
2243 let cmd = ValidateCommand {
2244 file: None,
2245 resolve: false,
2246 check_lock: false,
2247 sources: false,
2248 paths: false,
2249 format: OutputFormat::Json,
2250 verbose: false,
2251 quiet: false,
2252 strict: false,
2253 render: false,
2254 };
2255
2256 let result = cmd.execute_from_path(nonexistent_path).await;
2257 assert!(result.is_err());
2258 }
2260
2261 #[tokio::test]
2262 async fn test_execute_from_path_nonexistent_file_text() {
2263 let temp = TempDir::new().unwrap();
2264 let nonexistent_path = temp.path().join("nonexistent.toml");
2265
2266 let cmd = ValidateCommand {
2267 file: None,
2268 resolve: false,
2269 check_lock: false,
2270 sources: false,
2271 paths: false,
2272 format: OutputFormat::Text,
2273 verbose: false,
2274 quiet: false,
2275 strict: false,
2276 render: false,
2277 };
2278
2279 let result = cmd.execute_from_path(nonexistent_path).await;
2280 assert!(result.is_err());
2281 }
2283
2284 #[tokio::test]
2285 async fn test_validate_manifest_toml_syntax_error() {
2286 let temp = TempDir::new().unwrap();
2287 let manifest_path = temp.path().join("agpm.toml");
2288
2289 std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2291
2292 let cmd = ValidateCommand {
2293 file: None,
2294 resolve: false,
2295 check_lock: false,
2296 sources: false,
2297 paths: false,
2298 format: OutputFormat::Text,
2299 verbose: false,
2300 quiet: false,
2301 strict: false,
2302 render: false,
2303 };
2304
2305 let result = cmd.execute_from_path(manifest_path).await;
2306 assert!(result.is_err());
2307 }
2309
2310 #[tokio::test]
2311 async fn test_validate_manifest_toml_syntax_error_json() {
2312 let temp = TempDir::new().unwrap();
2313 let manifest_path = temp.path().join("agpm.toml");
2314
2315 std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2317
2318 let cmd = ValidateCommand {
2319 file: None,
2320 resolve: false,
2321 check_lock: false,
2322 sources: false,
2323 paths: false,
2324 format: OutputFormat::Json,
2325 verbose: false,
2326 quiet: true,
2327 strict: false,
2328 render: false,
2329 };
2330
2331 let result = cmd.execute_from_path(manifest_path).await;
2332 assert!(result.is_err());
2333 }
2335
2336 #[tokio::test]
2337 async fn test_validate_manifest_structure_error() {
2338 let temp = TempDir::new().unwrap();
2339 let manifest_path = temp.path().join("agpm.toml");
2340
2341 let mut manifest = crate::manifest::Manifest::new();
2343 manifest.add_dependency(
2344 "test".to_string(),
2345 crate::manifest::ResourceDependency::Detailed(Box::new(
2346 crate::manifest::DetailedDependency {
2347 source: Some("nonexistent".to_string()),
2348 path: "test.md".to_string(),
2349 version: None,
2350 command: None,
2351 branch: None,
2352 rev: None,
2353 args: None,
2354 target: None,
2355 filename: None,
2356 dependencies: None,
2357 tool: Some("claude-code".to_string()),
2358 flatten: None,
2359 install: None,
2360
2361 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2362 },
2363 )),
2364 true,
2365 );
2366 manifest.save(&manifest_path).unwrap();
2367
2368 let cmd = ValidateCommand {
2369 file: None,
2370 resolve: false,
2371 check_lock: false,
2372 sources: false,
2373 paths: false,
2374 format: OutputFormat::Text,
2375 verbose: false,
2376 quiet: false,
2377 strict: false,
2378 render: false,
2379 };
2380
2381 let result = cmd.execute_from_path(manifest_path).await;
2382 assert!(result.is_err());
2383 }
2385
2386 #[tokio::test]
2387 async fn test_validate_manifest_version_conflict() {
2388 let temp = TempDir::new().unwrap();
2389 let manifest_path = temp.path().join("agpm.toml");
2390
2391 std::fs::write(
2393 &manifest_path,
2394 r#"
2395[sources]
2396test = "https://github.com/test/repo.git"
2397
2398[agents]
2399shared-agent = { source = "test", path = "agent.md", version = "v1.0.0" }
2400another-agent = { source = "test", path = "agent.md", version = "v2.0.0" }
2401"#,
2402 )
2403 .unwrap();
2404
2405 let cmd = ValidateCommand {
2406 file: None,
2407 resolve: false,
2408 check_lock: false,
2409 sources: false,
2410 paths: false,
2411 format: OutputFormat::Json,
2412 verbose: false,
2413 quiet: true,
2414 strict: false,
2415 render: false,
2416 };
2417
2418 let result = cmd.execute_from_path(manifest_path).await;
2420 assert!(result.is_ok());
2422 }
2424
2425 #[tokio::test]
2426 async fn test_validate_with_outdated_version_warnings() {
2427 let temp = TempDir::new().unwrap();
2428 let manifest_path = temp.path().join("agpm.toml");
2429
2430 let mut manifest = crate::manifest::Manifest::new();
2432 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2433 manifest.add_dependency(
2434 "old-agent".to_string(),
2435 crate::manifest::ResourceDependency::Detailed(Box::new(
2436 crate::manifest::DetailedDependency {
2437 source: Some("test".to_string()),
2438 path: "old.md".to_string(),
2439 version: Some("v0.1.0".to_string()), command: None,
2441 branch: None,
2442 rev: None,
2443 args: None,
2444 target: None,
2445 filename: None,
2446 dependencies: None,
2447 tool: Some("claude-code".to_string()),
2448 flatten: None,
2449 install: None,
2450
2451 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2452 },
2453 )),
2454 true,
2455 );
2456 manifest.save(&manifest_path).unwrap();
2457
2458 let cmd = ValidateCommand {
2459 file: None,
2460 resolve: false,
2461 check_lock: false,
2462 sources: false,
2463 paths: false,
2464 format: OutputFormat::Text,
2465 verbose: false,
2466 quiet: false,
2467 strict: false,
2468 render: false,
2469 };
2470
2471 let result = cmd.execute_from_path(manifest_path).await;
2472 assert!(result.is_ok());
2473 }
2474
2475 #[tokio::test]
2476 async fn test_validate_resolve_with_error_json_output() {
2477 let temp = TempDir::new().unwrap();
2478 let manifest_path = temp.path().join("agpm.toml");
2479
2480 let mut manifest = crate::manifest::Manifest::new();
2482 manifest
2483 .add_source("test".to_string(), "https://github.com/nonexistent/repo.git".to_string());
2484 manifest.add_dependency(
2485 "failing-agent".to_string(),
2486 crate::manifest::ResourceDependency::Detailed(Box::new(
2487 crate::manifest::DetailedDependency {
2488 source: Some("test".to_string()),
2489 path: "test.md".to_string(),
2490 version: None,
2491 command: None,
2492 branch: None,
2493 rev: None,
2494 args: None,
2495 target: None,
2496 filename: None,
2497 dependencies: None,
2498 tool: Some("claude-code".to_string()),
2499 flatten: None,
2500 install: None,
2501
2502 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2503 },
2504 )),
2505 true,
2506 );
2507 manifest.save(&manifest_path).unwrap();
2508
2509 let cmd = ValidateCommand {
2510 file: None,
2511 resolve: true,
2512 check_lock: false,
2513 sources: false,
2514 paths: false,
2515 format: OutputFormat::Json,
2516 verbose: false,
2517 quiet: true,
2518 strict: false,
2519 render: false,
2520 };
2521
2522 let result = cmd.execute_from_path(manifest_path).await;
2523 let _ = result; }
2527
2528 #[tokio::test]
2529 async fn test_validate_resolve_dependency_not_found_error() {
2530 let temp = TempDir::new().unwrap();
2531 let manifest_path = temp.path().join("agpm.toml");
2532
2533 let mut manifest = crate::manifest::Manifest::new();
2535 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2536 manifest.add_dependency(
2537 "my-agent".to_string(),
2538 crate::manifest::ResourceDependency::Detailed(Box::new(
2539 crate::manifest::DetailedDependency {
2540 source: Some("test".to_string()),
2541 path: "agent.md".to_string(),
2542 version: None,
2543 command: None,
2544 branch: None,
2545 rev: None,
2546 args: None,
2547 target: None,
2548 filename: None,
2549 dependencies: None,
2550 tool: Some("claude-code".to_string()),
2551 flatten: None,
2552 install: None,
2553
2554 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2555 },
2556 )),
2557 true,
2558 );
2559 manifest.add_dependency(
2560 "utils".to_string(),
2561 crate::manifest::ResourceDependency::Detailed(Box::new(
2562 crate::manifest::DetailedDependency {
2563 source: Some("test".to_string()),
2564 path: "utils.md".to_string(),
2565 version: None,
2566 command: None,
2567 branch: None,
2568 rev: None,
2569 args: None,
2570 target: None,
2571 filename: None,
2572 dependencies: None,
2573 tool: Some("claude-code".to_string()),
2574 flatten: None,
2575 install: None,
2576
2577 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2578 },
2579 )),
2580 false,
2581 );
2582 manifest.save(&manifest_path).unwrap();
2583
2584 let cmd = ValidateCommand {
2585 file: None,
2586 resolve: true,
2587 check_lock: false,
2588 sources: false,
2589 paths: false,
2590 format: OutputFormat::Text,
2591 verbose: false,
2592 quiet: false,
2593 strict: false,
2594 render: false,
2595 };
2596
2597 let result = cmd.execute_from_path(manifest_path).await;
2598 let _ = result;
2600 }
2601
2602 #[tokio::test]
2603 async fn test_validate_sources_accessibility_error() {
2604 let temp = TempDir::new().unwrap();
2605 let manifest_path = temp.path().join("agpm.toml");
2606
2607 let nonexistent_path1 = temp.path().join("nonexistent1");
2610 let nonexistent_path2 = temp.path().join("nonexistent2");
2611
2612 let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2614 let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2615
2616 let mut manifest = crate::manifest::Manifest::new();
2617 manifest.add_source("official".to_string(), url1);
2618 manifest.add_source("community".to_string(), url2);
2619 manifest.save(&manifest_path).unwrap();
2620
2621 let cmd = ValidateCommand {
2622 file: None,
2623 resolve: false,
2624 check_lock: false,
2625 sources: true,
2626 paths: false,
2627 format: OutputFormat::Text,
2628 verbose: false,
2629 quiet: false,
2630 strict: false,
2631 render: false,
2632 };
2633
2634 let result = cmd.execute_from_path(manifest_path).await;
2635 let _ = result;
2637 }
2638
2639 #[tokio::test]
2640 async fn test_validate_sources_accessibility_error_json() {
2641 let temp = TempDir::new().unwrap();
2642 let manifest_path = temp.path().join("agpm.toml");
2643
2644 let nonexistent_path1 = temp.path().join("nonexistent1");
2647 let nonexistent_path2 = temp.path().join("nonexistent2");
2648
2649 let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2651 let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2652
2653 let mut manifest = crate::manifest::Manifest::new();
2654 manifest.add_source("official".to_string(), url1);
2655 manifest.add_source("community".to_string(), url2);
2656 manifest.save(&manifest_path).unwrap();
2657
2658 let cmd = ValidateCommand {
2659 file: None,
2660 resolve: false,
2661 check_lock: false,
2662 sources: true,
2663 paths: false,
2664 format: OutputFormat::Json,
2665 verbose: false,
2666 quiet: true,
2667 strict: false,
2668 render: false,
2669 };
2670
2671 let result = cmd.execute_from_path(manifest_path).await;
2672 let _ = result;
2674 }
2675
2676 #[tokio::test]
2677 async fn test_validate_check_paths_snippets_and_commands() {
2678 let temp = TempDir::new().unwrap();
2679 let manifest_path = temp.path().join("agpm.toml");
2680
2681 let mut manifest = crate::manifest::Manifest::new();
2683
2684 manifest.snippets.insert(
2686 "local-snippet".to_string(),
2687 crate::manifest::ResourceDependency::Detailed(Box::new(
2688 crate::manifest::DetailedDependency {
2689 source: None,
2690 path: "./snippets/local.md".to_string(),
2691 version: None,
2692 command: None,
2693 branch: None,
2694 rev: None,
2695 args: None,
2696 target: None,
2697 filename: None,
2698 dependencies: None,
2699 tool: Some("claude-code".to_string()),
2700 flatten: None,
2701 install: None,
2702
2703 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2704 },
2705 )),
2706 );
2707
2708 manifest.commands.insert(
2710 "local-command".to_string(),
2711 crate::manifest::ResourceDependency::Detailed(Box::new(
2712 crate::manifest::DetailedDependency {
2713 source: None,
2714 path: "./commands/deploy.md".to_string(),
2715 version: None,
2716 command: None,
2717 branch: None,
2718 rev: None,
2719 args: None,
2720 target: None,
2721 filename: None,
2722 dependencies: None,
2723 tool: Some("claude-code".to_string()),
2724 flatten: None,
2725 install: None,
2726
2727 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2728 },
2729 )),
2730 );
2731
2732 manifest.save(&manifest_path).unwrap();
2733
2734 std::fs::create_dir_all(temp.path().join("snippets")).unwrap();
2736 std::fs::create_dir_all(temp.path().join("commands")).unwrap();
2737 std::fs::write(temp.path().join("snippets/local.md"), "# Local Snippet").unwrap();
2738 std::fs::write(temp.path().join("commands/deploy.md"), "# Deploy Command").unwrap();
2739
2740 let cmd = ValidateCommand {
2741 file: None,
2742 resolve: false,
2743 check_lock: false,
2744 sources: false,
2745 paths: true, format: OutputFormat::Text,
2747 verbose: false,
2748 quiet: false,
2749 strict: false,
2750 render: false,
2751 };
2752
2753 let result = cmd.execute_from_path(manifest_path).await;
2754 assert!(result.is_ok());
2755 }
2757
2758 #[tokio::test]
2759 async fn test_validate_check_paths_missing_snippets_json() {
2760 let temp = TempDir::new().unwrap();
2761 let manifest_path = temp.path().join("agpm.toml");
2762
2763 let mut manifest = crate::manifest::Manifest::new();
2765 manifest.snippets.insert(
2766 "missing-snippet".to_string(),
2767 crate::manifest::ResourceDependency::Detailed(Box::new(
2768 crate::manifest::DetailedDependency {
2769 source: None,
2770 path: "./missing/snippet.md".to_string(),
2771 version: None,
2772 command: None,
2773 branch: None,
2774 rev: None,
2775 args: None,
2776 target: None,
2777 filename: None,
2778 dependencies: None,
2779 tool: Some("claude-code".to_string()),
2780 flatten: None,
2781 install: None,
2782
2783 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2784 },
2785 )),
2786 );
2787 manifest.save(&manifest_path).unwrap();
2788
2789 let cmd = ValidateCommand {
2790 file: None,
2791 resolve: false,
2792 check_lock: false,
2793 sources: false,
2794 paths: true,
2795 format: OutputFormat::Json, verbose: false,
2797 quiet: true,
2798 strict: false,
2799 render: false,
2800 };
2801
2802 let result = cmd.execute_from_path(manifest_path).await;
2803 assert!(result.is_err());
2804 }
2806
2807 #[tokio::test]
2808 async fn test_validate_lockfile_missing_warning() {
2809 let temp = TempDir::new().unwrap();
2810 let manifest_path = temp.path().join("agpm.toml");
2811
2812 let manifest = crate::manifest::Manifest::new();
2814 manifest.save(&manifest_path).unwrap();
2815
2816 let cmd = ValidateCommand {
2817 file: None,
2818 resolve: false,
2819 check_lock: true,
2820 sources: false,
2821 paths: false,
2822 format: OutputFormat::Text,
2823 verbose: true, quiet: false,
2825 strict: false,
2826 render: false,
2827 };
2828
2829 let result = cmd.execute_from_path(manifest_path).await;
2830 assert!(result.is_ok());
2831 }
2833
2834 #[tokio::test]
2835 async fn test_validate_lockfile_syntax_error_json() {
2836 let temp = TempDir::new().unwrap();
2837 let manifest_path = temp.path().join("agpm.toml");
2838 let lockfile_path = temp.path().join("agpm.lock");
2839
2840 let manifest = crate::manifest::Manifest::new();
2842 manifest.save(&manifest_path).unwrap();
2843
2844 std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
2846
2847 let cmd = ValidateCommand {
2848 file: None,
2849 resolve: false,
2850 check_lock: true,
2851 sources: false,
2852 paths: false,
2853 format: OutputFormat::Json,
2854 verbose: false,
2855 quiet: true,
2856 strict: false,
2857 render: false,
2858 };
2859
2860 let result = cmd.execute_from_path(manifest_path).await;
2861 assert!(result.is_err());
2862 }
2864
2865 #[tokio::test]
2866 async fn test_validate_lockfile_missing_dependencies() {
2867 let temp = TempDir::new().unwrap();
2868 let manifest_path = temp.path().join("agpm.toml");
2869 let lockfile_path = temp.path().join("agpm.lock");
2870
2871 let mut manifest = crate::manifest::Manifest::new();
2873 manifest.add_dependency(
2874 "missing-agent".to_string(),
2875 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
2876 true,
2877 );
2878 manifest.add_dependency(
2879 "missing-snippet".to_string(),
2880 crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
2881 false,
2882 );
2883 manifest.save(&manifest_path).unwrap();
2884
2885 let lockfile = crate::lockfile::LockFile::new();
2887 lockfile.save(&lockfile_path).unwrap();
2888
2889 let cmd = ValidateCommand {
2890 file: None,
2891 resolve: false,
2892 check_lock: true,
2893 sources: false,
2894 paths: false,
2895 format: OutputFormat::Text,
2896 verbose: false,
2897 quiet: false,
2898 strict: false,
2899 render: false,
2900 };
2901
2902 let result = cmd.execute_from_path(manifest_path).await;
2903 assert!(result.is_ok()); }
2906
2907 #[tokio::test]
2908 async fn test_validate_lockfile_extra_entries_error() {
2909 let temp = TempDir::new().unwrap();
2910 let manifest_path = temp.path().join("agpm.toml");
2911 let lockfile_path = temp.path().join("agpm.lock");
2912
2913 let manifest = crate::manifest::Manifest::new();
2915 manifest.save(&manifest_path).unwrap();
2916
2917 let mut lockfile = crate::lockfile::LockFile::new();
2919 lockfile.agents.push(crate::lockfile::LockedResource {
2920 name: "extra-agent".to_string(),
2921 source: Some("test".to_string()),
2922 url: Some("https://github.com/test/repo.git".to_string()),
2923 path: "test.md".to_string(),
2924 version: None,
2925 resolved_commit: Some("abc123".to_string()),
2926 checksum: "sha256:dummy".to_string(),
2927 installed_at: "agents/extra-agent.md".to_string(),
2928 dependencies: vec![],
2929 resource_type: crate::core::ResourceType::Agent,
2930
2931 tool: Some("claude-code".to_string()),
2932 manifest_alias: None,
2933 context_checksum: None,
2934 applied_patches: std::collections::BTreeMap::new(),
2935 install: None,
2936 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
2937 });
2938 lockfile.save(&lockfile_path).unwrap();
2939
2940 let cmd = ValidateCommand {
2941 file: None,
2942 resolve: false,
2943 check_lock: true,
2944 sources: false,
2945 paths: false,
2946 format: OutputFormat::Json,
2947 verbose: false,
2948 quiet: true,
2949 strict: false,
2950 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_with_json_output() {
2960 let temp = TempDir::new().unwrap();
2961 let manifest_path = temp.path().join("agpm.toml");
2962
2963 let manifest = crate::manifest::Manifest::new(); 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::Json,
2974 verbose: false,
2975 quiet: true,
2976 strict: true, render: false,
2978 };
2979
2980 let result = cmd.execute_from_path(manifest_path).await;
2981 assert!(result.is_err()); }
2984
2985 #[tokio::test]
2986 async fn test_validate_strict_mode_text_output() {
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, strict: true,
3004 render: false,
3005 };
3006
3007 let result = cmd.execute_from_path(manifest_path).await;
3008 assert!(result.is_err());
3009 }
3011
3012 #[tokio::test]
3013 async fn test_validate_final_success_with_warnings() {
3014 let temp = TempDir::new().unwrap();
3015 let manifest_path = temp.path().join("agpm.toml");
3016
3017 let manifest = crate::manifest::Manifest::new();
3019 manifest.save(&manifest_path).unwrap();
3020
3021 let cmd = ValidateCommand {
3022 file: None,
3023 resolve: false,
3024 check_lock: false,
3025 sources: false,
3026 paths: false,
3027 format: OutputFormat::Text,
3028 verbose: false,
3029 quiet: false,
3030 strict: false, render: false,
3032 };
3033
3034 let result = cmd.execute_from_path(manifest_path).await;
3035 assert!(result.is_ok());
3036 }
3038
3039 #[tokio::test]
3040 async fn test_validate_verbose_mode_with_summary() {
3041 let temp = TempDir::new().unwrap();
3042 let manifest_path = temp.path().join("agpm.toml");
3043
3044 let mut manifest = crate::manifest::Manifest::new();
3046 manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
3047 manifest.add_dependency(
3048 "test-agent".to_string(),
3049 crate::manifest::ResourceDependency::Simple("test.md".to_string()),
3050 true,
3051 );
3052 manifest.add_dependency(
3053 "test-snippet".to_string(),
3054 crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
3055 false,
3056 );
3057 manifest.save(&manifest_path).unwrap();
3058
3059 let cmd = ValidateCommand {
3060 file: None,
3061 resolve: false,
3062 check_lock: false,
3063 sources: false,
3064 paths: false,
3065 format: OutputFormat::Text,
3066 verbose: true, quiet: false,
3068 strict: false,
3069 render: false,
3070 };
3071
3072 let result = cmd.execute_from_path(manifest_path).await;
3073 assert!(result.is_ok());
3074 }
3076
3077 #[tokio::test]
3078 async fn test_validate_all_checks_enabled() {
3079 let temp = TempDir::new().unwrap();
3080 let manifest_path = temp.path().join("agpm.toml");
3081 let lockfile_path = temp.path().join("agpm.lock");
3082
3083 let mut manifest = Manifest::new();
3085 manifest.agents.insert(
3086 "test-agent".to_string(),
3087 ResourceDependency::Simple("local-agent.md".to_string()),
3088 );
3089 manifest.save(&manifest_path).unwrap();
3090
3091 let lockfile = crate::lockfile::LockFile::new();
3093 lockfile.save(&lockfile_path).unwrap();
3094
3095 let cmd = ValidateCommand {
3096 file: None,
3097 resolve: true,
3098 check_lock: true,
3099 sources: true,
3100 paths: true,
3101 format: OutputFormat::Text,
3102 verbose: true,
3103 quiet: false,
3104 strict: true,
3105 render: false,
3106 };
3107
3108 let result = cmd.execute_from_path(manifest_path).await;
3109 assert!(result.is_err() || result.is_ok());
3111 }
3112
3113 #[tokio::test]
3114 async fn test_validate_with_specific_file_path() {
3115 let temp = TempDir::new().unwrap();
3116 let custom_path = temp.path().join("custom-manifest.toml");
3117
3118 let manifest = Manifest::new();
3119 manifest.save(&custom_path).unwrap();
3120
3121 let cmd = ValidateCommand {
3122 file: Some(custom_path.to_string_lossy().to_string()),
3123 resolve: false,
3124 check_lock: false,
3125 sources: false,
3126 paths: false,
3127 format: OutputFormat::Text,
3128 verbose: false,
3129 quiet: false,
3130 strict: false,
3131 render: false,
3132 };
3133
3134 let result = cmd.execute().await;
3135 assert!(result.is_ok());
3136 }
3137
3138 #[tokio::test]
3139 async fn test_validate_sources_check_with_invalid_url() {
3140 let temp = TempDir::new().unwrap();
3141 let manifest_path = temp.path().join("agpm.toml");
3142
3143 let mut manifest = Manifest::new();
3144 manifest.sources.insert("invalid".to_string(), "not-a-valid-url".to_string());
3145 manifest.save(&manifest_path).unwrap();
3146
3147 let cmd = ValidateCommand {
3148 file: None,
3149 resolve: false,
3150 check_lock: false,
3151 sources: true,
3152 paths: false,
3153 format: OutputFormat::Text,
3154 verbose: false,
3155 quiet: false,
3156 strict: false,
3157 render: false,
3158 };
3159
3160 let result = cmd.execute_from_path(manifest_path).await;
3161 assert!(result.is_err()); }
3163
3164 #[tokio::test]
3165 async fn test_validation_results_with_errors_and_warnings() {
3166 let mut results = ValidationResults::default();
3167
3168 results.errors.push("Error 1".to_string());
3170 results.errors.push("Error 2".to_string());
3171
3172 results.warnings.push("Warning 1".to_string());
3174 results.warnings.push("Warning 2".to_string());
3175
3176 assert!(!results.errors.is_empty());
3177 assert_eq!(results.errors.len(), 2);
3178 assert_eq!(results.warnings.len(), 2);
3179 }
3180
3181 #[tokio::test]
3182 async fn test_output_format_equality() {
3183 assert_eq!(OutputFormat::Text, OutputFormat::Text);
3185 assert_eq!(OutputFormat::Json, OutputFormat::Json);
3186 assert_ne!(OutputFormat::Text, OutputFormat::Json);
3187 }
3188
3189 #[tokio::test]
3190 async fn test_validate_command_defaults() {
3191 let cmd = ValidateCommand {
3192 file: None,
3193 resolve: false,
3194 check_lock: false,
3195 sources: false,
3196 paths: false,
3197 format: OutputFormat::Text,
3198 verbose: false,
3199 quiet: false,
3200 strict: false,
3201 render: false,
3202 };
3203 assert_eq!(cmd.file, None);
3204 assert!(!cmd.resolve);
3205 assert!(!cmd.check_lock);
3206 assert!(!cmd.sources);
3207 assert!(!cmd.paths);
3208 assert_eq!(cmd.format, OutputFormat::Text);
3209 assert!(!cmd.verbose);
3210 assert!(!cmd.quiet);
3211 assert!(!cmd.strict);
3212 }
3213
3214 #[tokio::test]
3215 async fn test_json_output_format() {
3216 let temp = TempDir::new().unwrap();
3217 let manifest_path = temp.path().join("agpm.toml");
3218
3219 let manifest = Manifest::new();
3220 manifest.save(&manifest_path).unwrap();
3221
3222 let cmd = ValidateCommand {
3223 file: None,
3224 resolve: false,
3225 check_lock: false,
3226 sources: false,
3227 paths: false,
3228 format: OutputFormat::Json,
3229 verbose: false,
3230 quiet: false,
3231 strict: false,
3232 render: false,
3233 };
3234
3235 let result = cmd.execute_from_path(manifest_path).await;
3236 assert!(result.is_ok());
3237 }
3238
3239 #[tokio::test]
3240 async fn test_validation_with_verbose_mode() {
3241 let temp = TempDir::new().unwrap();
3242 let manifest_path = temp.path().join("agpm.toml");
3243
3244 let manifest = Manifest::new();
3245 manifest.save(&manifest_path).unwrap();
3246
3247 let cmd = ValidateCommand {
3248 file: None,
3249 resolve: false,
3250 check_lock: false,
3251 sources: false,
3252 paths: false,
3253 format: OutputFormat::Text,
3254 verbose: true,
3255 quiet: false,
3256 strict: false,
3257 render: false,
3258 };
3259
3260 let result = cmd.execute_from_path(manifest_path).await;
3261 assert!(result.is_ok());
3262 }
3263
3264 #[tokio::test]
3265 async fn test_validation_with_quiet_mode() {
3266 let temp = TempDir::new().unwrap();
3267 let manifest_path = temp.path().join("agpm.toml");
3268
3269 let manifest = Manifest::new();
3270 manifest.save(&manifest_path).unwrap();
3271
3272 let cmd = ValidateCommand {
3273 file: None,
3274 resolve: false,
3275 check_lock: false,
3276 sources: false,
3277 paths: false,
3278 format: OutputFormat::Text,
3279 verbose: false,
3280 quiet: true,
3281 strict: false,
3282 render: false,
3283 };
3284
3285 let result = cmd.execute_from_path(manifest_path).await;
3286 assert!(result.is_ok());
3287 }
3288
3289 #[tokio::test]
3290 async fn test_validation_with_strict_mode_and_warnings() {
3291 let temp = TempDir::new().unwrap();
3292 let manifest_path = temp.path().join("agpm.toml");
3293
3294 let manifest = Manifest::new();
3296 manifest.save(&manifest_path).unwrap();
3297
3298 let cmd = ValidateCommand {
3299 file: None,
3300 resolve: false,
3301 check_lock: false,
3302 sources: false,
3303 paths: false,
3304 format: OutputFormat::Text,
3305 verbose: false,
3306 quiet: false,
3307 strict: true, render: false,
3309 };
3310
3311 let result = cmd.execute_from_path(manifest_path).await;
3312 assert!(result.is_err()); }
3314
3315 #[tokio::test]
3316 async fn test_validation_with_local_paths_check() {
3317 let temp = TempDir::new().unwrap();
3318 let manifest_path = temp.path().join("agpm.toml");
3319
3320 let mut manifest = Manifest::new();
3321 manifest.agents.insert(
3322 "local-agent".to_string(),
3323 ResourceDependency::Simple("./missing-file.md".to_string()),
3324 );
3325 manifest.save(&manifest_path).unwrap();
3326
3327 let cmd = ValidateCommand {
3328 file: None,
3329 resolve: false,
3330 check_lock: false,
3331 sources: false,
3332 paths: true, format: OutputFormat::Text,
3334 verbose: false,
3335 quiet: false,
3336 strict: false,
3337 render: false,
3338 };
3339
3340 let result = cmd.execute_from_path(manifest_path).await;
3341 assert!(result.is_err()); }
3343
3344 #[tokio::test]
3345 async fn test_validation_with_existing_local_paths() {
3346 let temp = TempDir::new().unwrap();
3347 let manifest_path = temp.path().join("agpm.toml");
3348 let local_file = temp.path().join("agent.md");
3349
3350 std::fs::write(&local_file, "# Local Agent").unwrap();
3352
3353 let mut manifest = Manifest::new();
3354 manifest.agents.insert(
3355 "local-agent".to_string(),
3356 ResourceDependency::Simple("./agent.md".to_string()),
3357 );
3358 manifest.save(&manifest_path).unwrap();
3359
3360 let cmd = ValidateCommand {
3361 file: None,
3362 resolve: false,
3363 check_lock: false,
3364 sources: false,
3365 paths: true,
3366 format: OutputFormat::Text,
3367 verbose: false,
3368 quiet: false,
3369 strict: false,
3370 render: false,
3371 };
3372
3373 let result = cmd.execute_from_path(manifest_path).await;
3374 assert!(result.is_ok());
3375 }
3376
3377 #[tokio::test]
3378 async fn test_validation_with_lockfile_consistency_check_no_lockfile() {
3379 let temp = TempDir::new().unwrap();
3380 let manifest_path = temp.path().join("agpm.toml");
3381
3382 let mut manifest = Manifest::new();
3383 manifest
3384 .agents
3385 .insert("test-agent".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3386 manifest.save(&manifest_path).unwrap();
3387
3388 let cmd = ValidateCommand {
3389 file: None,
3390 resolve: false,
3391 check_lock: true, sources: false,
3393 paths: false,
3394 format: OutputFormat::Text,
3395 verbose: false,
3396 quiet: false,
3397 strict: false,
3398 render: false,
3399 };
3400
3401 let result = cmd.execute_from_path(manifest_path).await;
3402 assert!(result.is_ok()); }
3404
3405 #[tokio::test]
3406 async fn test_validation_with_inconsistent_lockfile() {
3407 let temp = TempDir::new().unwrap();
3408 let manifest_path = temp.path().join("agpm.toml");
3409 let lockfile_path = temp.path().join("agpm.lock");
3410
3411 let mut manifest = Manifest::new();
3413 manifest.agents.insert(
3414 "manifest-agent".to_string(),
3415 ResourceDependency::Simple("agent.md".to_string()),
3416 );
3417 manifest.save(&manifest_path).unwrap();
3418
3419 let mut lockfile = crate::lockfile::LockFile::new();
3421 lockfile.agents.push(crate::lockfile::LockedResource {
3422 name: "lockfile-agent".to_string(),
3423 source: None,
3424 url: None,
3425 path: "agent.md".to_string(),
3426 version: None,
3427 resolved_commit: None,
3428 checksum: "sha256:dummy".to_string(),
3429 installed_at: "agents/lockfile-agent.md".to_string(),
3430 dependencies: vec![],
3431 resource_type: crate::core::ResourceType::Agent,
3432
3433 tool: Some("claude-code".to_string()),
3434 manifest_alias: None,
3435 context_checksum: None,
3436 applied_patches: std::collections::BTreeMap::new(),
3437 install: None,
3438 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3439 });
3440 lockfile.save(&lockfile_path).unwrap();
3441
3442 let cmd = ValidateCommand {
3443 file: None,
3444 resolve: false,
3445 check_lock: true,
3446 sources: false,
3447 paths: false,
3448 format: OutputFormat::Text,
3449 verbose: false,
3450 quiet: false,
3451 strict: false,
3452 render: false,
3453 };
3454
3455 let result = cmd.execute_from_path(manifest_path).await;
3456 assert!(result.is_err()); }
3458
3459 #[tokio::test]
3460 async fn test_validation_with_invalid_lockfile_syntax() {
3461 let temp = TempDir::new().unwrap();
3462 let manifest_path = temp.path().join("agpm.toml");
3463 let lockfile_path = temp.path().join("agpm.lock");
3464
3465 let manifest = Manifest::new();
3466 manifest.save(&manifest_path).unwrap();
3467
3468 std::fs::write(&lockfile_path, "invalid toml syntax [[[").unwrap();
3470
3471 let cmd = ValidateCommand {
3472 file: None,
3473 resolve: false,
3474 check_lock: true,
3475 sources: false,
3476 paths: false,
3477 format: OutputFormat::Text,
3478 verbose: false,
3479 quiet: false,
3480 strict: false,
3481 render: false,
3482 };
3483
3484 let result = cmd.execute_from_path(manifest_path).await;
3485 assert!(result.is_err()); }
3487
3488 #[tokio::test]
3489 async fn test_validation_with_outdated_version_warning() {
3490 let temp = TempDir::new().unwrap();
3491 let manifest_path = temp.path().join("agpm.toml");
3492
3493 let mut manifest = Manifest::new();
3494 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3496 manifest.agents.insert(
3497 "old-agent".to_string(),
3498 ResourceDependency::Detailed(Box::new(crate::manifest::DetailedDependency {
3499 source: Some("test".to_string()),
3500 path: "agent.md".to_string(),
3501 version: Some("v0.1.0".to_string()),
3502 branch: None,
3503 rev: None,
3504 command: None,
3505 args: None,
3506 target: None,
3507 filename: None,
3508 dependencies: None,
3509 tool: Some("claude-code".to_string()),
3510 flatten: None,
3511 install: None,
3512
3513 template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
3514 })),
3515 );
3516 manifest.save(&manifest_path).unwrap();
3517
3518 let cmd = ValidateCommand {
3519 file: None,
3520 resolve: false,
3521 check_lock: false,
3522 sources: false,
3523 paths: false,
3524 format: OutputFormat::Text,
3525 verbose: false,
3526 quiet: false,
3527 strict: false,
3528 render: false,
3529 };
3530
3531 let result = cmd.execute_from_path(manifest_path).await;
3532 assert!(result.is_ok()); }
3534
3535 #[tokio::test]
3536 async fn test_validation_json_output_with_errors() {
3537 let temp = TempDir::new().unwrap();
3538 let manifest_path = temp.path().join("agpm.toml");
3539
3540 std::fs::write(&manifest_path, "invalid toml [[[ syntax").unwrap();
3542
3543 let cmd = ValidateCommand {
3544 file: None,
3545 resolve: false,
3546 check_lock: false,
3547 sources: false,
3548 paths: false,
3549 format: OutputFormat::Json,
3550 verbose: false,
3551 quiet: false,
3552 strict: false,
3553 render: false,
3554 };
3555
3556 let result = cmd.execute_from_path(manifest_path).await;
3557 assert!(result.is_err());
3558 }
3559
3560 #[tokio::test]
3561 async fn test_validation_with_manifest_not_found_json() {
3562 let temp = TempDir::new().unwrap();
3563 let manifest_path = temp.path().join("nonexistent.toml");
3564
3565 let cmd = ValidateCommand {
3566 file: None,
3567 resolve: false,
3568 check_lock: false,
3569 sources: false,
3570 paths: false,
3571 format: OutputFormat::Json,
3572 verbose: false,
3573 quiet: false,
3574 strict: false,
3575 render: false,
3576 };
3577
3578 let result = cmd.execute_from_path(manifest_path).await;
3579 assert!(result.is_err());
3580 }
3581
3582 #[tokio::test]
3583 async fn test_validation_with_manifest_not_found_text() {
3584 let temp = TempDir::new().unwrap();
3585 let manifest_path = temp.path().join("nonexistent.toml");
3586
3587 let cmd = ValidateCommand {
3588 file: None,
3589 resolve: false,
3590 check_lock: false,
3591 sources: false,
3592 paths: false,
3593 format: OutputFormat::Text,
3594 verbose: false,
3595 quiet: false,
3596 strict: false,
3597 render: false,
3598 };
3599
3600 let result = cmd.execute_from_path(manifest_path).await;
3601 assert!(result.is_err());
3602 }
3603
3604 #[tokio::test]
3605 async fn test_validation_with_missing_lockfile_dependencies() {
3606 let temp = TempDir::new().unwrap();
3607 let manifest_path = temp.path().join("agpm.toml");
3608 let lockfile_path = temp.path().join("agpm.lock");
3609
3610 let mut manifest = Manifest::new();
3612 manifest
3613 .agents
3614 .insert("agent1".to_string(), ResourceDependency::Simple("agent1.md".to_string()));
3615 manifest
3616 .agents
3617 .insert("agent2".to_string(), ResourceDependency::Simple("agent2.md".to_string()));
3618 manifest
3619 .snippets
3620 .insert("snippet1".to_string(), ResourceDependency::Simple("snippet1.md".to_string()));
3621 manifest.save(&manifest_path).unwrap();
3622
3623 let mut lockfile = crate::lockfile::LockFile::new();
3625 lockfile.agents.push(crate::lockfile::LockedResource {
3626 name: "agent1".to_string(),
3627 source: None,
3628 url: None,
3629 path: "agent1.md".to_string(),
3630 version: None,
3631 resolved_commit: None,
3632 checksum: "sha256:dummy".to_string(),
3633 installed_at: "agents/agent1.md".to_string(),
3634 dependencies: vec![],
3635 resource_type: crate::core::ResourceType::Agent,
3636
3637 tool: Some("claude-code".to_string()),
3638 manifest_alias: None,
3639 context_checksum: None,
3640 applied_patches: std::collections::BTreeMap::new(),
3641 install: None,
3642 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3643 });
3644 lockfile.save(&lockfile_path).unwrap();
3645
3646 let cmd = ValidateCommand {
3647 file: None,
3648 resolve: false,
3649 check_lock: true,
3650 sources: false,
3651 paths: false,
3652 format: OutputFormat::Text,
3653 verbose: false,
3654 quiet: false,
3655 strict: false,
3656 render: false,
3657 };
3658
3659 let result = cmd.execute_from_path(manifest_path).await;
3660 assert!(result.is_ok()); }
3662
3663 #[tokio::test]
3664 async fn test_execute_without_manifest_file() {
3665 let temp = TempDir::new().unwrap();
3667 let non_existent_manifest = temp.path().join("non_existent.toml");
3668
3669 let cmd = ValidateCommand {
3670 file: Some(non_existent_manifest.to_string_lossy().to_string()),
3671 resolve: false,
3672 check_lock: false,
3673 sources: false,
3674 paths: false,
3675 format: OutputFormat::Text,
3676 verbose: false,
3677 quiet: false,
3678 strict: false,
3679 render: false,
3680 };
3681
3682 let result = cmd.execute().await;
3683 assert!(result.is_err()); }
3685
3686 #[tokio::test]
3687 async fn test_execute_with_specified_file() {
3688 let temp = TempDir::new().unwrap();
3689 let custom_path = temp.path().join("custom.toml");
3690
3691 let manifest = Manifest::new();
3692 manifest.save(&custom_path).unwrap();
3693
3694 let cmd = ValidateCommand {
3695 file: Some(custom_path.to_string_lossy().to_string()),
3696 resolve: false,
3697 check_lock: false,
3698 sources: false,
3699 paths: false,
3700 format: OutputFormat::Text,
3701 verbose: false,
3702 quiet: false,
3703 strict: false,
3704 render: false,
3705 };
3706
3707 let result = cmd.execute().await;
3708 assert!(result.is_ok());
3709 }
3710
3711 #[tokio::test]
3712 async fn test_execute_with_nonexistent_specified_file() {
3713 let temp = TempDir::new().unwrap();
3714 let nonexistent = temp.path().join("nonexistent.toml");
3715
3716 let cmd = ValidateCommand {
3717 file: Some(nonexistent.to_string_lossy().to_string()),
3718 resolve: false,
3719 check_lock: false,
3720 sources: false,
3721 paths: false,
3722 format: OutputFormat::Text,
3723 verbose: false,
3724 quiet: false,
3725 strict: false,
3726 render: false,
3727 };
3728
3729 let result = cmd.execute().await;
3730 assert!(result.is_err());
3731 }
3732
3733 #[tokio::test]
3734 async fn test_validation_with_verbose_and_text_format() {
3735 let temp = TempDir::new().unwrap();
3736 let manifest_path = temp.path().join("agpm.toml");
3737
3738 let mut manifest = Manifest::new();
3739 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3740 manifest
3741 .agents
3742 .insert("agent1".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3743 manifest
3744 .snippets
3745 .insert("snippet1".to_string(), ResourceDependency::Simple("snippet.md".to_string()));
3746 manifest.save(&manifest_path).unwrap();
3747
3748 let cmd = ValidateCommand {
3749 file: None,
3750 resolve: false,
3751 check_lock: false,
3752 sources: false,
3753 paths: false,
3754 format: OutputFormat::Text,
3755 verbose: true,
3756 quiet: false,
3757 strict: false,
3758 render: false,
3759 };
3760
3761 let result = cmd.execute_from_path(manifest_path).await;
3762 assert!(result.is_ok());
3763 }
3764
3765 #[tokio::test]
3766 async fn test_file_reference_validation_with_valid_references() {
3767 use crate::lockfile::LockedResource;
3768 use std::fs;
3769
3770 let temp = TempDir::new().unwrap();
3771 let project_dir = temp.path();
3772
3773 let manifest_path = project_dir.join("agpm.toml");
3775 let mut manifest = Manifest::new();
3776 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3777 manifest.save(&manifest_path).unwrap();
3778
3779 let snippets_dir = project_dir.join(".agpm").join("snippets");
3781 fs::create_dir_all(&snippets_dir).unwrap();
3782 fs::write(snippets_dir.join("helper.md"), "# Helper\nSome content").unwrap();
3783
3784 let agents_dir = project_dir.join(".claude").join("agents");
3786 fs::create_dir_all(&agents_dir).unwrap();
3787 let agent_content = r#"---
3788title: Test Agent
3789---
3790
3791# Test Agent
3792
3793See [helper](.agpm/snippets/helper.md) for details.
3794"#;
3795 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3796
3797 let lockfile_path = project_dir.join("agpm.lock");
3799 let mut lockfile = crate::lockfile::LockFile::default();
3800 lockfile.agents.push(LockedResource {
3801 name: "test-agent".to_string(),
3802 source: None,
3803 path: "agents/test.md".to_string(),
3804 version: Some("v1.0.0".to_string()),
3805 resolved_commit: None,
3806 url: None,
3807 checksum: "abc123".to_string(),
3808 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3809 dependencies: vec![],
3810 resource_type: crate::core::ResourceType::Agent,
3811 tool: None,
3812 manifest_alias: None,
3813 context_checksum: None,
3814 applied_patches: std::collections::BTreeMap::new(),
3815 install: None,
3816 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3817 });
3818 lockfile.save(&lockfile_path).unwrap();
3819
3820 let cmd = ValidateCommand {
3821 file: None,
3822 resolve: false,
3823 check_lock: false,
3824 sources: false,
3825 paths: false,
3826 format: OutputFormat::Text,
3827 verbose: true,
3828 quiet: false,
3829 strict: false,
3830 render: true,
3831 };
3832
3833 let result = cmd.execute_from_path(manifest_path).await;
3834 assert!(result.is_ok());
3835 }
3836
3837 #[tokio::test]
3838 async fn test_file_reference_validation_with_broken_references() {
3839 use crate::lockfile::LockedResource;
3840 use std::fs;
3841
3842 let temp = TempDir::new().unwrap();
3843 let project_dir = temp.path();
3844
3845 let manifest_path = project_dir.join("agpm.toml");
3847 let mut manifest = Manifest::new();
3848 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3849 manifest.save(&manifest_path).unwrap();
3850
3851 let agents_dir = project_dir.join(".claude").join("agents");
3853 fs::create_dir_all(&agents_dir).unwrap();
3854 let agent_content = r#"---
3855title: Test Agent
3856---
3857
3858# Test Agent
3859
3860See [missing](.agpm/snippets/missing.md) for details.
3861Also check `.claude/nonexistent.md`.
3862"#;
3863 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3864
3865 let lockfile_path = project_dir.join("agpm.lock");
3867 let mut lockfile = crate::lockfile::LockFile::default();
3868 lockfile.agents.push(LockedResource {
3869 name: "test-agent".to_string(),
3870 source: None,
3871 path: "agents/test.md".to_string(),
3872 version: Some("v1.0.0".to_string()),
3873 resolved_commit: None,
3874 url: None,
3875 checksum: "abc123".to_string(),
3876 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3877 dependencies: vec![],
3878 resource_type: crate::core::ResourceType::Agent,
3879 tool: None,
3880 manifest_alias: None,
3881 context_checksum: None,
3882 applied_patches: std::collections::BTreeMap::new(),
3883 install: None,
3884 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3885 });
3886 lockfile.save(&lockfile_path).unwrap();
3887
3888 let cmd = ValidateCommand {
3889 file: None,
3890 resolve: false,
3891 check_lock: false,
3892 sources: false,
3893 paths: false,
3894 format: OutputFormat::Text,
3895 verbose: true,
3896 quiet: false,
3897 strict: false,
3898 render: true,
3899 };
3900
3901 let result = cmd.execute_from_path(manifest_path).await;
3902 assert!(result.is_err());
3903 let err_msg = format!("{:?}", result.unwrap_err());
3904 assert!(err_msg.contains("File reference validation failed"));
3905 }
3906
3907 #[tokio::test]
3908 async fn test_file_reference_validation_ignores_urls() {
3909 use crate::lockfile::LockedResource;
3910 use std::fs;
3911
3912 let temp = TempDir::new().unwrap();
3913 let project_dir = temp.path();
3914
3915 let manifest_path = project_dir.join("agpm.toml");
3917 let mut manifest = Manifest::new();
3918 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3919 manifest.save(&manifest_path).unwrap();
3920
3921 let agents_dir = project_dir.join(".claude").join("agents");
3923 fs::create_dir_all(&agents_dir).unwrap();
3924 let agent_content = r#"---
3925title: Test Agent
3926---
3927
3928# Test Agent
3929
3930Check [GitHub](https://github.com/user/repo) for source.
3931Visit http://example.com for more info.
3932"#;
3933 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3934
3935 let lockfile_path = project_dir.join("agpm.lock");
3937 let mut lockfile = crate::lockfile::LockFile::default();
3938 lockfile.agents.push(LockedResource {
3939 name: "test-agent".to_string(),
3940 source: None,
3941 path: "agents/test.md".to_string(),
3942 version: Some("v1.0.0".to_string()),
3943 resolved_commit: None,
3944 url: None,
3945 checksum: "abc123".to_string(),
3946 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3947 dependencies: vec![],
3948 resource_type: crate::core::ResourceType::Agent,
3949 tool: None,
3950 manifest_alias: None,
3951 context_checksum: None,
3952 applied_patches: std::collections::BTreeMap::new(),
3953 install: None,
3954 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3955 });
3956 lockfile.save(&lockfile_path).unwrap();
3957
3958 let cmd = ValidateCommand {
3959 file: None,
3960 resolve: false,
3961 check_lock: false,
3962 sources: false,
3963 paths: false,
3964 format: OutputFormat::Text,
3965 verbose: true,
3966 quiet: false,
3967 strict: false,
3968 render: true,
3969 };
3970
3971 let result = cmd.execute_from_path(manifest_path).await;
3972 assert!(result.is_ok());
3973 }
3974
3975 #[tokio::test]
3976 async fn test_file_reference_validation_ignores_code_blocks() {
3977 use crate::lockfile::LockedResource;
3978 use std::fs;
3979
3980 let temp = TempDir::new().unwrap();
3981 let project_dir = temp.path();
3982
3983 let manifest_path = project_dir.join("agpm.toml");
3985 let mut manifest = Manifest::new();
3986 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3987 manifest.save(&manifest_path).unwrap();
3988
3989 let agents_dir = project_dir.join(".claude").join("agents");
3991 fs::create_dir_all(&agents_dir).unwrap();
3992 let agent_content = r#"---
3993title: Test Agent
3994---
3995
3996# Test Agent
3997
3998```bash
3999# This reference in code should be ignored
4000cat .agpm/snippets/nonexistent.md
4001```
4002
4003Inline code `example.md` should also be ignored.
4004"#;
4005 fs::write(agents_dir.join("test.md"), agent_content).unwrap();
4006
4007 let lockfile_path = project_dir.join("agpm.lock");
4009 let mut lockfile = crate::lockfile::LockFile::default();
4010 lockfile.agents.push(LockedResource {
4011 name: "test-agent".to_string(),
4012 source: None,
4013 path: "agents/test.md".to_string(),
4014 version: Some("v1.0.0".to_string()),
4015 resolved_commit: None,
4016 url: None,
4017 checksum: "abc123".to_string(),
4018 installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
4019 dependencies: vec![],
4020 resource_type: crate::core::ResourceType::Agent,
4021 tool: None,
4022 manifest_alias: None,
4023 context_checksum: None,
4024 applied_patches: std::collections::BTreeMap::new(),
4025 install: None,
4026 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4027 });
4028 lockfile.save(&lockfile_path).unwrap();
4029
4030 let cmd = ValidateCommand {
4031 file: None,
4032 resolve: false,
4033 check_lock: false,
4034 sources: false,
4035 paths: false,
4036 format: OutputFormat::Text,
4037 verbose: true,
4038 quiet: false,
4039 strict: false,
4040 render: true,
4041 };
4042
4043 let result = cmd.execute_from_path(manifest_path).await;
4044 assert!(result.is_ok());
4045 }
4046
4047 #[tokio::test]
4048 async fn test_file_reference_validation_multiple_resources() {
4049 use crate::lockfile::LockedResource;
4050 use std::fs;
4051
4052 let temp = TempDir::new().unwrap();
4053 let project_dir = temp.path();
4054
4055 let manifest_path = project_dir.join("agpm.toml");
4057 let mut manifest = Manifest::new();
4058 manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
4059 manifest.save(&manifest_path).unwrap();
4060
4061 let snippets_dir = project_dir.join(".agpm").join("snippets");
4063 fs::create_dir_all(&snippets_dir).unwrap();
4064 fs::write(snippets_dir.join("util.md"), "# Utilities").unwrap();
4065
4066 let agents_dir = project_dir.join(".claude").join("agents");
4068 fs::create_dir_all(&agents_dir).unwrap();
4069 fs::write(agents_dir.join("agent1.md"), "# Agent 1\n\nSee [util](.agpm/snippets/util.md).")
4070 .unwrap();
4071
4072 let commands_dir = project_dir.join(".claude").join("commands");
4074 fs::create_dir_all(&commands_dir).unwrap();
4075 fs::write(commands_dir.join("cmd1.md"), "# Command\n\nCheck `.agpm/snippets/missing.md`.")
4076 .unwrap();
4077
4078 let lockfile_path = project_dir.join("agpm.lock");
4080 let mut lockfile = crate::lockfile::LockFile::default();
4081 lockfile.agents.push(LockedResource {
4082 name: "agent1".to_string(),
4083 source: None,
4084 path: "agents/agent1.md".to_string(),
4085 version: Some("v1.0.0".to_string()),
4086 resolved_commit: None,
4087 url: None,
4088 checksum: "abc123".to_string(),
4089 installed_at: normalize_path_for_storage(agents_dir.join("agent1.md")),
4090 dependencies: vec![],
4091 resource_type: crate::core::ResourceType::Agent,
4092 tool: None,
4093 manifest_alias: None,
4094 context_checksum: None,
4095 applied_patches: std::collections::BTreeMap::new(),
4096 install: None,
4097 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4098 });
4099 lockfile.commands.push(LockedResource {
4100 name: "cmd1".to_string(),
4101 source: None,
4102 path: "commands/cmd1.md".to_string(),
4103 version: Some("v1.0.0".to_string()),
4104 resolved_commit: None,
4105 url: None,
4106 checksum: "def456".to_string(),
4107 installed_at: normalize_path_for_storage(commands_dir.join("cmd1.md")),
4108 dependencies: vec![],
4109 resource_type: crate::core::ResourceType::Command,
4110 tool: None,
4111 manifest_alias: None,
4112 context_checksum: None,
4113 applied_patches: std::collections::BTreeMap::new(),
4114 install: None,
4115 variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4116 });
4117 lockfile.save(&lockfile_path).unwrap();
4118
4119 let cmd = ValidateCommand {
4120 file: None,
4121 resolve: false,
4122 check_lock: false,
4123 sources: false,
4124 paths: false,
4125 format: OutputFormat::Text,
4126 verbose: true,
4127 quiet: false,
4128 strict: false,
4129 render: true,
4130 };
4131
4132 let result = cmd.execute_from_path(manifest_path).await;
4133 assert!(result.is_err());
4134 let err_msg = format!("{:?}", result.unwrap_err());
4135 assert!(err_msg.contains("File reference validation failed"));
4136 }
4137}