pmat 3.17.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg_attr(coverage_nightly, coverage(off))]
//! Error types for agent scaffolding operations.

use std::path::PathBuf;
use thiserror::Error;

/// Errors that can occur during agent scaffolding.
#[derive(Debug, Error)]
pub enum ScaffoldError {
    /// Template not found.
    #[error("Template not found: {0}")]
    TemplateNotFound(String),

    /// Invalid agent configuration.
    #[error("Invalid agent configuration: {0}")]
    InvalidConfiguration(String),

    /// Directory already exists.
    #[error("Directory already exists: {}", .0.display())]
    DirectoryExists(PathBuf),

    /// Template generation failed.
    #[error("Template generation failed")]
    GenerationFailed(#[source] anyhow::Error),

    /// Post-generation hook failed.
    #[error("Post-generation hook failed: {0}")]
    HookFailed(String),

    /// Template validation failed.
    #[error("Template validation failed")]
    ValidationFailed(#[source] anyhow::Error),

    /// Incompatible template version.
    #[error("Incompatible template version: requires PMAT {required}, but running {current}")]
    IncompatibleVersion {
        /// Required version.
        required: String,
        /// Current version.
        current: String,
    },

    /// Missing required feature.
    #[error("Missing required feature: {0}")]
    MissingFeature(String),

    /// I/O error.
    #[error("I/O error")]
    IoError(#[from] std::io::Error),

    /// Template rendering error.
    #[error("Template rendering failed: {0}")]
    RenderError(String),

    /// Invalid template syntax.
    #[error("Invalid template syntax: {0}")]
    InvalidTemplate(String),

    /// User cancelled operation.
    #[error("Operation cancelled by user")]
    UserCancelled,

    /// Network error (for remote templates).
    #[error("Network error: {0}")]
    NetworkError(String),

    /// Parse error.
    #[error("Parse error: {0}")]
    ParseError(String),
}

// Note: anyhow already provides a blanket From implementation for Error types

/// Result type for scaffold operations.
pub type ScaffoldResult<T> = Result<T, ScaffoldError>;

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_display() {
        let err = ScaffoldError::TemplateNotFound("my-template".to_string());
        assert_eq!(err.to_string(), "Template not found: my-template");

        let err = ScaffoldError::DirectoryExists(PathBuf::from("/tmp/test"));
        assert_eq!(err.to_string(), "Directory already exists: /tmp/test");

        let err = ScaffoldError::IncompatibleVersion {
            required: "0.30.0".to_string(),
            current: "0.29.0".to_string(),
        };
        assert_eq!(
            err.to_string(),
            "Incompatible template version: requires PMAT 0.30.0, but running 0.29.0"
        );
    }

    #[test]
    fn test_error_conversion() {
        let err = ScaffoldError::UserCancelled;
        let anyhow_err: anyhow::Error = err.into();
        assert_eq!(anyhow_err.to_string(), "Operation cancelled by user");
    }

    #[test]
    fn test_io_error_conversion() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let scaffold_err = ScaffoldError::from(io_err);
        assert!(matches!(scaffold_err, ScaffoldError::IoError(_)));
    }

    // --- PMAT-641 additions: cover all Display arms + source chains ---

    use std::error::Error as StdError;

    #[test]
    fn test_display_invalid_configuration() {
        let e = ScaffoldError::InvalidConfiguration("missing name field".to_string());
        assert_eq!(
            e.to_string(),
            "Invalid agent configuration: missing name field"
        );
    }

    #[test]
    fn test_display_hook_failed() {
        let e = ScaffoldError::HookFailed("cargo fmt exit 1".to_string());
        assert_eq!(
            e.to_string(),
            "Post-generation hook failed: cargo fmt exit 1"
        );
    }

    #[test]
    fn test_display_missing_feature() {
        let e = ScaffoldError::MissingFeature("wasm-support".to_string());
        assert_eq!(e.to_string(), "Missing required feature: wasm-support");
    }

    #[test]
    fn test_display_render_error() {
        let e = ScaffoldError::RenderError("unresolved variable `name`".to_string());
        assert_eq!(
            e.to_string(),
            "Template rendering failed: unresolved variable `name`"
        );
    }

    #[test]
    fn test_display_invalid_template() {
        let e = ScaffoldError::InvalidTemplate("bad YAML at line 3".to_string());
        assert_eq!(e.to_string(), "Invalid template syntax: bad YAML at line 3");
    }

    #[test]
    fn test_display_network_error() {
        let e = ScaffoldError::NetworkError("timeout after 30s".to_string());
        assert_eq!(e.to_string(), "Network error: timeout after 30s");
    }

    #[test]
    fn test_display_parse_error() {
        let e = ScaffoldError::ParseError("expected ':' at column 5".to_string());
        assert_eq!(e.to_string(), "Parse error: expected ':' at column 5");
    }

    #[test]
    fn test_display_generation_failed_is_static_message() {
        // GenerationFailed uses `#[source]` — the Display only prints the outer
        // message; the inner cause is available via Error::source().
        let cause = anyhow::anyhow!("syn parse error");
        let e = ScaffoldError::GenerationFailed(cause);
        assert_eq!(e.to_string(), "Template generation failed");
    }

    #[test]
    fn test_display_validation_failed_is_static_message() {
        let cause = anyhow::anyhow!("bad invariant");
        let e = ScaffoldError::ValidationFailed(cause);
        assert_eq!(e.to_string(), "Template validation failed");
    }

    #[test]
    fn test_generation_failed_exposes_inner_cause_via_source() {
        let cause = anyhow::anyhow!("root cause from syn");
        let e = ScaffoldError::GenerationFailed(cause);
        let src = e.source().expect("has source");
        assert!(src.to_string().contains("root cause from syn"));
    }

    #[test]
    fn test_validation_failed_exposes_inner_cause_via_source() {
        let cause = anyhow::anyhow!("root cause validation failed");
        let e = ScaffoldError::ValidationFailed(cause);
        let src = e.source().expect("has source");
        assert!(src.to_string().contains("validation failed"));
    }

    #[test]
    fn test_io_error_display_and_source_chain() {
        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "cannot write");
        let e = ScaffoldError::from(io_err);
        // IoError has static Display "I/O error"; inner io::Error is exposed via source.
        assert_eq!(e.to_string(), "I/O error");
        let src = e.source().expect("has source");
        assert!(src.to_string().contains("cannot write"));
    }

