cmx-core 0.3.0

Embeddable core for installing agent skills across platforms — cmx-aware lockfile tracking, plan/apply installs, version guards
Documentation
//! Test helpers shared across unit and integration tests.
//!
//! Available in two contexts:
//! - Within `cmx-core` itself: via `#[cfg(test)]`
//! - In downstream crates (e.g. `cmx`): via the `test-support` feature flag

pub fn agent_content(name: &str, desc: &str) -> String {
    format!("---\nname: {name}\ndescription: {desc}\n---\n# {name}\n")
}

pub fn skill_content(desc: &str) -> String {
    format!("---\ndescription: {desc}\n---\n# skill\n")
}

pub fn versioned_skill_content(desc: &str, version: &str) -> String {
    format!("---\ndescription: {desc}\nversion: {version}\n---\n# skill\n")
}

pub fn versioned_agent_content(name: &str, desc: &str, version: &str) -> String {
    format!("---\nname: {name}\ndescription: {desc}\nversion: {version}\n---\n# {name}\n")
}

pub fn metadata_versioned_agent_content(name: &str, desc: &str, version: &str) -> String {
    format!(
        "---\nname: {name}\ndescription: {desc}\nmetadata:\n  version: \"{version}\"\n  author: Test\n---\n# {name}\n"
    )
}

pub fn metadata_versioned_skill_content(desc: &str, version: &str) -> String {
    format!(
        "---\ndescription: {desc}\nmetadata:\n  version: \"{version}\"\n  author: Test\n---\n# skill\n"
    )
}

pub fn deprecated_agent_content(name: &str, desc: &str, reason: &str, replacement: &str) -> String {
    format!(
        "---\nname: {name}\ndescription: {desc}\ndeprecated: true\ndeprecated_reason: {reason}\ndeprecated_replacement: {replacement}\n---\n"
    )
}

pub fn make_local_entry(
    path: impl Into<std::path::PathBuf>,
    last_updated: Option<String>,
) -> crate::types::SourceEntry {
    use crate::types::{SourceEntry, SourceType};
    SourceEntry {
        source_type: SourceType::Local,
        path: Some(path.into()),
        url: None,
        local_clone: None,
        branch: None,
        last_updated,
    }
}

pub fn make_git_entry(
    url: &str,
    clone_path: impl Into<std::path::PathBuf>,
    branch: &str,
    last_updated: Option<String>,
) -> crate::types::SourceEntry {
    use crate::types::{SourceEntry, SourceType};
    SourceEntry {
        source_type: SourceType::Git,
        path: None,
        url: Some(url.to_string()),
        local_clone: Some(clone_path.into()),
        branch: Some(branch.to_string()),
        last_updated,
    }
}

pub fn make_lock_entry_builder(
    kind: crate::types::ArtifactKind,
    repo: &str,
    path: &str,
) -> crate::types::LockEntry {
    use crate::types::{LockEntry, LockSource};
    LockEntry {
        artifact_type: kind,
        version: None,
        installed_at: "2024-01-01T00:00:00Z".to_string(),
        source: LockSource {
            repo: repo.to_string(),
            path: path.to_string(),
        },
        source_checksum: "sha256:placeholder".to_string(),
        installed_checksum: "sha256:placeholder".to_string(),
    }
}

pub fn make_lock_entry_versioned(
    kind: crate::types::ArtifactKind,
    version: &str,
    repo: &str,
    path: &str,
) -> crate::types::LockEntry {
    let mut entry = make_lock_entry_builder(kind, repo, path);
    entry.version = Some(version.to_string());
    entry
}

pub fn make_lock_entry_with_checksum(
    kind: crate::types::ArtifactKind,
    version: Option<&str>,
    repo: &str,
    path: &str,
    checksum: &str,
) -> crate::types::LockEntry {
    let mut entry = make_lock_entry_builder(kind, repo, path);
    entry.version = version.map(str::to_string);
    entry.source_checksum = checksum.to_string();
    entry.installed_checksum = checksum.to_string();
    entry
}

