use anyhow::Result;
use clap::Args;
use colored::Colorize;
use std::path::PathBuf;
use crate::cache::Cache;
use crate::manifest::{Manifest, find_manifest_with_optional};
use crate::resolver::DependencyResolver;
#[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,
}
#[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!("Local paths not found"));
} else if !self.quiet {
println!("{} {}", "✗".red(), error_msg);
}
return Err(anyhow::anyhow!("Local paths not found"));
}
}
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());
}
}
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 {
if !validation_results.warnings.is_empty() {
for warning in &validation_results.warnings {
println!("⚠ Warning: {warning}");
}
}
if validation_results.valid {
println!("✓ Valid manifest");
}
}
}
}
Ok(())
}
}
#[derive(serde::Serialize)]
struct ValidationResults {
valid: bool,
manifest_valid: bool,
dependencies_resolvable: bool,
sources_accessible: bool,
local_paths_exist: bool,
lockfile_consistent: bool,
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,
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,
};
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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,
};
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: "claude-code".to_string(),
});
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,
};
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, };
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,
};
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: "claude-code".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,
};
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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,
};
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,
};
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,
};
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: "claude-code".to_string(),
}],
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,
};
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,
};
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, };
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,
};
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: "claude-code".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: false,
strict: 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://{}", source_dir.display().to_string().replace('\\', "/"));
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,
};
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: "claude-code".to_string(),
})),
);
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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: "claude-code".to_string(),
},
)),
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,
};
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: "claude-code".to_string(),
},
)),
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: "claude-code".to_string(),
},
)),
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,
};
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://{}", nonexistent_path1.display().to_string().replace('\\', "/"));
let url2 = format!("file://{}", nonexistent_path2.display().to_string().replace('\\', "/"));
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,
};
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://{}", nonexistent_path1.display().to_string().replace('\\', "/"));
let url2 = format!("file://{}", nonexistent_path2.display().to_string().replace('\\', "/"));
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,
};
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: "claude-code".to_string(),
},
)),
);
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: "claude-code".to_string(),
},
)),
);
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,
};
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: "claude-code".to_string(),
},
)),
);
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,
};
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,
};
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,
};
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,
};
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: "claude-code".to_string(),
});
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,
};
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, };
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,
};
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, };
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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, };
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,
};
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,
};
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,
};
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: "claude-code".to_string(),
});
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,
};
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,
};
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: "claude-code".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,
};
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,
};
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,
};
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,
};
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: "claude-code".to_string(),
});
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,
};
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,
};
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,
};
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,
};
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,
};
let result = cmd.execute_from_path(manifest_path).await;
assert!(result.is_ok());
}
}