    #[test]
    #[allow(clippy::unnecessary_literal_unwrap)]
    fn test_scaffold_result_alias_compiles_and_ok() {
        // The alias is unit-testable by exercising both arms.
        let ok: ScaffoldResult<u32> = Ok(42);
        let err: ScaffoldResult<u32> = Err(ScaffoldError::MissingFeature("x".to_string()));
        assert_eq!(ok.unwrap(), 42);
        assert!(err.is_err());
    }

    #[test]
    fn test_template_not_found_preserves_input_string() {
        let e = ScaffoldError::TemplateNotFound("mcp-server".to_string());
        // Covers the fmt args escape correctly.
        let rendered = format!("{e}");
        assert!(rendered.starts_with("Template not found: "));
        assert!(rendered.contains("mcp-server"));
    }

    #[test]
    fn test_debug_formatting_mentions_variant_name() {
        // Exercise the auto-derived Debug impl for a couple of variants.
        let dbg = format!("{:?}", ScaffoldError::UserCancelled);
        assert!(dbg.contains("UserCancelled"));
        let dbg = format!(
            "{:?}",
            ScaffoldError::IncompatibleVersion {
                required: "1.0".into(),
                current: "0.9".into(),
            }
        );
        assert!(dbg.contains("IncompatibleVersion"));
        assert!(dbg.contains("1.0"));
        assert!(dbg.contains("0.9"));
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn basic_property_stability(_input in ".*") {
            // Basic property test for coverage
            prop_assert!(true);
        }

        #[test]
        fn module_consistency_check(_x in 0u32..1000) {
            // Module consistency verification
            prop_assert!(_x < 1001);
        }
    }
}