pub fn save_lock_with_entry(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    name: &str,
    entry: crate::types::LockEntry,
    scope: crate::types::InstallScope,
) {
    use crate::types::LockFile;
    use std::collections::BTreeMap;
    let mut packages = BTreeMap::new();
    packages.insert(name.to_string(), entry);
    let lock = LockFile {
        version: 1,
        packages,
    };
    crate::lockfile::save(&lock, scope, fs, paths).unwrap();
}

pub fn setup_source(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    source_name: &str,
    source_path: &str,
) {
    use crate::types::SourcesFile;
    use chrono::Utc;
    use std::collections::BTreeMap;

    let sources = SourcesFile {
        version: 1,
        sources: {
            let mut m = BTreeMap::new();
            m.insert(
                source_name.to_string(),
                make_local_entry(source_path, Some(Utc::now().to_rfc3339())),
            );
            m
        },
    };
    let sources_json = serde_json::to_string_pretty(&sources).unwrap();
    fs.add_file(paths.sources_path(), sources_json);
}

pub fn setup_sources(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    entries: &[(&str, &str)],
) {
    use crate::types::SourcesFile;
    use chrono::Utc;
    use std::collections::BTreeMap;

    let sources = SourcesFile {
        version: 1,
        sources: {
            let mut m = BTreeMap::new();
            for &(name, path) in entries {
                m.insert(name.to_string(), make_local_entry(path, Some(Utc::now().to_rfc3339())));
            }
            m
        },
    };
    let sources_json = serde_json::to_string_pretty(&sources).unwrap();
    fs.add_file(paths.sources_path(), sources_json);
}

pub fn setup_source_with_versioned_agent(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    source_name: &str,
    source_path: &str,
    agent_name: &str,
    version: &str,
) {
    setup_source(fs, paths, source_name, source_path);
    fs.add_file(
        format!("{source_path}/agents/{agent_name}.md"),
        versioned_agent_content(agent_name, "A test agent", version),
    );
}

pub fn setup_source_with_skill(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    source_name: &str,
    source_path: &str,
    skill_name: &str,
    skill_version: &str,
) {
    setup_source(fs, paths, source_name, source_path);
    fs.add_file(
        format!("{source_path}/{skill_name}/SKILL.md"),
        versioned_skill_content("A test skill", skill_version),
    );
}

pub fn sample_lock_entry() -> crate::types::LockEntry {
    use crate::types::{ArtifactKind, LockEntry, LockSource};
    LockEntry {
        artifact_type: ArtifactKind::Agent,
        version: Some("1.0.0".to_string()),
        installed_at: "2024-01-01T00:00:00Z".to_string(),
        source: LockSource {
            repo: "guidelines".to_string(),
            path: "agents/my-agent.md".to_string(),
        },
        source_checksum: "sha256:abc123".to_string(),
        installed_checksum: "sha256:def456".to_string(),
    }
}

pub fn sample_lock_file() -> crate::types::LockFile {
    use crate::types::LockFile;
    use std::collections::BTreeMap;
    let mut packages = BTreeMap::new();
    packages.insert("my-agent".to_string(), sample_lock_entry());
    LockFile {
        version: 1,
        packages,
    }
}

pub fn install_agent_on_disk(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    name: &str,
    content: &str,
    scope: crate::types::InstallScope,
) {
    let path = paths
        .installed_artifact_path(crate::types::ArtifactKind::Agent, name, scope)
        .expect("install_agent_on_disk: caller uses platform that supports Agent");
    fs.add_file(path, content);
}

pub fn install_skill_on_disk(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    name: &str,
    content: &str,
    scope: crate::types::InstallScope,
) {
    let dir = paths
        .installed_artifact_path(crate::types::ArtifactKind::Skill, name, scope)
        .expect("install_skill_on_disk: caller uses platform that supports Skill");
    fs.add_file(dir.join("SKILL.md"), content);
}

pub fn add_skill(
    fs: &crate::gateway::fakes::FakeFilesystem,
    dir: impl AsRef<std::path::Path>,
    name: &str,
    desc: &str,
) {
    fs.add_file(dir.as_ref().join(name).join("SKILL.md"), skill_content(desc));
}

