acp-runtime 0.1.2

Rust SDK for the Agent Communication Protocol (ACP)
Documentation
use std::sync::Arc;

use acp_runtime::agent::AcpAgent;
use acp_runtime::messages::DeliveryMode;
use acp_runtime::options::AcpAgentOptions;
use acp_runtime::overlay_framework::{OverlayClient, OverlayFrameworkRuntime};
use httpmock::prelude::*;
use serde_json::{Map, Value};

#[test]
fn overlay_runtime_exposes_well_known_headers_and_rejects_non_object_body() {
    let temp = tempfile::tempdir().expect("tempdir");
    let agent = AcpAgent::load_or_create(
        "agent:overlay.runtime@localhost:9541",
        Some(AcpAgentOptions {
            storage_dir: temp.path().join("runtime-agent"),
            endpoint: Some("http://localhost:9541/acp/inbox".to_string()),
            allow_insecure_http: true,
            discovery_scheme: "http".to_string(),
            ..AcpAgentOptions::default()
        }),
    )
    .expect("runtime agent");
    let mut runtime = OverlayFrameworkRuntime::create(
        agent,
        "http://localhost:9541",
        Arc::new(|payload| {
            let mut response = Map::new();
            response.insert("accepted".to_string(), Value::Bool(true));
            response.insert("echo".to_string(), Value::Object(payload.clone()));
            Some(response)
        }),
        None,
    )
    .expect("overlay runtime");

    let headers = OverlayFrameworkRuntime::well_known_headers();
    assert_eq!(
        Some("public, max-age=300"),
        headers.get("Cache-Control").and_then(Value::as_str)
    );
    let well_known = runtime
        .well_known_document()
        .expect("well-known document should be built");
    assert_eq!(
        Some("agent:overlay.runtime@localhost:9541"),
        well_known.get("agent_id").and_then(Value::as_str)
    );
    assert_eq!(
        Some("1.0"),
        well_known.get("version").and_then(Value::as_str)
    );

    let invalid_response =
        runtime.handle_message_body(&Value::Array(vec![Value::String("invalid".to_string())]));
    assert_eq!(400, invalid_response.status_code);
    assert_eq!(
        Some("FAILED"),
        invalid_response.body.get("state").and_then(Value::as_str)
    );
    assert_eq!(
        Some("POLICY_REJECTED"),
        invalid_response
            .body
            .get("reason_code")
            .and_then(Value::as_str)
    );
}

#[test]
fn overlay_client_bootstraps_well_known_and_sends_payload() {
    let server = MockServer::start();
    let base_url = format!("http://127.0.0.1:{}", server.port());

    let temp = tempfile::tempdir().expect("tempdir");
    let receiver = AcpAgent::load_or_create(
        "agent:overlay.receiver@localhost:9542",
        Some(AcpAgentOptions {
            storage_dir: temp.path().join("receiver"),
            endpoint: Some(format!("{base_url}/acp/inbox")),
            allow_insecure_http: true,
            discovery_scheme: "http".to_string(),
            ..AcpAgentOptions::default()
        }),
    )
    .expect("receiver agent");
    let well_known = receiver
        .build_well_known_document(Some(&base_url), None)
        .expect("receiver well-known");
    let mut identity_payload = Map::new();
    identity_payload.insert(
        "identity_document".to_string(),
        Value::Object(receiver.identity_document.clone()),
    );

    let well_known_mock = server.mock(|when, then| {
        when.method(GET).path("/.well-known/acp");
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::to_string(&Value::Object(well_known.clone()))
                    .expect("serialize well-known"),
            );
    });
    let identity_mock = server.mock(|when, then| {
        when.method(GET).path("/api/v1/acp/identity");
        then.status(200)
            .header("Content-Type", "application/json")
            .body(
                serde_json::to_string(&Value::Object(identity_payload.clone()))
                    .expect("serialize identity payload"),
            );
    });
    let inbox_mock = server.mock(|when, then| {
        when.method(POST).path("/acp/inbox");
        then.status(200)
            .header("Content-Type", "application/json")
            .body("{\"status\":\"accepted\"}");
    });

    let sender = AcpAgent::load_or_create(
        "agent:overlay.sender@localhost:9543",
        Some(AcpAgentOptions {
            storage_dir: temp.path().join("sender"),
            allow_insecure_http: true,
            discovery_scheme: "http".to_string(),
            ..AcpAgentOptions::default()
        }),
    )
    .expect("sender agent");
    let mut client = OverlayClient::create(sender);
    let mut payload = Map::new();
    payload.insert(
        "kind".to_string(),
        Value::String("overlay-rust-test".to_string()),
    );

    let response = client
        .send_acp(
            &base_url,
            payload,
            None,
            Some("overlay:rust:outbound".to_string()),
            Some(DeliveryMode::Auto),
            120,
        )
        .expect("overlay outbound send");
    let target = response
        .get("target")
        .and_then(Value::as_object)
        .expect("target should be present");
    assert_eq!(
        Some("agent:overlay.receiver@localhost:9542"),
        target.get("agent_id").and_then(Value::as_str)
    );
    assert_eq!(
        Some(format!("{base_url}/.well-known/acp").as_str()),
        target.get("well_known_url").and_then(Value::as_str)
    );
    let outcomes = response
        .get("send_result")
        .and_then(Value::as_object)
        .and_then(|send_result| send_result.get("outcomes"))
        .and_then(Value::as_array)
        .expect("outcomes should be present");
    assert_eq!(1, outcomes.len());
    let state = outcomes[0]
        .get("state")
        .and_then(Value::as_str)
        .expect("delivery state should be present");
    assert!(
        matches!(state, "DELIVERED" | "ACKNOWLEDGED"),
        "unexpected delivery state: {state}"
    );

    well_known_mock.assert();
    identity_mock.assert();
    inbox_mock.assert();
}