use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use contextvm_sdk::core::constants::CTXVM_MESSAGES_KIND;
use contextvm_sdk::core::types::EncryptionMode;
use contextvm_sdk::relay::mock::MockRelayPool;
use contextvm_sdk::transport::base::BaseTransport;
use contextvm_sdk::transport::client::{NostrClientTransport, NostrClientTransportConfig};
use contextvm_sdk::transport::oversized_transfer::{
build_oversized_frames, OversizedFrame, OversizedSenderOptions, OversizedTransferConfig,
};
use contextvm_sdk::transport::server::{NostrServerTransport, NostrServerTransportConfig};
use contextvm_sdk::{
progress_aware_options, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
PeerRequestOptionsExt, RelayPoolTrait,
};
use nostr_sdk::prelude::*;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{
CallToolRequestParams, CallToolResult, Content, ErrorData, Implementation, RawContent,
ServerCapabilities,
};
use rmcp::service::ServiceError;
use rmcp::{schemars, tool, tool_handler, tool_router, ClientHandler, ServerHandler, ServiceExt};
async fn let_event_loops_start() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
async fn start_client_with(
client_pool: MockRelayPool,
config: NostrClientTransportConfig,
) -> (
NostrClientTransport,
tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>,
) {
let mut client = NostrClientTransport::with_relay_pool(
config,
Arc::new(client_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create client transport");
let rx = client.take_message_receiver().expect("client rx");
client.start().await.expect("client start");
let_event_loops_start().await;
(client, rx)
}
async fn start_client(
client_pool: MockRelayPool,
server_pubkey: &PublicKey,
oversized: OversizedTransferConfig,
) -> (
NostrClientTransport,
tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>,
) {
start_client_with(
client_pool,
NostrClientTransportConfig::default()
.with_relay_urls(vec!["wss://mock.relay".to_string()])
.with_server_pubkey(server_pubkey.to_hex())
.with_encryption_mode(EncryptionMode::Disabled)
.with_oversized_transfer(oversized),
)
.await
}
fn greybox_server_base(server_pool: &Arc<MockRelayPool>) -> BaseTransport {
BaseTransport {
relay_pool: Arc::clone(server_pool) as Arc<dyn RelayPoolTrait>,
encryption_mode: EncryptionMode::Disabled,
is_connected: true,
}
}
async fn publish_frame(
base: &BaseTransport,
recipient: &PublicKey,
tags: &[Tag],
frame: JsonRpcNotification,
) {
base.send_mcp_message(
&JsonRpcMessage::Notification(frame),
recipient,
CTXVM_MESSAGES_KIND,
tags.to_vec(),
Some(false),
None,
)
.await
.expect("publish frame");
}
async fn poll_for_event(
pool: &MockRelayPool,
what: &str,
pred: impl Fn(&Event) -> bool,
) -> EventId {
for _ in 0..200 {
if let Some(event) = pool
.stored_events()
.await
.iter()
.find(|e| e.kind == Kind::Custom(CTXVM_MESSAGES_KIND) && pred(e))
{
return event.id;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("{what} never reached the relay store");
}
async fn count_oversized_frames(pool: &MockRelayPool) -> usize {
pool.stored_events()
.await
.iter()
.filter(|e| e.kind == Kind::Custom(CTXVM_MESSAGES_KIND))
.filter(|e| {
serde_json::from_str::<serde_json::Value>(&e.content)
.ok()
.is_some_and(|v| v.get("params").and_then(|p| p.get("cvm")).is_some())
})
.count()
}
fn is_frame_of_type(event: &Event, frame_type: &str) -> bool {
serde_json::from_str::<serde_json::Value>(&event.content)
.ok()
.and_then(|v| {
v.get("params")
.and_then(|p| p.get("cvm"))
.and_then(|c| c.get("frameType"))
.and_then(|f| f.as_str().map(|f| f == frame_type))
})
.unwrap_or(false)
}
async fn recv_within(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>,
ms: u64,
what: &str,
) -> JsonRpcMessage {
tokio::time::timeout(Duration::from_millis(ms), rx.recv())
.await
.unwrap_or_else(|_| panic!("timed out waiting for {what}"))
.expect("client channel closed")
}
async fn try_recv_response(
rx: &mut tokio::sync::mpsc::UnboundedReceiver<JsonRpcMessage>,
ms: u64,
) -> Option<JsonRpcMessage> {
let deadline = tokio::time::Instant::now() + Duration::from_millis(ms);
loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Err(_) => return None,
Ok(None) => panic!("client channel closed"),
Ok(Some(msg)) if msg.is_response() => return Some(msg),
Ok(Some(_)) => continue,
}
}
}
fn assert_stripped_forward(
msg: JsonRpcMessage,
expected_token: &serde_json::Value,
expected_progress: u64,
) {
let JsonRpcMessage::Notification(n) = msg else {
panic!("expected a stripped progress notification");
};
assert_eq!(n.method, "notifications/progress");
let params = n.params.expect("forwarded progress has params");
assert_eq!(
¶ms["progressToken"], expected_token,
"token must be restored to the original JSON value, got {params}"
);
assert_eq!(params["progress"], serde_json::json!(expected_progress));
assert!(
params.get("cvm").is_none(),
"cvm payload must be stripped, got {params}"
);
}
struct DelayedRelayPool {
inner: Arc<MockRelayPool>,
publish_delay: Duration,
}
#[async_trait]
impl RelayPoolTrait for DelayedRelayPool {
async fn connect(&self, relay_urls: &[String]) -> contextvm_sdk::Result<()> {
self.inner.connect(relay_urls).await
}
async fn disconnect(&self) -> contextvm_sdk::Result<()> {
self.inner.disconnect().await
}
async fn publish_event(&self, event: &Event) -> contextvm_sdk::Result<EventId> {
tokio::time::sleep(self.publish_delay).await;
self.inner.publish_event(event).await
}
async fn publish(&self, builder: EventBuilder) -> contextvm_sdk::Result<EventId> {
tokio::time::sleep(self.publish_delay).await;
self.inner.publish(builder).await
}
async fn sign(&self, builder: EventBuilder) -> contextvm_sdk::Result<Event> {
self.inner.sign(builder).await
}
async fn signer(&self) -> contextvm_sdk::Result<Arc<dyn NostrSigner>> {
self.inner.signer().await
}
fn notifications(&self) -> tokio::sync::broadcast::Receiver<RelayPoolNotification> {
self.inner.notifications()
}
async fn public_key(&self) -> contextvm_sdk::Result<PublicKey> {
self.inner.public_key().await
}
async fn subscribe(&self, filters: Vec<Filter>) -> contextvm_sdk::Result<()> {
self.inner.subscribe(filters).await
}
async fn publish_to(
&self,
urls: &[String],
builder: EventBuilder,
) -> contextvm_sdk::Result<EventId> {
self.inner.publish_to(urls, builder).await
}
async fn fetch_events(
&self,
filters: Vec<Filter>,
timeout: Duration,
) -> contextvm_sdk::Result<Vec<Event>> {
self.inner.fetch_events(filters, timeout).await
}
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct BigParams {
len: usize,
}
#[derive(Clone)]
struct BigServer {}
impl BigServer {
fn new() -> Self {
Self {}
}
}
#[tool_router]
impl BigServer {
#[tool(description = "Return a text payload of `len` bytes")]
fn big(
&self,
Parameters(BigParams { len }): Parameters<BigParams>,
) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![Content::text(
"B".repeat(len),
)]))
}
}
#[tool_handler]
impl ServerHandler for BigServer {
fn get_info(&self) -> rmcp::model::ServerInfo {
rmcp::model::ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new("oversized-e2e-server", "0.1.0"))
}
}
#[derive(Clone, Default)]
struct DemoClient;
impl ClientHandler for DemoClient {}
fn first_text(result: &CallToolResult) -> String {
result
.content
.iter()
.find_map(|c| match &c.raw {
RawContent::Text(t) => Some(t.text.clone()),
_ => None,
})
.unwrap_or_default()
}
fn call_params(name: &'static str, args: serde_json::Value) -> CallToolRequestParams {
let mut params = CallToolRequestParams::new(name);
if let Ok(v) = serde_json::from_value(args) {
params = params.with_arguments(v);
}
params
}
async fn wait_for_rmcp_request(pool: &MockRelayPool) -> (EventId, String) {
for _ in 0..500 {
for event in pool.stored_events().await {
if event.kind != Kind::Custom(CTXVM_MESSAGES_KIND) {
continue;
}
let Ok(v) = serde_json::from_str::<serde_json::Value>(&event.content) else {
continue;
};
if v.get("method").and_then(|m| m.as_str()) != Some("tools/call") {
continue;
}
let token = match v
.get("params")
.and_then(|p| p.get("_meta"))
.and_then(|m| m.get("progressToken"))
{
Some(serde_json::Value::Number(n)) => n.to_string(),
Some(serde_json::Value::String(s)) => s.clone(),
_ => continue,
};
return (event.id, token);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("rmcp tools/call request never reached the relay store");
}
async fn assert_cancelled_with_reason(pool: &MockRelayPool, reason: &str) {
for _ in 0..200 {
for event in pool.stored_events().await {
if event.kind != Kind::Custom(CTXVM_MESSAGES_KIND) {
continue;
}
let Ok(v) = serde_json::from_str::<serde_json::Value>(&event.content) else {
continue;
};
if v.get("method").and_then(|m| m.as_str()) == Some("notifications/cancelled")
&& v.get("params")
.and_then(|p| p.get("reason"))
.and_then(|r| r.as_str())
== Some(reason)
{
return;
}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("no notifications/cancelled with reason {reason:?} observed");
}
async fn spawn_greybox_call(
client_pool: MockRelayPool,
server_pubkey: &PublicKey,
idle: Duration,
max_total: Duration,
) -> tokio::task::JoinHandle<(Result<CallToolResult, ServiceError>, Duration)> {
let client_transport = NostrClientTransport::with_relay_pool(
NostrClientTransportConfig::default()
.with_relay_urls(vec!["wss://mock.relay".to_string()])
.with_server_pubkey(server_pubkey.to_hex())
.with_encryption_mode(EncryptionMode::Disabled)
.with_stateless(true)
.with_oversized_transfer(OversizedTransferConfig::enabled()),
Arc::new(client_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create client transport");
let client = DemoClient
.serve(client_transport)
.await
.expect("client init (stateless)");
tokio::spawn(async move {
let started = tokio::time::Instant::now();
let result = client
.peer()
.call_tool_with_options(
call_params("big", serde_json::json!({ "len": 1 })),
progress_aware_options(idle, max_total),
)
.await;
drop(client);
(result, started.elapsed())
})
}
#[test]
fn harness_mock_relay_pair_is_linked() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
assert_ne!(
client_pool.mock_public_key(),
server_pool.mock_public_key(),
"paired mock pools must have distinct signing identities"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn oversized_progress_token_restored() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let (client, mut client_rx) = start_client(
client_pool,
&server_pubkey,
OversizedTransferConfig::enabled(),
)
.await;
let request = JsonRpcMessage::Request(JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: serde_json::json!("e4-1"),
method: "tools/call".to_string(),
params: Some(serde_json::json!({ "_meta": { "progressToken": 7 } })),
});
client.send(&request).await.expect("send request");
let request_event_id = poll_for_event(&server_pool, "request event", |e| {
serde_json::from_str::<serde_json::Value>(&e.content)
.ok()
.is_some_and(|v| v.get("method").and_then(|m| m.as_str()) == Some("tools/call"))
})
.await;
let response = JsonRpcMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::json!("e4-1"),
result: serde_json::json!({ "blob": "Z".repeat(90) }),
});
let serialized = serde_json::to_string(&response).unwrap();
let frames = build_oversized_frames(
&serialized,
&OversizedSenderOptions::new("7").with_chunk_size(serialized.len().div_ceil(3)),
)
.unwrap();
assert_eq!(frames.chunks.len(), 3, "harness wants exactly 3 chunks");
let base = greybox_server_base(&server_pool);
let tags = BaseTransport::create_response_tags(&client_pubkey, &request_event_id);
for frame in frames.into_ordered() {
publish_frame(&base, &client_pubkey, &tags, frame).await;
}
for expected_progress in 1u64..=4 {
let msg = recv_within(&mut client_rx, 1000, "stripped progress forward").await;
assert_stripped_forward(msg, &serde_json::json!(7), expected_progress);
}
let msg = recv_within(&mut client_rx, 1000, "reassembled response").await;
assert!(msg.is_response());
assert_eq!(msg.id(), Some(&serde_json::json!("e4-1")));
let extra = tokio::time::timeout(Duration::from_millis(150), client_rx.recv()).await;
assert!(
extra.is_err(),
"no extra forward expected for the end frame"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn accept_frame_forwards_one_progress_reset() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let (client, mut client_rx) = start_client(
client_pool,
&server_pubkey,
OversizedTransferConfig::enabled()
.with_threshold(600)
.with_chunk_size(600),
)
.await;
let client = Arc::new(client);
let request = JsonRpcMessage::Request(JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: serde_json::json!("e5-1"),
method: "tools/call".to_string(),
params: Some(serde_json::json!({
"_meta": { "progressToken": 8 },
"blob": "Q".repeat(2000),
})),
});
let send_task = {
let client = Arc::clone(&client);
tokio::spawn(async move { client.send(&request).await })
};
let start_event_id = poll_for_event(&server_pool, "start frame", |e| {
is_frame_of_type(e, "start")
})
.await;
let accept = OversizedFrame::Accept
.into_progress_notification("8", 2, None)
.expect("build accept frame");
let tags = BaseTransport::create_response_tags(&client_pubkey, &start_event_id);
publish_frame(
&greybox_server_base(&server_pool),
&client_pubkey,
&tags,
accept,
)
.await;
send_task
.await
.expect("send task join")
.expect("oversized send completes after accept");
let msg = recv_within(&mut client_rx, 1000, "accept progress forward").await;
assert_stripped_forward(msg, &serde_json::json!(8), 2);
let extra = tokio::time::timeout(Duration::from_millis(150), client_rx.recv()).await;
assert!(
extra.is_err(),
"exactly one forward expected for the accept handshake"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn watchdog_reaps_stalled_inbound_transfer_client() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let (_client, mut client_rx) = start_client_with(
client_pool,
NostrClientTransportConfig::default()
.with_relay_urls(vec!["wss://mock.relay".to_string()])
.with_server_pubkey(server_pubkey.to_hex())
.with_encryption_mode(EncryptionMode::Disabled)
.with_timeout(Duration::from_secs(2))
.with_oversized_transfer(
OversizedTransferConfig::enabled().with_transfer_timeout_ms(200),
),
)
.await;
let payload = JsonRpcMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::json!("w6-1"),
result: serde_json::json!({ "blob": "Y".repeat(90) }),
});
let serialized = serde_json::to_string(&payload).unwrap();
let build = || {
build_oversized_frames(
&serialized,
&OversizedSenderOptions::new("w6").with_chunk_size(serialized.len().div_ceil(3)),
)
.expect("build frames")
};
let base = greybox_server_base(&server_pool);
let tags = BaseTransport::create_recipient_tags(&client_pubkey);
let mut stalled = build().into_ordered();
let rest = stalled.split_off(2);
for frame in stalled {
publish_frame(&base, &client_pubkey, &tags, frame).await;
}
tokio::time::sleep(Duration::from_secs(3)).await;
for frame in rest {
publish_frame(&base, &client_pubkey, &tags, frame).await;
}
assert!(
try_recv_response(&mut client_rx, 500).await.is_none(),
"a reaped transfer must never deliver a message"
);
for frame in build().into_ordered() {
publish_frame(&base, &client_pubkey, &tags, frame).await;
}
let delivered = try_recv_response(&mut client_rx, 1500)
.await
.expect("fresh same-token transfer must deliver after the reap");
assert_eq!(delivered.id(), Some(&serde_json::json!("w6-1")));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn watchdog_reaps_stalled_inbound_transfer_server() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let mut server = NostrServerTransport::with_relay_pool(
NostrServerTransportConfig::default()
.with_encryption_mode(EncryptionMode::Disabled)
.with_oversized_transfer(
OversizedTransferConfig::enabled().with_transfer_timeout_ms(200),
),
Arc::clone(&server_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create server transport");
let mut server_rx = server.take_message_receiver().expect("server rx");
server.start().await.expect("server start");
let_event_loops_start().await;
let request = JsonRpcMessage::Request(JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: serde_json::json!("w7-1"),
method: "tools/call".to_string(),
params: Some(serde_json::json!({
"_meta": { "progressToken": "w7" },
"blob": "Q".repeat(300),
})),
});
let serialized = serde_json::to_string(&request).unwrap();
let build = || {
build_oversized_frames(
&serialized,
&OversizedSenderOptions::new("w7")
.with_chunk_size(96)
.with_accept_handshake(true),
)
.expect("build frames")
};
let base = BaseTransport {
relay_pool: Arc::new(client_pool) as Arc<dyn RelayPoolTrait>,
encryption_mode: EncryptionMode::Disabled,
is_connected: true,
};
let tags = BaseTransport::create_recipient_tags(&server_pubkey);
let mut stalled = build().into_ordered();
let rest = stalled.split_off(2);
for frame in stalled {
publish_frame(&base, &server_pubkey, &tags, frame).await;
}
tokio::time::sleep(Duration::from_secs(3)).await;
for frame in rest {
publish_frame(&base, &server_pubkey, &tags, frame).await;
}
let nothing = tokio::time::timeout(Duration::from_millis(500), server_rx.recv()).await;
assert!(
nothing.is_err(),
"a reaped transfer must never deliver a request"
);
for frame in build().into_ordered() {
publish_frame(&base, &server_pubkey, &tags, frame).await;
}
let incoming = tokio::time::timeout(Duration::from_millis(1500), server_rx.recv())
.await
.expect("fresh same-token transfer must deliver after the reap")
.expect("server channel closed");
assert_eq!(incoming.message.method(), Some("tools/call"));
assert_eq!(incoming.message.id(), Some(&serde_json::json!("w7-1")));
}
#[tokio::test(start_paused = true)]
async fn oversized_response_progress_resets_idle_timeout() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let delayed: Arc<dyn RelayPoolTrait> = Arc::new(DelayedRelayPool {
inner: Arc::clone(&server_pool),
publish_delay: Duration::from_millis(150),
});
let server_transport = NostrServerTransport::with_relay_pool(
NostrServerTransportConfig::default()
.with_encryption_mode(EncryptionMode::Disabled)
.with_oversized_transfer(OversizedTransferConfig::enabled()),
delayed,
)
.await
.expect("create server transport");
let client_transport = NostrClientTransport::with_relay_pool(
NostrClientTransportConfig::default()
.with_relay_urls(vec!["wss://mock.relay".to_string()])
.with_server_pubkey(server_pubkey.to_hex())
.with_encryption_mode(EncryptionMode::Disabled)
.with_oversized_transfer(OversizedTransferConfig::enabled()),
Arc::new(client_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create client transport");
let server_handle = tokio::spawn(async move {
let running = BigServer::new()
.serve(server_transport)
.await
.expect("server serve failed");
let _ = running.waiting().await;
});
tokio::time::sleep(Duration::from_millis(20)).await;
let client = tokio::time::timeout(Duration::from_secs(10), DemoClient.serve(client_transport))
.await
.expect("client startup timed out")
.expect("client init failed");
let result = client
.peer()
.call_tool_with_options(
call_params("big", serde_json::json!({ "len": 240_000 })),
progress_aware_options(Duration::from_millis(400), Duration::from_secs(10)),
)
.await
.expect("oversized call must succeed via per-chunk idle resets");
assert_eq!(first_text(&result).len(), 240_000);
let frames = count_oversized_frames(&server_pool).await;
assert!(frames >= 5, "expected ≥5 oversized frames, got {frames}");
server_handle.abort();
}
#[tokio::test(start_paused = true)]
async fn oversized_stalled_transfer_trips_idle_timeout() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let idle = Duration::from_millis(400);
let call = spawn_greybox_call(client_pool, &server_pubkey, idle, Duration::from_secs(10)).await;
let (request_event_id, wire_token) = wait_for_rmcp_request(&server_pool).await;
let payload = JsonRpcMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::json!(0),
result: serde_json::json!({ "blob": "S".repeat(200) }),
});
let serialized = serde_json::to_string(&payload).unwrap();
let frames = build_oversized_frames(
&serialized,
&OversizedSenderOptions::new(&wire_token).with_chunk_size(serialized.len().div_ceil(5)),
)
.expect("build frames");
let base = greybox_server_base(&server_pool);
let tags = BaseTransport::create_response_tags(&client_pubkey, &request_event_id);
for frame in frames.into_ordered().into_iter().take(3) {
publish_frame(&base, &client_pubkey, &tags, frame).await;
tokio::time::sleep(Duration::from_millis(200)).await;
}
let (result, elapsed) = call.await.expect("join call task");
match result {
Err(ServiceError::Timeout { timeout }) => assert_eq!(timeout, idle, "idle timer fired"),
other => panic!("expected idle Timeout, got {other:?}"),
}
assert!(
elapsed >= Duration::from_millis(700),
"resets must precede expiry; elapsed {elapsed:?}"
);
assert_cancelled_with_reason(&server_pool, "request timeout").await;
}
#[tokio::test(start_paused = true)]
async fn oversized_trickle_trips_max_total_timeout() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let idle = Duration::from_millis(400);
let max_total = Duration::from_secs(1);
let call = spawn_greybox_call(client_pool, &server_pubkey, idle, max_total).await;
let (request_event_id, wire_token) = wait_for_rmcp_request(&server_pool).await;
let payload = JsonRpcMessage::Response(JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::json!(0),
result: serde_json::json!({ "blob": "T".repeat(500) }),
});
let serialized = serde_json::to_string(&payload).unwrap();
let frames = build_oversized_frames(
&serialized,
&OversizedSenderOptions::new(&wire_token).with_chunk_size(40),
)
.expect("build frames");
let base = greybox_server_base(&server_pool);
let tags = BaseTransport::create_response_tags(&client_pubkey, &request_event_id);
for frame in frames.into_ordered().into_iter().take(10) {
publish_frame(&base, &client_pubkey, &tags, frame).await;
tokio::time::sleep(Duration::from_millis(150)).await;
}
let (result, elapsed) = call.await.expect("join call task");
match result {
Err(ServiceError::Timeout { timeout }) => {
assert_eq!(timeout, max_total, "max-total timer fired")
}
other => panic!("expected max-total Timeout, got {other:?}"),
}
assert!(
elapsed >= Duration::from_millis(900) && elapsed <= Duration::from_millis(1400),
"max-total should cap the call at ~1 s; elapsed {elapsed:?}"
);
assert_cancelled_with_reason(&server_pool, "maximum total timeout exceeded").await;
}
fn event_has_tag(event: &Event, name: &str) -> bool {
event
.tags
.iter()
.any(|t| t.clone().to_vec().first().map(String::as_str) == Some(name))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn oversized_default_on_e2e_roundtrip() {
let (client_pool, server_pool) = MockRelayPool::create_pair();
let server_pubkey = server_pool.mock_public_key();
let client_pubkey = client_pool.mock_public_key();
let server_pool = Arc::new(server_pool);
let server_transport = NostrServerTransport::with_relay_pool(
NostrServerTransportConfig::default().with_encryption_mode(EncryptionMode::Disabled),
Arc::clone(&server_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create server transport");
let client_transport = NostrClientTransport::with_relay_pool(
NostrClientTransportConfig::default()
.with_relay_urls(vec!["wss://mock.relay".to_string()])
.with_server_pubkey(server_pubkey.to_hex())
.with_encryption_mode(EncryptionMode::Disabled),
Arc::new(client_pool) as Arc<dyn RelayPoolTrait>,
)
.await
.expect("create client transport");
let server_handle = tokio::spawn(async move {
let running = BigServer::new()
.serve(server_transport)
.await
.expect("server serve failed");
let _ = running.waiting().await;
});
tokio::time::sleep(Duration::from_millis(20)).await;
let client = tokio::time::timeout(Duration::from_secs(10), DemoClient.serve(client_transport))
.await
.expect("client startup timed out")
.expect("client init failed");
let result = tokio::time::timeout(
Duration::from_secs(15),
client.call_tool(call_params("big", serde_json::json!({ "len": 80_000 }))),
)
.await
.expect("default-on oversized roundtrip timed out")
.expect("call_tool failed");
assert_eq!(
first_text(&result).len(),
80_000,
"the >65 535-byte payload must roundtrip intact"
);
let frames = count_oversized_frames(&server_pool).await;
assert!(
frames >= 3,
"expected at least start+chunk+end cvm frames, got {frames}"
);
let events = server_pool.stored_events().await;
assert!(
events
.iter()
.any(|e| e.pubkey == client_pubkey && event_has_tag(e, "support_oversized_transfer")),
"client's first request must advertise support_oversized_transfer"
);
assert!(
events
.iter()
.any(|e| e.pubkey == server_pubkey && event_has_tag(e, "support_oversized_transfer")),
"server's first response must advertise support_oversized_transfer"
);
server_handle.abort();
}