bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Managed Cargo artifact preparation without writing to Cargo's global bin directory.

use std::path::{Path, PathBuf};

use crate::config::schema::CargoInstall;
use crate::error::ForgeError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SourceMode {
    Online,
    CacheOnly,
    Offline,
}

#[derive(Debug, Clone)]
pub(crate) struct ResolvedCargo {
    pub(crate) component: String,
    pub(crate) fingerprint: String,
    pub(crate) staging: PathBuf,
    pub(crate) target_dir: PathBuf,
    pub(crate) source_mode: SourceMode,
}

impl ResolvedCargo {
    pub(crate) fn resolve(
        component: &str,
        specification: &CargoInstall,
        work_root: &Path,
        source_mode: SourceMode,
    ) -> Result<Self, ForgeError> {
        let version = specification.version.trim_start_matches('=');
        if version.is_empty()
            || version
                .bytes()
                .any(|byte| matches!(byte, b'*' | b'^' | b'~' | b'>' | b'<'))
        {
            return Err(ForgeError::Config(format!(
                "Cargo component {component} must resolve to a pinned version"
            )));
        }
        if specification.source.as_deref().is_some_and(|source| {
            source.starts_with("git+")
                && specification.revision.as_deref().is_none_or(str::is_empty)
        }) {
            return Err(ForgeError::Config(format!(
                "Cargo Git component {component} must pin a revision"
            )));
        }
        let source_digest = specification.source_digest();
        let lock_digest = specification.lock_digest();
        let fingerprint = specification.artifact_fingerprint(&source_digest, &lock_digest);
        Ok(Self {
            component: component.to_string(),
            staging: work_root.join("staging").join(&fingerprint),
            target_dir: work_root.join("targets").join(&fingerprint),
            fingerprint,
            source_mode,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;
    use std::path::Path;

    use crate::backends::cargo::{CargoInstall, ResolvedCargo, SourceMode};

    fn specification() -> CargoInstall {
        CargoInstall {
            crate_name: "demo".into(),
            version: "=1.2.3".into(),
            source: None,
            revision: None,
            locked: true,
            features: vec!["json".into()],
            bins: vec!["demo".into()],
            target: None,
            toolchain: None,
            profile: "release".into(),
            build_env_allow: Vec::new(),
        }
    }

    #[test]
    fn fingerprints_get_isolated_target_shards() {
        let resolved = ResolvedCargo::resolve(
            "demo",
            &specification(),
            Path::new("/work"),
            SourceMode::Online,
        )
        .unwrap();
        assert!(resolved.target_dir.ends_with(&resolved.fingerprint));
    }

    #[test]
    fn rejects_floating_versions_and_git_revisions() {
        let mut spec = specification();
        spec.version = "^1".into();
        assert!(
            ResolvedCargo::resolve("demo", &spec, Path::new("/work"), SourceMode::Online).is_err()
        );
        spec.version = "=1.2.3".into();
        spec.source = Some("git+https://example.invalid/demo".into());
        assert!(
            ResolvedCargo::resolve("demo", &spec, Path::new("/work"), SourceMode::Online).is_err()
        );
    }

    #[test]
    fn explicitly_unlocked_cargo_has_a_distinct_fingerprint() {
        let mut spec = specification();
        let locked =
            ResolvedCargo::resolve("demo", &spec, Path::new("/work"), SourceMode::Online).unwrap();
        spec.locked = false;
        let unlocked =
            ResolvedCargo::resolve("demo", &spec, Path::new("/work"), SourceMode::Online).unwrap();
        assert_ne!(locked.fingerprint, unlocked.fingerprint);
    }

    #[test]
    fn stress_fingerprints_and_multi_bin_verification_are_isolated() {
        let work =
            std::env::temp_dir().join(format!("bot-forge-cargo-stress-{}", std::process::id()));
        let mut fingerprints = BTreeSet::new();
        for index in 0..20 {
            let mut spec = specification();
            spec.crate_name = format!("crate-{index}");
            spec.bins = vec![format!("bin-{index}-a"), format!("bin-{index}-b")];
            let resolved = ResolvedCargo::resolve(
                &format!("component-{index}"),
                &spec,
                &work,
                SourceMode::Offline,
            )
            .unwrap();
            assert!(fingerprints.insert(resolved.fingerprint.clone()));
            assert_eq!(resolved.source_mode, SourceMode::Offline);
        }
        assert_eq!(fingerprints.len(), 20);
        let first = ResolvedCargo::resolve(
            "component-duplicate",
            &specification(),
            &work,
            SourceMode::CacheOnly,
        )
        .unwrap();
        let second = ResolvedCargo::resolve(
            "component-duplicate",
            &specification(),
            &work,
            SourceMode::CacheOnly,
        )
        .unwrap();
        assert_eq!(first.fingerprint, second.fingerprint);
        assert_eq!(first.target_dir, second.target_dir);
    }
}