wasm-capability-contract 0.4.0

Generic, domain-agnostic capability pattern: CapabilityEngine/CapabilityRegistry/CapabilityDispatcher trait shapes + component/capability types. Trait definitions only -- see wasm-capability-core for this pattern's own default implementation, extracted from agent-runtime's ADR-001 (agent-runtime#31, ADR-011).
Documentation
//! Integration-level checks spanning this crate's whole public surface —
//! proves the pieces compose the way a real manifest's
//! `granted_capabilities` array would, not just that each type works in
//! isolation.

use wasm_capability_contract::{CapabilityGrant, CapabilityScope};

/// A realistic `granted_capabilities` array with several distinct
/// capabilities and scope shapes -- deliberately generic, not tied to
/// any one consumer's specific capability names.
const GRANTED_CAPABILITIES_JSON: &str = r#"[
    { "name": "some-capability", "scope": { "allowed": ["api.example.com"] } },
    { "name": "another-capability", "scope": { "allowed": ["method-a", "method-b"] } },
    { "name": "no-restrictions-capability", "scope": { "allowed": ["*"] } },
    { "name": "empty-scope-capability", "scope": { "allowed": [] } }
]"#;

/// @covers: CapabilityGrant, CapabilityScope
/// A realistic manifest fragment must deserialize into every grant it
/// declares, each with the exact `allowed` entries it names — field-by-
/// field, not just "it parsed."
#[test]
fn test_manifest_fragment_deserializes_into_grants_with_correct_scope() {
    let grants: Vec<CapabilityGrant> = serde_json::from_str(GRANTED_CAPABILITIES_JSON)
        .unwrap_or_else(|e| panic!("manifest fragment must deserialize: {e}"));
    assert_eq!(grants.len(), 4, "expected exactly four grants");

    let by_name = |name: &str| {
        grants
            .iter()
            .find(|g| g.name == name)
            .unwrap_or_else(|| panic!("expected a grant named '{name}'"))
    };

    assert_eq!(
        by_name("some-capability").scope,
        CapabilityScope {
            allowed: vec!["api.example.com".to_string()]
        }
    );
    assert_eq!(
        by_name("another-capability").scope,
        CapabilityScope {
            allowed: vec!["method-a".to_string(), "method-b".to_string()]
        }
    );
    assert_eq!(
        by_name("no-restrictions-capability").scope,
        CapabilityScope {
            allowed: vec!["*".to_string()]
        }
    );
    assert_eq!(
        by_name("empty-scope-capability").scope,
        CapabilityScope { allowed: vec![] }
    );
}

/// @covers: CapabilityGrant
/// The full grant list must round-trip through JSON unchanged as a whole
/// collection, not just member-by-member — proves `Vec<CapabilityGrant>`
/// itself (the exact shape `HostManifest.granted_capabilities` uses)
/// serializes losslessly.
#[test]
fn test_full_granted_capabilities_list_round_trips_unchanged() {
    let grants: Vec<CapabilityGrant> = serde_json::from_str(GRANTED_CAPABILITIES_JSON)
        .unwrap_or_else(|e| panic!("must deserialize: {e}"));
    let json = serde_json::to_string(&grants).unwrap_or_else(|e| panic!("must serialize: {e}"));
    let restored: Vec<CapabilityGrant> =
        serde_json::from_str(&json).unwrap_or_else(|e| panic!("must re-deserialize: {e}"));
    assert_eq!(restored, grants);
}