#![cfg(feature = "integration-real-tests")]
#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use async_trait::async_trait;
use meerkat_comms::{
CommsRuntime, PeerRequestResponseAuthority, RuntimeCommsCommandHandle, ToolContext,
handle_tools_call_with_context,
};
use meerkat_core::agent::CommsRuntime as CoreCommsRuntime;
use meerkat_core::comms::TrustedPeerDescriptor;
use meerkat_core::handles::PeerInteractionHandle;
use meerkat_core::{
BlobId, BlobPayload, BlobRef, BlobStore, BlobStoreError, ContentBlock, ContentInput,
DslTransitionError, ImageData, InboundPeerRequestState, InteractionStreamState,
OutboundPeerRequestState, PeerCorrelationId, ResponseStatus, ToolDispatchContext,
};
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
static INPROC_REGISTRY_LOCK: std::sync::LazyLock<tokio::sync::Mutex<()>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(()));
async fn add_generated_peer_projection_trust(
runtime: &CommsRuntime,
peer: TrustedPeerDescriptor,
context: &'static str,
) {
let endpoint = meerkat_runtime::meerkat_machine::dsl::PeerEndpoint::from(&peer);
let state = meerkat_runtime::meerkat_machine::dsl::MeerkatMachineState {
lifecycle_phase: meerkat_runtime::meerkat_machine::dsl::MeerkatPhase::Attached,
session_id: Some(meerkat_runtime::meerkat_machine::dsl::SessionId::from(
"comms-e2e-test-reconcile",
)),
..Default::default()
};
let authority = Arc::new(std::sync::Mutex::new(
meerkat_runtime::meerkat_machine::dsl::MeerkatMachineAuthority::recover_from_state(state)
.expect("test MeerkatMachine state must be recoverable"),
));
let dsl = Arc::new(meerkat_runtime::handles::HandleDslAuthority::from_shared(
Arc::clone(&authority),
));
meerkat_runtime::handles::RuntimePeerCommsHandle::install_generated_on(dsl, runtime)
.unwrap_or_else(|error| panic!("{context}: install generated peer comms owner: {error}"));
let transition = {
let mut guard = authority
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
meerkat_runtime::meerkat_machine::dsl::MeerkatMachineMutator::apply(
&mut *guard,
meerkat_runtime::meerkat_machine::dsl::MeerkatMachineInput::ApplyMobPeerOverlay {
epoch: 1,
endpoints: std::collections::BTreeSet::from([endpoint.clone()]),
},
)
.expect("ApplyMobPeerOverlay input")
};
let obligation =
meerkat_runtime::protocol_comms_trust_reconcile::extract_obligations_with_freshness(
&transition,
meerkat_runtime::protocol_comms_trust_reconcile::PeerProjectionFreshnessAuthority::from_authority(
Arc::clone(&authority),
),
)
.pop()
.expect("generated reconcile obligation");
CoreCommsRuntime::apply_trust_mutation(
runtime,
meerkat_core::comms::CommsTrustMutation::AddTrustedPeer {
authority: meerkat_runtime::protocol_comms_trust_reconcile::authority_for_endpoint(
&obligation,
&endpoint,
)
.expect("generated peer projection add authority"),
peer,
},
)
.await
.unwrap_or_else(|error| panic!("{context}: {error}"));
}
#[derive(Default)]
struct TestBlobStore {
blobs: tokio::sync::RwLock<HashMap<BlobId, BlobPayload>>,
}
#[async_trait]
impl BlobStore for TestBlobStore {
async fn put_image(&self, media_type: &str, data: &str) -> Result<BlobRef, BlobStoreError> {
let blob_id = BlobId::new(format!("sha256:e2e-{media_type}-{data}"));
let payload = BlobPayload {
blob_id: blob_id.clone(),
media_type: media_type.to_string(),
data: data.to_string(),
};
self.blobs.write().await.insert(blob_id.clone(), payload);
Ok(BlobRef {
blob_id,
media_type: media_type.to_string(),
})
}
async fn get(&self, blob_id: &BlobId) -> Result<BlobPayload, BlobStoreError> {
self.blobs
.read()
.await
.get(blob_id)
.cloned()
.ok_or_else(|| BlobStoreError::NotFound(blob_id.clone()))
}
async fn delete(&self, blob_id: &BlobId) -> Result<(), BlobStoreError> {
self.blobs.write().await.remove(blob_id);
Ok(())
}
fn is_persistent(&self) -> bool {
false
}
}
#[derive(Default)]
struct TestPeerInteractionHandle {
outbound: parking_lot::Mutex<HashMap<PeerCorrelationId, OutboundPeerRequestState>>,
inbound: parking_lot::Mutex<HashMap<PeerCorrelationId, InboundPeerRequestState>>,
inbound_modes:
parking_lot::Mutex<HashMap<PeerCorrelationId, meerkat_core::types::HandlingMode>>,
}
impl TestPeerInteractionHandle {
fn rejection(context: &'static str, corr_id: PeerCorrelationId) -> DslTransitionError {
DslTransitionError::guard_rejected(context, format!("e2e rejected corr_id {corr_id}"))
}
}
impl meerkat_core::handles::PeerInteractionHandle for TestPeerInteractionHandle {
fn request_sent(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.outbound
.lock()
.insert(corr_id, OutboundPeerRequestState::Sent);
Ok(())
}
fn response_progress(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
if !self.outbound.lock().contains_key(&corr_id) {
return Err(Self::rejection(
"PeerInteractionHandle::response_progress",
corr_id,
));
}
self.outbound
.lock()
.insert(corr_id, OutboundPeerRequestState::AcceptedProgress);
Ok(())
}
fn response_terminal(
&self,
corr_id: PeerCorrelationId,
_disposition: meerkat_core::handles::PeerTerminalDisposition,
) -> Result<(), DslTransitionError> {
if self.outbound.lock().remove(&corr_id).is_none() {
return Err(Self::rejection(
"PeerInteractionHandle::response_terminal",
corr_id,
));
}
Ok(())
}
fn response_rejected(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
if self.outbound.lock().remove(&corr_id).is_none() {
return Err(Self::rejection(
"PeerInteractionHandle::response_rejected",
corr_id,
));
}
Ok(())
}
fn request_timed_out(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
if self.outbound.lock().remove(&corr_id).is_none() {
return Err(Self::rejection(
"PeerInteractionHandle::request_timed_out",
corr_id,
));
}
Ok(())
}
fn request_send_failed(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
if self.outbound.lock().remove(&corr_id).is_none() {
return Err(Self::rejection(
"PeerInteractionHandle::request_send_failed",
corr_id,
));
}
Ok(())
}
fn request_received(
&self,
corr_id: PeerCorrelationId,
handling_mode: meerkat_core::types::HandlingMode,
) -> Result<(), DslTransitionError> {
self.inbound
.lock()
.insert(corr_id, InboundPeerRequestState::Received);
self.inbound_modes.lock().insert(corr_id, handling_mode);
Ok(())
}
fn classify_response_reply(
&self,
status: meerkat_core::ResponseStatus,
) -> Result<meerkat_core::TerminalityClass, DslTransitionError> {
let generated = meerkat_runtime::handles::RuntimePeerInteractionHandle::ephemeral();
meerkat_core::handles::PeerInteractionHandle::classify_response_reply(&generated, status)
}
fn response_replied(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
if self.inbound.lock().remove(&corr_id).is_none() {
return Err(Self::rejection(
"PeerInteractionHandle::response_replied",
corr_id,
));
}
self.inbound_modes.lock().remove(&corr_id);
Ok(())
}
fn outbound_state(&self, corr_id: PeerCorrelationId) -> Option<OutboundPeerRequestState> {
self.outbound.lock().get(&corr_id).copied()
}
fn inbound_state(&self, corr_id: PeerCorrelationId) -> Option<InboundPeerRequestState> {
self.inbound.lock().get(&corr_id).copied()
}
fn inbound_handling_mode(
&self,
corr_id: PeerCorrelationId,
) -> Option<meerkat_core::types::HandlingMode> {
self.inbound_modes.lock().get(&corr_id).copied()
}
fn install_cleanup_observer(
&self,
_observer: Arc<dyn meerkat_core::handles::PeerInteractionCleanupObserver>,
) {
}
}
#[derive(Default)]
struct TestInteractionStreamHandle {
states: parking_lot::Mutex<HashMap<PeerCorrelationId, InteractionStreamState>>,
}
impl meerkat_core::handles::InteractionStreamHandle for TestInteractionStreamHandle {
fn reserved(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.states
.lock()
.insert(corr_id, InteractionStreamState::Reserved);
Ok(())
}
fn attached(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.states
.lock()
.insert(corr_id, InteractionStreamState::Attached);
Ok(())
}
fn completed(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.states.lock().remove(&corr_id);
Ok(())
}
fn expired(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.states.lock().remove(&corr_id);
Ok(())
}
fn closed_early(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
self.states.lock().remove(&corr_id);
Ok(())
}
fn state(&self, corr_id: PeerCorrelationId) -> Option<InteractionStreamState> {
self.states.lock().get(&corr_id).copied()
}
fn install_cleanup_observer(
&self,
_observer: Arc<dyn meerkat_core::handles::InteractionStreamCleanupObserver>,
) {
}
}
fn install_test_authority(
runtime: &Arc<CommsRuntime>,
peer_handle: Arc<TestPeerInteractionHandle>,
) {
runtime.install_peer_request_response_authority(PeerRequestResponseAuthority::new(
peer_handle,
Arc::new(TestInteractionStreamHandle::default()),
));
}
fn tool_context(runtime: &Arc<CommsRuntime>) -> ToolContext {
let material = runtime.tool_material();
let router = material.router().clone();
let trusted_peers = material.trusted_peers_shared();
let runtime = material.into_runtime();
ToolContext {
router,
trusted_peers,
runtime: Some(RuntimeCommsCommandHandle::new(runtime)),
}
}
#[tokio::test]
#[ignore = "lane:e2e-smoke"]
async fn e2e_smoke_mcp_multimodal_blob_current_turn_request_response_loop() {
let _lock = INPROC_REGISTRY_LOCK.lock().await;
meerkat_comms::InprocRegistry::global().clear();
let suffix = Uuid::new_v4().simple().to_string();
let name_a = format!("e2e-image-a-{suffix}");
let name_b = format!("e2e-image-b-{suffix}");
let mut runtime_a = CommsRuntime::inproc_only(&name_a).expect("runtime A");
let mut runtime_b = CommsRuntime::inproc_only(&name_b).expect("runtime B");
let blob_store_a = Arc::new(TestBlobStore::default());
let blob_store_b = Arc::new(TestBlobStore::default());
let message_blob = blob_store_a
.put_image("image/png", "bWVzc2FnZS1ibG9i")
.await
.expect("message blob");
let response_blob = blob_store_b
.put_image("image/png", "cmVzcG9uc2UtYmxvYg==")
.await
.expect("response blob");
runtime_a.set_blob_store(blob_store_a);
runtime_b.set_blob_store(blob_store_b);
let runtime_a = Arc::new(runtime_a);
let runtime_b = Arc::new(runtime_b);
let peer_a = Arc::new(TestPeerInteractionHandle::default());
let peer_b = Arc::new(TestPeerInteractionHandle::default());
install_test_authority(&runtime_a, peer_a);
install_test_authority(&runtime_b, peer_b.clone());
let peer_b_id = runtime_b.public_key().to_peer_id();
let peer_a_id = runtime_a.public_key().to_peer_id();
add_generated_peer_projection_trust(
runtime_a.as_ref(),
TrustedPeerDescriptor::unsigned_with_pubkey(
name_b.clone(),
peer_b_id.to_string(),
*runtime_b.public_key().as_bytes(),
format!("inproc://{name_b}"),
)
.expect("B trusted descriptor"),
"A trusts B",
)
.await;
add_generated_peer_projection_trust(
runtime_b.as_ref(),
TrustedPeerDescriptor::unsigned_with_pubkey(
name_a.clone(),
peer_a_id.to_string(),
*runtime_a.public_key().as_bytes(),
format!("inproc://{name_a}"),
)
.expect("A trusted descriptor"),
"B trusts A",
)
.await;
let ctx_a = tool_context(&runtime_a);
let ctx_b = tool_context(&runtime_b);
handle_tools_call_with_context(
&ctx_a,
"send_message",
&json!({
"peer_id": peer_b_id,
"display_name": name_b,
"body": "A->B blob prelude",
"blocks": [
{"type": "image_ref", "source": "blob", "blob_id": message_blob.blob_id, "media_type": "image/png"},
{"type": "text", "text": "Prelude after the image so body synthesis has to decide what to do."}
],
"handling_mode": "queue"
}),
&ToolDispatchContext::default(),
)
.await
.expect("send blob-backed message");
let b_messages = CoreCommsRuntime::drain_inbox_interactions(runtime_b.as_ref()).await;
assert_eq!(b_messages.len(), 1);
match &b_messages[0].content {
meerkat_core::InteractionContent::Message { body, blocks } => {
assert_eq!(
body,
"Prelude after the image so body synthesis has to decide what to do."
);
let blocks = blocks.as_ref().expect("message blocks");
assert_eq!(blocks.len(), 2);
assert!(matches!(
&blocks[0],
ContentBlock::Image {
media_type,
data: ImageData::Inline { data }
} if media_type == "image/png" && data == "bWVzc2FnZS1ibG9i"
));
assert!(matches!(
&blocks[1],
ContentBlock::Text { text } if text == "Prelude after the image so body synthesis has to decide what to do."
));
}
other => panic!("expected B message, got {other:?}"),
}
let request_context =
ToolDispatchContext::from_current_turn_input(&ContentInput::Blocks(vec![
ContentBlock::Text {
text: "not an image".to_string(),
},
ContentBlock::Image {
media_type: "image/png".to_string(),
data: ImageData::Inline {
data: "Y3VycmVudC10dXJuLWltYWdl".to_string(),
},
},
]));
handle_tools_call_with_context(
&ctx_a,
"send_request",
&json!({
"peer_id": peer_b_id,
"display_name": name_b,
"intent": "checksum_token",
"params": {"subject": "describe-current-turn-and-answer-with-image"},
"blocks": [
{"type": "text", "text": "Request body text block"},
{"type": "image_ref", "source": "current_turn", "index": 0}
],
"handling_mode": "steer"
}),
&request_context,
)
.await
.expect("send current-turn image request");
let b_requests = CoreCommsRuntime::drain_inbox_interactions(runtime_b.as_ref()).await;
assert_eq!(b_requests.len(), 1);
let request_id = b_requests[0].id;
match &b_requests[0].content {
meerkat_core::InteractionContent::Request {
intent,
params,
blocks,
} => {
assert_eq!(intent, "checksum_token");
assert_eq!(
params["subject"],
"describe-current-turn-and-answer-with-image"
);
let blocks = blocks.as_ref().expect("request blocks");
assert!(matches!(
&blocks[1],
ContentBlock::Image {
data: ImageData::Inline { data },
..
} if data == "Y3VycmVudC10dXJuLWltYWdl"
));
}
other => panic!("expected B request, got {other:?}"),
}
peer_b
.request_received(
PeerCorrelationId::from_uuid(request_id.0),
meerkat_core::types::HandlingMode::Queue,
)
.expect("seed B inbound request authority");
handle_tools_call_with_context(
&ctx_b,
"send_response",
&json!({
"peer_id": peer_a_id,
"display_name": name_a,
"in_reply_to": request_id.0.to_string(),
"status": "completed",
"result": {
"request_intent": "checksum_token",
"request_subject": "describe-current-turn-and-answer-with-image",
"token": "blob-current-turn-response-ok"
},
"blocks": [
{"type": "text", "text": "B response text block"},
{"type": "image_ref", "source": "blob", "blob_id": response_blob.blob_id, "media_type": "image/png"}
],
"handling_mode": "steer"
}),
&ToolDispatchContext::default(),
)
.await
.expect("send blob-backed response");
let a_responses = CoreCommsRuntime::drain_inbox_interactions(runtime_a.as_ref()).await;
assert_eq!(a_responses.len(), 1);
match &a_responses[0].content {
meerkat_core::InteractionContent::Response {
in_reply_to,
status,
result,
blocks,
} => {
assert_eq!(*in_reply_to, request_id);
assert_eq!(*status, ResponseStatus::Completed);
assert_eq!(result["token"], "blob-current-turn-response-ok");
let blocks = blocks.as_ref().expect("response blocks");
assert!(matches!(
&blocks[1],
ContentBlock::Image {
media_type,
data: ImageData::Inline { data }
} if media_type == "image/png" && data == "cmVzcG9uc2UtYmxvYg=="
));
}
other => panic!("expected A response, got {other:?}"),
}
}