v0.2.7-pre.006

This commit is contained in:
2026-08-22 19:56:20 +02:00
parent 6e3a0fa034
commit 8721e54b18
13 changed files with 1576 additions and 191 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
// version: 2
// version: 3
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
@@ -380,3 +380,185 @@ async fn websocket_repeated_connect_close_cycles_are_bounded() {
.expect("repeated close server must remain bounded")
.expect("repeated close server task must join");
}
async fn send_notification(
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
method: &str,
remote_subscription_id: u64,
result: serde_json::Value,
) {
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": {
"result": result,
"subscription": remote_subscription_id
}
});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send");
}
async fn wait_for_subscription_state<T>(subscription: &crate::WsSubscription<T>, expected: crate::WsSubscriptionState) {
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
loop {
if subscription.state() == expected {
return;
}
assert!(tokio::time::Instant::now() < deadline, "subscription did not reach expected state: {expected:?}");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
}
#[tokio::test(flavor = "current_thread")]
async fn websocket_generic_subscription_registers_remote_id_dispatches_typed_notification_and_unsubscribes() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
assert_eq!(subscribe.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("slotSubscribe"));
send_result(&mut websocket, &subscribe, serde_json::json!(41)).await;
send_notification(&mut websocket, "slotNotification", 41, serde_json::json!({"slot": 9001})).await;
let unsubscribe = read_request(&mut websocket).await;
assert_eq!(unsubscribe.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("slotUnsubscribe"));
assert_eq!(unsubscribe.get("params"), std::option::Option::Some(&serde_json::json!([41])));
send_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let mut subscription = session
.subscribe_typed(crate::WsSubscriptionKind::Slot, std::vec::Vec::new(), |value| {
return std::result::Result::Ok(value);
})
.await
.expect("generic slot subscribe must succeed");
assert_eq!(subscription.id().get(), 1);
assert_eq!(subscription.kind(), crate::WsSubscriptionKind::Slot);
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
let snapshot = session.snapshot();
assert_eq!(snapshot.subscription_count(), 1);
assert_eq!(snapshot.subscriptions()[0].id(), subscription.id());
assert!(snapshot.subscriptions()[0].remote_bound());
let notification = subscription.recv().await.expect("typed notification channel must remain open").expect("notification must decode");
assert_eq!(notification, serde_json::json!({"slot": 9001}));
assert!(subscription.unsubscribe().await.expect("unsubscribe must succeed"));
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
tokio::task::yield_now().await;
assert_eq!(session.snapshot().subscription_count(), 0);
server.await.expect("local server task must complete");
}
#[tokio::test(flavor = "current_thread")]
async fn websocket_remote_subscription_mapping_routes_multiple_families_to_stable_local_ids() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let first = read_request(&mut websocket).await;
send_result(&mut websocket, &first, serde_json::json!(77)).await;
let second = read_request(&mut websocket).await;
send_result(&mut websocket, &second, serde_json::json!(12)).await;
send_notification(&mut websocket, "logsNotification", 12, serde_json::json!({"family": "logs"})).await;
send_notification(&mut websocket, "accountNotification", 77, serde_json::json!({"family": "account"})).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let mut account = session
.subscribe_typed(crate::WsSubscriptionKind::Account, std::vec![serde_json::json!("account")], |value| {
return std::result::Result::Ok(value);
})
.await
.expect("account subscribe must succeed");
let mut logs = session
.subscribe_typed(crate::WsSubscriptionKind::Logs, std::vec![serde_json::json!("all")], |value| {
return std::result::Result::Ok(value);
})
.await
.expect("logs subscribe must succeed");
assert_eq!(account.id().get(), 1);
assert_eq!(logs.id().get(), 2);
assert_eq!(logs.recv().await.expect("logs notification must exist").expect("logs notification must decode"), serde_json::json!({"family": "logs"}));
assert_eq!(
account.recv().await.expect("account notification must exist").expect("account notification must decode"),
serde_json::json!({"family": "account"})
);
server.await.expect("local server task must complete");
}
#[tokio::test(flavor = "current_thread")]
async fn websocket_unknown_remote_subscription_notification_is_ignored_without_affecting_registered_subscription() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
send_result(&mut websocket, &subscribe, serde_json::json!(3)).await;
send_notification(&mut websocket, "rootNotification", 999, serde_json::json!(100)).await;
send_notification(&mut websocket, "rootNotification", 3, serde_json::json!(101)).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let mut subscription = session
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
return std::result::Result::Ok(value);
})
.await
.expect("root subscribe must succeed");
let notification = subscription.recv().await.expect("valid notification must exist").expect("valid notification must decode");
assert_eq!(notification, serde_json::json!(101));
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
server.await.expect("local server task must complete");
}
#[tokio::test(flavor = "current_thread")]
async fn websocket_notification_method_mismatch_fails_only_the_logical_subscription() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
send_result(&mut websocket, &subscribe, serde_json::json!(5)).await;
send_notification(&mut websocket, "rootNotification", 5, serde_json::json!(1)).await;
let request = read_request(&mut websocket).await;
send_result(&mut websocket, &request, serde_json::json!(true)).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let mut subscription = session
.subscribe_typed(crate::WsSubscriptionKind::Slot, std::vec::Vec::new(), |value| {
return std::result::Result::Ok(value);
})
.await
.expect("slot subscribe must succeed");
wait_for_subscription_state(&subscription, crate::WsSubscriptionState::Failed).await;
assert!(subscription.recv().await.is_none());
assert_eq!(session.state(), crate::WsSessionState::Active);
let result = session.execute_json_rpc("afterMismatch", std::vec::Vec::new()).await.expect("physical session must remain usable");
assert_eq!(result, serde_json::json!(true));
server.await.expect("local server task must complete");
}
#[tokio::test(flavor = "current_thread")]
async fn websocket_typed_notification_decode_failure_fails_only_one_subscription_and_surfaces_error() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
send_result(&mut websocket, &subscribe, serde_json::json!(8)).await;
send_notification(&mut websocket, "rootNotification", 8, serde_json::json!("not-a-slot")).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let mut subscription = session
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
return match value.as_u64() {
std::option::Option::Some(slot) => std::result::Result::Ok(slot),
std::option::Option::None => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "fixture root notification must be numeric"))
},
};
})
.await
.expect("root subscribe must succeed");
let error = subscription.recv().await.expect("decode error must be delivered").expect_err("fixture payload must fail typed decoder");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
wait_for_subscription_state(&subscription, crate::WsSubscriptionState::Failed).await;
assert_eq!(session.state(), crate::WsSessionState::Active);
server.await.expect("local server task must complete");
}