pub fn setup_source_with_agent(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    source_name: &str,
    source_path: &str,
    agent_name: &str,
) {
    setup_source(fs, paths, source_name, source_path);
    fs.add_file(
        format!("{source_path}/agents/{agent_name}.md"),
        agent_content(agent_name, "A test agent"),
    );
}

pub fn test_paths() -> crate::paths::ConfigPaths {
    use std::path::PathBuf;
    crate::paths::ConfigPaths::for_test(
        PathBuf::from("/home/testuser"),
        PathBuf::from("/home/testuser/.config/context-mixer"),
    )
}

pub fn test_paths_for(platform: crate::platform::Platform) -> crate::paths::ConfigPaths {
    use std::path::PathBuf;
    crate::paths::ConfigPaths::for_test_with_platform(
        PathBuf::from("/home/testuser"),
        PathBuf::from("/home/testuser/.config/context-mixer"),
        platform,
    )
}

pub fn setup_empty_sources(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
) {
    let sources = crate::types::SourcesFile::default();
    fs.add_file(paths.sources_path(), serde_json::to_string_pretty(&sources).unwrap());
}

pub fn setup_source_git(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    source_name: &str,
    url: &str,
    clone_path: impl Into<std::path::PathBuf>,
    branch: &str,
    last_updated: Option<String>,
) {
    let mut sources = crate::types::SourcesFile::default();
    sources
        .sources
        .insert(source_name.to_string(), make_git_entry(url, clone_path, branch, last_updated));
    fs.add_file(paths.sources_path(), serde_json::to_string_pretty(&sources).unwrap());
}

pub fn setup_sources_from_entries(
    fs: &crate::gateway::fakes::FakeFilesystem,
    paths: &crate::paths::ConfigPaths,
    entries: &[(&str, crate::types::SourceEntry)],
) {
    let mut sources = crate::types::SourcesFile::default();
    for (name, entry) in entries {
        sources.sources.insert(name.to_string(), entry.clone());
    }
    fs.add_file(paths.sources_path(), serde_json::to_string_pretty(&sources).unwrap());
}

pub fn make_ctx<'a>(
    fs: &'a crate::gateway::fakes::FakeFilesystem,
    git: &'a crate::gateway::fakes::FakeGitClient,
    clock: &'a crate::gateway::fakes::FakeClock,
    paths: &'a crate::paths::ConfigPaths,
) -> crate::context::AppContext<'a> {
    crate::context::AppContext {
        fs,
        git,
        clock,
        paths,
        llm: None,
    }
}

pub struct TestContext {
    pub fs: crate::gateway::fakes::FakeFilesystem,
    pub git: crate::gateway::fakes::FakeGitClient,
    pub clock: crate::gateway::fakes::FakeClock,
    pub paths: crate::paths::ConfigPaths,
}

impl TestContext {
    pub fn new() -> Self {
        Self::build(test_paths(), crate::gateway::fakes::FakeClock::at(chrono::Utc::now()))
    }

    pub fn for_platform(platform: crate::platform::Platform) -> Self {
        Self::build(
            test_paths_for(platform),
            crate::gateway::fakes::FakeClock::at(chrono::Utc::now()),
        )
    }

    pub fn at(time: chrono::DateTime<chrono::Utc>) -> Self {
        Self::build(test_paths(), crate::gateway::fakes::FakeClock::at(time))
    }

    fn build(paths: crate::paths::ConfigPaths, clock: crate::gateway::fakes::FakeClock) -> Self {
        Self {
            fs: crate::gateway::fakes::FakeFilesystem::new(),
            git: crate::gateway::fakes::FakeGitClient::new(),
            clock,
            paths,
        }
    }

    pub fn ctx(&self) -> crate::context::AppContext<'_> {
        make_ctx(&self.fs, &self.git, &self.clock, &self.paths)
    }
}

impl Default for TestContext {
    fn default() -> Self {
        Self::new()
    }
}