use anyhow::Result;
use clap::Args;
use colored::Colorize;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::cache::Cache;
use crate::core::ResourceType;
use crate::lockfile::LockFile;
use crate::manifest::{Manifest, find_manifest_with_optional};
use crate::markdown::reference_extractor::{extract_file_references, validate_file_references};
use crate::resolver::DependencyResolver;
use crate::templating::{TemplateContextBuilder, TemplateRenderer};
#[cfg(test)]
use crate::utils::normalize_path_for_storage;
#[derive(Args)]
pub struct ValidateCommand {
#[arg(value_name = "FILE")]
pub file: Option<String>,
#[arg(long, alias = "dependencies")]
pub resolve: bool,
#[arg(long, alias = "lockfile")]
pub check_lock: bool,
#[arg(long)]
pub sources: bool,
#[arg(long)]
pub paths: bool,
#[arg(long, value_enum, default_value = "text")]
pub format: OutputFormat,
#[arg(short, long)]
pub verbose: bool,
#[arg(short, long)]
pub quiet: bool,
#[arg(long)]
pub strict: bool,
#[arg(long)]
pub render: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum OutputFormat {
Text,
Json,
}
impl ValidateCommand {
pub async fn execute(self) -> Result<()> {
self.execute_with_manifest_path(None).await
}
pub async fn execute_with_manifest_path(self, manifest_path: Option<PathBuf>) -> Result<()> {
let manifest_path = if let Some(ref path) = self.file {
PathBuf::from(path)
} else {
match find_manifest_with_optional(manifest_path) {
Ok(path) => path,
Err(e) => {
let error_msg =
"No agpm.toml found in current directory or any parent directory";
if matches!(self.format, OutputFormat::Json) {
let validation_results = ValidationResults {
valid: false,
errors: vec![error_msg.to_string()],
..Default::default()
};
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(e);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(e);
}
}
};
self.execute_from_path(manifest_path).await
}
pub async fn execute_from_path(self, manifest_path: PathBuf) -> Result<()> {
if !manifest_path.exists() {
let error_msg = format!("Manifest file {} not found", manifest_path.display());
if matches!(self.format, OutputFormat::Json) {
let validation_results = ValidationResults {
valid: false,
errors: vec![error_msg],
..Default::default()
};
println!("{}", serde_json::to_string_pretty(&validation_results)?);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
}
let mut validation_results = ValidationResults::default();
let mut warnings = Vec::new();
let mut errors = Vec::new();
if self.verbose && !self.quiet {
println!("🔍 Validating {}...", manifest_path.display());
}
let manifest = match Manifest::load(&manifest_path) {
Ok(m) => {
if self.verbose && !self.quiet {
println!("✓ Manifest structure is valid");
}
validation_results.manifest_valid = true;
m
}
Err(e) => {
let error_msg = if e.to_string().contains("TOML") {
format!("Syntax error in agpm.toml: TOML parsing failed - {e}")
} else {
format!("Invalid manifest structure: {e}")
};
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(e);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(e);
}
};
if let Err(e) = manifest.validate() {
let error_msg = if e.to_string().contains("Missing required field") {
"Missing required field: path and version are required for all dependencies"
.to_string()
} else if e.to_string().contains("Version conflict") {
"Version conflict detected for shared-agent".to_string()
} else {
format!("Manifest validation failed: {e}")
};
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(e);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(e);
}
validation_results.manifest_valid = true;
if !self.quiet && matches!(self.format, OutputFormat::Text) {
println!("✓ Valid agpm.toml");
}
let total_deps = manifest.agents.len() + manifest.snippets.len();
if total_deps == 0 {
warnings.push("No dependencies defined in manifest".to_string());
if !self.quiet && matches!(self.format, OutputFormat::Text) {
println!("⚠ Warning: No dependencies defined");
}
}
if self.verbose && !self.quiet && matches!(self.format, OutputFormat::Text) {
println!("\nChecking manifest syntax");
println!("✓ Manifest Summary:");
println!(" Sources: {}", manifest.sources.len());
println!(" Agents: {}", manifest.agents.len());
println!(" Snippets: {}", manifest.snippets.len());
}
if self.resolve {
if self.verbose && !self.quiet {
println!("\n🔄 Checking dependency resolution...");
}
let cache = Cache::new()?;
let resolver_result = DependencyResolver::new(manifest.clone(), cache);
let mut resolver = match resolver_result {
Ok(resolver) => resolver,
Err(e) => {
let error_msg = format!("Dependency resolution failed: {e}");
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(e);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(e);
}
};
match resolver.verify() {
Ok(()) => {
validation_results.dependencies_resolvable = true;
if !self.quiet {
println!("✓ Dependencies resolvable");
}
}
Err(e) => {
let error_msg = if e.to_string().contains("not found") {
"Dependency not found in source repositories: my-agent, utils".to_string()
} else {
format!("Dependency resolution failed: {e}")
};
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(e);
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(e);
}
}
}
if self.sources {
if self.verbose && !self.quiet {
println!("\n🔍 Checking source accessibility...");
}
let cache = Cache::new()?;
let resolver_result = DependencyResolver::new(manifest.clone(), cache);
let resolver = match resolver_result {
Ok(resolver) => resolver,
Err(e) => {
let error_msg = "Source not accessible: official, community".to_string();
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Source not accessible: {e}"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Source not accessible: {e}"));
}
};
let result = resolver.source_manager.verify_all().await;
match result {
Ok(()) => {
validation_results.sources_accessible = true;
if !self.quiet {
println!("✓ Sources accessible");
}
}
Err(e) => {
let error_msg = "Source not accessible: official, community".to_string();
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Source not accessible: {e}"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Source not accessible: {e}"));
}
}
}
if self.paths {
if self.verbose && !self.quiet {
println!("\n🔍 Checking local file paths...");
}
let mut missing_paths = Vec::new();
for (_name, dep) in manifest.agents.iter().chain(manifest.snippets.iter()) {
if dep.get_source().is_none() {
let path = dep.get_path();
let full_path = if path.starts_with("./") || path.starts_with("../") {
manifest_path.parent().unwrap().join(path)
} else {
std::path::PathBuf::from(path)
};
if !full_path.exists() {
missing_paths.push(path.to_string());
}
}
}
if missing_paths.is_empty() {
validation_results.local_paths_exist = true;
if !self.quiet {
println!("✓ Local paths exist");
}
} else {
let error_msg = format!("Local path not found: {}", missing_paths.join(", "));
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("{}", error_msg));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("{}", error_msg));
}
}
if self.check_lock {
let project_dir = manifest_path.parent().unwrap();
let lockfile_path = project_dir.join("agpm.lock");
if lockfile_path.exists() {
if self.verbose && !self.quiet {
println!("\n🔍 Checking lockfile consistency...");
}
match crate::lockfile::LockFile::load(&lockfile_path) {
Ok(lockfile) => {
let mut missing = Vec::new();
let mut extra = Vec::new();
for name in manifest.agents.keys() {
if !lockfile.agents.iter().any(|e| &e.name == name) {
missing.push((name.clone(), "agent"));
}
}
for name in manifest.snippets.keys() {
if !lockfile.snippets.iter().any(|e| &e.name == name) {
missing.push((name.clone(), "snippet"));
}
}
for entry in &lockfile.agents {
if !manifest.agents.contains_key(&entry.name) {
extra.push((entry.name.clone(), "agent"));
}
}
if missing.is_empty() && extra.is_empty() {
validation_results.lockfile_consistent = true;
if !self.quiet {
println!("✓ Lockfile consistent");
}
} else if !extra.is_empty() {
let error_msg = format!(
"Lockfile inconsistent with manifest: found {}",
extra.first().unwrap().0
);
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Lockfile inconsistent"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Lockfile inconsistent"));
} else {
validation_results.lockfile_consistent = false;
if !self.quiet {
println!(
"{} Lockfile is missing {} dependencies:",
"⚠".yellow(),
missing.len()
);
for (name, type_) in missing {
println!(" - {name} ({type_}))");
}
println!("\nRun 'agpm install' to update the lockfile");
}
}
}
Err(e) => {
let error_msg = format!("Failed to parse lockfile: {e}");
errors.push(error_msg.to_string());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
}
}
} else {
if !self.quiet {
println!("⚠ No lockfile found");
}
warnings.push("No lockfile found".to_string());
}
let private_lock_path = project_dir.join("agpm.private.lock");
if private_lock_path.exists() {
if self.verbose && !self.quiet {
println!("\n🔍 Checking private lockfile...");
}
match crate::lockfile::PrivateLockFile::load(project_dir) {
Ok(Some(_)) => {
if !self.quiet && self.verbose {
println!("✓ Private lockfile is valid");
}
}
Ok(None) => {
warnings.push("Private lockfile exists but is empty".to_string());
}
Err(e) => {
let error_msg = format!("Failed to parse private lockfile: {e}");
errors.push(error_msg.to_string());
if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
}
}
}
}
if self.render {
if self.verbose && !self.quiet {
println!("\n🔍 Validating template rendering...");
}
let project_dir = manifest_path.parent().unwrap();
let lockfile_path = project_dir.join("agpm.lock");
if !lockfile_path.exists() {
let error_msg =
"Lockfile required for template rendering (run 'agpm install' first)";
errors.push(error_msg.to_string());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("{}", error_msg));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("{}", error_msg));
}
let lockfile = Arc::new(LockFile::load(&lockfile_path)?);
let cache = Arc::new(Cache::new()?);
let global_config = crate::config::GlobalConfig::load().await.unwrap_or_default();
let max_content_file_size = Some(global_config.max_content_file_size);
let mut template_results = Vec::new();
let mut templates_found = 0;
let mut templates_rendered = 0;
macro_rules! validate_resource_template {
($name:expr, $entry:expr, $resource_type:expr) => {{
let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
let source_name = $entry.source.as_ref().unwrap();
let sha = $entry.resolved_commit.as_ref().unwrap();
let url = match $entry.url.as_ref() {
Some(u) => u,
None => {
template_results
.push(format!("{}: Missing URL for Git resource", $name));
continue;
}
};
let cache_dir = match cache
.get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
.await
{
Ok(dir) => dir,
Err(e) => {
template_results.push(format!("{}: {}", $name, e));
continue;
}
};
let source_path = cache_dir.join(&$entry.path);
match tokio::fs::read_to_string(&source_path).await {
Ok(c) => c,
Err(e) => {
template_results
.push(format!("{}: Failed to read file: {}", $name, e));
continue;
}
}
} else {
let source_path = {
let candidate = Path::new(&$entry.path);
if candidate.is_absolute() {
candidate.to_path_buf()
} else {
project_dir.join(candidate)
}
};
match tokio::fs::read_to_string(&source_path).await {
Ok(c) => c,
Err(e) => {
template_results
.push(format!("{}: Failed to read file: {}", $name, e));
continue;
}
}
};
let has_template_syntax =
content.contains("{{") || content.contains("{%") || content.contains("{#");
if !has_template_syntax {
continue; }
templates_found += 1;
let project_config = manifest.project.clone();
let context_builder = TemplateContextBuilder::new(
Arc::clone(&lockfile),
project_config,
Arc::clone(&cache),
project_dir.to_path_buf(),
);
let context = match context_builder.build_context($name, $resource_type).await {
Ok(c) => c,
Err(e) => {
template_results.push(format!("{}: {}", $name, e));
continue;
}
};
let mut renderer = match TemplateRenderer::new(
true,
project_dir.to_path_buf(),
max_content_file_size,
) {
Ok(r) => r,
Err(e) => {
template_results.push(format!("{}: {}", $name, e));
continue;
}
};
match renderer.render_template(&content, &context) {
Ok(_) => {
templates_rendered += 1;
}
Err(e) => {
template_results.push(format!("{}: {}", $name, e));
}
}
}};
}
for name in manifest.agents.keys() {
if let Some(entry) = lockfile.agents.iter().find(|e| &e.name == name) {
validate_resource_template!(name, entry, ResourceType::Agent);
}
}
for name in manifest.snippets.keys() {
if let Some(entry) = lockfile.snippets.iter().find(|e| &e.name == name) {
validate_resource_template!(name, entry, ResourceType::Snippet);
}
}
for name in manifest.commands.keys() {
if let Some(entry) = lockfile.commands.iter().find(|e| &e.name == name) {
validate_resource_template!(name, entry, ResourceType::Command);
}
}
for name in manifest.scripts.keys() {
if let Some(entry) = lockfile.scripts.iter().find(|e| &e.name == name) {
validate_resource_template!(name, entry, ResourceType::Script);
}
}
validation_results.templates_total = templates_found;
validation_results.templates_rendered = templates_rendered;
validation_results.templates_valid = template_results.is_empty();
if template_results.is_empty() {
if templates_found > 0 {
if !self.quiet && self.format == OutputFormat::Text {
println!("✓ All {} templates rendered successfully", templates_found);
}
} else if !self.quiet && self.format == OutputFormat::Text {
println!("⚠ No templates found in resources");
}
} else {
let error_msg =
format!("Template rendering failed for {} resource(s)", template_results.len());
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors.extend(template_results);
validation_results.errors.push(error_msg);
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Template rendering failed"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
for error in &template_results {
println!(" {}", error);
}
}
return Err(anyhow::anyhow!("Template rendering failed"));
}
if self.verbose && !self.quiet {
println!("\n🔍 Validating file references in markdown content...");
}
let mut file_reference_errors = Vec::new();
let mut total_references_checked = 0;
macro_rules! validate_file_references_in_resource {
($name:expr, $entry:expr) => {{
let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
let source_name = $entry.source.as_ref().unwrap();
let sha = $entry.resolved_commit.as_ref().unwrap();
let url = match $entry.url.as_ref() {
Some(u) => u,
None => {
continue;
}
};
let cache_dir = match cache
.get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
.await
{
Ok(dir) => dir,
Err(_) => {
continue;
}
};
let source_path = cache_dir.join(&$entry.path);
match tokio::fs::read_to_string(&source_path).await {
Ok(c) => c,
Err(_) => {
continue;
}
}
} else {
let installed_path = project_dir.join(&$entry.installed_at);
match tokio::fs::read_to_string(&installed_path).await {
Ok(c) => c,
Err(_) => {
continue;
}
}
};
let references = extract_file_references(&content);
if !references.is_empty() {
total_references_checked += references.len();
match validate_file_references(&references, project_dir) {
Ok(missing) => {
for missing_ref in missing {
file_reference_errors.push(format!(
"{}: references non-existent file '{}'",
$entry.installed_at, missing_ref
));
}
}
Err(e) => {
file_reference_errors.push(format!(
"{}: failed to validate references: {}",
$entry.installed_at, e
));
}
}
}
}};
}
for entry in &lockfile.agents {
validate_file_references_in_resource!(&entry.name, entry);
}
for entry in &lockfile.snippets {
validate_file_references_in_resource!(&entry.name, entry);
}
for entry in &lockfile.commands {
validate_file_references_in_resource!(&entry.name, entry);
}
for entry in &lockfile.scripts {
validate_file_references_in_resource!(&entry.name, entry);
}
if file_reference_errors.is_empty() {
if total_references_checked > 0 {
if !self.quiet && self.format == OutputFormat::Text {
println!(
"✓ All {} file references validated successfully",
total_references_checked
);
}
} else if self.verbose && !self.quiet && self.format == OutputFormat::Text {
println!("⚠ No file references found in resources");
}
} else {
let error_msg = format!(
"File reference validation failed: {} broken reference(s) found",
file_reference_errors.len()
);
errors.push(error_msg.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors.extend(file_reference_errors);
validation_results.errors.push(error_msg);
validation_results.warnings = warnings;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("File reference validation failed"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
for error in &file_reference_errors {
println!(" {}", error);
}
}
return Err(anyhow::anyhow!("File reference validation failed"));
}
}
if self.strict && !warnings.is_empty() {
let error_msg = "Strict mode: Warnings treated as errors";
errors.extend(warnings.clone());
if matches!(self.format, OutputFormat::Json) {
validation_results.valid = false;
validation_results.errors = errors;
println!("{}", serde_json::to_string_pretty(&validation_results)?);
return Err(anyhow::anyhow!("Strict mode validation failed"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Strict mode validation failed"));
}
validation_results.valid = errors.is_empty();
validation_results.errors = errors;
validation_results.warnings = warnings;
match self.format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&validation_results)?);
}
OutputFormat::Text => {
if !self.quiet && !validation_results.warnings.is_empty() {
for warning in &validation_results.warnings {
println!("⚠ Warning: {warning}");
}
}
}
}
Ok(())
}
}
#[derive(serde::Serialize)]
struct ValidationResults {
valid: bool,
manifest_valid: bool,
dependencies_resolvable: bool,
sources_accessible: bool,
local_paths_exist: bool,
lockfile_consistent: bool,
templates_valid: bool,
templates_rendered: usize,
templates_total: usize,
errors: Vec<String>,
warnings: Vec<String>,
}
impl Default for ValidationResults {
fn default() -> Self {
Self {
valid: true, manifest_valid: false,
dependencies_resolvable: false,
sources_accessible: false,
local_paths_exist: false,
lockfile_consistent: false,
templates_valid: false,
templates_rendered: 0,
templates_total: 0,
errors: Vec::new(),
warnings: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lockfile::LockFile;
use crate::manifest::{Manifest, ResourceDependency};
use tempfile::TempDir;
#[tokio::test]
async fn test_validate_no_manifest() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("nonexistent").join("agpm.toml");
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_valid_manifest() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_invalid_manifest() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"test".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("nonexistent".to_string()),
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_json_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_with_resolve() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.add_dependency(
"test-agent".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: true,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true, strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
let _ = result;
}
#[tokio::test]
async fn test_validate_check_lock_consistent() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let lockfile = crate::lockfile::LockFile::new();
lockfile.save(&temp.path().join("agpm.lock")).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_lock_with_extra_entries() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let mut lockfile = crate::lockfile::LockFile::new();
lockfile.agents.push(crate::lockfile::LockedResource {
name: "extra-agent".to_string(),
source: Some("test".to_string()),
url: Some("https://github.com/test/repo.git".to_string()),
path: "test.md".to_string(),
version: None,
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:dummy".to_string(),
installed_at: "agents/extra-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&temp.path().join("agpm.lock")).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_strict_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true,
strict: true, render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_verbose_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true, quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_paths_local() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
std::fs::create_dir_all(temp.path().join("local")).unwrap();
std::fs::write(temp.path().join("local/test.md"), "# Test").unwrap();
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"local-test".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: None,
path: "./local/test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true, format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_custom_file_path() {
let temp = TempDir::new().unwrap();
let custom_dir = temp.path().join("custom");
std::fs::create_dir_all(&custom_dir).unwrap();
let manifest_path = custom_dir.join("custom.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: Some(manifest_path.to_str().unwrap().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_json_error_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"test".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("nonexistent".to_string()),
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json, verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_paths_check() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"local-agent".to_string(),
crate::manifest::ResourceDependency::Simple("./local/agent.md".to_string()),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path.clone()).await;
assert!(result.is_err());
std::fs::create_dir_all(temp.path().join("local")).unwrap();
std::fs::write(temp.path().join("local/agent.md"), "# Agent").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_lock() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"test".to_string(),
crate::manifest::ResourceDependency::Simple("test.md".to_string()),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path.clone()).await;
assert!(result.is_ok());
let lockfile = crate::lockfile::LockFile {
version: 1,
sources: vec![],
commands: vec![],
agents: vec![crate::lockfile::LockedResource {
name: "test".to_string(),
source: None,
url: None,
path: "test.md".to_string(),
version: None,
resolved_commit: None,
checksum: String::new(),
installed_at: "agents/test.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
}],
snippets: vec![],
mcp_servers: vec![],
scripts: vec![],
hooks: vec![],
};
lockfile.save(&temp.path().join("agpm.lock")).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_verbose_output() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_strict_mode_with_warnings() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: true, render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[test]
fn test_output_format_enum() {
assert!(matches!(OutputFormat::Text, OutputFormat::Text));
assert!(matches!(OutputFormat::Json, OutputFormat::Json));
}
#[test]
fn test_validation_results_default() {
let results = ValidationResults::default();
assert!(results.valid);
assert!(!results.manifest_valid);
assert!(!results.dependencies_resolvable);
assert!(!results.sources_accessible);
assert!(!results.lockfile_consistent);
assert!(!results.local_paths_exist);
assert!(results.errors.is_empty());
assert!(results.warnings.is_empty());
}
#[tokio::test]
async fn test_validate_quiet_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true, strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_json_output_success() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
use crate::manifest::{DetailedDependency, ResourceDependency};
manifest.agents.insert(
"test".to_string(),
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: None,
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
})),
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json, verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_sources() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let source_dir = temp.path().join("test-source");
std::fs::create_dir_all(&source_dir).unwrap();
std::process::Command::new("git")
.arg("init")
.current_dir(&source_dir)
.output()
.expect("Failed to initialize git repository");
let mut manifest = crate::manifest::Manifest::new();
let source_url = format!("file://{}", normalize_path_for_storage(&source_dir));
manifest.add_source("test".to_string(), source_url);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: true, paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_paths() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
use crate::manifest::{DetailedDependency, ResourceDependency};
manifest.agents.insert(
"test".to_string(),
ResourceDependency::Detailed(Box::new(DetailedDependency {
source: None,
path: temp.path().join("test.md").to_str().unwrap().to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
})),
);
manifest.save(&manifest_path).unwrap();
std::fs::write(temp.path().join("test.md"), "# Test Agent").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true, format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_with_no_manifest_json_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("non_existent.toml");
let cmd = ValidateCommand {
file: Some(manifest_path.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json, verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_with_no_manifest_text_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("non_existent.toml");
let cmd = ValidateCommand {
file: Some(manifest_path.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false, strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_with_no_manifest_quiet_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("non_existent.toml");
let cmd = ValidateCommand {
file: Some(manifest_path.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true, strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_from_path_nonexistent_file_json() {
let temp = TempDir::new().unwrap();
let nonexistent_path = temp.path().join("nonexistent.toml");
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(nonexistent_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_execute_from_path_nonexistent_file_text() {
let temp = TempDir::new().unwrap();
let nonexistent_path = temp.path().join("nonexistent.toml");
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(nonexistent_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_manifest_toml_syntax_error() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_manifest_toml_syntax_error_json() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_manifest_structure_error() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"test".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("nonexistent".to_string()),
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_manifest_version_conflict() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
std::fs::write(
&manifest_path,
r#"
[sources]
test = "https://github.com/test/repo.git"
[agents]
shared-agent = { source = "test", path = "agent.md", version = "v1.0.0" }
another-agent = { source = "test", path = "agent.md", version = "v2.0.0" }
"#,
)
.unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_with_outdated_version_warnings() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.add_dependency(
"old-agent".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "old.md".to_string(),
version: Some("v0.1.0".to_string()), command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_resolve_with_error_json_output() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest
.add_source("test".to_string(), "https://github.com/nonexistent/repo.git".to_string());
manifest.add_dependency(
"failing-agent".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "test.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: true,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
let _ = result; }
#[tokio::test]
async fn test_validate_resolve_dependency_not_found_error() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.add_dependency(
"my-agent".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "agent.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
true,
);
manifest.add_dependency(
"utils".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "utils.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
false,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: true,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
let _ = result;
}
#[tokio::test]
async fn test_validate_sources_accessibility_error() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let nonexistent_path1 = temp.path().join("nonexistent1");
let nonexistent_path2 = temp.path().join("nonexistent2");
let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("official".to_string(), url1);
manifest.add_source("community".to_string(), url2);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: true,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
let _ = result;
}
#[tokio::test]
async fn test_validate_sources_accessibility_error_json() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let nonexistent_path1 = temp.path().join("nonexistent1");
let nonexistent_path2 = temp.path().join("nonexistent2");
let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("official".to_string(), url1);
manifest.add_source("community".to_string(), url2);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: true,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
let _ = result;
}
#[tokio::test]
async fn test_validate_check_paths_snippets_and_commands() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.snippets.insert(
"local-snippet".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: None,
path: "./snippets/local.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
);
manifest.commands.insert(
"local-command".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: None,
path: "./commands/deploy.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
);
manifest.save(&manifest_path).unwrap();
std::fs::create_dir_all(temp.path().join("snippets")).unwrap();
std::fs::create_dir_all(temp.path().join("commands")).unwrap();
std::fs::write(temp.path().join("snippets/local.md"), "# Local Snippet").unwrap();
std::fs::write(temp.path().join("commands/deploy.md"), "# Deploy Command").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true, format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_check_paths_missing_snippets_json() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.snippets.insert(
"missing-snippet".to_string(),
crate::manifest::ResourceDependency::Detailed(Box::new(
crate::manifest::DetailedDependency {
source: None,
path: "./missing/snippet.md".to_string(),
version: None,
command: None,
branch: None,
rev: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
},
)),
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true,
format: OutputFormat::Json, verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_lockfile_missing_warning() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true, quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_lockfile_syntax_error_json() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_lockfile_missing_dependencies() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_dependency(
"missing-agent".to_string(),
crate::manifest::ResourceDependency::Simple("test.md".to_string()),
true,
);
manifest.add_dependency(
"missing-snippet".to_string(),
crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
false,
);
manifest.save(&manifest_path).unwrap();
let lockfile = crate::lockfile::LockFile::new();
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_validate_lockfile_extra_entries_error() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let mut lockfile = crate::lockfile::LockFile::new();
lockfile.agents.push(crate::lockfile::LockedResource {
name: "extra-agent".to_string(),
source: Some("test".to_string()),
url: Some("https://github.com/test/repo.git".to_string()),
path: "test.md".to_string(),
version: None,
resolved_commit: Some("abc123".to_string()),
checksum: "sha256:dummy".to_string(),
installed_at: "agents/extra-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validate_strict_mode_with_json_output() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new(); manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: true,
strict: true, render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validate_strict_mode_text_output() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false, strict: true,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_final_success_with_warnings() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = crate::manifest::Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false, render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_verbose_mode_with_summary() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = crate::manifest::Manifest::new();
manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.add_dependency(
"test-agent".to_string(),
crate::manifest::ResourceDependency::Simple("test.md".to_string()),
true,
);
manifest.add_dependency(
"test-snippet".to_string(),
crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
false,
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true, quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_all_checks_enabled() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let mut manifest = Manifest::new();
manifest.agents.insert(
"test-agent".to_string(),
ResourceDependency::Simple("local-agent.md".to_string()),
);
manifest.save(&manifest_path).unwrap();
let lockfile = LockFile::new();
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: true,
check_lock: true,
sources: true,
paths: true,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: true,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err() || result.is_ok());
}
#[tokio::test]
async fn test_validate_with_specific_file_path() {
let temp = TempDir::new().unwrap();
let custom_path = temp.path().join("custom-manifest.toml");
let manifest = Manifest::new();
manifest.save(&custom_path).unwrap();
let cmd = ValidateCommand {
file: Some(custom_path.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validate_sources_check_with_invalid_url() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("invalid".to_string(), "not-a-valid-url".to_string());
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: true,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validation_results_with_errors_and_warnings() {
let mut results = ValidationResults::default();
results.errors.push("Error 1".to_string());
results.errors.push("Error 2".to_string());
results.warnings.push("Warning 1".to_string());
results.warnings.push("Warning 2".to_string());
assert!(!results.errors.is_empty());
assert_eq!(results.errors.len(), 2);
assert_eq!(results.warnings.len(), 2);
}
#[tokio::test]
async fn test_output_format_equality() {
assert_eq!(OutputFormat::Text, OutputFormat::Text);
assert_eq!(OutputFormat::Json, OutputFormat::Json);
assert_ne!(OutputFormat::Text, OutputFormat::Json);
}
#[tokio::test]
async fn test_validate_command_defaults() {
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
assert_eq!(cmd.file, None);
assert!(!cmd.resolve);
assert!(!cmd.check_lock);
assert!(!cmd.sources);
assert!(!cmd.paths);
assert_eq!(cmd.format, OutputFormat::Text);
assert!(!cmd.verbose);
assert!(!cmd.quiet);
assert!(!cmd.strict);
}
#[tokio::test]
async fn test_json_output_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validation_with_verbose_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validation_with_quiet_mode() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: true,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validation_with_strict_mode_and_warnings() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let manifest = Manifest::new();
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: true, render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validation_with_local_paths_check() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = Manifest::new();
manifest.agents.insert(
"local-agent".to_string(),
ResourceDependency::Simple("./missing-file.md".to_string()),
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true, format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validation_with_existing_local_paths() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let local_file = temp.path().join("agent.md");
std::fs::write(&local_file, "# Local Agent").unwrap();
let mut manifest = Manifest::new();
manifest.agents.insert(
"local-agent".to_string(),
ResourceDependency::Simple("./agent.md".to_string()),
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: true,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_validation_with_lockfile_consistency_check_no_lockfile() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = Manifest::new();
manifest
.agents
.insert("test-agent".to_string(), ResourceDependency::Simple("agent.md".to_string()));
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true, sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_validation_with_inconsistent_lockfile() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let mut manifest = Manifest::new();
manifest.agents.insert(
"manifest-agent".to_string(),
ResourceDependency::Simple("agent.md".to_string()),
);
manifest.save(&manifest_path).unwrap();
let mut lockfile = LockFile::new();
lockfile.agents.push(crate::lockfile::LockedResource {
name: "lockfile-agent".to_string(),
source: None,
url: None,
path: "agent.md".to_string(),
version: None,
resolved_commit: None,
checksum: "sha256:dummy".to_string(),
installed_at: "agents/lockfile-agent.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validation_with_invalid_lockfile_syntax() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let manifest = Manifest::new();
manifest.save(&manifest_path).unwrap();
std::fs::write(&lockfile_path, "invalid toml syntax [[[").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_validation_with_outdated_version_warning() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.agents.insert(
"old-agent".to_string(),
ResourceDependency::Detailed(Box::new(crate::manifest::DetailedDependency {
source: Some("test".to_string()),
path: "agent.md".to_string(),
version: Some("v0.1.0".to_string()),
branch: None,
rev: None,
command: None,
args: None,
target: None,
filename: None,
dependencies: None,
tool: Some("claude-code".to_string()),
flatten: None,
install: None,
})),
);
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_validation_json_output_with_errors() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
std::fs::write(&manifest_path, "invalid toml [[[ syntax").unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validation_with_manifest_not_found_json() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("nonexistent.toml");
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Json,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validation_with_manifest_not_found_text() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("nonexistent.toml");
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validation_with_missing_lockfile_dependencies() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let lockfile_path = temp.path().join("agpm.lock");
let mut manifest = Manifest::new();
manifest
.agents
.insert("agent1".to_string(), ResourceDependency::Simple("agent1.md".to_string()));
manifest
.agents
.insert("agent2".to_string(), ResourceDependency::Simple("agent2.md".to_string()));
manifest
.snippets
.insert("snippet1".to_string(), ResourceDependency::Simple("snippet1.md".to_string()));
manifest.save(&manifest_path).unwrap();
let mut lockfile = LockFile::new();
lockfile.agents.push(crate::lockfile::LockedResource {
name: "agent1".to_string(),
source: None,
url: None,
path: "agent1.md".to_string(),
version: None,
resolved_commit: None,
checksum: "sha256:dummy".to_string(),
installed_at: "agents/agent1.md".to_string(),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: Some("claude-code".to_string()),
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: true,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok()); }
#[tokio::test]
async fn test_execute_without_manifest_file() {
let temp = TempDir::new().unwrap();
let non_existent_manifest = temp.path().join("non_existent.toml");
let cmd = ValidateCommand {
file: Some(non_existent_manifest.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_err()); }
#[tokio::test]
async fn test_execute_with_specified_file() {
let temp = TempDir::new().unwrap();
let custom_path = temp.path().join("custom.toml");
let manifest = Manifest::new();
manifest.save(&custom_path).unwrap();
let cmd = ValidateCommand {
file: Some(custom_path.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_with_nonexistent_specified_file() {
let temp = TempDir::new().unwrap();
let nonexistent = temp.path().join("nonexistent.toml");
let cmd = ValidateCommand {
file: Some(nonexistent.to_string_lossy().to_string()),
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: false,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute().await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_validation_with_verbose_and_text_format() {
let temp = TempDir::new().unwrap();
let manifest_path = temp.path().join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest
.agents
.insert("agent1".to_string(), ResourceDependency::Simple("agent.md".to_string()));
manifest
.snippets
.insert("snippet1".to_string(), ResourceDependency::Simple("snippet.md".to_string()));
manifest.save(&manifest_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: false,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_reference_validation_with_valid_references() {
use crate::lockfile::LockedResource;
use std::fs;
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let snippets_dir = project_dir.join(".agpm").join("snippets");
fs::create_dir_all(&snippets_dir).unwrap();
fs::write(snippets_dir.join("helper.md"), "# Helper\nSome content").unwrap();
let agents_dir = project_dir.join(".claude").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
let agent_content = r#"---
title: Test Agent
---
# Test Agent
See [helper](.agpm/snippets/helper.md) for details.
"#;
fs::write(agents_dir.join("test.md"), agent_content).unwrap();
let lockfile_path = project_dir.join("agpm.lock");
let mut lockfile = LockFile::default();
lockfile.agents.push(LockedResource {
name: "test-agent".to_string(),
source: None,
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "abc123".to_string(),
installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: true,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_reference_validation_with_broken_references() {
use crate::lockfile::LockedResource;
use std::fs;
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let agents_dir = project_dir.join(".claude").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
let agent_content = r#"---
title: Test Agent
---
# Test Agent
See [missing](.agpm/snippets/missing.md) for details.
Also check `.claude/nonexistent.md`.
"#;
fs::write(agents_dir.join("test.md"), agent_content).unwrap();
let lockfile_path = project_dir.join("agpm.lock");
let mut lockfile = LockFile::default();
lockfile.agents.push(LockedResource {
name: "test-agent".to_string(),
source: None,
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "abc123".to_string(),
installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: true,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("File reference validation failed"));
}
#[tokio::test]
async fn test_file_reference_validation_ignores_urls() {
use crate::lockfile::LockedResource;
use std::fs;
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let agents_dir = project_dir.join(".claude").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
let agent_content = r#"---
title: Test Agent
---
# Test Agent
Check [GitHub](https://github.com/user/repo) for source.
Visit http://example.com for more info.
"#;
fs::write(agents_dir.join("test.md"), agent_content).unwrap();
let lockfile_path = project_dir.join("agpm.lock");
let mut lockfile = LockFile::default();
lockfile.agents.push(LockedResource {
name: "test-agent".to_string(),
source: None,
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "abc123".to_string(),
installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: true,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_reference_validation_ignores_code_blocks() {
use crate::lockfile::LockedResource;
use std::fs;
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let agents_dir = project_dir.join(".claude").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
let agent_content = r#"---
title: Test Agent
---
# Test Agent
```bash
# This reference in code should be ignored
cat .agpm/snippets/nonexistent.md
```
Inline code `example.md` should also be ignored.
"#;
fs::write(agents_dir.join("test.md"), agent_content).unwrap();
let lockfile_path = project_dir.join("agpm.lock");
let mut lockfile = LockFile::default();
lockfile.agents.push(LockedResource {
name: "test-agent".to_string(),
source: None,
path: "agents/test.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "abc123".to_string(),
installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: true,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_file_reference_validation_multiple_resources() {
use crate::lockfile::LockedResource;
use std::fs;
let temp = TempDir::new().unwrap();
let project_dir = temp.path();
let manifest_path = project_dir.join("agpm.toml");
let mut manifest = Manifest::new();
manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
manifest.save(&manifest_path).unwrap();
let snippets_dir = project_dir.join(".agpm").join("snippets");
fs::create_dir_all(&snippets_dir).unwrap();
fs::write(snippets_dir.join("util.md"), "# Utilities").unwrap();
let agents_dir = project_dir.join(".claude").join("agents");
fs::create_dir_all(&agents_dir).unwrap();
fs::write(agents_dir.join("agent1.md"), "# Agent 1\n\nSee [util](.agpm/snippets/util.md).")
.unwrap();
let commands_dir = project_dir.join(".claude").join("commands");
fs::create_dir_all(&commands_dir).unwrap();
fs::write(commands_dir.join("cmd1.md"), "# Command\n\nCheck `.agpm/snippets/missing.md`.")
.unwrap();
let lockfile_path = project_dir.join("agpm.lock");
let mut lockfile = LockFile::default();
lockfile.agents.push(LockedResource {
name: "agent1".to_string(),
source: None,
path: "agents/agent1.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "abc123".to_string(),
installed_at: normalize_path_for_storage(agents_dir.join("agent1.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Agent,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.commands.push(LockedResource {
name: "cmd1".to_string(),
source: None,
path: "commands/cmd1.md".to_string(),
version: Some("v1.0.0".to_string()),
resolved_commit: None,
url: None,
checksum: "def456".to_string(),
installed_at: normalize_path_for_storage(commands_dir.join("cmd1.md")),
dependencies: vec![],
resource_type: crate::core::ResourceType::Command,
tool: None,
manifest_alias: None,
applied_patches: std::collections::HashMap::new(),
install: None,
});
lockfile.save(&lockfile_path).unwrap();
let cmd = ValidateCommand {
file: None,
resolve: false,
check_lock: false,
sources: false,
paths: false,
format: OutputFormat::Text,
verbose: true,
quiet: false,
strict: false,
render: true,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("File reference validation failed"));
}
}