agent-plugins 0.1.1

Sans-IO implementation of the Agent Plugins specification (agent-plugins.org): load, validate, and prepare agent plugins from any IO
Documentation
//! Runs the cross-language conformance corpus in `../fixtures/cases`.
//!
//! Each case is a plugin package plus an `expected.json` describing the
//! outcome: loaded or rejected, which skills and servers survived, and which
//! diagnostic rules fired. The same corpus drives the TypeScript
//! implementation's test suite.

use std::path::PathBuf;

use agent_plugins::{load_dir, McpOutcome};
use serde_json::Value;

fn fixture_cases() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/cases")
}

#[test]
fn conformance_corpus() {
    let mut ran = 0;
    let mut failures = Vec::new();
    let mut cases: Vec<_> = std::fs::read_dir(fixture_cases())
        .expect("fixture corpus present")
        .map(|e| e.expect("readable fixture entry").path())
        .filter(|p| p.is_dir())
        .collect();
    cases.sort();

    for case_dir in cases {
        let case = case_dir.file_name().unwrap().to_string_lossy().into_owned();
        let expected: Value = serde_json::from_slice(
            &std::fs::read(case_dir.join("expected.json")).expect("expected.json present"),
        )
        .expect("expected.json is valid JSON");

        if let Err(problem) = check_case(&case_dir.join("plugin"), &expected) {
            failures.push(format!("{case}: {problem}"));
        }
        ran += 1;
    }

    assert!(ran > 0, "no fixture cases found");
    assert!(
        failures.is_empty(),
        "{} of {ran} conformance case(s) failed:\n  {}",
        failures.len(),
        failures.join("\n  ")
    );
}

fn check_case(plugin_dir: &std::path::Path, expected: &Value) -> Result<(), String> {
    let outcome = load_dir(plugin_dir);
    match expected["outcome"].as_str().expect("outcome field") {
        "rejected" => {
            let rejection = match outcome {
                Err(rejection) => rejection,
                Ok(plugin) => {
                    return Err(format!(
                        "expected rejection, but plugin {:?} loaded",
                        plugin.manifest.name.as_str()
                    ))
                }
            };
            let want = expected["reason"].as_str().expect("reason field");
            if rejection.code() != want {
                return Err(format!(
                    "expected rejection reason {want}, got {} ({rejection})",
                    rejection.code()
                ));
            }
        }
        "loaded" => {
            let plugin = outcome.map_err(|r| format!("unexpected rejection: {r}"))?;

            let want_name = expected["name"].as_str().expect("name field");
            if plugin.manifest.name.as_str() != want_name {
                return Err(format!("expected name {want_name}, got {}", plugin.manifest.name));
            }

            let got_skills: Vec<&str> =
                plugin.skills.iter().map(|s| s.meta.name.as_str()).collect();
            let want_skills: Vec<&str> = expected["skills"]
                .as_array()
                .expect("skills field")
                .iter()
                .map(|v| v.as_str().unwrap())
                .collect();
            if got_skills != want_skills {
                return Err(format!("expected skills {want_skills:?}, got {got_skills:?}"));
            }

            let (got_mcp, got_servers) = match &plugin.mcp {
                McpOutcome::Absent => ("absent", Vec::new()),
                McpOutcome::Disabled(_) => ("disabled", Vec::new()),
                McpOutcome::Configured(config) => {
                    ("configured", config.servers.iter().map(|e| e.name.as_str()).collect())
                }
                other => return Err(format!("unrecognized MCP outcome {other:?}")),
            };
            let want_mcp = expected["mcp"].as_str().expect("mcp field");
            if got_mcp != want_mcp {
                return Err(format!("expected mcp {want_mcp}, got {got_mcp}"));
            }
            if let Some(want_servers) = expected["servers"].as_array() {
                let want_servers: Vec<&str> =
                    want_servers.iter().map(|v| v.as_str().unwrap()).collect();
                if got_servers != want_servers {
                    return Err(format!("expected servers {want_servers:?}, got {got_servers:?}"));
                }
            }

            let mut got_rules: Vec<&str> =
                plugin.diagnostics.iter().map(|d| d.rule.code()).collect();
            let mut want_rules: Vec<&str> = expected["diagnostics"]
                .as_array()
                .expect("diagnostics field")
                .iter()
                .map(|v| v.as_str().unwrap())
                .collect();
            got_rules.sort_unstable();
            want_rules.sort_unstable();
            if got_rules != want_rules {
                return Err(format!(
                    "expected diagnostics {want_rules:?}, got {got_rules:?} — {:#?}",
                    plugin.diagnostics
                ));
            }
        }
        other => panic!("unknown expected outcome {other:?}"),
    }
    Ok(())
}