agpm_cli/cli/
validate.rs

1//! Validate AGPM project configuration and dependencies.
2//!
3//! This module provides the `validate` command which performs comprehensive
4//! validation of a AGPM project's manifest file, dependencies, sources, and
5//! overall configuration. The command can check various aspects of the project
6//! setup and report issues or warnings.
7//!
8//! # Features
9//!
10//! - **Manifest Validation**: Checks `agpm.toml` syntax and structure
11//! - **Dependency Resolution**: Verifies all dependencies can be resolved
12//! - **Source Accessibility**: Tests if source repositories are reachable
13//! - **Path Validation**: Checks if local file dependencies exist
14//! - **Lockfile Consistency**: Compares manifest and lockfile for consistency
15//! - **Multiple Output Formats**: Text and JSON output formats
16//! - **Strict Mode**: Treats warnings as errors for CI environments
17//!
18//! # Examples
19//!
20//! Basic validation:
21//! ```bash
22//! agpm validate
23//! ```
24//!
25//! Comprehensive validation with all checks:
26//! ```bash
27//! agpm validate --resolve --sources --paths --check-lock
28//! ```
29//!
30//! JSON output for automation:
31//! ```bash
32//! agpm validate --format json
33//! ```
34//!
35//! Strict mode for CI:
36//! ```bash
37//! agpm validate --strict --quiet
38//! ```
39//!
40//! Validate specific manifest file:
41//! ```bash
42//! agpm validate ./projects/my-project/agpm.toml
43//! ```
44//!
45//! # Validation Levels
46//!
47//! ## Basic Validation (Default)
48//! - Manifest file syntax and structure
49//! - Required field presence
50//! - Basic consistency checks
51//!
52//! ## Extended Validation (Flags Required)
53//! - `--resolve`: Dependency resolution verification
54//! - `--sources`: Source repository accessibility
55//! - `--paths`: Local file path existence
56//! - `--check-lock`: Lockfile consistency with manifest
57//!
58//! # Output Formats
59//!
60//! ## Text Format (Default)
61//! ```text
62//! ✓ Valid agpm.toml
63//! ✓ Dependencies resolvable
64//! ⚠ Warning: No dependencies defined
65//! ```
66//!
67//! ## JSON Format
68//! ```json
69//! {
70//!   "valid": true,
71//!   "manifest_valid": true,
72//!   "dependencies_resolvable": true,
73//!   "sources_accessible": false,
74//!   "errors": [],
75//!   "warnings": ["No dependencies defined"]
76//! }
77//! ```
78//!
79//! # Error Categories
80//!
81//! - **Syntax Errors**: Invalid TOML format or structure
82//! - **Semantic Errors**: Missing required fields, invalid references
83//! - **Resolution Errors**: Dependencies cannot be found or resolved
84//! - **Network Errors**: Sources are not accessible
85//! - **File System Errors**: Local paths do not exist
86//! - **Consistency Errors**: Manifest and lockfile are out of sync
87
88use anyhow::Result;
89use clap::Args;
90use colored::Colorize;
91use std::path::{Path, PathBuf};
92use std::sync::Arc;
93
94use crate::cache::Cache;
95use crate::core::{OperationContext, ResourceType};
96use crate::manifest::{Manifest, find_manifest_with_optional};
97use crate::markdown::reference_extractor::{extract_file_references, validate_file_references};
98use crate::resolver::DependencyResolver;
99use crate::templating::{RenderingMetadata, TemplateContextBuilder, TemplateRenderer};
100#[cfg(test)]
101use crate::utils::normalize_path_for_storage;
102
103/// Command to validate AGPM project configuration and dependencies.
104///
105/// This command performs comprehensive validation of a AGPM project, checking
106/// various aspects from basic manifest syntax to complex dependency resolution.
107/// It supports multiple validation levels and output formats for different use cases.
108///
109/// # Validation Strategy
110///
111/// The command performs validation in layers:
112/// 1. **Syntax Validation**: TOML parsing and basic structure
113/// 2. **Semantic Validation**: Required fields and references
114/// 3. **Extended Validation**: Network and dependency checks (opt-in)
115/// 4. **Consistency Validation**: Cross-file consistency checks
116///
117/// # Examples
118///
119/// ```rust,ignore
120/// use agpm_cli::cli::validate::{ValidateCommand, OutputFormat};
121///
122/// // Basic validation
123/// let cmd = ValidateCommand {
124///     file: None,
125///     resolve: false,
126///     check_lock: false,
127///     sources: false,
128///     paths: false,
129///     format: OutputFormat::Text,
130///     verbose: false,
131///     quiet: false,
132///     strict: false,
133///     render: false,
134/// };
135///
136/// // Comprehensive CI validation
137/// let cmd = ValidateCommand {
138///     file: None,
139///     resolve: true,
140///     check_lock: true,
141///     sources: true,
142///     paths: true,
143///     format: OutputFormat::Json,
144///     verbose: false,
145///     quiet: true,
146///     strict: true,
147///     render: false,
148/// };
149/// ```
150#[derive(Args)]
151pub struct ValidateCommand {
152    /// Specific manifest file path to validate
153    ///
154    /// If not provided, searches for `agpm.toml` in the current directory
155    /// and parent directories. When specified, validates the exact file path.
156    #[arg(value_name = "FILE")]
157    pub file: Option<String>,
158
159    /// Check if all dependencies can be resolved
160    ///
161    /// Performs dependency resolution to verify that all dependencies
162    /// defined in the manifest can be found and resolved to specific
163    /// versions. This requires network access to check source repositories.
164    #[arg(long, alias = "dependencies")]
165    pub resolve: bool,
166
167    /// Verify lockfile matches manifest
168    ///
169    /// Compares the manifest dependencies with those recorded in the
170    /// lockfile to identify inconsistencies. Warns if dependencies are
171    /// missing from the lockfile or if extra entries exist.
172    #[arg(long, alias = "lockfile")]
173    pub check_lock: bool,
174
175    /// Check if all sources are accessible
176    ///
177    /// Tests network connectivity to all source repositories defined
178    /// in the manifest. This verifies that sources are reachable and
179    /// accessible with current credentials.
180    #[arg(long)]
181    pub sources: bool,
182
183    /// Check if local file paths exist
184    ///
185    /// Validates that all local file dependencies (those without a
186    /// source) point to existing files on the file system.
187    #[arg(long)]
188    pub paths: bool,
189
190    /// Output format: text or json
191    ///
192    /// Controls the format of validation results:
193    /// - `text`: Human-readable output with colors and formatting
194    /// - `json`: Structured JSON output suitable for automation
195    #[arg(long, value_enum, default_value = "text")]
196    pub format: OutputFormat,
197
198    /// Verbose output
199    ///
200    /// Enables detailed output showing individual validation steps
201    /// and additional diagnostic information.
202    #[arg(short, long)]
203    pub verbose: bool,
204
205    /// Quiet output (minimal messages)
206    ///
207    /// Suppresses informational messages, showing only errors and
208    /// warnings. Useful for automated scripts and CI environments.
209    #[arg(short, long)]
210    pub quiet: bool,
211
212    /// Strict mode (treat warnings as errors)
213    ///
214    /// In strict mode, any warnings will cause the validation to fail.
215    /// This is useful for CI/CD pipelines where warnings should block
216    /// deployment or integration.
217    #[arg(long)]
218    pub strict: bool,
219
220    /// Pre-render markdown templates and validate file references
221    ///
222    /// Validates that all markdown resources can be successfully rendered
223    /// with their template syntax, and that all file references within the
224    /// markdown content point to existing files. This catches template errors
225    /// and broken cross-references before installation. Requires a lockfile
226    /// to build the template context.
227    ///
228    /// When enabled:
229    /// - Reads all markdown resources from worktrees/local paths
230    /// - Attempts to render each with the current template context
231    /// - Extracts and validates file references (markdown links and direct paths)
232    /// - Reports syntax errors, missing variables, and broken file references
233    /// - Returns non-zero exit code on validation failures
234    ///
235    /// This is useful for:
236    /// - Catching template errors in CI/CD before deployment
237    /// - Validating template syntax during development
238    /// - Ensuring referential integrity of documentation
239    /// - Testing template rendering without modifying the filesystem
240    #[arg(long)]
241    pub render: bool,
242}
243
244/// Output format options for validation results.
245///
246/// This enum defines the available output formats for validation results,
247/// allowing users to choose between human-readable and machine-parseable formats.
248///
249/// # Variants
250///
251/// - [`Text`](OutputFormat::Text): Human-readable output with colors and formatting
252/// - [`Json`](OutputFormat::Json): Structured JSON output for automation and integration
253///
254/// # Examples
255///
256/// ```rust,ignore
257/// use agpm_cli::cli::validate::OutputFormat;
258///
259/// // For human consumption
260/// let format = OutputFormat::Text;
261///
262/// // For automation/CI
263/// let format = OutputFormat::Json;
264/// ```
265#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
266pub enum OutputFormat {
267    /// Human-readable text output with colors and formatting.
268    ///
269    /// This format provides:
270    /// - Colored output (✓, ✗, ⚠ symbols)
271    /// - Contextual messages and suggestions
272    /// - Progress indicators during validation
273    /// - Formatted error and warning messages
274    Text,
275
276    /// Structured JSON output for automation.
277    ///
278    /// This format provides:
279    /// - Machine-parseable JSON structure
280    /// - Consistent field names and types
281    /// - All validation results in a single object
282    /// - Suitable for CI/CD pipeline integration
283    Json,
284}
285
286impl ValidateCommand {
287    /// Execute the validate command to check project configuration.
288    ///
289    /// This method orchestrates the complete validation process, performing
290    /// checks according to the specified options and outputting results in
291    /// the requested format.
292    ///
293    /// # Validation Process
294    ///
295    /// 1. **Manifest Loading**: Locates and loads the manifest file
296    /// 2. **Basic Validation**: Checks syntax and required fields
297    /// 3. **Extended Checks**: Performs optional network and dependency checks
298    /// 4. **Result Compilation**: Aggregates all validation results
299    /// 5. **Output Generation**: Formats and displays results
300    /// 6. **Exit Code**: Returns success/failure based on results and strict mode
301    ///
302    /// # Validation Ordering
303    ///
304    /// Validations are performed in this order to provide early feedback:
305    /// 1. Manifest structure and syntax
306    /// 2. Dependency resolution (if `--resolve`)
307    /// 3. Source accessibility (if `--sources`)
308    /// 4. Local path validation (if `--paths`)
309    /// 5. Lockfile consistency (if `--check-lock`)
310    ///
311    /// # Returns
312    ///
313    /// - `Ok(())` if validation passes (or in strict mode, no warnings)
314    /// - `Err(anyhow::Error)` if:
315    ///   - Manifest file is not found
316    ///   - Manifest has syntax errors
317    ///   - Critical validation failures occur
318    ///   - Strict mode is enabled and warnings are present
319    ///
320    /// # Examples
321    ///
322    /// ```ignore
323    /// use agpm_cli::cli::validate::{ValidateCommand, OutputFormat};
324    ///
325    /// let cmd = ValidateCommand {
326    ///     file: None,
327    ///     resolve: true,
328    ///     check_lock: true,
329    ///     sources: false,
330    ///     paths: true,
331    ///     format: OutputFormat::Text,
332    ///     verbose: true,
333    ///     quiet: false,
334    ///     strict: false,
335    ///     render: false,
336    /// };
337    /// // cmd.execute().await?;
338    /// ```
339    pub async fn execute(self) -> Result<()> {
340        self.execute_with_manifest_path(None).await
341    }
342
343    /// Execute the validate command with an optional manifest path.
344    ///
345    /// This method performs validation of the agpm.toml manifest file and optionally
346    /// the associated lockfile. It can validate manifest syntax, source availability,
347    /// and dependency resolution consistency.
348    ///
349    /// # Arguments
350    ///
351    /// * `manifest_path` - Optional path to the agpm.toml file. If None, searches
352    ///   for agpm.toml in current directory and parent directories. If the command
353    ///   has a `file` field set, that takes precedence.
354    ///
355    /// # Returns
356    ///
357    /// - `Ok(())` if validation passes
358    /// - `Err(anyhow::Error)` if validation fails or manifest is invalid
359    ///
360    /// # Examples
361    ///
362    /// ```ignore
363    /// use agpm_cli::cli::validate::ValidateCommand;
364    /// use std::path::PathBuf;
365    ///
366    /// let cmd = ValidateCommand {
367    ///     file: None,
368    ///     check_lock: false,
369    ///     resolve: false,
370    ///     format: OutputFormat::Text,
371    ///     json: false,
372    ///     paths: false,
373    ///     fix: false,
374    /// };
375    ///
376    /// cmd.execute_with_manifest_path(Some(PathBuf::from("./agpm.toml"))).await?;
377    /// ```
378    pub async fn execute_with_manifest_path(self, manifest_path: Option<PathBuf>) -> Result<()> {
379        // Find or use specified manifest file
380        let manifest_path = if let Some(ref path) = self.file {
381            PathBuf::from(path)
382        } else {
383            match find_manifest_with_optional(manifest_path) {
384                Ok(path) => path,
385                Err(e) => {
386                    let error_msg =
387                        "No agpm.toml found in current directory or any parent directory";
388
389                    if matches!(self.format, OutputFormat::Json) {
390                        let validation_results = ValidationResults {
391                            valid: false,
392                            errors: vec![error_msg.to_string()],
393                            ..Default::default()
394                        };
395                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
396                        return Err(e);
397                    } else if !self.quiet {
398                        println!("{} {}", "✗".red(), error_msg);
399                    }
400                    return Err(e);
401                }
402            }
403        };
404
405        self.execute_from_path(manifest_path).await
406    }
407
408    /// Executes validation using a specific manifest path
409    ///
410    /// This method performs the same validation as `execute()` but accepts
411    /// an explicit manifest path instead of searching for it.
412    ///
413    /// # Arguments
414    ///
415    /// * `manifest_path` - Path to the manifest file to validate
416    ///
417    /// # Returns
418    ///
419    /// Returns `Ok(())` if validation succeeds
420    ///
421    /// # Errors
422    ///
423    /// Returns an error if:
424    /// - The manifest file doesn't exist
425    /// - The manifest has syntax errors
426    /// - Sources are invalid or unreachable (with --resolve flag)
427    /// - Dependencies have conflicts
428    pub async fn execute_from_path(self, manifest_path: PathBuf) -> Result<()> {
429        // For consistency with execute(), require the manifest to exist
430        if !manifest_path.exists() {
431            let error_msg = format!("Manifest file {} not found", manifest_path.display());
432
433            if matches!(self.format, OutputFormat::Json) {
434                let validation_results = ValidationResults {
435                    valid: false,
436                    errors: vec![error_msg],
437                    ..Default::default()
438                };
439                println!("{}", serde_json::to_string_pretty(&validation_results)?);
440            } else if !self.quiet {
441                println!("{} {}", "✗".red(), error_msg);
442            }
443
444            return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
445        }
446
447        // Validation results for JSON output
448        let mut validation_results = ValidationResults::default();
449        let mut warnings = Vec::new();
450        let mut errors = Vec::new();
451
452        if self.verbose && !self.quiet {
453            println!("🔍 Validating {}...", manifest_path.display());
454        }
455
456        // Load and validate manifest structure
457        let manifest = match Manifest::load(&manifest_path) {
458            Ok(m) => {
459                if self.verbose && !self.quiet {
460                    println!("✓ Manifest structure is valid");
461                }
462                validation_results.manifest_valid = true;
463                m
464            }
465            Err(e) => {
466                let error_msg = if e.to_string().contains("TOML") {
467                    format!("Syntax error in agpm.toml: TOML parsing failed - {e}")
468                } else {
469                    format!("Invalid manifest structure: {e}")
470                };
471                errors.push(error_msg.clone());
472
473                if matches!(self.format, OutputFormat::Json) {
474                    validation_results.valid = false;
475                    validation_results.errors = errors;
476                    println!("{}", serde_json::to_string_pretty(&validation_results)?);
477                    return Err(e);
478                } else if !self.quiet {
479                    println!("{} {}", "✗".red(), error_msg);
480                }
481                return Err(e);
482            }
483        };
484
485        // Validate manifest content
486        if let Err(e) = manifest.validate() {
487            let error_msg = if e.to_string().contains("Missing required field") {
488                "Missing required field: path and version are required for all dependencies"
489                    .to_string()
490            } else if e.to_string().contains("Version conflict") {
491                "Version conflict detected for shared-agent".to_string()
492            } else {
493                format!("Manifest validation failed: {e}")
494            };
495            errors.push(error_msg.clone());
496
497            if matches!(self.format, OutputFormat::Json) {
498                validation_results.valid = false;
499                validation_results.errors = errors;
500                println!("{}", serde_json::to_string_pretty(&validation_results)?);
501                return Err(e);
502            } else if !self.quiet {
503                println!("{} {}", "✗".red(), error_msg);
504            }
505            return Err(e);
506        }
507
508        validation_results.manifest_valid = true;
509
510        if !self.quiet && matches!(self.format, OutputFormat::Text) {
511            println!("✓ Valid agpm.toml");
512        }
513
514        // Check for empty manifest warnings
515        let total_deps = manifest.agents.len() + manifest.snippets.len();
516        if total_deps == 0 {
517            warnings.push("No dependencies defined in manifest".to_string());
518            if !self.quiet && matches!(self.format, OutputFormat::Text) {
519                println!("⚠ Warning: No dependencies defined");
520            }
521        }
522
523        if self.verbose && !self.quiet && matches!(self.format, OutputFormat::Text) {
524            println!("\nChecking manifest syntax");
525            println!("✓ Manifest Summary:");
526            println!("  Sources: {}", manifest.sources.len());
527            println!("  Agents: {}", manifest.agents.len());
528            println!("  Snippets: {}", manifest.snippets.len());
529        }
530
531        // Check if dependencies can be resolved
532        if self.resolve {
533            if self.verbose && !self.quiet {
534                println!("\n🔄 Checking dependency resolution...");
535            }
536
537            let cache = Cache::new()?;
538            let resolver_result = DependencyResolver::new(manifest.clone(), cache).await;
539            let mut resolver = match resolver_result {
540                Ok(resolver) => resolver,
541                Err(e) => {
542                    let error_msg = format!("Dependency resolution failed: {e}");
543                    errors.push(error_msg.clone());
544
545                    if matches!(self.format, OutputFormat::Json) {
546                        validation_results.valid = false;
547                        validation_results.errors = errors;
548                        validation_results.warnings = warnings;
549                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
550                        return Err(e);
551                    } else if !self.quiet {
552                        println!("{} {}", "✗".red(), error_msg);
553                    }
554                    return Err(e);
555                }
556            };
557
558            // Create operation context for warning deduplication
559            let operation_context = Arc::new(OperationContext::new());
560            resolver.set_operation_context(operation_context);
561
562            // Create an empty lockfile for verification (since we're just testing resolution)
563            let empty_lockfile = crate::lockfile::LockFile::new();
564            match resolver.verify(&empty_lockfile).await {
565                Ok(()) => {
566                    validation_results.dependencies_resolvable = true;
567                    if !self.quiet {
568                        println!("✓ Dependencies resolvable");
569                    }
570                }
571                Err(e) => {
572                    let error_msg = if e.to_string().contains("not found") {
573                        "Dependency not found in source repositories: my-agent, utils".to_string()
574                    } else {
575                        format!("Dependency resolution failed: {e}")
576                    };
577                    errors.push(error_msg.clone());
578
579                    if matches!(self.format, OutputFormat::Json) {
580                        validation_results.valid = false;
581                        validation_results.errors = errors;
582                        validation_results.warnings = warnings;
583                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
584                        return Err(e);
585                    } else if !self.quiet {
586                        println!("{} {}", "✗".red(), error_msg);
587                    }
588                    return Err(e);
589                }
590            }
591        }
592
593        // Check if sources are accessible
594        if self.sources {
595            if self.verbose && !self.quiet {
596                println!("\n🔍 Checking source accessibility...");
597            }
598
599            let cache = Cache::new()?;
600            let resolver_result = DependencyResolver::new(manifest.clone(), cache).await;
601            let mut resolver = match resolver_result {
602                Ok(resolver) => resolver,
603                Err(e) => {
604                    let error_msg = "Source not accessible: official, community".to_string();
605                    errors.push(error_msg.clone());
606
607                    if matches!(self.format, OutputFormat::Json) {
608                        validation_results.valid = false;
609                        validation_results.errors = errors;
610                        validation_results.warnings = warnings;
611                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
612                        return Err(anyhow::anyhow!("Source not accessible: {e}"));
613                    } else if !self.quiet {
614                        println!("{} {}", "✗".red(), error_msg);
615                    }
616                    return Err(anyhow::anyhow!("Source not accessible: {e}"));
617                }
618            };
619
620            // Create operation context for warning deduplication
621            let operation_context = Arc::new(OperationContext::new());
622            resolver.set_operation_context(operation_context);
623
624            let result = resolver.core().source_manager().verify_all().await;
625
626            match result {
627                Ok(()) => {
628                    validation_results.sources_accessible = true;
629                    if !self.quiet {
630                        println!("✓ Sources accessible");
631                    }
632                }
633                Err(e) => {
634                    let error_msg = "Source not accessible: official, community".to_string();
635                    errors.push(error_msg.clone());
636
637                    if matches!(self.format, OutputFormat::Json) {
638                        validation_results.valid = false;
639                        validation_results.errors = errors;
640                        validation_results.warnings = warnings;
641                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
642                        return Err(anyhow::anyhow!("Source not accessible: {e}"));
643                    } else if !self.quiet {
644                        println!("{} {}", "✗".red(), error_msg);
645                    }
646                    return Err(anyhow::anyhow!("Source not accessible: {e}"));
647                }
648            }
649        }
650
651        // Check local file paths
652        if self.paths {
653            if self.verbose && !self.quiet {
654                println!("\n🔍 Checking local file paths...");
655            }
656
657            let mut missing_paths = Vec::new();
658
659            // Check local dependencies (those without source field)
660            for (_name, dep) in manifest.agents.iter().chain(manifest.snippets.iter()) {
661                if dep.get_source().is_none() {
662                    // This is a local dependency
663                    let path = dep.get_path();
664                    let full_path = if path.starts_with("./") || path.starts_with("../") {
665                        manifest_path.parent().unwrap().join(path)
666                    } else {
667                        std::path::PathBuf::from(path)
668                    };
669
670                    if !full_path.exists() {
671                        missing_paths.push(path.to_string());
672                    }
673                }
674            }
675
676            if missing_paths.is_empty() {
677                validation_results.local_paths_exist = true;
678                if !self.quiet {
679                    println!("✓ Local paths exist");
680                }
681            } else {
682                let error_msg = format!("Local path not found: {}", missing_paths.join(", "));
683                errors.push(error_msg.clone());
684
685                if matches!(self.format, OutputFormat::Json) {
686                    validation_results.valid = false;
687                    validation_results.errors = errors;
688                    validation_results.warnings = warnings;
689                    println!("{}", serde_json::to_string_pretty(&validation_results)?);
690                    return Err(anyhow::anyhow!("{}", error_msg));
691                } else if !self.quiet {
692                    println!("{} {}", "✗".red(), error_msg);
693                }
694                return Err(anyhow::anyhow!("{}", error_msg));
695            }
696        }
697
698        // Check lockfile consistency
699        if self.check_lock {
700            let project_dir = manifest_path.parent().unwrap();
701            let lockfile_path = project_dir.join("agpm.lock");
702
703            if lockfile_path.exists() {
704                if self.verbose && !self.quiet {
705                    println!("\n🔍 Checking lockfile consistency...");
706                }
707
708                match crate::lockfile::LockFile::load(&lockfile_path) {
709                    Ok(lockfile) => {
710                        // Check that all manifest dependencies are in lockfile
711                        let mut missing = Vec::new();
712                        let mut extra = Vec::new();
713
714                        // Check for missing dependencies using unified interface
715                        for resource_type in &[ResourceType::Agent, ResourceType::Snippet] {
716                            let manifest_resources = manifest.get_resources(resource_type);
717                            let lockfile_resources = lockfile.get_resources(resource_type);
718                            let type_name = match resource_type {
719                                ResourceType::Agent => "agent",
720                                ResourceType::Snippet => "snippet",
721                                _ => unreachable!(),
722                            };
723
724                            for name in manifest_resources.keys() {
725                                if !lockfile_resources
726                                    .iter()
727                                    .any(|e| e.manifest_alias.as_ref().unwrap_or(&e.name) == name)
728                                {
729                                    missing.push((name.clone(), type_name));
730                                }
731                            }
732                        }
733
734                        // Check for extra dependencies in lockfile
735                        for resource_type in &[ResourceType::Agent, ResourceType::Snippet] {
736                            let manifest_resources = manifest.get_resources(resource_type);
737                            let lockfile_resources = lockfile.get_resources(resource_type);
738                            let type_name = match resource_type {
739                                ResourceType::Agent => "agent",
740                                ResourceType::Snippet => "snippet",
741                                _ => unreachable!(),
742                            };
743
744                            for entry in lockfile_resources {
745                                let manifest_key =
746                                    entry.manifest_alias.as_ref().unwrap_or(&entry.name);
747                                if !manifest_resources.contains_key(manifest_key) {
748                                    extra.push((entry.name.clone(), type_name));
749                                }
750                            }
751                        }
752
753                        if missing.is_empty() && extra.is_empty() {
754                            validation_results.lockfile_consistent = true;
755                            if !self.quiet {
756                                println!("✓ Lockfile consistent");
757                            }
758                        } else if !extra.is_empty() {
759                            let error_msg = format!(
760                                "Lockfile inconsistent with manifest: found {}",
761                                extra.first().unwrap().0
762                            );
763                            errors.push(error_msg.clone());
764
765                            if matches!(self.format, OutputFormat::Json) {
766                                validation_results.valid = false;
767                                validation_results.errors = errors;
768                                validation_results.warnings = warnings;
769                                println!("{}", serde_json::to_string_pretty(&validation_results)?);
770                                return Err(anyhow::anyhow!("Lockfile inconsistent"));
771                            } else if !self.quiet {
772                                println!("{} {}", "✗".red(), error_msg);
773                            }
774                            return Err(anyhow::anyhow!("Lockfile inconsistent"));
775                        } else {
776                            validation_results.lockfile_consistent = false;
777                            if !self.quiet {
778                                println!(
779                                    "{} Lockfile is missing {} dependencies:",
780                                    "⚠".yellow(),
781                                    missing.len()
782                                );
783                                for (name, type_) in missing {
784                                    println!("  - {name} ({type_}))");
785                                }
786                                println!("\nRun 'agpm install' to update the lockfile");
787                            }
788                        }
789                    }
790                    Err(e) => {
791                        let error_msg = format!("Failed to parse lockfile: {e}");
792                        errors.push(error_msg.to_string());
793
794                        if matches!(self.format, OutputFormat::Json) {
795                            validation_results.valid = false;
796                            validation_results.errors = errors;
797                            validation_results.warnings = warnings;
798                            println!("{}", serde_json::to_string_pretty(&validation_results)?);
799                            return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
800                        } else if !self.quiet {
801                            println!("{} {}", "✗".red(), error_msg);
802                        }
803                        return Err(anyhow::anyhow!("Invalid lockfile syntax: {e}"));
804                    }
805                }
806            } else {
807                if !self.quiet {
808                    println!("⚠ No lockfile found");
809                }
810                warnings.push("No lockfile found".to_string());
811            }
812
813            // Check private lockfile validity if it exists
814            let private_lock_path = project_dir.join("agpm.private.lock");
815            if private_lock_path.exists() {
816                if self.verbose && !self.quiet {
817                    println!("\n🔍 Checking private lockfile...");
818                }
819
820                match crate::lockfile::PrivateLockFile::load(project_dir) {
821                    Ok(Some(_)) => {
822                        if !self.quiet && self.verbose {
823                            println!("✓ Private lockfile is valid");
824                        }
825                    }
826                    Ok(None) => {
827                        // File exists but couldn't be loaded - this shouldn't happen
828                        warnings.push("Private lockfile exists but is empty".to_string());
829                    }
830                    Err(e) => {
831                        let error_msg = format!("Failed to parse private lockfile: {e}");
832                        errors.push(error_msg.to_string());
833                        if !self.quiet {
834                            println!("{} {}", "✗".red(), error_msg);
835                        }
836                    }
837                }
838            }
839        }
840
841        // Validate template rendering if requested
842        if self.render {
843            if self.verbose && !self.quiet {
844                println!("\n🔍 Validating template rendering...");
845            }
846
847            // Load lockfile - required for template context
848            let project_dir = manifest_path.parent().unwrap();
849            let lockfile_path = project_dir.join("agpm.lock");
850
851            if !lockfile_path.exists() {
852                let error_msg =
853                    "Lockfile required for template rendering (run 'agpm install' first)";
854                errors.push(error_msg.to_string());
855
856                if matches!(self.format, OutputFormat::Json) {
857                    validation_results.valid = false;
858                    validation_results.errors = errors;
859                    validation_results.warnings = warnings;
860                    println!("{}", serde_json::to_string_pretty(&validation_results)?);
861                    return Err(anyhow::anyhow!("{}", error_msg));
862                } else if !self.quiet {
863                    println!("{} {}", "✗".red(), error_msg);
864                }
865                return Err(anyhow::anyhow!("{}", error_msg));
866            }
867
868            // Create command context for enhanced lockfile loading
869            let command_context = crate::cli::common::CommandContext::new(
870                manifest.clone(),
871                project_dir.to_path_buf(),
872            )?;
873
874            // Use enhanced lockfile loading with automatic regeneration
875            let lockfile = match command_context
876                .load_lockfile_with_regeneration(true, "validate")?
877            {
878                Some(lockfile) => Arc::new(lockfile),
879                None => {
880                    return Err(anyhow::anyhow!(
881                        "Lockfile was invalid and has been removed. Run 'agpm install' to regenerate it first."
882                    ));
883                }
884            };
885            let cache = Arc::new(Cache::new()?);
886
887            // Load global config for template rendering settings
888            let global_config = crate::config::GlobalConfig::load().await.unwrap_or_default();
889            let max_content_file_size = Some(global_config.max_content_file_size);
890
891            // Collect all markdown resources from manifest
892            let mut template_results = Vec::new();
893            let mut templates_found = 0;
894            let mut templates_rendered = 0;
895
896            // Helper macro to validate template rendering
897            macro_rules! validate_resource_template {
898                ($name:expr, $entry:expr, $resource_type:expr) => {{
899                    // Read the resource content
900                    let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
901                        // Git resource - read from worktree
902                        let source_name = $entry.source.as_ref().unwrap();
903                        let sha = $entry.resolved_commit.as_ref().unwrap();
904                        let url = match $entry.url.as_ref() {
905                            Some(u) => u,
906                            None => {
907                                template_results
908                                    .push(format!("{}: Missing URL for Git resource", $name));
909                                continue;
910                            }
911                        };
912
913                        let cache_dir = match cache
914                            .get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
915                            .await
916                        {
917                            Ok(dir) => dir,
918                            Err(e) => {
919                                template_results.push(format!("{}: {}", $name, e));
920                                continue;
921                            }
922                        };
923
924                        let source_path = cache_dir.join(&$entry.path);
925                        match tokio::fs::read_to_string(&source_path).await {
926                            Ok(c) => c,
927                            Err(e) => {
928                                template_results.push(format!(
929                                    "{}: Failed to read file '{}': {}",
930                                    $name,
931                                    source_path.display(),
932                                    e
933                                ));
934                                continue;
935                            }
936                        }
937                    } else {
938                        // Local resource - read from project directory
939                        let source_path = {
940                            let candidate = Path::new(&$entry.path);
941                            if candidate.is_absolute() {
942                                candidate.to_path_buf()
943                            } else {
944                                project_dir.join(candidate)
945                            }
946                        };
947
948                        match tokio::fs::read_to_string(&source_path).await {
949                            Ok(c) => c,
950                            Err(e) => {
951                                template_results.push(format!(
952                                    "{}: Failed to read file '{}': {}",
953                                    $name,
954                                    source_path.display(),
955                                    e
956                                ));
957                                continue;
958                            }
959                        }
960                    };
961
962                    // Check if it contains template syntax
963                    let has_template_syntax =
964                        content.contains("{{") || content.contains("{%") || content.contains("{#");
965
966                    if !has_template_syntax {
967                        continue; // Not a template
968                    }
969
970                    templates_found += 1;
971
972                    // Build template context
973                    let project_config = manifest.project.clone();
974                    let context_builder = TemplateContextBuilder::new(
975                        Arc::clone(&lockfile),
976                        project_config,
977                        Arc::clone(&cache),
978                        project_dir.to_path_buf(),
979                    );
980                    // Use canonical name from lockfile entry, not manifest key
981                    let resource_id = crate::lockfile::ResourceId::new(
982                        $entry.name.clone(),
983                        $entry.source.clone(),
984                        $entry.tool.clone(),
985                        $resource_type,
986                        $entry.variant_inputs.hash().to_string(),
987                    );
988                    let context = match context_builder
989                        .build_context(&resource_id, $entry.variant_inputs.json())
990                        .await
991                    {
992                        Ok((c, _checksum)) => c,
993                        Err(e) => {
994                            template_results.push(format!("{}: {}", $name, e));
995                            continue;
996                        }
997                    };
998
999                    // Try to render
1000                    let mut renderer = match TemplateRenderer::new(
1001                        true,
1002                        project_dir.to_path_buf(),
1003                        max_content_file_size,
1004                    ) {
1005                        Ok(r) => r,
1006                        Err(e) => {
1007                            template_results.push(format!("{}: {}", $name, e));
1008                            continue;
1009                        }
1010                    };
1011
1012                    // Create rendering metadata for better error messages
1013                    let rendering_metadata = RenderingMetadata {
1014                        resource_name: $entry.name.clone(),
1015                        resource_type: $resource_type,
1016                        dependency_chain: vec![], // Could be enhanced to include parent info
1017                        source_path: Some($entry.path.clone().into()),
1018                        depth: 0,
1019                    };
1020
1021                    match renderer.render_template(&content, &context, Some(&rendering_metadata)) {
1022                        Ok(_) => {
1023                            templates_rendered += 1;
1024                        }
1025                        Err(e) => {
1026                            template_results.push(format!("{}: {}", $name, e));
1027                        }
1028                    }
1029                }};
1030            }
1031
1032            // Process each resource type
1033            // Use manifest_alias (if present) when matching manifest keys to lockfile entries
1034            for resource_type in &[
1035                ResourceType::Agent,
1036                ResourceType::Snippet,
1037                ResourceType::Command,
1038                ResourceType::Script,
1039            ] {
1040                let manifest_resources = manifest.get_resources(resource_type);
1041                let lockfile_resources = lockfile.get_resources(resource_type);
1042
1043                for name in manifest_resources.keys() {
1044                    if let Some(entry) = lockfile_resources
1045                        .iter()
1046                        .find(|e| e.manifest_alias.as_ref().unwrap_or(&e.name) == name)
1047                    {
1048                        validate_resource_template!(name, entry, *resource_type);
1049                    }
1050                }
1051            }
1052
1053            // Update validation results
1054            validation_results.templates_total = templates_found;
1055            validation_results.templates_rendered = templates_rendered;
1056            validation_results.templates_valid = template_results.is_empty();
1057
1058            // Report results (only for text output, not JSON)
1059            if template_results.is_empty() {
1060                if templates_found > 0 {
1061                    if !self.quiet && self.format == OutputFormat::Text {
1062                        println!("✓ All {} templates rendered successfully", templates_found);
1063                    }
1064                } else if !self.quiet && self.format == OutputFormat::Text {
1065                    println!("⚠ No templates found in resources");
1066                }
1067            } else {
1068                let error_msg =
1069                    format!("Template rendering failed for {} resource(s)", template_results.len());
1070                errors.push(error_msg.clone());
1071
1072                if matches!(self.format, OutputFormat::Json) {
1073                    validation_results.valid = false;
1074                    validation_results.errors.extend(template_results);
1075                    validation_results.errors.push(error_msg);
1076                    validation_results.warnings = warnings;
1077                    println!("{}", serde_json::to_string_pretty(&validation_results)?);
1078                    return Err(anyhow::anyhow!("Template rendering failed"));
1079                } else if !self.quiet {
1080                    println!("{} {}", "✗".red(), error_msg);
1081                    for error in &template_results {
1082                        println!("  {}", error);
1083                    }
1084                }
1085                return Err(anyhow::anyhow!("Template rendering failed"));
1086            }
1087
1088            // Validate file references in markdown content
1089            if self.verbose && !self.quiet {
1090                println!("\n🔍 Validating file references in markdown content...");
1091            }
1092
1093            let mut file_reference_errors = Vec::new();
1094            let mut total_references_checked = 0;
1095
1096            // Helper macro to validate file references in markdown resources
1097            macro_rules! validate_file_references_in_resource {
1098                ($name:expr, $entry:expr) => {{
1099                    // Read the resource content
1100                    let content = if $entry.source.is_some() && $entry.resolved_commit.is_some() {
1101                        // Git resource - read from worktree
1102                        let source_name = $entry.source.as_ref().unwrap();
1103                        let sha = $entry.resolved_commit.as_ref().unwrap();
1104                        let url = match $entry.url.as_ref() {
1105                            Some(u) => u,
1106                            None => {
1107                                continue;
1108                            }
1109                        };
1110
1111                        let cache_dir = match cache
1112                            .get_or_create_worktree_for_sha(source_name, url, sha, Some($name))
1113                            .await
1114                        {
1115                            Ok(dir) => dir,
1116                            Err(_) => {
1117                                continue;
1118                            }
1119                        };
1120
1121                        let source_path = cache_dir.join(&$entry.path);
1122                        match tokio::fs::read_to_string(&source_path).await {
1123                            Ok(c) => c,
1124                            Err(e) => {
1125                                tracing::debug!(
1126                                    "Failed to read source file '{}' for reference validation: {}",
1127                                    source_path.display(),
1128                                    e
1129                                );
1130                                continue;
1131                            }
1132                        }
1133                    } else {
1134                        // Local resource - read from installed location
1135                        let installed_path = project_dir.join(&$entry.installed_at);
1136
1137                        match tokio::fs::read_to_string(&installed_path).await {
1138                            Ok(c) => c,
1139                            Err(e) => {
1140                                tracing::debug!(
1141                                    "Failed to read installed file '{}' for reference validation: {}",
1142                                    installed_path.display(),
1143                                    e
1144                                );
1145                                continue;
1146                            }
1147                        }
1148                    };
1149
1150                    // Extract file references from markdown content
1151                    let references = extract_file_references(&content);
1152
1153                    if !references.is_empty() {
1154                        total_references_checked += references.len();
1155
1156                        // Validate each reference exists
1157                        match validate_file_references(&references, project_dir) {
1158                            Ok(missing) => {
1159                                for missing_ref in missing {
1160                                    file_reference_errors.push(format!(
1161                                        "{}: references non-existent file '{}'",
1162                                        $entry.installed_at, missing_ref
1163                                    ));
1164                                }
1165                            }
1166                            Err(e) => {
1167                                file_reference_errors.push(format!(
1168                                    "{}: failed to validate references: {}",
1169                                    $entry.installed_at, e
1170                                ));
1171                            }
1172                        }
1173                    }
1174                }};
1175            }
1176
1177            // Process each markdown resource type from lockfile
1178            for entry in &lockfile.agents {
1179                validate_file_references_in_resource!(&entry.name, entry);
1180            }
1181
1182            for entry in &lockfile.snippets {
1183                validate_file_references_in_resource!(&entry.name, entry);
1184            }
1185
1186            for entry in &lockfile.commands {
1187                validate_file_references_in_resource!(&entry.name, entry);
1188            }
1189
1190            for entry in &lockfile.scripts {
1191                validate_file_references_in_resource!(&entry.name, entry);
1192            }
1193
1194            // Report file reference validation results
1195            if file_reference_errors.is_empty() {
1196                if total_references_checked > 0 {
1197                    if !self.quiet && self.format == OutputFormat::Text {
1198                        println!(
1199                            "✓ All {} file references validated successfully",
1200                            total_references_checked
1201                        );
1202                    }
1203                } else if self.verbose && !self.quiet && self.format == OutputFormat::Text {
1204                    println!("⚠ No file references found in resources");
1205                }
1206            } else {
1207                let error_msg = format!(
1208                    "File reference validation failed: {} broken reference(s) found",
1209                    file_reference_errors.len()
1210                );
1211                errors.push(error_msg.clone());
1212
1213                if matches!(self.format, OutputFormat::Json) {
1214                    validation_results.valid = false;
1215                    validation_results.errors.extend(file_reference_errors);
1216                    validation_results.errors.push(error_msg);
1217                    validation_results.warnings = warnings;
1218                    println!("{}", serde_json::to_string_pretty(&validation_results)?);
1219                    return Err(anyhow::anyhow!("File reference validation failed"));
1220                } else if !self.quiet {
1221                    println!("{} {}", "✗".red(), error_msg);
1222                    for error in &file_reference_errors {
1223                        println!("  {}", error);
1224                    }
1225                }
1226                return Err(anyhow::anyhow!("File reference validation failed"));
1227            }
1228        }
1229
1230        // Handle strict mode - treat warnings as errors
1231        if self.strict && !warnings.is_empty() {
1232            let error_msg = "Strict mode: Warnings treated as errors";
1233            errors.extend(warnings.clone());
1234
1235            if matches!(self.format, OutputFormat::Json) {
1236                validation_results.valid = false;
1237                validation_results.errors = errors;
1238                println!("{}", serde_json::to_string_pretty(&validation_results)?);
1239                return Err(anyhow::anyhow!("Strict mode validation failed"));
1240            } else if !self.quiet {
1241                println!("{} {}", "✗".red(), error_msg);
1242            }
1243            return Err(anyhow::anyhow!("Strict mode validation failed"));
1244        }
1245
1246        // Set final validation status
1247        validation_results.valid = errors.is_empty();
1248        validation_results.errors = errors;
1249        validation_results.warnings = warnings;
1250
1251        // Output results
1252        match self.format {
1253            OutputFormat::Json => {
1254                println!("{}", serde_json::to_string_pretty(&validation_results)?);
1255            }
1256            OutputFormat::Text => {
1257                if !self.quiet && !validation_results.warnings.is_empty() {
1258                    for warning in &validation_results.warnings {
1259                        println!("⚠ Warning: {warning}");
1260                    }
1261                }
1262                // Individual validation steps already printed their success messages
1263            }
1264        }
1265
1266        Ok(())
1267    }
1268}
1269
1270/// Results structure for validation operations, used primarily for JSON output.
1271///
1272/// This struct aggregates all validation results into a single structure that
1273/// can be serialized to JSON for machine consumption. Each field represents
1274/// the result of a specific validation check.
1275///
1276/// # Fields
1277///
1278/// - `valid`: Overall validation status (no errors, or warnings in strict mode)
1279/// - `manifest_valid`: Whether the manifest file is syntactically valid
1280/// - `dependencies_resolvable`: Whether all dependencies can be resolved
1281/// - `sources_accessible`: Whether all source repositories are accessible
1282/// - `local_paths_exist`: Whether all local file dependencies exist
1283/// - `lockfile_consistent`: Whether the lockfile matches the manifest
1284/// - `errors`: List of error messages that caused validation to fail
1285/// - `warnings`: List of warning messages (non-fatal issues)
1286///
1287/// # JSON Output Example
1288///
1289/// ```json
1290/// {
1291///   "valid": true,
1292///   "manifest_valid": true,
1293///   "dependencies_resolvable": true,
1294///   "sources_accessible": true,
1295///   "local_paths_exist": true,
1296///   "lockfile_consistent": false,
1297///   "errors": [],
1298///   "warnings": ["Lockfile is missing 2 dependencies"]
1299/// }
1300/// ```
1301#[derive(serde::Serialize)]
1302struct ValidationResults {
1303    /// Overall validation status - true if no errors (and no warnings in strict mode)
1304    valid: bool,
1305    /// Whether the manifest file syntax and structure is valid
1306    manifest_valid: bool,
1307    /// Whether all dependencies can be resolved to specific versions
1308    dependencies_resolvable: bool,
1309    /// Whether all source repositories are accessible via network
1310    sources_accessible: bool,
1311    /// Whether all local file dependencies point to existing files
1312    local_paths_exist: bool,
1313    /// Whether the lockfile is consistent with the manifest
1314    lockfile_consistent: bool,
1315    /// Whether all templates rendered successfully (when --render is used)
1316    templates_valid: bool,
1317    /// Number of templates successfully rendered
1318    templates_rendered: usize,
1319    /// Total number of templates found
1320    templates_total: usize,
1321    /// List of error messages that caused validation failure
1322    errors: Vec<String>,
1323    /// List of warning messages (non-fatal issues)
1324    warnings: Vec<String>,
1325}
1326
1327impl Default for ValidationResults {
1328    fn default() -> Self {
1329        Self {
1330            valid: true, // Default to true as expected by test
1331            manifest_valid: false,
1332            dependencies_resolvable: false,
1333            sources_accessible: false,
1334            local_paths_exist: false,
1335            lockfile_consistent: false,
1336            templates_valid: false,
1337            templates_rendered: 0,
1338            templates_total: 0,
1339            errors: Vec::new(),
1340            warnings: Vec::new(),
1341        }
1342    }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    use super::*;
1348    use crate::manifest::{Manifest, ResourceDependency};
1349
1350    use tempfile::TempDir;
1351
1352    #[tokio::test]
1353    async fn test_validate_no_manifest() {
1354        let temp = TempDir::new().unwrap();
1355        let manifest_path = temp.path().join("nonexistent").join("agpm.toml");
1356
1357        let cmd = ValidateCommand {
1358            file: None,
1359            resolve: false,
1360            check_lock: false,
1361            sources: false,
1362            paths: false,
1363            format: OutputFormat::Text,
1364            verbose: false,
1365            quiet: false,
1366            strict: false,
1367            render: false,
1368        };
1369
1370        let result = cmd.execute_from_path(manifest_path).await;
1371        assert!(result.is_err());
1372    }
1373
1374    #[tokio::test]
1375    async fn test_validate_valid_manifest() {
1376        let temp = TempDir::new().unwrap();
1377        let manifest_path = temp.path().join("agpm.toml");
1378
1379        // Create valid manifest
1380        let mut manifest = crate::manifest::Manifest::new();
1381        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1382        manifest.save(&manifest_path).unwrap();
1383
1384        let cmd = ValidateCommand {
1385            file: None,
1386            resolve: false,
1387            check_lock: false,
1388            sources: false,
1389            paths: false,
1390            format: OutputFormat::Text,
1391            verbose: false,
1392            quiet: false,
1393            strict: false,
1394            render: false,
1395        };
1396
1397        let result = cmd.execute_from_path(manifest_path).await;
1398        assert!(result.is_ok());
1399    }
1400
1401    #[tokio::test]
1402    async fn test_validate_invalid_manifest() {
1403        let temp = TempDir::new().unwrap();
1404        let manifest_path = temp.path().join("agpm.toml");
1405
1406        // Create invalid manifest (dependency without source)
1407        let mut manifest = crate::manifest::Manifest::new();
1408        manifest.add_dependency(
1409            "test".to_string(),
1410            crate::manifest::ResourceDependency::Detailed(Box::new(
1411                crate::manifest::DetailedDependency {
1412                    source: Some("nonexistent".to_string()),
1413                    path: "test.md".to_string(),
1414                    version: None,
1415                    command: None,
1416                    branch: None,
1417                    rev: None,
1418                    args: None,
1419                    target: None,
1420                    filename: None,
1421                    dependencies: None,
1422                    tool: Some("claude-code".to_string()),
1423                    flatten: None,
1424                    install: None,
1425
1426                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1427                },
1428            )),
1429            true,
1430        );
1431        manifest.save(&manifest_path).unwrap();
1432
1433        let cmd = ValidateCommand {
1434            file: None,
1435            resolve: false,
1436            check_lock: false,
1437            sources: false,
1438            paths: false,
1439            format: OutputFormat::Text,
1440            verbose: false,
1441            quiet: false,
1442            strict: false,
1443            render: false,
1444        };
1445
1446        let result = cmd.execute_from_path(manifest_path).await;
1447        assert!(result.is_err());
1448    }
1449
1450    #[tokio::test]
1451    async fn test_validate_json_format() {
1452        let temp = TempDir::new().unwrap();
1453        let manifest_path = temp.path().join("agpm.toml");
1454
1455        // Create valid manifest
1456        let mut manifest = crate::manifest::Manifest::new();
1457        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1458        manifest.save(&manifest_path).unwrap();
1459
1460        let cmd = ValidateCommand {
1461            file: None,
1462            resolve: false,
1463            check_lock: false,
1464            sources: false,
1465            paths: false,
1466            format: OutputFormat::Json,
1467            verbose: false,
1468            quiet: true,
1469            strict: false,
1470            render: false,
1471        };
1472
1473        let result = cmd.execute_from_path(manifest_path).await;
1474        assert!(result.is_ok());
1475    }
1476
1477    #[tokio::test]
1478    async fn test_validate_with_resolve() {
1479        let temp = TempDir::new().unwrap();
1480        let manifest_path = temp.path().join("agpm.toml");
1481
1482        // Create manifest with a source dependency that needs resolving
1483        let mut manifest = crate::manifest::Manifest::new();
1484        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1485        manifest.add_dependency(
1486            "test-agent".to_string(),
1487            crate::manifest::ResourceDependency::Detailed(Box::new(
1488                crate::manifest::DetailedDependency {
1489                    source: Some("test".to_string()),
1490                    path: "test.md".to_string(),
1491                    version: None,
1492                    command: None,
1493                    branch: None,
1494                    rev: None,
1495                    args: None,
1496                    target: None,
1497                    filename: None,
1498                    dependencies: None,
1499                    tool: Some("claude-code".to_string()),
1500                    flatten: None,
1501                    install: None,
1502
1503                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1504                },
1505            )),
1506            true,
1507        );
1508        manifest.save(&manifest_path).unwrap();
1509
1510        let cmd = ValidateCommand {
1511            file: None,
1512            resolve: true,
1513            check_lock: false,
1514            sources: false,
1515            paths: false,
1516            format: OutputFormat::Text,
1517            verbose: false,
1518            quiet: true, // Make quiet to avoid output
1519            strict: false,
1520            render: false,
1521        };
1522
1523        let result = cmd.execute_from_path(manifest_path).await;
1524        // For now, just check that the command runs without panicking
1525        // The actual success/failure depends on resolver implementation
1526        let _ = result;
1527    }
1528
1529    #[tokio::test]
1530    async fn test_validate_check_lock_consistent() {
1531        let temp = TempDir::new().unwrap();
1532        let manifest_path = temp.path().join("agpm.toml");
1533
1534        // Create a simple manifest without dependencies
1535        let manifest = crate::manifest::Manifest::new();
1536        manifest.save(&manifest_path).unwrap();
1537
1538        // Create an empty lockfile (consistent with no dependencies)
1539        let lockfile = crate::lockfile::LockFile::new();
1540        lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1541
1542        let cmd = ValidateCommand {
1543            file: None,
1544            resolve: false,
1545            check_lock: true,
1546            sources: false,
1547            paths: false,
1548            format: OutputFormat::Text,
1549            verbose: false,
1550            quiet: true,
1551            strict: false,
1552            render: false,
1553        };
1554
1555        let result = cmd.execute_from_path(manifest_path).await;
1556        // Empty manifest and empty lockfile are consistent
1557        assert!(result.is_ok());
1558    }
1559
1560    #[tokio::test]
1561    async fn test_validate_check_lock_with_extra_entries() {
1562        let temp = TempDir::new().unwrap();
1563        let manifest_path = temp.path().join("agpm.toml");
1564
1565        // Create empty manifest
1566        let manifest = crate::manifest::Manifest::new();
1567        manifest.save(&manifest_path).unwrap();
1568
1569        // Create lockfile with an entry (extra entry not in manifest)
1570        let mut lockfile = crate::lockfile::LockFile::new();
1571        lockfile.agents.push(crate::lockfile::LockedResource {
1572            name: "extra-agent".to_string(),
1573            source: Some("test".to_string()),
1574            url: Some("https://github.com/test/repo.git".to_string()),
1575            path: "test.md".to_string(),
1576            version: None,
1577            resolved_commit: Some("abc123".to_string()),
1578            checksum: "sha256:dummy".to_string(),
1579            installed_at: "agents/extra-agent.md".to_string(),
1580            dependencies: vec![],
1581            resource_type: crate::core::ResourceType::Agent,
1582
1583            tool: Some("claude-code".to_string()),
1584            manifest_alias: None,
1585            context_checksum: None,
1586            applied_patches: std::collections::BTreeMap::new(),
1587            install: None,
1588            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1589        });
1590        lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1591
1592        let cmd = ValidateCommand {
1593            file: None,
1594            resolve: false,
1595            check_lock: true,
1596            sources: false,
1597            paths: false,
1598            format: OutputFormat::Text,
1599            verbose: false,
1600            quiet: true,
1601            strict: false,
1602            render: false,
1603        };
1604
1605        let result = cmd.execute_from_path(manifest_path).await;
1606        // Should fail due to extra entries in lockfile
1607        assert!(result.is_err());
1608    }
1609
1610    #[tokio::test]
1611    async fn test_validate_strict_mode() {
1612        let temp = TempDir::new().unwrap();
1613        let manifest_path = temp.path().join("agpm.toml");
1614
1615        // Create manifest with warning (empty sources)
1616        let manifest = crate::manifest::Manifest::new();
1617        manifest.save(&manifest_path).unwrap();
1618
1619        let cmd = ValidateCommand {
1620            file: None,
1621            resolve: false,
1622            check_lock: false,
1623            sources: false,
1624            paths: false,
1625            format: OutputFormat::Text,
1626            verbose: false,
1627            quiet: true,
1628            strict: true, // Strict mode treats warnings as errors
1629            render: false,
1630        };
1631
1632        let result = cmd.execute_from_path(manifest_path).await;
1633        // Should fail in strict mode due to warnings
1634        assert!(result.is_err());
1635    }
1636
1637    #[tokio::test]
1638    async fn test_validate_verbose_mode() {
1639        let temp = TempDir::new().unwrap();
1640        let manifest_path = temp.path().join("agpm.toml");
1641
1642        // Create valid manifest
1643        let mut manifest = crate::manifest::Manifest::new();
1644        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1645        manifest.save(&manifest_path).unwrap();
1646
1647        let cmd = ValidateCommand {
1648            file: None,
1649            resolve: false,
1650            check_lock: false,
1651            sources: false,
1652            paths: false,
1653            format: OutputFormat::Text,
1654            verbose: true, // Enable verbose output
1655            quiet: false,
1656            strict: false,
1657            render: false,
1658        };
1659
1660        let result = cmd.execute_from_path(manifest_path).await;
1661        assert!(result.is_ok());
1662    }
1663
1664    #[tokio::test]
1665    async fn test_validate_check_paths_local() {
1666        let temp = TempDir::new().unwrap();
1667        let manifest_path = temp.path().join("agpm.toml");
1668
1669        // Create a local file to reference
1670        std::fs::create_dir_all(temp.path().join("local")).unwrap();
1671        std::fs::write(temp.path().join("local/test.md"), "# Test").unwrap();
1672
1673        // Create manifest with local dependency
1674        let mut manifest = crate::manifest::Manifest::new();
1675        manifest.add_dependency(
1676            "local-test".to_string(),
1677            crate::manifest::ResourceDependency::Detailed(Box::new(
1678                crate::manifest::DetailedDependency {
1679                    source: None,
1680                    path: "./local/test.md".to_string(),
1681                    version: None,
1682                    command: None,
1683                    branch: None,
1684                    rev: None,
1685                    args: None,
1686                    target: None,
1687                    filename: None,
1688                    dependencies: None,
1689                    tool: Some("claude-code".to_string()),
1690                    flatten: None,
1691                    install: None,
1692
1693                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1694                },
1695            )),
1696            true,
1697        );
1698        manifest.save(&manifest_path).unwrap();
1699
1700        let cmd = ValidateCommand {
1701            file: None,
1702            resolve: false,
1703            check_lock: false,
1704            sources: false,
1705            paths: true, // Check local paths
1706            format: OutputFormat::Text,
1707            verbose: false,
1708            quiet: false,
1709            strict: false,
1710            render: false,
1711        };
1712
1713        let result = cmd.execute_from_path(manifest_path).await;
1714        assert!(result.is_ok());
1715    }
1716
1717    #[tokio::test]
1718    async fn test_validate_custom_file_path() {
1719        let temp = TempDir::new().unwrap();
1720
1721        // Create manifest in custom location
1722        let custom_dir = temp.path().join("custom");
1723        std::fs::create_dir_all(&custom_dir).unwrap();
1724        let manifest_path = custom_dir.join("custom.toml");
1725
1726        let mut manifest = crate::manifest::Manifest::new();
1727        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
1728        manifest.save(&manifest_path).unwrap();
1729
1730        let cmd = ValidateCommand {
1731            file: Some(manifest_path.to_str().unwrap().to_string()),
1732            resolve: false,
1733            check_lock: false,
1734            sources: false,
1735            paths: false,
1736            format: OutputFormat::Text,
1737            verbose: false,
1738            quiet: false,
1739            strict: false,
1740            render: false,
1741        };
1742
1743        let result = cmd.execute_from_path(manifest_path).await;
1744        assert!(result.is_ok());
1745    }
1746
1747    #[tokio::test]
1748    async fn test_validate_json_error_format() {
1749        let temp = TempDir::new().unwrap();
1750        let manifest_path = temp.path().join("agpm.toml");
1751
1752        // Create invalid manifest
1753        let mut manifest = crate::manifest::Manifest::new();
1754        manifest.add_dependency(
1755            "test".to_string(),
1756            crate::manifest::ResourceDependency::Detailed(Box::new(
1757                crate::manifest::DetailedDependency {
1758                    source: Some("nonexistent".to_string()),
1759                    path: "test.md".to_string(),
1760                    version: None,
1761                    command: None,
1762                    branch: None,
1763                    rev: None,
1764                    args: None,
1765                    target: None,
1766                    filename: None,
1767                    dependencies: None,
1768                    tool: Some("claude-code".to_string()),
1769                    flatten: None,
1770                    install: None,
1771
1772                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
1773                },
1774            )),
1775            true,
1776        );
1777        manifest.save(&manifest_path).unwrap();
1778
1779        let cmd = ValidateCommand {
1780            file: None,
1781            resolve: false,
1782            check_lock: false,
1783            sources: false,
1784            paths: false,
1785            format: OutputFormat::Json, // JSON format for errors
1786            verbose: false,
1787            quiet: true,
1788            strict: false,
1789            render: false,
1790        };
1791
1792        let result = cmd.execute_from_path(manifest_path).await;
1793        assert!(result.is_err());
1794    }
1795
1796    #[tokio::test]
1797    async fn test_validate_paths_check() {
1798        let temp = TempDir::new().unwrap();
1799        let manifest_path = temp.path().join("agpm.toml");
1800
1801        // Create manifest with local dependency
1802        let mut manifest = crate::manifest::Manifest::new();
1803        manifest.add_dependency(
1804            "local-agent".to_string(),
1805            crate::manifest::ResourceDependency::Simple("./local/agent.md".to_string()),
1806            true,
1807        );
1808        manifest.save(&manifest_path).unwrap();
1809
1810        // Test with missing path
1811        let cmd = ValidateCommand {
1812            file: None,
1813            resolve: false,
1814            check_lock: false,
1815            sources: false,
1816            paths: true,
1817            format: OutputFormat::Text,
1818            verbose: false,
1819            quiet: false,
1820            strict: false,
1821            render: false,
1822        };
1823
1824        let result = cmd.execute_from_path(manifest_path.clone()).await;
1825        assert!(result.is_err());
1826
1827        // Create the path and test again
1828        std::fs::create_dir_all(temp.path().join("local")).unwrap();
1829        std::fs::write(temp.path().join("local/agent.md"), "# Agent").unwrap();
1830
1831        let cmd = ValidateCommand {
1832            file: None,
1833            resolve: false,
1834            check_lock: false,
1835            sources: false,
1836            paths: true,
1837            format: OutputFormat::Text,
1838            verbose: false,
1839            quiet: false,
1840            strict: false,
1841            render: false,
1842        };
1843
1844        let result = cmd.execute_from_path(manifest_path).await;
1845        assert!(result.is_ok());
1846    }
1847
1848    #[tokio::test]
1849    async fn test_validate_check_lock() {
1850        let temp = TempDir::new().unwrap();
1851        let manifest_path = temp.path().join("agpm.toml");
1852
1853        // Create manifest
1854        let mut manifest = crate::manifest::Manifest::new();
1855        manifest.add_dependency(
1856            "test".to_string(),
1857            crate::manifest::ResourceDependency::Simple("test.md".to_string()),
1858            true,
1859        );
1860        manifest.save(&manifest_path).unwrap();
1861
1862        // Test without lockfile
1863        let cmd = ValidateCommand {
1864            file: None,
1865            resolve: false,
1866            check_lock: true,
1867            sources: false,
1868            paths: false,
1869            format: OutputFormat::Text,
1870            verbose: false,
1871            quiet: false,
1872            strict: false,
1873            render: false,
1874        };
1875
1876        let result = cmd.execute_from_path(manifest_path.clone()).await;
1877        assert!(result.is_ok()); // Should succeed with warning
1878
1879        // Create lockfile with matching dependencies
1880        let lockfile = crate::lockfile::LockFile {
1881            version: 1,
1882            sources: vec![],
1883            commands: vec![],
1884            agents: vec![crate::lockfile::LockedResource {
1885                name: "test".to_string(),
1886                source: None,
1887                url: None,
1888                path: "test.md".to_string(),
1889                version: None,
1890                resolved_commit: None,
1891                checksum: String::new(),
1892                installed_at: "agents/test.md".to_string(),
1893                dependencies: vec![],
1894                resource_type: crate::core::ResourceType::Agent,
1895
1896                tool: Some("claude-code".to_string()),
1897                manifest_alias: None,
1898                context_checksum: None,
1899                applied_patches: std::collections::BTreeMap::new(),
1900                install: None,
1901                variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
1902            }],
1903            snippets: vec![],
1904            mcp_servers: vec![],
1905            scripts: vec![],
1906            hooks: vec![],
1907        };
1908        lockfile.save(&temp.path().join("agpm.lock")).unwrap();
1909
1910        let cmd = ValidateCommand {
1911            file: None,
1912            resolve: false,
1913            check_lock: true,
1914            sources: false,
1915            paths: false,
1916            format: OutputFormat::Text,
1917            verbose: false,
1918            quiet: false,
1919            strict: false,
1920            render: false,
1921        };
1922
1923        let result = cmd.execute_from_path(manifest_path).await;
1924        assert!(result.is_ok());
1925    }
1926
1927    #[tokio::test]
1928    async fn test_validate_verbose_output() {
1929        let temp = TempDir::new().unwrap();
1930        let manifest_path = temp.path().join("agpm.toml");
1931
1932        let manifest = crate::manifest::Manifest::new();
1933        manifest.save(&manifest_path).unwrap();
1934
1935        let cmd = ValidateCommand {
1936            file: None,
1937            resolve: false,
1938            check_lock: false,
1939            sources: false,
1940            paths: false,
1941            format: OutputFormat::Text,
1942            verbose: true,
1943            quiet: false,
1944            strict: false,
1945            render: false,
1946        };
1947
1948        let result = cmd.execute_from_path(manifest_path).await;
1949        assert!(result.is_ok());
1950    }
1951
1952    #[tokio::test]
1953    async fn test_validate_strict_mode_with_warnings() {
1954        let temp = TempDir::new().unwrap();
1955        let manifest_path = temp.path().join("agpm.toml");
1956
1957        // Create manifest that will have warnings
1958        let manifest = crate::manifest::Manifest::new();
1959        manifest.save(&manifest_path).unwrap();
1960
1961        // Without lockfile, should have warning
1962        let cmd = ValidateCommand {
1963            file: None,
1964            resolve: false,
1965            check_lock: true,
1966            sources: false,
1967            paths: false,
1968            format: OutputFormat::Text,
1969            verbose: false,
1970            quiet: false,
1971            strict: true, // Strict mode
1972            render: false,
1973        };
1974
1975        let result = cmd.execute_from_path(manifest_path).await;
1976        assert!(result.is_err()); // Should fail in strict mode with warnings
1977    }
1978
1979    #[test]
1980    fn test_output_format_enum() {
1981        // Test that the output format enum works correctly
1982        assert!(matches!(OutputFormat::Text, OutputFormat::Text));
1983        assert!(matches!(OutputFormat::Json, OutputFormat::Json));
1984    }
1985
1986    #[test]
1987    fn test_validation_results_default() {
1988        let results = ValidationResults::default();
1989        // Default should be true for valid
1990        assert!(results.valid);
1991        // These should be false by default (not checked yet)
1992        assert!(!results.manifest_valid);
1993        assert!(!results.dependencies_resolvable);
1994        assert!(!results.sources_accessible);
1995        assert!(!results.lockfile_consistent);
1996        assert!(!results.local_paths_exist);
1997        assert!(results.errors.is_empty());
1998        assert!(results.warnings.is_empty());
1999    }
2000
2001    #[tokio::test]
2002    async fn test_validate_quiet_mode() {
2003        let temp = TempDir::new().unwrap();
2004        let manifest_path = temp.path().join("agpm.toml");
2005
2006        // Create valid manifest
2007        let manifest = crate::manifest::Manifest::new();
2008        manifest.save(&manifest_path).unwrap();
2009
2010        let cmd = ValidateCommand {
2011            file: None,
2012            resolve: false,
2013            check_lock: false,
2014            sources: false,
2015            paths: false,
2016            format: OutputFormat::Text,
2017            verbose: false,
2018            quiet: true, // Enable quiet
2019            strict: false,
2020            render: false,
2021        };
2022
2023        let result = cmd.execute_from_path(manifest_path).await;
2024        assert!(result.is_ok());
2025    }
2026
2027    #[tokio::test]
2028    async fn test_validate_json_output_success() {
2029        let temp = TempDir::new().unwrap();
2030        let manifest_path = temp.path().join("agpm.toml");
2031
2032        // Create valid manifest with dependencies
2033        let mut manifest = crate::manifest::Manifest::new();
2034        use crate::manifest::{DetailedDependency, ResourceDependency};
2035
2036        manifest.agents.insert(
2037            "test".to_string(),
2038            ResourceDependency::Detailed(Box::new(DetailedDependency {
2039                source: None,
2040                path: "test.md".to_string(),
2041                version: None,
2042                command: None,
2043                branch: None,
2044                rev: None,
2045                args: None,
2046                target: None,
2047                filename: None,
2048                dependencies: None,
2049                tool: Some("claude-code".to_string()),
2050                flatten: None,
2051                install: None,
2052
2053                template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2054            })),
2055        );
2056        manifest.save(&manifest_path).unwrap();
2057
2058        let cmd = ValidateCommand {
2059            file: None,
2060            resolve: false,
2061            check_lock: false,
2062            sources: false,
2063            paths: false,
2064            format: OutputFormat::Json, // JSON output
2065            verbose: false,
2066            quiet: false,
2067            strict: false,
2068            render: false,
2069        };
2070
2071        let result = cmd.execute_from_path(manifest_path).await;
2072        assert!(result.is_ok());
2073    }
2074
2075    #[tokio::test]
2076    async fn test_validate_check_sources() {
2077        let temp = TempDir::new().unwrap();
2078        let manifest_path = temp.path().join("agpm.toml");
2079
2080        // Create a local git repository to use as a mock source
2081        let source_dir = temp.path().join("test-source");
2082        std::fs::create_dir_all(&source_dir).unwrap();
2083
2084        // Initialize it as a git repository
2085        std::process::Command::new("git")
2086            .arg("init")
2087            .current_dir(&source_dir)
2088            .output()
2089            .expect("Failed to initialize git repository");
2090
2091        // Create manifest with local file:// URL to avoid network access
2092        let mut manifest = crate::manifest::Manifest::new();
2093        let source_url = format!("file://{}", normalize_path_for_storage(&source_dir));
2094        manifest.add_source("test".to_string(), source_url);
2095        manifest.save(&manifest_path).unwrap();
2096
2097        let cmd = ValidateCommand {
2098            file: None,
2099            resolve: false,
2100            check_lock: false,
2101            sources: true, // Check sources
2102            paths: false,
2103            format: OutputFormat::Text,
2104            verbose: false,
2105            quiet: false,
2106            strict: false,
2107            render: false,
2108        };
2109
2110        // This will check if the local source is accessible
2111        let result = cmd.execute_from_path(manifest_path).await;
2112        // Local file:// URL should be accessible
2113        assert!(result.is_ok());
2114    }
2115
2116    #[tokio::test]
2117    async fn test_validate_check_paths() {
2118        let temp = TempDir::new().unwrap();
2119        let manifest_path = temp.path().join("agpm.toml");
2120
2121        // Create manifest with local dependency
2122        let mut manifest = crate::manifest::Manifest::new();
2123        use crate::manifest::{DetailedDependency, ResourceDependency};
2124
2125        manifest.agents.insert(
2126            "test".to_string(),
2127            ResourceDependency::Detailed(Box::new(DetailedDependency {
2128                source: None,
2129                path: temp.path().join("test.md").to_str().unwrap().to_string(),
2130                version: None,
2131                command: None,
2132                branch: None,
2133                rev: None,
2134                args: None,
2135                target: None,
2136                filename: None,
2137                dependencies: None,
2138                tool: Some("claude-code".to_string()),
2139                flatten: None,
2140                install: None,
2141
2142                template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2143            })),
2144        );
2145        manifest.save(&manifest_path).unwrap();
2146
2147        // Create the referenced file
2148        std::fs::write(temp.path().join("test.md"), "# Test Agent").unwrap();
2149
2150        let cmd = ValidateCommand {
2151            file: None,
2152            resolve: false,
2153            check_lock: false,
2154            sources: false,
2155            paths: true, // Check paths
2156            format: OutputFormat::Text,
2157            verbose: false,
2158            quiet: false,
2159            strict: false,
2160            render: false,
2161        };
2162
2163        let result = cmd.execute_from_path(manifest_path).await;
2164        assert!(result.is_ok());
2165    }
2166
2167    // Additional comprehensive tests for uncovered lines start here
2168
2169    #[tokio::test]
2170    async fn test_execute_with_no_manifest_json_format() {
2171        let temp = TempDir::new().unwrap();
2172        let manifest_path = temp.path().join("non_existent.toml");
2173
2174        let cmd = ValidateCommand {
2175            file: Some(manifest_path.to_string_lossy().to_string()),
2176            resolve: false,
2177            check_lock: false,
2178            sources: false,
2179            paths: false,
2180            format: OutputFormat::Json, // Test JSON output for no manifest found
2181            verbose: false,
2182            quiet: false,
2183            strict: false,
2184            render: false,
2185        };
2186
2187        let result = cmd.execute().await;
2188        assert!(result.is_err());
2189        // This tests lines 335-342 (JSON format for missing manifest)
2190    }
2191
2192    #[tokio::test]
2193    async fn test_execute_with_no_manifest_text_format() {
2194        let temp = TempDir::new().unwrap();
2195        let manifest_path = temp.path().join("non_existent.toml");
2196
2197        let cmd = ValidateCommand {
2198            file: Some(manifest_path.to_string_lossy().to_string()),
2199            resolve: false,
2200            check_lock: false,
2201            sources: false,
2202            paths: false,
2203            format: OutputFormat::Text,
2204            verbose: false,
2205            quiet: false, // Not quiet - should print error message
2206            strict: false,
2207            render: false,
2208        };
2209
2210        let result = cmd.execute().await;
2211        assert!(result.is_err());
2212        // This tests lines 343-344 (text format for missing manifest)
2213    }
2214
2215    #[tokio::test]
2216    async fn test_execute_with_no_manifest_quiet_mode() {
2217        let temp = TempDir::new().unwrap();
2218        let manifest_path = temp.path().join("non_existent.toml");
2219
2220        let cmd = ValidateCommand {
2221            file: Some(manifest_path.to_string_lossy().to_string()),
2222            resolve: false,
2223            check_lock: false,
2224            sources: false,
2225            paths: false,
2226            format: OutputFormat::Text,
2227            verbose: false,
2228            quiet: true, // Quiet mode - should not print
2229            strict: false,
2230            render: false,
2231        };
2232
2233        let result = cmd.execute().await;
2234        assert!(result.is_err());
2235        // This tests the else branch (quiet mode)
2236    }
2237
2238    #[tokio::test]
2239    async fn test_execute_from_path_nonexistent_file_json() {
2240        let temp = TempDir::new().unwrap();
2241        let nonexistent_path = temp.path().join("nonexistent.toml");
2242
2243        let cmd = ValidateCommand {
2244            file: None,
2245            resolve: false,
2246            check_lock: false,
2247            sources: false,
2248            paths: false,
2249            format: OutputFormat::Json,
2250            verbose: false,
2251            quiet: false,
2252            strict: false,
2253            render: false,
2254        };
2255
2256        let result = cmd.execute_from_path(nonexistent_path).await;
2257        assert!(result.is_err());
2258        // This tests lines 379-385 (JSON output for nonexistent manifest file)
2259    }
2260
2261    #[tokio::test]
2262    async fn test_execute_from_path_nonexistent_file_text() {
2263        let temp = TempDir::new().unwrap();
2264        let nonexistent_path = temp.path().join("nonexistent.toml");
2265
2266        let cmd = ValidateCommand {
2267            file: None,
2268            resolve: false,
2269            check_lock: false,
2270            sources: false,
2271            paths: false,
2272            format: OutputFormat::Text,
2273            verbose: false,
2274            quiet: false,
2275            strict: false,
2276            render: false,
2277        };
2278
2279        let result = cmd.execute_from_path(nonexistent_path).await;
2280        assert!(result.is_err());
2281        // This tests lines 386-387 (text output for nonexistent manifest file)
2282    }
2283
2284    #[tokio::test]
2285    async fn test_validate_manifest_toml_syntax_error() {
2286        let temp = TempDir::new().unwrap();
2287        let manifest_path = temp.path().join("agpm.toml");
2288
2289        // Create invalid TOML file
2290        std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2291
2292        let cmd = ValidateCommand {
2293            file: None,
2294            resolve: false,
2295            check_lock: false,
2296            sources: false,
2297            paths: false,
2298            format: OutputFormat::Text,
2299            verbose: false,
2300            quiet: false,
2301            strict: false,
2302            render: false,
2303        };
2304
2305        let result = cmd.execute_from_path(manifest_path).await;
2306        assert!(result.is_err());
2307        // This tests lines 415-416 (TOML syntax error detection)
2308    }
2309
2310    #[tokio::test]
2311    async fn test_validate_manifest_toml_syntax_error_json() {
2312        let temp = TempDir::new().unwrap();
2313        let manifest_path = temp.path().join("agpm.toml");
2314
2315        // Create invalid TOML file
2316        std::fs::write(&manifest_path, "invalid toml syntax [[[").unwrap();
2317
2318        let cmd = ValidateCommand {
2319            file: None,
2320            resolve: false,
2321            check_lock: false,
2322            sources: false,
2323            paths: false,
2324            format: OutputFormat::Json,
2325            verbose: false,
2326            quiet: true,
2327            strict: false,
2328            render: false,
2329        };
2330
2331        let result = cmd.execute_from_path(manifest_path).await;
2332        assert!(result.is_err());
2333        // This tests lines 422-426 (JSON output for TOML syntax error)
2334    }
2335
2336    #[tokio::test]
2337    async fn test_validate_manifest_structure_error() {
2338        let temp = TempDir::new().unwrap();
2339        let manifest_path = temp.path().join("agpm.toml");
2340
2341        // Create manifest with invalid structure
2342        let mut manifest = crate::manifest::Manifest::new();
2343        manifest.add_dependency(
2344            "test".to_string(),
2345            crate::manifest::ResourceDependency::Detailed(Box::new(
2346                crate::manifest::DetailedDependency {
2347                    source: Some("nonexistent".to_string()),
2348                    path: "test.md".to_string(),
2349                    version: None,
2350                    command: None,
2351                    branch: None,
2352                    rev: None,
2353                    args: None,
2354                    target: None,
2355                    filename: None,
2356                    dependencies: None,
2357                    tool: Some("claude-code".to_string()),
2358                    flatten: None,
2359                    install: None,
2360
2361                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2362                },
2363            )),
2364            true,
2365        );
2366        manifest.save(&manifest_path).unwrap();
2367
2368        let cmd = ValidateCommand {
2369            file: None,
2370            resolve: false,
2371            check_lock: false,
2372            sources: false,
2373            paths: false,
2374            format: OutputFormat::Text,
2375            verbose: false,
2376            quiet: false,
2377            strict: false,
2378            render: false,
2379        };
2380
2381        let result = cmd.execute_from_path(manifest_path).await;
2382        assert!(result.is_err());
2383        // This tests manifest validation errors (lines 435-455)
2384    }
2385
2386    #[tokio::test]
2387    async fn test_validate_manifest_version_conflict() {
2388        let temp = TempDir::new().unwrap();
2389        let manifest_path = temp.path().join("agpm.toml");
2390
2391        // Create a test manifest file that would trigger version conflict detection
2392        std::fs::write(
2393            &manifest_path,
2394            r#"
2395[sources]
2396test = "https://github.com/test/repo.git"
2397
2398[agents]
2399shared-agent = { source = "test", path = "agent.md", version = "v1.0.0" }
2400another-agent = { source = "test", path = "agent.md", version = "v2.0.0" }
2401"#,
2402        )
2403        .unwrap();
2404
2405        let cmd = ValidateCommand {
2406            file: None,
2407            resolve: false,
2408            check_lock: false,
2409            sources: false,
2410            paths: false,
2411            format: OutputFormat::Json,
2412            verbose: false,
2413            quiet: true,
2414            strict: false,
2415            render: false,
2416        };
2417
2418        // Version conflicts are automatically resolved during installation
2419        let result = cmd.execute_from_path(manifest_path).await;
2420        // Version conflicts are typically warnings, not errors
2421        assert!(result.is_ok());
2422        // This tests lines 439-442 (version conflict detection)
2423    }
2424
2425    #[tokio::test]
2426    async fn test_validate_with_outdated_version_warnings() {
2427        let temp = TempDir::new().unwrap();
2428        let manifest_path = temp.path().join("agpm.toml");
2429
2430        // Create manifest with v0.x versions (potentially outdated)
2431        let mut manifest = crate::manifest::Manifest::new();
2432        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2433        manifest.add_dependency(
2434            "old-agent".to_string(),
2435            crate::manifest::ResourceDependency::Detailed(Box::new(
2436                crate::manifest::DetailedDependency {
2437                    source: Some("test".to_string()),
2438                    path: "old.md".to_string(),
2439                    version: Some("v0.1.0".to_string()), // This should trigger warning
2440                    command: None,
2441                    branch: None,
2442                    rev: None,
2443                    args: None,
2444                    target: None,
2445                    filename: None,
2446                    dependencies: None,
2447                    tool: Some("claude-code".to_string()),
2448                    flatten: None,
2449                    install: None,
2450
2451                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2452                },
2453            )),
2454            true,
2455        );
2456        manifest.save(&manifest_path).unwrap();
2457
2458        let cmd = ValidateCommand {
2459            file: None,
2460            resolve: false,
2461            check_lock: false,
2462            sources: false,
2463            paths: false,
2464            format: OutputFormat::Text,
2465            verbose: false,
2466            quiet: false,
2467            strict: false,
2468            render: false,
2469        };
2470
2471        let result = cmd.execute_from_path(manifest_path).await;
2472        assert!(result.is_ok());
2473    }
2474
2475    #[tokio::test]
2476    async fn test_validate_resolve_with_error_json_output() {
2477        let temp = TempDir::new().unwrap();
2478        let manifest_path = temp.path().join("agpm.toml");
2479
2480        // Create manifest with dependency that will fail to resolve
2481        let mut manifest = crate::manifest::Manifest::new();
2482        manifest
2483            .add_source("test".to_string(), "https://github.com/nonexistent/repo.git".to_string());
2484        manifest.add_dependency(
2485            "failing-agent".to_string(),
2486            crate::manifest::ResourceDependency::Detailed(Box::new(
2487                crate::manifest::DetailedDependency {
2488                    source: Some("test".to_string()),
2489                    path: "test.md".to_string(),
2490                    version: None,
2491                    command: None,
2492                    branch: None,
2493                    rev: None,
2494                    args: None,
2495                    target: None,
2496                    filename: None,
2497                    dependencies: None,
2498                    tool: Some("claude-code".to_string()),
2499                    flatten: None,
2500                    install: None,
2501
2502                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2503                },
2504            )),
2505            true,
2506        );
2507        manifest.save(&manifest_path).unwrap();
2508
2509        let cmd = ValidateCommand {
2510            file: None,
2511            resolve: true,
2512            check_lock: false,
2513            sources: false,
2514            paths: false,
2515            format: OutputFormat::Json,
2516            verbose: false,
2517            quiet: true,
2518            strict: false,
2519            render: false,
2520        };
2521
2522        let result = cmd.execute_from_path(manifest_path).await;
2523        // This will likely fail due to network issues or nonexistent repo
2524        // This tests lines 515-520 and 549-554 (JSON output for resolve errors)
2525        let _ = result; // Don't assert success/failure as it depends on network
2526    }
2527
2528    #[tokio::test]
2529    async fn test_validate_resolve_dependency_not_found_error() {
2530        let temp = TempDir::new().unwrap();
2531        let manifest_path = temp.path().join("agpm.toml");
2532
2533        // Create manifest with dependencies that will fail resolution
2534        let mut manifest = crate::manifest::Manifest::new();
2535        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
2536        manifest.add_dependency(
2537            "my-agent".to_string(),
2538            crate::manifest::ResourceDependency::Detailed(Box::new(
2539                crate::manifest::DetailedDependency {
2540                    source: Some("test".to_string()),
2541                    path: "agent.md".to_string(),
2542                    version: None,
2543                    command: None,
2544                    branch: None,
2545                    rev: None,
2546                    args: None,
2547                    target: None,
2548                    filename: None,
2549                    dependencies: None,
2550                    tool: Some("claude-code".to_string()),
2551                    flatten: None,
2552                    install: None,
2553
2554                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2555                },
2556            )),
2557            true,
2558        );
2559        manifest.add_dependency(
2560            "utils".to_string(),
2561            crate::manifest::ResourceDependency::Detailed(Box::new(
2562                crate::manifest::DetailedDependency {
2563                    source: Some("test".to_string()),
2564                    path: "utils.md".to_string(),
2565                    version: None,
2566                    command: None,
2567                    branch: None,
2568                    rev: None,
2569                    args: None,
2570                    target: None,
2571                    filename: None,
2572                    dependencies: None,
2573                    tool: Some("claude-code".to_string()),
2574                    flatten: None,
2575                    install: None,
2576
2577                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2578                },
2579            )),
2580            false,
2581        );
2582        manifest.save(&manifest_path).unwrap();
2583
2584        let cmd = ValidateCommand {
2585            file: None,
2586            resolve: true,
2587            check_lock: false,
2588            sources: false,
2589            paths: false,
2590            format: OutputFormat::Text,
2591            verbose: false,
2592            quiet: false,
2593            strict: false,
2594            render: false,
2595        };
2596
2597        let result = cmd.execute_from_path(manifest_path).await;
2598        // This tests lines 538-541 (specific dependency not found error message)
2599        let _ = result;
2600    }
2601
2602    #[tokio::test]
2603    async fn test_validate_sources_accessibility_error() {
2604        let temp = TempDir::new().unwrap();
2605        let manifest_path = temp.path().join("agpm.toml");
2606
2607        // Create manifest with sources that will fail accessibility check
2608        // Use file:// URLs pointing to non-existent local paths
2609        let nonexistent_path1 = temp.path().join("nonexistent1");
2610        let nonexistent_path2 = temp.path().join("nonexistent2");
2611
2612        // Convert to file:// URLs with proper formatting for Windows
2613        let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2614        let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2615
2616        let mut manifest = crate::manifest::Manifest::new();
2617        manifest.add_source("official".to_string(), url1);
2618        manifest.add_source("community".to_string(), url2);
2619        manifest.save(&manifest_path).unwrap();
2620
2621        let cmd = ValidateCommand {
2622            file: None,
2623            resolve: false,
2624            check_lock: false,
2625            sources: true,
2626            paths: false,
2627            format: OutputFormat::Text,
2628            verbose: false,
2629            quiet: false,
2630            strict: false,
2631            render: false,
2632        };
2633
2634        let result = cmd.execute_from_path(manifest_path).await;
2635        // This tests lines 578-580, 613-615 (source accessibility error messages)
2636        let _ = result;
2637    }
2638
2639    #[tokio::test]
2640    async fn test_validate_sources_accessibility_error_json() {
2641        let temp = TempDir::new().unwrap();
2642        let manifest_path = temp.path().join("agpm.toml");
2643
2644        // Create manifest with sources that will fail accessibility check
2645        // Use file:// URLs pointing to non-existent local paths
2646        let nonexistent_path1 = temp.path().join("nonexistent1");
2647        let nonexistent_path2 = temp.path().join("nonexistent2");
2648
2649        // Convert to file:// URLs with proper formatting for Windows
2650        let url1 = format!("file://{}", normalize_path_for_storage(&nonexistent_path1));
2651        let url2 = format!("file://{}", normalize_path_for_storage(&nonexistent_path2));
2652
2653        let mut manifest = crate::manifest::Manifest::new();
2654        manifest.add_source("official".to_string(), url1);
2655        manifest.add_source("community".to_string(), url2);
2656        manifest.save(&manifest_path).unwrap();
2657
2658        let cmd = ValidateCommand {
2659            file: None,
2660            resolve: false,
2661            check_lock: false,
2662            sources: true,
2663            paths: false,
2664            format: OutputFormat::Json,
2665            verbose: false,
2666            quiet: true,
2667            strict: false,
2668            render: false,
2669        };
2670
2671        let result = cmd.execute_from_path(manifest_path).await;
2672        // This tests lines 586-590, 621-625 (JSON source accessibility error)
2673        let _ = result;
2674    }
2675
2676    #[tokio::test]
2677    async fn test_validate_check_paths_snippets_and_commands() {
2678        let temp = TempDir::new().unwrap();
2679        let manifest_path = temp.path().join("agpm.toml");
2680
2681        // Create manifest with local dependencies for snippets and commands (not just agents)
2682        let mut manifest = crate::manifest::Manifest::new();
2683
2684        // Add local snippet
2685        manifest.snippets.insert(
2686            "local-snippet".to_string(),
2687            crate::manifest::ResourceDependency::Detailed(Box::new(
2688                crate::manifest::DetailedDependency {
2689                    source: None,
2690                    path: "./snippets/local.md".to_string(),
2691                    version: None,
2692                    command: None,
2693                    branch: None,
2694                    rev: None,
2695                    args: None,
2696                    target: None,
2697                    filename: None,
2698                    dependencies: None,
2699                    tool: Some("claude-code".to_string()),
2700                    flatten: None,
2701                    install: None,
2702
2703                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2704                },
2705            )),
2706        );
2707
2708        // Add local command
2709        manifest.commands.insert(
2710            "local-command".to_string(),
2711            crate::manifest::ResourceDependency::Detailed(Box::new(
2712                crate::manifest::DetailedDependency {
2713                    source: None,
2714                    path: "./commands/deploy.md".to_string(),
2715                    version: None,
2716                    command: None,
2717                    branch: None,
2718                    rev: None,
2719                    args: None,
2720                    target: None,
2721                    filename: None,
2722                    dependencies: None,
2723                    tool: Some("claude-code".to_string()),
2724                    flatten: None,
2725                    install: None,
2726
2727                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2728                },
2729            )),
2730        );
2731
2732        manifest.save(&manifest_path).unwrap();
2733
2734        // Create the referenced files
2735        std::fs::create_dir_all(temp.path().join("snippets")).unwrap();
2736        std::fs::create_dir_all(temp.path().join("commands")).unwrap();
2737        std::fs::write(temp.path().join("snippets/local.md"), "# Local Snippet").unwrap();
2738        std::fs::write(temp.path().join("commands/deploy.md"), "# Deploy Command").unwrap();
2739
2740        let cmd = ValidateCommand {
2741            file: None,
2742            resolve: false,
2743            check_lock: false,
2744            sources: false,
2745            paths: true, // Check paths for all resource types
2746            format: OutputFormat::Text,
2747            verbose: false,
2748            quiet: false,
2749            strict: false,
2750            render: false,
2751        };
2752
2753        let result = cmd.execute_from_path(manifest_path).await;
2754        assert!(result.is_ok());
2755        // This tests path checking for snippets and commands, not just agents
2756    }
2757
2758    #[tokio::test]
2759    async fn test_validate_check_paths_missing_snippets_json() {
2760        let temp = TempDir::new().unwrap();
2761        let manifest_path = temp.path().join("agpm.toml");
2762
2763        // Create manifest with missing local snippet
2764        let mut manifest = crate::manifest::Manifest::new();
2765        manifest.snippets.insert(
2766            "missing-snippet".to_string(),
2767            crate::manifest::ResourceDependency::Detailed(Box::new(
2768                crate::manifest::DetailedDependency {
2769                    source: None,
2770                    path: "./missing/snippet.md".to_string(),
2771                    version: None,
2772                    command: None,
2773                    branch: None,
2774                    rev: None,
2775                    args: None,
2776                    target: None,
2777                    filename: None,
2778                    dependencies: None,
2779                    tool: Some("claude-code".to_string()),
2780                    flatten: None,
2781                    install: None,
2782
2783                    template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
2784                },
2785            )),
2786        );
2787        manifest.save(&manifest_path).unwrap();
2788
2789        let cmd = ValidateCommand {
2790            file: None,
2791            resolve: false,
2792            check_lock: false,
2793            sources: false,
2794            paths: true,
2795            format: OutputFormat::Json, // Test JSON output for missing paths
2796            verbose: false,
2797            quiet: true,
2798            strict: false,
2799            render: false,
2800        };
2801
2802        let result = cmd.execute_from_path(manifest_path).await;
2803        assert!(result.is_err());
2804        // This tests lines 734-738 (JSON output for missing local paths)
2805    }
2806
2807    #[tokio::test]
2808    async fn test_validate_lockfile_missing_warning() {
2809        let temp = TempDir::new().unwrap();
2810        let manifest_path = temp.path().join("agpm.toml");
2811
2812        // Create manifest but no lockfile
2813        let manifest = crate::manifest::Manifest::new();
2814        manifest.save(&manifest_path).unwrap();
2815
2816        let cmd = ValidateCommand {
2817            file: None,
2818            resolve: false,
2819            check_lock: true,
2820            sources: false,
2821            paths: false,
2822            format: OutputFormat::Text,
2823            verbose: true, // Test verbose mode with lockfile check
2824            quiet: false,
2825            strict: false,
2826            render: false,
2827        };
2828
2829        let result = cmd.execute_from_path(manifest_path).await;
2830        assert!(result.is_ok());
2831        // This tests lines 759, 753-756 (verbose mode and missing lockfile warning)
2832    }
2833
2834    #[tokio::test]
2835    async fn test_validate_lockfile_syntax_error_json() {
2836        let temp = TempDir::new().unwrap();
2837        let manifest_path = temp.path().join("agpm.toml");
2838        let lockfile_path = temp.path().join("agpm.lock");
2839
2840        // Create valid manifest
2841        let manifest = crate::manifest::Manifest::new();
2842        manifest.save(&manifest_path).unwrap();
2843
2844        // Create invalid lockfile
2845        std::fs::write(&lockfile_path, "invalid toml [[[").unwrap();
2846
2847        let cmd = ValidateCommand {
2848            file: None,
2849            resolve: false,
2850            check_lock: true,
2851            sources: false,
2852            paths: false,
2853            format: OutputFormat::Json,
2854            verbose: false,
2855            quiet: true,
2856            strict: false,
2857            render: false,
2858        };
2859
2860        let result = cmd.execute_from_path(manifest_path).await;
2861        assert!(result.is_err());
2862        // This tests lines 829-834 (JSON output for invalid lockfile syntax)
2863    }
2864
2865    #[tokio::test]
2866    async fn test_validate_lockfile_missing_dependencies() {
2867        let temp = TempDir::new().unwrap();
2868        let manifest_path = temp.path().join("agpm.toml");
2869        let lockfile_path = temp.path().join("agpm.lock");
2870
2871        // Create manifest with dependencies
2872        let mut manifest = crate::manifest::Manifest::new();
2873        manifest.add_dependency(
2874            "missing-agent".to_string(),
2875            crate::manifest::ResourceDependency::Simple("test.md".to_string()),
2876            true,
2877        );
2878        manifest.add_dependency(
2879            "missing-snippet".to_string(),
2880            crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
2881            false,
2882        );
2883        manifest.save(&manifest_path).unwrap();
2884
2885        // Create empty lockfile (missing the manifest dependencies)
2886        let lockfile = crate::lockfile::LockFile::new();
2887        lockfile.save(&lockfile_path).unwrap();
2888
2889        let cmd = ValidateCommand {
2890            file: None,
2891            resolve: false,
2892            check_lock: true,
2893            sources: false,
2894            paths: false,
2895            format: OutputFormat::Text,
2896            verbose: false,
2897            quiet: false,
2898            strict: false,
2899            render: false,
2900        };
2901
2902        let result = cmd.execute_from_path(manifest_path).await;
2903        assert!(result.is_ok()); // Missing dependencies are warnings, not errors
2904        // This tests lines 775-777, 811-822 (missing dependencies in lockfile)
2905    }
2906
2907    #[tokio::test]
2908    async fn test_validate_lockfile_extra_entries_error() {
2909        let temp = TempDir::new().unwrap();
2910        let manifest_path = temp.path().join("agpm.toml");
2911        let lockfile_path = temp.path().join("agpm.lock");
2912
2913        // Create empty manifest
2914        let manifest = crate::manifest::Manifest::new();
2915        manifest.save(&manifest_path).unwrap();
2916
2917        // Create lockfile with extra entries
2918        let mut lockfile = crate::lockfile::LockFile::new();
2919        lockfile.agents.push(crate::lockfile::LockedResource {
2920            name: "extra-agent".to_string(),
2921            source: Some("test".to_string()),
2922            url: Some("https://github.com/test/repo.git".to_string()),
2923            path: "test.md".to_string(),
2924            version: None,
2925            resolved_commit: Some("abc123".to_string()),
2926            checksum: "sha256:dummy".to_string(),
2927            installed_at: "agents/extra-agent.md".to_string(),
2928            dependencies: vec![],
2929            resource_type: crate::core::ResourceType::Agent,
2930
2931            tool: Some("claude-code".to_string()),
2932            manifest_alias: None,
2933            context_checksum: None,
2934            applied_patches: std::collections::BTreeMap::new(),
2935            install: None,
2936            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
2937        });
2938        lockfile.save(&lockfile_path).unwrap();
2939
2940        let cmd = ValidateCommand {
2941            file: None,
2942            resolve: false,
2943            check_lock: true,
2944            sources: false,
2945            paths: false,
2946            format: OutputFormat::Json,
2947            verbose: false,
2948            quiet: true,
2949            strict: false,
2950            render: false,
2951        };
2952
2953        let result = cmd.execute_from_path(manifest_path).await;
2954        assert!(result.is_err()); // Extra entries cause errors
2955        // This tests lines 801-804, 807 (extra entries in lockfile error)
2956    }
2957
2958    #[tokio::test]
2959    async fn test_validate_strict_mode_with_json_output() {
2960        let temp = TempDir::new().unwrap();
2961        let manifest_path = temp.path().join("agpm.toml");
2962
2963        // Create manifest that will generate warnings
2964        let manifest = crate::manifest::Manifest::new(); // Empty manifest generates "no dependencies" warning
2965        manifest.save(&manifest_path).unwrap();
2966
2967        let cmd = ValidateCommand {
2968            file: None,
2969            resolve: false,
2970            check_lock: false,
2971            sources: false,
2972            paths: false,
2973            format: OutputFormat::Json,
2974            verbose: false,
2975            quiet: true,
2976            strict: true, // Strict mode with JSON output
2977            render: false,
2978        };
2979
2980        let result = cmd.execute_from_path(manifest_path).await;
2981        assert!(result.is_err()); // Strict mode treats warnings as errors
2982        // This tests lines 849-852 (strict mode with JSON output)
2983    }
2984
2985    #[tokio::test]
2986    async fn test_validate_strict_mode_text_output() {
2987        let temp = TempDir::new().unwrap();
2988        let manifest_path = temp.path().join("agpm.toml");
2989
2990        // Create manifest that will generate warnings
2991        let manifest = crate::manifest::Manifest::new();
2992        manifest.save(&manifest_path).unwrap();
2993
2994        let cmd = ValidateCommand {
2995            file: None,
2996            resolve: false,
2997            check_lock: false,
2998            sources: false,
2999            paths: false,
3000            format: OutputFormat::Text,
3001            verbose: false,
3002            quiet: false, // Not quiet - should print error message
3003            strict: true,
3004            render: false,
3005        };
3006
3007        let result = cmd.execute_from_path(manifest_path).await;
3008        assert!(result.is_err());
3009        // This tests lines 854-855 (strict mode with text output)
3010    }
3011
3012    #[tokio::test]
3013    async fn test_validate_final_success_with_warnings() {
3014        let temp = TempDir::new().unwrap();
3015        let manifest_path = temp.path().join("agpm.toml");
3016
3017        // Create manifest that will have warnings but no errors
3018        let manifest = crate::manifest::Manifest::new();
3019        manifest.save(&manifest_path).unwrap();
3020
3021        let cmd = ValidateCommand {
3022            file: None,
3023            resolve: false,
3024            check_lock: false,
3025            sources: false,
3026            paths: false,
3027            format: OutputFormat::Text,
3028            verbose: false,
3029            quiet: false,
3030            strict: false, // Not strict - warnings don't cause failure
3031            render: false,
3032        };
3033
3034        let result = cmd.execute_from_path(manifest_path).await;
3035        assert!(result.is_ok());
3036        // This tests the final success path with warnings displayed (lines 872-879)
3037    }
3038
3039    #[tokio::test]
3040    async fn test_validate_verbose_mode_with_summary() {
3041        let temp = TempDir::new().unwrap();
3042        let manifest_path = temp.path().join("agpm.toml");
3043
3044        // Create manifest with some content for summary
3045        let mut manifest = crate::manifest::Manifest::new();
3046        manifest.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
3047        manifest.add_dependency(
3048            "test-agent".to_string(),
3049            crate::manifest::ResourceDependency::Simple("test.md".to_string()),
3050            true,
3051        );
3052        manifest.add_dependency(
3053            "test-snippet".to_string(),
3054            crate::manifest::ResourceDependency::Simple("snippet.md".to_string()),
3055            false,
3056        );
3057        manifest.save(&manifest_path).unwrap();
3058
3059        let cmd = ValidateCommand {
3060            file: None,
3061            resolve: false,
3062            check_lock: false,
3063            sources: false,
3064            paths: false,
3065            format: OutputFormat::Text,
3066            verbose: true, // Verbose mode to show summary
3067            quiet: false,
3068            strict: false,
3069            render: false,
3070        };
3071
3072        let result = cmd.execute_from_path(manifest_path).await;
3073        assert!(result.is_ok());
3074        // This tests lines 484-490 (verbose mode summary output)
3075    }
3076
3077    #[tokio::test]
3078    async fn test_validate_all_checks_enabled() {
3079        let temp = TempDir::new().unwrap();
3080        let manifest_path = temp.path().join("agpm.toml");
3081        let lockfile_path = temp.path().join("agpm.lock");
3082
3083        // Create a manifest with dependencies
3084        let mut manifest = Manifest::new();
3085        manifest.agents.insert(
3086            "test-agent".to_string(),
3087            ResourceDependency::Simple("local-agent.md".to_string()),
3088        );
3089        manifest.save(&manifest_path).unwrap();
3090
3091        // Create lockfile
3092        let lockfile = crate::lockfile::LockFile::new();
3093        lockfile.save(&lockfile_path).unwrap();
3094
3095        let cmd = ValidateCommand {
3096            file: None,
3097            resolve: true,
3098            check_lock: true,
3099            sources: true,
3100            paths: true,
3101            format: OutputFormat::Text,
3102            verbose: true,
3103            quiet: false,
3104            strict: true,
3105            render: false,
3106        };
3107
3108        let result = cmd.execute_from_path(manifest_path).await;
3109        // May have warnings but should complete
3110        assert!(result.is_err() || result.is_ok());
3111    }
3112
3113    #[tokio::test]
3114    async fn test_validate_with_specific_file_path() {
3115        let temp = TempDir::new().unwrap();
3116        let custom_path = temp.path().join("custom-manifest.toml");
3117
3118        let manifest = Manifest::new();
3119        manifest.save(&custom_path).unwrap();
3120
3121        let cmd = ValidateCommand {
3122            file: Some(custom_path.to_string_lossy().to_string()),
3123            resolve: false,
3124            check_lock: false,
3125            sources: false,
3126            paths: false,
3127            format: OutputFormat::Text,
3128            verbose: false,
3129            quiet: false,
3130            strict: false,
3131            render: false,
3132        };
3133
3134        let result = cmd.execute().await;
3135        assert!(result.is_ok());
3136    }
3137
3138    #[tokio::test]
3139    async fn test_validate_sources_check_with_invalid_url() {
3140        let temp = TempDir::new().unwrap();
3141        let manifest_path = temp.path().join("agpm.toml");
3142
3143        let mut manifest = Manifest::new();
3144        manifest.sources.insert("invalid".to_string(), "not-a-valid-url".to_string());
3145        manifest.save(&manifest_path).unwrap();
3146
3147        let cmd = ValidateCommand {
3148            file: None,
3149            resolve: false,
3150            check_lock: false,
3151            sources: true,
3152            paths: false,
3153            format: OutputFormat::Text,
3154            verbose: false,
3155            quiet: false,
3156            strict: false,
3157            render: false,
3158        };
3159
3160        let result = cmd.execute_from_path(manifest_path).await;
3161        assert!(result.is_err()); // Should fail with invalid URL error
3162    }
3163
3164    #[tokio::test]
3165    async fn test_validation_results_with_errors_and_warnings() {
3166        let mut results = ValidationResults::default();
3167
3168        // Add errors
3169        results.errors.push("Error 1".to_string());
3170        results.errors.push("Error 2".to_string());
3171
3172        // Add warnings
3173        results.warnings.push("Warning 1".to_string());
3174        results.warnings.push("Warning 2".to_string());
3175
3176        assert!(!results.errors.is_empty());
3177        assert_eq!(results.errors.len(), 2);
3178        assert_eq!(results.warnings.len(), 2);
3179    }
3180
3181    #[tokio::test]
3182    async fn test_output_format_equality() {
3183        // Test PartialEq implementation
3184        assert_eq!(OutputFormat::Text, OutputFormat::Text);
3185        assert_eq!(OutputFormat::Json, OutputFormat::Json);
3186        assert_ne!(OutputFormat::Text, OutputFormat::Json);
3187    }
3188
3189    #[tokio::test]
3190    async fn test_validate_command_defaults() {
3191        let cmd = ValidateCommand {
3192            file: None,
3193            resolve: false,
3194            check_lock: false,
3195            sources: false,
3196            paths: false,
3197            format: OutputFormat::Text,
3198            verbose: false,
3199            quiet: false,
3200            strict: false,
3201            render: false,
3202        };
3203        assert_eq!(cmd.file, None);
3204        assert!(!cmd.resolve);
3205        assert!(!cmd.check_lock);
3206        assert!(!cmd.sources);
3207        assert!(!cmd.paths);
3208        assert_eq!(cmd.format, OutputFormat::Text);
3209        assert!(!cmd.verbose);
3210        assert!(!cmd.quiet);
3211        assert!(!cmd.strict);
3212    }
3213
3214    #[tokio::test]
3215    async fn test_json_output_format() {
3216        let temp = TempDir::new().unwrap();
3217        let manifest_path = temp.path().join("agpm.toml");
3218
3219        let manifest = Manifest::new();
3220        manifest.save(&manifest_path).unwrap();
3221
3222        let cmd = ValidateCommand {
3223            file: None,
3224            resolve: false,
3225            check_lock: false,
3226            sources: false,
3227            paths: false,
3228            format: OutputFormat::Json,
3229            verbose: false,
3230            quiet: false,
3231            strict: false,
3232            render: false,
3233        };
3234
3235        let result = cmd.execute_from_path(manifest_path).await;
3236        assert!(result.is_ok());
3237    }
3238
3239    #[tokio::test]
3240    async fn test_validation_with_verbose_mode() {
3241        let temp = TempDir::new().unwrap();
3242        let manifest_path = temp.path().join("agpm.toml");
3243
3244        let manifest = Manifest::new();
3245        manifest.save(&manifest_path).unwrap();
3246
3247        let cmd = ValidateCommand {
3248            file: None,
3249            resolve: false,
3250            check_lock: false,
3251            sources: false,
3252            paths: false,
3253            format: OutputFormat::Text,
3254            verbose: true,
3255            quiet: false,
3256            strict: false,
3257            render: false,
3258        };
3259
3260        let result = cmd.execute_from_path(manifest_path).await;
3261        assert!(result.is_ok());
3262    }
3263
3264    #[tokio::test]
3265    async fn test_validation_with_quiet_mode() {
3266        let temp = TempDir::new().unwrap();
3267        let manifest_path = temp.path().join("agpm.toml");
3268
3269        let manifest = Manifest::new();
3270        manifest.save(&manifest_path).unwrap();
3271
3272        let cmd = ValidateCommand {
3273            file: None,
3274            resolve: false,
3275            check_lock: false,
3276            sources: false,
3277            paths: false,
3278            format: OutputFormat::Text,
3279            verbose: false,
3280            quiet: true,
3281            strict: false,
3282            render: false,
3283        };
3284
3285        let result = cmd.execute_from_path(manifest_path).await;
3286        assert!(result.is_ok());
3287    }
3288
3289    #[tokio::test]
3290    async fn test_validation_with_strict_mode_and_warnings() {
3291        let temp = TempDir::new().unwrap();
3292        let manifest_path = temp.path().join("agpm.toml");
3293
3294        // Create empty manifest to trigger warning
3295        let manifest = Manifest::new();
3296        manifest.save(&manifest_path).unwrap();
3297
3298        let cmd = ValidateCommand {
3299            file: None,
3300            resolve: false,
3301            check_lock: false,
3302            sources: false,
3303            paths: false,
3304            format: OutputFormat::Text,
3305            verbose: false,
3306            quiet: false,
3307            strict: true, // Strict mode will fail on warnings
3308            render: false,
3309        };
3310
3311        let result = cmd.execute_from_path(manifest_path).await;
3312        assert!(result.is_err()); // Should fail due to warning in strict mode
3313    }
3314
3315    #[tokio::test]
3316    async fn test_validation_with_local_paths_check() {
3317        let temp = TempDir::new().unwrap();
3318        let manifest_path = temp.path().join("agpm.toml");
3319
3320        let mut manifest = Manifest::new();
3321        manifest.agents.insert(
3322            "local-agent".to_string(),
3323            ResourceDependency::Simple("./missing-file.md".to_string()),
3324        );
3325        manifest.save(&manifest_path).unwrap();
3326
3327        let cmd = ValidateCommand {
3328            file: None,
3329            resolve: false,
3330            check_lock: false,
3331            sources: false,
3332            paths: true, // Enable path checking
3333            format: OutputFormat::Text,
3334            verbose: false,
3335            quiet: false,
3336            strict: false,
3337            render: false,
3338        };
3339
3340        let result = cmd.execute_from_path(manifest_path).await;
3341        assert!(result.is_err()); // Should fail due to missing local path
3342    }
3343
3344    #[tokio::test]
3345    async fn test_validation_with_existing_local_paths() {
3346        let temp = TempDir::new().unwrap();
3347        let manifest_path = temp.path().join("agpm.toml");
3348        let local_file = temp.path().join("agent.md");
3349
3350        // Create the local file
3351        std::fs::write(&local_file, "# Local Agent").unwrap();
3352
3353        let mut manifest = Manifest::new();
3354        manifest.agents.insert(
3355            "local-agent".to_string(),
3356            ResourceDependency::Simple("./agent.md".to_string()),
3357        );
3358        manifest.save(&manifest_path).unwrap();
3359
3360        let cmd = ValidateCommand {
3361            file: None,
3362            resolve: false,
3363            check_lock: false,
3364            sources: false,
3365            paths: true,
3366            format: OutputFormat::Text,
3367            verbose: false,
3368            quiet: false,
3369            strict: false,
3370            render: false,
3371        };
3372
3373        let result = cmd.execute_from_path(manifest_path).await;
3374        assert!(result.is_ok());
3375    }
3376
3377    #[tokio::test]
3378    async fn test_validation_with_lockfile_consistency_check_no_lockfile() {
3379        let temp = TempDir::new().unwrap();
3380        let manifest_path = temp.path().join("agpm.toml");
3381
3382        let mut manifest = Manifest::new();
3383        manifest
3384            .agents
3385            .insert("test-agent".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3386        manifest.save(&manifest_path).unwrap();
3387
3388        let cmd = ValidateCommand {
3389            file: None,
3390            resolve: false,
3391            check_lock: true, // Enable lockfile checking
3392            sources: false,
3393            paths: false,
3394            format: OutputFormat::Text,
3395            verbose: false,
3396            quiet: false,
3397            strict: false,
3398            render: false,
3399        };
3400
3401        let result = cmd.execute_from_path(manifest_path).await;
3402        assert!(result.is_ok()); // Should pass but with warning
3403    }
3404
3405    #[tokio::test]
3406    async fn test_validation_with_inconsistent_lockfile() {
3407        let temp = TempDir::new().unwrap();
3408        let manifest_path = temp.path().join("agpm.toml");
3409        let lockfile_path = temp.path().join("agpm.lock");
3410
3411        // Create manifest with agent
3412        let mut manifest = Manifest::new();
3413        manifest.agents.insert(
3414            "manifest-agent".to_string(),
3415            ResourceDependency::Simple("agent.md".to_string()),
3416        );
3417        manifest.save(&manifest_path).unwrap();
3418
3419        // Create lockfile with different agent
3420        let mut lockfile = crate::lockfile::LockFile::new();
3421        lockfile.agents.push(crate::lockfile::LockedResource {
3422            name: "lockfile-agent".to_string(),
3423            source: None,
3424            url: None,
3425            path: "agent.md".to_string(),
3426            version: None,
3427            resolved_commit: None,
3428            checksum: "sha256:dummy".to_string(),
3429            installed_at: "agents/lockfile-agent.md".to_string(),
3430            dependencies: vec![],
3431            resource_type: crate::core::ResourceType::Agent,
3432
3433            tool: Some("claude-code".to_string()),
3434            manifest_alias: None,
3435            context_checksum: None,
3436            applied_patches: std::collections::BTreeMap::new(),
3437            install: None,
3438            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3439        });
3440        lockfile.save(&lockfile_path).unwrap();
3441
3442        let cmd = ValidateCommand {
3443            file: None,
3444            resolve: false,
3445            check_lock: true,
3446            sources: false,
3447            paths: false,
3448            format: OutputFormat::Text,
3449            verbose: false,
3450            quiet: false,
3451            strict: false,
3452            render: false,
3453        };
3454
3455        let result = cmd.execute_from_path(manifest_path).await;
3456        assert!(result.is_err()); // Should fail due to inconsistency
3457    }
3458
3459    #[tokio::test]
3460    async fn test_validation_with_invalid_lockfile_syntax() {
3461        let temp = TempDir::new().unwrap();
3462        let manifest_path = temp.path().join("agpm.toml");
3463        let lockfile_path = temp.path().join("agpm.lock");
3464
3465        let manifest = Manifest::new();
3466        manifest.save(&manifest_path).unwrap();
3467
3468        // Write invalid TOML to lockfile
3469        std::fs::write(&lockfile_path, "invalid toml syntax [[[").unwrap();
3470
3471        let cmd = ValidateCommand {
3472            file: None,
3473            resolve: false,
3474            check_lock: true,
3475            sources: false,
3476            paths: false,
3477            format: OutputFormat::Text,
3478            verbose: false,
3479            quiet: false,
3480            strict: false,
3481            render: false,
3482        };
3483
3484        let result = cmd.execute_from_path(manifest_path).await;
3485        assert!(result.is_err()); // Should fail due to invalid lockfile
3486    }
3487
3488    #[tokio::test]
3489    async fn test_validation_with_outdated_version_warning() {
3490        let temp = TempDir::new().unwrap();
3491        let manifest_path = temp.path().join("agpm.toml");
3492
3493        let mut manifest = Manifest::new();
3494        // Add the source that's referenced
3495        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3496        manifest.agents.insert(
3497            "old-agent".to_string(),
3498            ResourceDependency::Detailed(Box::new(crate::manifest::DetailedDependency {
3499                source: Some("test".to_string()),
3500                path: "agent.md".to_string(),
3501                version: Some("v0.1.0".to_string()),
3502                branch: None,
3503                rev: None,
3504                command: None,
3505                args: None,
3506                target: None,
3507                filename: None,
3508                dependencies: None,
3509                tool: Some("claude-code".to_string()),
3510                flatten: None,
3511                install: None,
3512
3513                template_vars: Some(serde_json::Value::Object(serde_json::Map::new())),
3514            })),
3515        );
3516        manifest.save(&manifest_path).unwrap();
3517
3518        let cmd = ValidateCommand {
3519            file: None,
3520            resolve: false,
3521            check_lock: false,
3522            sources: false,
3523            paths: false,
3524            format: OutputFormat::Text,
3525            verbose: false,
3526            quiet: false,
3527            strict: false,
3528            render: false,
3529        };
3530
3531        let result = cmd.execute_from_path(manifest_path).await;
3532        assert!(result.is_ok()); // Should pass but with warning
3533    }
3534
3535    #[tokio::test]
3536    async fn test_validation_json_output_with_errors() {
3537        let temp = TempDir::new().unwrap();
3538        let manifest_path = temp.path().join("agpm.toml");
3539
3540        // Write invalid TOML
3541        std::fs::write(&manifest_path, "invalid toml [[[ syntax").unwrap();
3542
3543        let cmd = ValidateCommand {
3544            file: None,
3545            resolve: false,
3546            check_lock: false,
3547            sources: false,
3548            paths: false,
3549            format: OutputFormat::Json,
3550            verbose: false,
3551            quiet: false,
3552            strict: false,
3553            render: false,
3554        };
3555
3556        let result = cmd.execute_from_path(manifest_path).await;
3557        assert!(result.is_err());
3558    }
3559
3560    #[tokio::test]
3561    async fn test_validation_with_manifest_not_found_json() {
3562        let temp = TempDir::new().unwrap();
3563        let manifest_path = temp.path().join("nonexistent.toml");
3564
3565        let cmd = ValidateCommand {
3566            file: None,
3567            resolve: false,
3568            check_lock: false,
3569            sources: false,
3570            paths: false,
3571            format: OutputFormat::Json,
3572            verbose: false,
3573            quiet: false,
3574            strict: false,
3575            render: false,
3576        };
3577
3578        let result = cmd.execute_from_path(manifest_path).await;
3579        assert!(result.is_err());
3580    }
3581
3582    #[tokio::test]
3583    async fn test_validation_with_manifest_not_found_text() {
3584        let temp = TempDir::new().unwrap();
3585        let manifest_path = temp.path().join("nonexistent.toml");
3586
3587        let cmd = ValidateCommand {
3588            file: None,
3589            resolve: false,
3590            check_lock: false,
3591            sources: false,
3592            paths: false,
3593            format: OutputFormat::Text,
3594            verbose: false,
3595            quiet: false,
3596            strict: false,
3597            render: false,
3598        };
3599
3600        let result = cmd.execute_from_path(manifest_path).await;
3601        assert!(result.is_err());
3602    }
3603
3604    #[tokio::test]
3605    async fn test_validation_with_missing_lockfile_dependencies() {
3606        let temp = TempDir::new().unwrap();
3607        let manifest_path = temp.path().join("agpm.toml");
3608        let lockfile_path = temp.path().join("agpm.lock");
3609
3610        // Create manifest with multiple dependencies
3611        let mut manifest = Manifest::new();
3612        manifest
3613            .agents
3614            .insert("agent1".to_string(), ResourceDependency::Simple("agent1.md".to_string()));
3615        manifest
3616            .agents
3617            .insert("agent2".to_string(), ResourceDependency::Simple("agent2.md".to_string()));
3618        manifest
3619            .snippets
3620            .insert("snippet1".to_string(), ResourceDependency::Simple("snippet1.md".to_string()));
3621        manifest.save(&manifest_path).unwrap();
3622
3623        // Create lockfile missing some dependencies
3624        let mut lockfile = crate::lockfile::LockFile::new();
3625        lockfile.agents.push(crate::lockfile::LockedResource {
3626            name: "agent1".to_string(),
3627            source: None,
3628            url: None,
3629            path: "agent1.md".to_string(),
3630            version: None,
3631            resolved_commit: None,
3632            checksum: "sha256:dummy".to_string(),
3633            installed_at: "agents/agent1.md".to_string(),
3634            dependencies: vec![],
3635            resource_type: crate::core::ResourceType::Agent,
3636
3637            tool: Some("claude-code".to_string()),
3638            manifest_alias: None,
3639            context_checksum: None,
3640            applied_patches: std::collections::BTreeMap::new(),
3641            install: None,
3642            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3643        });
3644        lockfile.save(&lockfile_path).unwrap();
3645
3646        let cmd = ValidateCommand {
3647            file: None,
3648            resolve: false,
3649            check_lock: true,
3650            sources: false,
3651            paths: false,
3652            format: OutputFormat::Text,
3653            verbose: false,
3654            quiet: false,
3655            strict: false,
3656            render: false,
3657        };
3658
3659        let result = cmd.execute_from_path(manifest_path).await;
3660        assert!(result.is_ok()); // Should pass but report missing dependencies
3661    }
3662
3663    #[tokio::test]
3664    async fn test_execute_without_manifest_file() {
3665        // Test when no manifest file exists - use temp directory with specific non-existent file
3666        let temp = TempDir::new().unwrap();
3667        let non_existent_manifest = temp.path().join("non_existent.toml");
3668
3669        let cmd = ValidateCommand {
3670            file: Some(non_existent_manifest.to_string_lossy().to_string()),
3671            resolve: false,
3672            check_lock: false,
3673            sources: false,
3674            paths: false,
3675            format: OutputFormat::Text,
3676            verbose: false,
3677            quiet: false,
3678            strict: false,
3679            render: false,
3680        };
3681
3682        let result = cmd.execute().await;
3683        assert!(result.is_err()); // Should fail when no manifest found
3684    }
3685
3686    #[tokio::test]
3687    async fn test_execute_with_specified_file() {
3688        let temp = TempDir::new().unwrap();
3689        let custom_path = temp.path().join("custom.toml");
3690
3691        let manifest = Manifest::new();
3692        manifest.save(&custom_path).unwrap();
3693
3694        let cmd = ValidateCommand {
3695            file: Some(custom_path.to_string_lossy().to_string()),
3696            resolve: false,
3697            check_lock: false,
3698            sources: false,
3699            paths: false,
3700            format: OutputFormat::Text,
3701            verbose: false,
3702            quiet: false,
3703            strict: false,
3704            render: false,
3705        };
3706
3707        let result = cmd.execute().await;
3708        assert!(result.is_ok());
3709    }
3710
3711    #[tokio::test]
3712    async fn test_execute_with_nonexistent_specified_file() {
3713        let temp = TempDir::new().unwrap();
3714        let nonexistent = temp.path().join("nonexistent.toml");
3715
3716        let cmd = ValidateCommand {
3717            file: Some(nonexistent.to_string_lossy().to_string()),
3718            resolve: false,
3719            check_lock: false,
3720            sources: false,
3721            paths: false,
3722            format: OutputFormat::Text,
3723            verbose: false,
3724            quiet: false,
3725            strict: false,
3726            render: false,
3727        };
3728
3729        let result = cmd.execute().await;
3730        assert!(result.is_err());
3731    }
3732
3733    #[tokio::test]
3734    async fn test_validation_with_verbose_and_text_format() {
3735        let temp = TempDir::new().unwrap();
3736        let manifest_path = temp.path().join("agpm.toml");
3737
3738        let mut manifest = Manifest::new();
3739        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3740        manifest
3741            .agents
3742            .insert("agent1".to_string(), ResourceDependency::Simple("agent.md".to_string()));
3743        manifest
3744            .snippets
3745            .insert("snippet1".to_string(), ResourceDependency::Simple("snippet.md".to_string()));
3746        manifest.save(&manifest_path).unwrap();
3747
3748        let cmd = ValidateCommand {
3749            file: None,
3750            resolve: false,
3751            check_lock: false,
3752            sources: false,
3753            paths: false,
3754            format: OutputFormat::Text,
3755            verbose: true,
3756            quiet: false,
3757            strict: false,
3758            render: false,
3759        };
3760
3761        let result = cmd.execute_from_path(manifest_path).await;
3762        assert!(result.is_ok());
3763    }
3764
3765    #[tokio::test]
3766    async fn test_file_reference_validation_with_valid_references() {
3767        use crate::lockfile::LockedResource;
3768        use std::fs;
3769
3770        let temp = TempDir::new().unwrap();
3771        let project_dir = temp.path();
3772
3773        // Create manifest
3774        let manifest_path = project_dir.join("agpm.toml");
3775        let mut manifest = Manifest::new();
3776        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3777        manifest.save(&manifest_path).unwrap();
3778
3779        // Create referenced files
3780        let snippets_dir = project_dir.join(".agpm").join("snippets");
3781        fs::create_dir_all(&snippets_dir).unwrap();
3782        fs::write(snippets_dir.join("helper.md"), "# Helper\nSome content").unwrap();
3783
3784        // Create agent with valid file reference
3785        let agents_dir = project_dir.join(".claude").join("agents");
3786        fs::create_dir_all(&agents_dir).unwrap();
3787        let agent_content = r#"---
3788title: Test Agent
3789---
3790
3791# Test Agent
3792
3793See [helper](.agpm/snippets/helper.md) for details.
3794"#;
3795        fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3796
3797        // Create lockfile
3798        let lockfile_path = project_dir.join("agpm.lock");
3799        let mut lockfile = crate::lockfile::LockFile::default();
3800        lockfile.agents.push(LockedResource {
3801            name: "test-agent".to_string(),
3802            source: None,
3803            path: "agents/test.md".to_string(),
3804            version: Some("v1.0.0".to_string()),
3805            resolved_commit: None,
3806            url: None,
3807            checksum: "abc123".to_string(),
3808            installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3809            dependencies: vec![],
3810            resource_type: crate::core::ResourceType::Agent,
3811            tool: None,
3812            manifest_alias: None,
3813            context_checksum: None,
3814            applied_patches: std::collections::BTreeMap::new(),
3815            install: None,
3816            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3817        });
3818        lockfile.save(&lockfile_path).unwrap();
3819
3820        let cmd = ValidateCommand {
3821            file: None,
3822            resolve: false,
3823            check_lock: false,
3824            sources: false,
3825            paths: false,
3826            format: OutputFormat::Text,
3827            verbose: true,
3828            quiet: false,
3829            strict: false,
3830            render: true,
3831        };
3832
3833        let result = cmd.execute_from_path(manifest_path).await;
3834        assert!(result.is_ok());
3835    }
3836
3837    #[tokio::test]
3838    async fn test_file_reference_validation_with_broken_references() {
3839        use crate::lockfile::LockedResource;
3840        use std::fs;
3841
3842        let temp = TempDir::new().unwrap();
3843        let project_dir = temp.path();
3844
3845        // Create manifest
3846        let manifest_path = project_dir.join("agpm.toml");
3847        let mut manifest = Manifest::new();
3848        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3849        manifest.save(&manifest_path).unwrap();
3850
3851        // Create agent with broken file reference (file doesn't exist)
3852        let agents_dir = project_dir.join(".claude").join("agents");
3853        fs::create_dir_all(&agents_dir).unwrap();
3854        let agent_content = r#"---
3855title: Test Agent
3856---
3857
3858# Test Agent
3859
3860See [missing](.agpm/snippets/missing.md) for details.
3861Also check `.claude/nonexistent.md`.
3862"#;
3863        fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3864
3865        // Create lockfile
3866        let lockfile_path = project_dir.join("agpm.lock");
3867        let mut lockfile = crate::lockfile::LockFile::default();
3868        lockfile.agents.push(LockedResource {
3869            name: "test-agent".to_string(),
3870            source: None,
3871            path: "agents/test.md".to_string(),
3872            version: Some("v1.0.0".to_string()),
3873            resolved_commit: None,
3874            url: None,
3875            checksum: "abc123".to_string(),
3876            installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3877            dependencies: vec![],
3878            resource_type: crate::core::ResourceType::Agent,
3879            tool: None,
3880            manifest_alias: None,
3881            context_checksum: None,
3882            applied_patches: std::collections::BTreeMap::new(),
3883            install: None,
3884            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3885        });
3886        lockfile.save(&lockfile_path).unwrap();
3887
3888        let cmd = ValidateCommand {
3889            file: None,
3890            resolve: false,
3891            check_lock: false,
3892            sources: false,
3893            paths: false,
3894            format: OutputFormat::Text,
3895            verbose: true,
3896            quiet: false,
3897            strict: false,
3898            render: true,
3899        };
3900
3901        let result = cmd.execute_from_path(manifest_path).await;
3902        assert!(result.is_err());
3903        let err_msg = format!("{:?}", result.unwrap_err());
3904        assert!(err_msg.contains("File reference validation failed"));
3905    }
3906
3907    #[tokio::test]
3908    async fn test_file_reference_validation_ignores_urls() {
3909        use crate::lockfile::LockedResource;
3910        use std::fs;
3911
3912        let temp = TempDir::new().unwrap();
3913        let project_dir = temp.path();
3914
3915        // Create manifest
3916        let manifest_path = project_dir.join("agpm.toml");
3917        let mut manifest = Manifest::new();
3918        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3919        manifest.save(&manifest_path).unwrap();
3920
3921        // Create agent with URL references (should be ignored)
3922        let agents_dir = project_dir.join(".claude").join("agents");
3923        fs::create_dir_all(&agents_dir).unwrap();
3924        let agent_content = r#"---
3925title: Test Agent
3926---
3927
3928# Test Agent
3929
3930Check [GitHub](https://github.com/user/repo) for source.
3931Visit http://example.com for more info.
3932"#;
3933        fs::write(agents_dir.join("test.md"), agent_content).unwrap();
3934
3935        // Create lockfile
3936        let lockfile_path = project_dir.join("agpm.lock");
3937        let mut lockfile = crate::lockfile::LockFile::default();
3938        lockfile.agents.push(LockedResource {
3939            name: "test-agent".to_string(),
3940            source: None,
3941            path: "agents/test.md".to_string(),
3942            version: Some("v1.0.0".to_string()),
3943            resolved_commit: None,
3944            url: None,
3945            checksum: "abc123".to_string(),
3946            installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
3947            dependencies: vec![],
3948            resource_type: crate::core::ResourceType::Agent,
3949            tool: None,
3950            manifest_alias: None,
3951            context_checksum: None,
3952            applied_patches: std::collections::BTreeMap::new(),
3953            install: None,
3954            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
3955        });
3956        lockfile.save(&lockfile_path).unwrap();
3957
3958        let cmd = ValidateCommand {
3959            file: None,
3960            resolve: false,
3961            check_lock: false,
3962            sources: false,
3963            paths: false,
3964            format: OutputFormat::Text,
3965            verbose: true,
3966            quiet: false,
3967            strict: false,
3968            render: true,
3969        };
3970
3971        let result = cmd.execute_from_path(manifest_path).await;
3972        assert!(result.is_ok());
3973    }
3974
3975    #[tokio::test]
3976    async fn test_file_reference_validation_ignores_code_blocks() {
3977        use crate::lockfile::LockedResource;
3978        use std::fs;
3979
3980        let temp = TempDir::new().unwrap();
3981        let project_dir = temp.path();
3982
3983        // Create manifest
3984        let manifest_path = project_dir.join("agpm.toml");
3985        let mut manifest = Manifest::new();
3986        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
3987        manifest.save(&manifest_path).unwrap();
3988
3989        // Create agent with file references in code blocks (should be ignored)
3990        let agents_dir = project_dir.join(".claude").join("agents");
3991        fs::create_dir_all(&agents_dir).unwrap();
3992        let agent_content = r#"---
3993title: Test Agent
3994---
3995
3996# Test Agent
3997
3998```bash
3999# This reference in code should be ignored
4000cat .agpm/snippets/nonexistent.md
4001```
4002
4003Inline code `example.md` should also be ignored.
4004"#;
4005        fs::write(agents_dir.join("test.md"), agent_content).unwrap();
4006
4007        // Create lockfile
4008        let lockfile_path = project_dir.join("agpm.lock");
4009        let mut lockfile = crate::lockfile::LockFile::default();
4010        lockfile.agents.push(LockedResource {
4011            name: "test-agent".to_string(),
4012            source: None,
4013            path: "agents/test.md".to_string(),
4014            version: Some("v1.0.0".to_string()),
4015            resolved_commit: None,
4016            url: None,
4017            checksum: "abc123".to_string(),
4018            installed_at: normalize_path_for_storage(agents_dir.join("test.md")),
4019            dependencies: vec![],
4020            resource_type: crate::core::ResourceType::Agent,
4021            tool: None,
4022            manifest_alias: None,
4023            context_checksum: None,
4024            applied_patches: std::collections::BTreeMap::new(),
4025            install: None,
4026            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4027        });
4028        lockfile.save(&lockfile_path).unwrap();
4029
4030        let cmd = ValidateCommand {
4031            file: None,
4032            resolve: false,
4033            check_lock: false,
4034            sources: false,
4035            paths: false,
4036            format: OutputFormat::Text,
4037            verbose: true,
4038            quiet: false,
4039            strict: false,
4040            render: true,
4041        };
4042
4043        let result = cmd.execute_from_path(manifest_path).await;
4044        assert!(result.is_ok());
4045    }
4046
4047    #[tokio::test]
4048    async fn test_file_reference_validation_multiple_resources() {
4049        use crate::lockfile::LockedResource;
4050        use std::fs;
4051
4052        let temp = TempDir::new().unwrap();
4053        let project_dir = temp.path();
4054
4055        // Create manifest
4056        let manifest_path = project_dir.join("agpm.toml");
4057        let mut manifest = Manifest::new();
4058        manifest.sources.insert("test".to_string(), "https://github.com/test/repo.git".to_string());
4059        manifest.save(&manifest_path).unwrap();
4060
4061        // Create referenced snippets
4062        let snippets_dir = project_dir.join(".agpm").join("snippets");
4063        fs::create_dir_all(&snippets_dir).unwrap();
4064        fs::write(snippets_dir.join("util.md"), "# Utilities").unwrap();
4065
4066        // Create agent with valid reference
4067        let agents_dir = project_dir.join(".claude").join("agents");
4068        fs::create_dir_all(&agents_dir).unwrap();
4069        fs::write(agents_dir.join("agent1.md"), "# Agent 1\n\nSee [util](.agpm/snippets/util.md).")
4070            .unwrap();
4071
4072        // Create command with broken reference
4073        let commands_dir = project_dir.join(".claude").join("commands");
4074        fs::create_dir_all(&commands_dir).unwrap();
4075        fs::write(commands_dir.join("cmd1.md"), "# Command\n\nCheck `.agpm/snippets/missing.md`.")
4076            .unwrap();
4077
4078        // Create lockfile
4079        let lockfile_path = project_dir.join("agpm.lock");
4080        let mut lockfile = crate::lockfile::LockFile::default();
4081        lockfile.agents.push(LockedResource {
4082            name: "agent1".to_string(),
4083            source: None,
4084            path: "agents/agent1.md".to_string(),
4085            version: Some("v1.0.0".to_string()),
4086            resolved_commit: None,
4087            url: None,
4088            checksum: "abc123".to_string(),
4089            installed_at: normalize_path_for_storage(agents_dir.join("agent1.md")),
4090            dependencies: vec![],
4091            resource_type: crate::core::ResourceType::Agent,
4092            tool: None,
4093            manifest_alias: None,
4094            context_checksum: None,
4095            applied_patches: std::collections::BTreeMap::new(),
4096            install: None,
4097            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4098        });
4099        lockfile.commands.push(LockedResource {
4100            name: "cmd1".to_string(),
4101            source: None,
4102            path: "commands/cmd1.md".to_string(),
4103            version: Some("v1.0.0".to_string()),
4104            resolved_commit: None,
4105            url: None,
4106            checksum: "def456".to_string(),
4107            installed_at: normalize_path_for_storage(commands_dir.join("cmd1.md")),
4108            dependencies: vec![],
4109            resource_type: crate::core::ResourceType::Command,
4110            tool: None,
4111            manifest_alias: None,
4112            context_checksum: None,
4113            applied_patches: std::collections::BTreeMap::new(),
4114            install: None,
4115            variant_inputs: crate::resolver::lockfile_builder::VariantInputs::default(),
4116        });
4117        lockfile.save(&lockfile_path).unwrap();
4118
4119        let cmd = ValidateCommand {
4120            file: None,
4121            resolve: false,
4122            check_lock: false,
4123            sources: false,
4124            paths: false,
4125            format: OutputFormat::Text,
4126            verbose: true,
4127            quiet: false,
4128            strict: false,
4129            render: true,
4130        };
4131
4132        let result = cmd.execute_from_path(manifest_path).await;
4133        assert!(result.is_err());
4134        let err_msg = format!("{:?}", result.unwrap_err());
4135        assert!(err_msg.contains("File reference validation failed"));
4136    }
4137}