pub mod common;
use std::time::Duration;
use futures::{SinkExt, StreamExt};
use nodedb_lite::engine::crdt::CrdtEngine;
use nodedb_types::sync::shape::{ShapeDefinition, ShapeType};
use nodedb_types::sync::wire::{
DeltaPushMsg, ResyncReason, ResyncRequestMsg, ShapeSnapshotMsg, ShapeSubscribeMsg, SyncFrame,
SyncMessageType,
};
use tokio_tungstenite::tungstenite::Message;
use common::origin::{OriginServer, connect_and_handshake};
async fn subscribe_shape(
ws: &mut tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
shape_id: &str,
collection: &str,
) -> ShapeSnapshotMsg {
let subscribe = ShapeSubscribeMsg {
shape: ShapeDefinition {
shape_id: shape_id.into(),
tenant_id: 0,
shape_type: ShapeType::Document {
collection: collection.into(),
predicate: Vec::new(),
},
description: "interop-resync-test".into(),
field_filter: vec![],
},
};
let bytes = SyncFrame::try_encode(SyncMessageType::ShapeSubscribe, &subscribe)
.expect("encode ShapeSubscribe")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send ShapeSubscribe");
loop {
let resp = tokio::time::timeout(Duration::from_secs(10), ws.next())
.await
.expect("timeout waiting for ShapeSnapshot")
.expect("stream closed before snapshot")
.expect("WebSocket read error");
let frame = SyncFrame::from_bytes(resp.into_data().as_ref()).expect("decode frame");
if frame.msg_type == SyncMessageType::ShapeSnapshot {
return frame
.decode_body::<ShapeSnapshotMsg>()
.expect("decode ShapeSnapshotMsg");
}
}
}
#[tokio::test]
async fn shape_subscribe_returns_snapshot() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let snapshot = subscribe_shape(&mut ws, "resync-shape-a", "resync_test").await;
assert_eq!(
snapshot.shape_id, "resync-shape-a",
"snapshot must echo the shape_id we subscribed to"
);
}
#[tokio::test]
async fn snapshot_lsn_reflects_wal_state() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let mut engine = CrdtEngine::new(4001).expect("create engine");
engine
.upsert("lsn_verify", "doc-lsn", &[("x", loro::LoroValue::I64(1))])
.expect("upsert");
let delta_bytes = engine.pending_deltas()[0].delta_bytes.clone();
let push_msg = DeltaPushMsg {
collection: "lsn_verify".into(),
document_id: "doc-lsn".into(),
delta: delta_bytes,
peer_id: 4001,
mutation_id: 1,
checksum: 0,
device_valid_time_ms: None,
producer_id: 0,
epoch: 0,
seq: 0,
device_id: 0,
delta_signature: [0u8; 32],
};
let bytes = SyncFrame::try_encode(SyncMessageType::DeltaPush, &push_msg)
.expect("encode DeltaPush")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send DeltaPush");
let _ = tokio::time::timeout(Duration::from_secs(10), ws.next()).await;
let snapshot = subscribe_shape(&mut ws, "lsn-shape", "lsn_verify").await;
let _ = snapshot.snapshot_lsn;
}
#[tokio::test]
async fn concurrent_deltas_from_two_peers() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let mut engine1 = CrdtEngine::new(4002).expect("create engine1");
engine1
.upsert(
"concurrent",
"doc-p1",
&[("author", loro::LoroValue::String("peer1".into()))],
)
.expect("upsert engine1");
let mut engine2 = CrdtEngine::new(4003).expect("create engine2");
engine2
.upsert(
"concurrent",
"doc-p2",
&[("author", loro::LoroValue::String("peer2".into()))],
)
.expect("upsert engine2");
for (peer_id, mutation_id, engine, doc_id) in [
(4002u64, 1u64, &engine1, "doc-p1"),
(4003u64, 2u64, &engine2, "doc-p2"),
] {
let delta_bytes = engine.pending_deltas()[0].delta_bytes.clone();
let msg = DeltaPushMsg {
collection: "concurrent".into(),
document_id: doc_id.into(),
delta: delta_bytes,
peer_id,
mutation_id,
checksum: 0,
device_valid_time_ms: None,
producer_id: 0,
epoch: 0,
seq: 0,
device_id: 0,
delta_signature: [0u8; 32],
};
let bytes = SyncFrame::try_encode(SyncMessageType::DeltaPush, &msg)
.expect("encode DeltaPush")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send DeltaPush");
}
let mut terminal = 0;
while terminal < 2 {
let resp = tokio::time::timeout(Duration::from_secs(10), ws.next())
.await
.unwrap_or_else(|_| panic!("timeout after {terminal} terminal response(s)"))
.unwrap_or_else(|| panic!("stream closed after {terminal} terminal response(s)"))
.unwrap_or_else(|e| panic!("read error after {terminal} terminal response(s): {e}"));
let frame = SyncFrame::from_bytes(resp.into_data().as_ref())
.unwrap_or_else(|| panic!("bad frame after {terminal} terminal response(s)"));
match frame.msg_type {
SyncMessageType::DeltaAck | SyncMessageType::DeltaReject => terminal += 1,
other => {
eprintln!("skipping server-initiated frame while awaiting acks: {other:?}");
}
}
}
}
#[tokio::test]
async fn resync_request_returns_snapshot_with_real_shape_id() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
subscribe_shape(&mut ws, "resync-rt-shape", "resync_rt_collection").await;
let resync_msg = ResyncRequestMsg {
reason: ResyncReason::SequenceGap {
expected: 1,
received: 3,
},
from_mutation_id: 1,
collection: String::new(),
shape_id: "resync-rt-shape".into(),
};
let bytes = SyncFrame::try_encode(SyncMessageType::ResyncRequest, &resync_msg)
.expect("encode ResyncRequest")
.to_bytes();
ws.send(Message::Binary(bytes.into()))
.await
.expect("send ResyncRequest");
let snapshot: ShapeSnapshotMsg = loop {
let frame_msg = tokio::time::timeout(Duration::from_secs(10), ws.next())
.await
.expect("timeout waiting for ShapeSnapshot after ResyncRequest")
.expect("stream closed before ShapeSnapshot")
.expect("WebSocket read error");
let frame =
SyncFrame::from_bytes(frame_msg.into_data().as_ref()).expect("decode sync frame");
if frame.msg_type == SyncMessageType::ShapeSnapshot {
break frame
.decode_body::<ShapeSnapshotMsg>()
.expect("decode ShapeSnapshotMsg");
}
};
assert_eq!(
snapshot.shape_id, "resync-rt-shape",
"resync handler must echo the real shape_id from the persistent registry, \
not a synthesised 'resync:' prefix — if this fails the registry was not populated"
);
}
#[tokio::test]
async fn double_shape_subscribe_returns_two_snapshots() {
let Some(_server) = OriginServer::try_spawn() else {
eprintln!("SKIP: Origin binary unavailable (set NODEDB_BIN or run via `cargo nextest`)");
return;
};
let mut ws = connect_and_handshake(_server.ws_url).await;
let _snap1 = subscribe_shape(&mut ws, "double-shape", "double_collection").await;
let _snap2 = subscribe_shape(&mut ws, "double-shape", "double_collection").await;
}