agentcarousel 0.3.1

Evaluate agents and skills with YAML fixtures, run cases (mock or live), and keep run rows in SQLite for reports and evidence export.
Documentation
use assert_cmd::Command;
use mockito::Matcher;
use std::fs;

fn workspace_root() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|path| path.parent())
        .map(std::path::PathBuf::from)
        .expect("workspace root")
}

#[test]
fn bundle_pull_fetches_manifest_and_files() {
    let mut server = mockito::Server::new();
    let base = server.url();

    let manifest = serde_json::json!({
        "bundle_id": "test/pull-fixture",
        "bundle_version": "1.0.0",
        "fixtures": [{
            "path": "skills/hello.yaml",
            "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        }],
        "mocks": []
    });

    let m_manifest = server
        .mock("GET", "/v1/bundles/pull-fixture-1.0.0/manifest")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(manifest.to_string())
        .create();

    let m_file = server
        .mock(
            "GET",
            Matcher::Regex(r"/v1/bundles/pull-fixture-1\.0\.0/file\?path=.*".into()),
        )
        .with_status(200)
        .with_header("content-type", "application/octet-stream")
        .with_body("")
        .create();

    let tmp = tempfile::tempdir().expect("tempdir");
    let out = tmp.path().join("pulled");

    let root = workspace_root();
    let assert = Command::cargo_bin("agentcarousel")
        .unwrap()
        .current_dir(&root)
        .args([
            "bundle",
            "pull",
            "pull-fixture-1.0.0",
            "--url",
            base.trim_end_matches('/'),
            "-o",
        ])
        .arg(&out)
        .assert()
        .success();

    let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
    assert!(
        stdout.contains("bundle pull: wrote"),
        "expected success banner, got: {stdout:?}"
    );

    m_manifest.assert();
    m_file.assert();

    assert!(out.join("bundle.manifest.json").exists());
    assert!(out.join("skills").join("hello.yaml").exists());
    let written = fs::read_to_string(out.join("skills").join("hello.yaml")).expect("read");
    assert_eq!(written, "");
}

#[test]
fn bundle_pull_verify_checks_hashes() {
    let mut server = mockito::Server::new();
    let base = server.url();

    let manifest = serde_json::json!({
        "bundle_id": "test/pull-verify",
        "bundle_version": "1.0.0",
        "fixtures": [{
            "path": "x.txt",
            "sha256": "deadbeef"
        }],
        "mocks": []
    });

    server
        .mock("GET", "/v1/bundles/pull-verify-1.0.0/manifest")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(manifest.to_string())
        .create();

    server
        .mock(
            "GET",
            Matcher::Regex(r"/v1/bundles/pull-verify-1\.0\.0/file\?path=.*".into()),
        )
        .with_status(200)
        .with_body("wrong-bytes")
        .create();

    let tmp = tempfile::tempdir().expect("tempdir");
    let out = tmp.path().join("bad");

    let root = workspace_root();
    let assert = Command::cargo_bin("agentcarousel")
        .unwrap()
        .current_dir(&root)
        .args([
            "bundle",
            "pull",
            "pull-verify-1.0.0",
            "--url",
            base.trim_end_matches('/'),
            "-o",
        ])
        .arg(&out)
        .arg("--verify")
        .assert()
        .failure();

    let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
    assert!(
        stderr.contains("hash mismatch") || stderr.contains("error:"),
        "expected verify/hash error, got: {stderr:?}"
    );
}