cargo-ai 0.2.0

Build lightweight AI agents with Cargo. Powered by Rust. Declared in JSON.
use super::key::TemplateCacheKey;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;

pub(super) fn prune_stale_template_cache(
    root: &Path,
    active_key: &TemplateCacheKey,
) -> Result<usize, String> {
    if !root.exists() {
        return Ok(0);
    }

    let mut removed_parent_count = 0;
    for hash_entry in fs::read_dir(root).map_err(|error| {
        format!(
            "Failed to read template cache root '{}': {error}",
            root.display()
        )
    })? {
        let hash_entry = hash_entry.map_err(|error| {
            format!(
                "Failed to read template cache entry under '{}': {error}",
                root.display()
            )
        })?;
        let hash_path = hash_entry.path();
        if !hash_path.is_dir() {
            continue;
        }

        if hash_entry.file_name() != OsStr::new(&active_key.binary_sha256) {
            fs::remove_dir_all(&hash_path).map_err(|error| {
                format!(
                    "Failed to remove stale template cache parent '{}': {error}",
                    hash_path.display()
                )
            })?;
            removed_parent_count += 1;
            continue;
        }

        removed_parent_count += prune_stale_rustc_parents(&hash_path, active_key)?;
    }

    Ok(removed_parent_count)
}

fn prune_stale_rustc_parents(
    binary_root: &Path,
    active_key: &TemplateCacheKey,
) -> Result<usize, String> {
    let mut removed_parent_count = 0;
    for rustc_entry in fs::read_dir(binary_root).map_err(|error| {
        format!(
            "Failed to read Cargo AI template cache parent '{}': {error}",
            binary_root.display()
        )
    })? {
        let rustc_entry = rustc_entry.map_err(|error| {
            format!(
                "Failed to read rustc template cache entry under '{}': {error}",
                binary_root.display()
            )
        })?;
        let rustc_path = rustc_entry.path();
        if !rustc_path.is_dir() {
            continue;
        }

        if rustc_entry.file_name() != OsStr::new(&active_key.rustc_version) {
            fs::remove_dir_all(&rustc_path).map_err(|error| {
                format!(
                    "Failed to remove stale rustc template cache parent '{}': {error}",
                    rustc_path.display()
                )
            })?;
            removed_parent_count += 1;
        }
    }

    Ok(removed_parent_count)
}

#[cfg(test)]
mod tests {
    use super::prune_stale_template_cache;
    use crate::agent_builder::template_cache::key::TemplateCacheKey;
    use std::fs;
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::time::{SystemTime, UNIX_EPOCH};

    static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);

    fn temp_test_dir() -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after epoch")
            .as_nanos();
        let sequence = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
        let path =
            std::env::temp_dir().join(format!("cargo-ai-template-cache-test-{nanos}-{sequence}"));
        fs::create_dir_all(&path).expect("test directory should be creatable");
        path
    }

    #[test]
    fn prune_stale_template_cache_keeps_active_parent_and_sibling_targets() {
        let root = temp_test_dir();
        let active_key = TemplateCacheKey {
            binary_sha256: "sha-current".to_string(),
            rustc_version: "rustc-1.91.1".to_string(),
            target_triple: "aarch64-apple-darwin".to_string(),
        };

        let stale_hash_target = root
            .join("sha-old")
            .join("rustc-1.90.0")
            .join("aarch64-apple-darwin");
        let stale_rustc_target = root
            .join("sha-current")
            .join("rustc-1.90.0")
            .join("aarch64-apple-darwin");
        let active_target = root
            .join("sha-current")
            .join("rustc-1.91.1")
            .join("aarch64-apple-darwin");
        let sibling_target = root
            .join("sha-current")
            .join("rustc-1.91.1")
            .join("x86_64-pc-windows-msvc");

        for path in [
            &stale_hash_target,
            &stale_rustc_target,
            &active_target,
            &sibling_target,
        ] {
            fs::create_dir_all(path).expect("template cache fixture should be creatable");
        }

        let removed = prune_stale_template_cache(&root, &active_key)
            .expect("pruning stale template cache should succeed");

        assert_eq!(removed, 2);
        assert!(!root.join("sha-old").exists());
        assert!(!root.join("sha-current").join("rustc-1.90.0").exists());
        assert!(active_target.exists());
        assert!(sibling_target.exists());

        let _ = fs::remove_dir_all(root);
    }

    #[test]
    fn prune_stale_template_cache_is_noop_when_root_missing() {
        let missing_root = std::env::temp_dir().join("cargo-ai-template-cache-missing-root");
        let active_key = TemplateCacheKey {
            binary_sha256: "sha-current".to_string(),
            rustc_version: "rustc-1.91.1".to_string(),
            target_triple: "aarch64-apple-darwin".to_string(),
        };

        let removed = prune_stale_template_cache(&missing_root, &active_key)
            .expect("missing cache root should be treated as empty");
        assert_eq!(removed, 0);
    }
}