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
//! Real-implementation tests for the `CapabilityEngine` trait, via a real,
//! hand-written test double (not a mocking-library mock) —
//! `EchoEngineDouble`, matching every other contract in this repo's own
//! established convention. A real, technology-specific `CapabilityEngine`
//! implementation (e.g. wrapping `wasmtime`) lives in its own downstream
//! consumer crate instead, with its own behavioral tests.

use std::sync::Arc;
use std::time::Duration;

use futures::future::BoxFuture;
use wasm_capability_contract::{
    CapabilityEngine, CapabilityError, ComponentHandle, ComponentInvokeRequest,
    ComponentInvokeResponse, ComponentLoadRequest, ComponentLoadResponse, ComponentManifest,
    ResourceLimits,
};

struct EchoEngineDouble {
    reject_load: bool,
}

impl CapabilityEngine for EchoEngineDouble {
    fn load(
        &self,
        req: ComponentLoadRequest,
    ) -> BoxFuture<'_, Result<ComponentLoadResponse, CapabilityError>> {
        Box::pin(async move {
            if self.reject_load {
                return Err(CapabilityError::InvalidManifest(
                    "double rejects this load".to_string(),
                ));
            }
            Ok(ComponentLoadResponse {
                handle: ComponentHandle(req.manifest.component_id),
            })
        })
    }

    fn invoke(
        &self,
        req: ComponentInvokeRequest,
    ) -> BoxFuture<'_, Result<ComponentInvokeResponse, CapabilityError>> {
        Box::pin(async move {
            if req.payload.is_empty() {
                return Err(CapabilityError::InvalidOutput(
                    "echo double refuses empty payloads".to_string(),
                ));
            }
            Ok(ComponentInvokeResponse {
                payload: req.payload,
            })
        })
    }
}

fn sample_manifest() -> ComponentManifest {
    ComponentManifest {
        component_id: "echo-handler".to_string(),
        contract_version: "swe:edge-handler@0.2.0".to_string(),
        handler_export: "echo".to_string(),
        resource_limits: ResourceLimits {
            max_memory_bytes: 16 * 1024 * 1024,
            invoke_timeout_ms: 5_000,
            max_concurrency: 8,
            max_payload_bytes: 256 * 1024,
        },
        capabilities: vec![],
    }
}

/// @covers: CapabilityEngine
/// The trait must be object-safe -- `Arc<dyn CapabilityEngine>` is how
/// every real consumer (the wasmtime-backed adapter) is expected to hold
/// one.
fn _accept(_engine: &dyn CapabilityEngine) {}

/// @covers: CapabilityEngine::load
#[tokio::test]
async fn test_load_a_fresh_component_happy() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: false });
    let loaded = engine
        .load(ComponentLoadRequest {
            manifest: sample_manifest(),
            component_bytes: vec![0, 1, 2, 3],
        })
        .await
        .unwrap_or_else(|e| panic!("load must succeed: {e}"));
    assert_eq!(loaded.handle, ComponentHandle("echo-handler".to_string()));
}

/// @covers: CapabilityEngine::load
#[tokio::test]
async fn test_load_rejected_bytes_error() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: true });
    let result = engine
        .load(ComponentLoadRequest {
            manifest: sample_manifest(),
            component_bytes: vec![0xff, 0xff],
        })
        .await;
    assert!(matches!(result, Err(CapabilityError::InvalidManifest(_))));
}

/// @covers: CapabilityEngine::load
/// The empty-bytes boundary: loading with zero component bytes must
/// still go through the same real code path as a non-empty payload, not
/// panic on an empty `Vec`.
#[tokio::test]
async fn test_load_empty_component_bytes_edge() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: false });
    let loaded = engine
        .load(ComponentLoadRequest {
            manifest: sample_manifest(),
            component_bytes: vec![],
        })
        .await
        .unwrap_or_else(|e| panic!("load with empty bytes must still succeed on this double: {e}"));
    assert_eq!(loaded.handle, ComponentHandle("echo-handler".to_string()));
}

/// @covers: CapabilityEngine::invoke
#[tokio::test]
async fn test_invoke_a_loaded_component_happy() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: false });
    let invoked = engine
        .invoke(ComponentInvokeRequest {
            handle: ComponentHandle("echo-handler".to_string()),
            payload: b"hello".to_vec(),
            deadline: Duration::from_secs(1),
        })
        .await
        .unwrap_or_else(|e| panic!("invoke must succeed: {e}"));
    assert_eq!(
        invoked.payload,
        b"hello".to_vec(),
        "the double must echo back exactly what it received"
    );
}

/// @covers: CapabilityEngine::invoke
#[tokio::test]
async fn test_invoke_empty_payload_error() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: false });
    let result = engine
        .invoke(ComponentInvokeRequest {
            handle: ComponentHandle("echo-handler".to_string()),
            payload: vec![],
            deadline: Duration::from_secs(1),
        })
        .await;
    assert!(matches!(result, Err(CapabilityError::InvalidOutput(_))));
}

/// @covers: CapabilityEngine::invoke
/// The single-byte-payload boundary: the smallest possible non-empty
/// payload must still round-trip, proving the empty-payload rejection is
/// specifically about emptiness, not an off-by-one on a larger minimum.
#[tokio::test]
async fn test_invoke_single_byte_payload_edge() {
    let engine: Arc<dyn CapabilityEngine> = Arc::new(EchoEngineDouble { reject_load: false });
    let invoked = engine
        .invoke(ComponentInvokeRequest {
            handle: ComponentHandle("echo-handler".to_string()),
            payload: vec![0],
            deadline: Duration::from_secs(1),
        })
        .await
        .unwrap_or_else(|e| panic!("invoke with a single-byte payload must succeed: {e}"));
    assert_eq!(invoked.payload, vec![0]);
}