capo-agent 0.7.0

Coding-agent library built on motosan-agent-loop. Composable, embeddable.
Documentation
#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]

pub const MAX_NAME_LENGTH: usize = 64;
pub const MAX_DESCRIPTION_LENGTH: usize = 1024;

pub fn validate_name(name: &str) -> Vec<String> {
    let mut errors = Vec::new();
    if name.len() > MAX_NAME_LENGTH {
        errors.push(format!(
            "name exceeds {MAX_NAME_LENGTH} characters ({} got)",
            name.len()
        ));
    }
    if !name
        .chars()
        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
    {
        errors.push(
            "name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)".into(),
        );
    }
    if name.starts_with('-') || name.ends_with('-') {
        errors.push("name must not start or end with a hyphen".into());
    }
    if name.contains("--") {
        errors.push("name must not contain consecutive hyphens".into());
    }
    errors
}

pub fn validate_description(description: Option<&str>) -> Vec<String> {
    let mut errors = Vec::new();
    match description.map(str::trim) {
        None | Some("") => errors.push("description is required".into()),
        Some(d) if d.len() > MAX_DESCRIPTION_LENGTH => {
            errors.push(format!(
                "description exceeds {MAX_DESCRIPTION_LENGTH} characters ({} got)",
                d.len()
            ));
        }
        _ => {}
    }
    errors
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn valid_name_passes() {
        assert_eq!(validate_name("rust-error-triage"), Vec::<String>::new());
        assert_eq!(validate_name("a"), Vec::<String>::new());
        assert_eq!(validate_name("a1-b2-c3"), Vec::<String>::new());
    }

    #[test]
    fn name_too_long_fails() {
        let n = "a".repeat(MAX_NAME_LENGTH + 1);
        let errors = validate_name(&n);
        assert!(errors.iter().any(|e| e.contains("exceeds")), "{errors:?}");
    }

    #[test]
    fn name_uppercase_fails() {
        let errors = validate_name("Foo");
        assert!(
            errors.iter().any(|e| e.contains("invalid characters")),
            "{errors:?}"
        );
    }

    #[test]
    fn name_underscore_fails() {
        let errors = validate_name("rust_error_triage");
        assert!(
            errors.iter().any(|e| e.contains("invalid characters")),
            "{errors:?}"
        );
    }

    #[test]
    fn name_leading_hyphen_fails() {
        let errors = validate_name("-foo");
        assert!(
            errors
                .iter()
                .any(|e| e.contains("start or end with a hyphen")),
            "{errors:?}"
        );
    }

    #[test]
    fn name_trailing_hyphen_fails() {
        let errors = validate_name("foo-");
        assert!(
            errors
                .iter()
                .any(|e| e.contains("start or end with a hyphen")),
            "{errors:?}"
        );
    }

    #[test]
    fn name_consecutive_hyphens_fail() {
        let errors = validate_name("foo--bar");
        assert!(
            errors.iter().any(|e| e.contains("consecutive hyphens")),
            "{errors:?}"
        );
    }

    #[test]
    fn valid_description_passes() {
        assert_eq!(validate_description(Some("ok")), Vec::<String>::new());
    }

    #[test]
    fn missing_description_fails() {
        assert!(validate_description(None)
            .iter()
            .any(|e| e.contains("required")));
        assert!(validate_description(Some(""))
            .iter()
            .any(|e| e.contains("required")));
        assert!(validate_description(Some("   "))
            .iter()
            .any(|e| e.contains("required")));
    }

    #[test]
    fn description_too_long_fails() {
        let d = "x".repeat(MAX_DESCRIPTION_LENGTH + 1);
        assert!(validate_description(Some(&d))
            .iter()
            .any(|e| e.contains("exceeds")));
    }
}