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