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