commit 3adaff5d93801ed2867d418b612b3751508be307
parent 85f6775af36d6ef2d0aa5704a81a1888002caee7
Author: triesap <tyson@radroots.org>
Date: Sat, 28 Mar 2026 01:26:47 +0000
tests: add simplex interop coverage and fixture policy
- add radroots-simplex-interop-tests with synthetic stack coverage across SMP agent and chat layers
- codify the rr-synth fixture policy and synthetic-domain host requirements
- add an opt-in local upstream reachability contract behind environment gating
- register the new crate in the rr-rs workspace and verify it with cargo tests
Diffstat:
6 files changed, 392 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2454,6 +2454,19 @@ dependencies = [
]
[[package]]
+name = "radroots-simplex-interop-tests"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "radroots-simplex-agent-proto",
+ "radroots-simplex-agent-runtime",
+ "radroots-simplex-chat-proto",
+ "radroots-simplex-smp-crypto",
+ "radroots-simplex-smp-proto",
+ "radroots-simplex-smp-transport",
+ "serde_json",
+]
+
+[[package]]
name = "radroots-simplex-smp-crypto"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -21,6 +21,7 @@ members = [
"crates/simplex-agent-runtime",
"crates/simplex-agent-store",
"crates/simplex-chat-proto",
+ "crates/simplex-interop-tests",
"crates/simplex-smp-crypto",
"crates/simplex-smp-proto",
"crates/simplex-smp-transport",
@@ -69,6 +70,7 @@ radroots-simplex-agent-proto = { path = "crates/simplex-agent-proto", version =
radroots-simplex-agent-runtime = { path = "crates/simplex-agent-runtime", version = "0.1.0-alpha.1", default-features = false }
radroots-simplex-agent-store = { path = "crates/simplex-agent-store", version = "0.1.0-alpha.1", default-features = false }
radroots-simplex-chat-proto = { path = "crates/simplex-chat-proto", version = "0.1.0-alpha.1", default-features = false }
+radroots-simplex-interop-tests = { path = "crates/simplex-interop-tests", version = "0.1.0-alpha.1", default-features = false }
radroots-simplex-smp-crypto = { path = "crates/simplex-smp-crypto", version = "0.1.0-alpha.1", default-features = false }
radroots-simplex-smp-proto = { path = "crates/simplex-smp-proto", version = "0.1.0-alpha.1", default-features = false }
radroots-simplex-smp-transport = { path = "crates/simplex-smp-transport", version = "0.1.0-alpha.1", default-features = false }
diff --git a/crates/simplex-interop-tests/Cargo.toml b/crates/simplex-interop-tests/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "radroots-simplex-interop-tests"
+version = "0.1.0-alpha.1"
+edition.workspace = true
+authors = [
+ "Radroots Authors",
+]
+rust-version.workspace = true
+license.workspace = true
+description = "synthetic fixture and opt-in local-service interop coverage for the radroots SimpleX stack"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots-simplex-interop-tests"
+readme.workspace = true
+
+[features]
+default = ["std"]
+std = [
+ "radroots-simplex-agent-proto/std",
+ "radroots-simplex-agent-runtime/std",
+ "radroots-simplex-chat-proto/std",
+ "radroots-simplex-smp-crypto/std",
+ "radroots-simplex-smp-proto/std",
+ "radroots-simplex-smp-transport/std",
+ "serde_json/std",
+]
+
+[dependencies]
+radroots-simplex-agent-proto = { workspace = true, default-features = false }
+radroots-simplex-agent-runtime = { workspace = true, default-features = false }
+radroots-simplex-chat-proto = { workspace = true, default-features = false }
+radroots-simplex-smp-crypto = { workspace = true, default-features = false }
+radroots-simplex-smp-proto = { workspace = true, default-features = false }
+radroots-simplex-smp-transport = { workspace = true, default-features = false }
+serde_json = { workspace = true, default-features = false, features = ["alloc"] }
diff --git a/crates/simplex-interop-tests/src/fixtures.rs b/crates/simplex-interop-tests/src/fixtures.rs
@@ -0,0 +1,39 @@
+use alloc::vec::Vec;
+use radroots_simplex_chat_proto::prelude::{RadrootsSimplexChatMessage, decode_messages};
+use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueUri;
+
+pub const fn synthetic_fixture_id() -> &'static str {
+ "rr-synth/stack/duplex-v1"
+}
+
+pub const fn synthetic_connection_id() -> &'static str {
+ "rr-synth-conn-001"
+}
+
+pub fn synthetic_invitation_queue() -> RadrootsSimplexSmpQueueUri {
+ RadrootsSimplexSmpQueueUri::parse(
+ "smp://cnItc3ludGg@relay.synthetic.invalid/aW52aXRl#/?v=4&dh=cnItc3ludGgtZGg&q=m",
+ )
+ .unwrap()
+}
+
+pub fn synthetic_reply_queue() -> RadrootsSimplexSmpQueueUri {
+ RadrootsSimplexSmpQueueUri::parse(
+ "smp://cnItc3ludGg@reply.synthetic.invalid/cmVwbHk#/?v=4&dh=cnItc3ludGgtcmVwbHk&q=m",
+ )
+ .unwrap()
+}
+
+pub fn synthetic_chat_messages() -> Vec<RadrootsSimplexChatMessage> {
+ decode_messages(
+ br#"[{
+ "v":"1-16",
+ "msgId":"AQ",
+ "event":"x.msg.new",
+ "params":{
+ "content":{"type":"text","text":"hello from rr-synth"}
+ }
+ }]"#,
+ )
+ .unwrap()
+}
diff --git a/crates/simplex-interop-tests/src/lib.rs b/crates/simplex-interop-tests/src/lib.rs
@@ -0,0 +1,192 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+#![forbid(unsafe_code)]
+#![doc = r#"
+`radroots-simplex-interop-tests` owns the synthetic fixture policy for the rr-rs
+SimpleX stack.
+
+Rules:
+- committed fixtures must use the `rr-synth/*` namespace.
+- committed server hosts must stay in obviously synthetic domains such as
+ `.invalid`, `.example`, or `.test`.
+- committed tests must not copy or derive realistic queue URIs, certificates,
+ ciphertext, or traffic from `refs/*` or external captures.
+- black-box local upstream checks are opt-in through environment variables and
+ are never required for the default workspace verify lane.
+"#]
+
+extern crate alloc;
+
+pub mod fixtures;
+pub mod policy;
+
+#[cfg(test)]
+mod tests {
+ use crate::fixtures::{
+ synthetic_chat_messages, synthetic_connection_id, synthetic_fixture_id,
+ synthetic_invitation_queue, synthetic_reply_queue,
+ };
+ use crate::policy::{RadrootsSimplexInteropFixturePolicy, RadrootsSimplexInteropLocalUpstream};
+ use radroots_simplex_agent_proto::prelude::{
+ RadrootsSimplexAgentDecryptedMessage, RadrootsSimplexAgentEncryptedPayload,
+ RadrootsSimplexAgentEnvelope, RadrootsSimplexAgentMessage,
+ RadrootsSimplexAgentMessageFrame, RadrootsSimplexAgentMessageHeader,
+ decode_agent_message_frame, decode_decrypted_message, decode_envelope,
+ encode_agent_message_frame, encode_decrypted_message, encode_envelope,
+ };
+ use radroots_simplex_agent_runtime::prelude::{
+ RadrootsSimplexAgentRuntime, RadrootsSimplexAgentRuntimeBuilder,
+ RadrootsSimplexAgentRuntimeEvent,
+ };
+ use radroots_simplex_chat_proto::prelude::{decode_messages, encode_compressed_batch};
+ use radroots_simplex_smp_crypto::prelude::{
+ RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope,
+ };
+ use radroots_simplex_smp_proto::prelude::{
+ RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RadrootsSimplexSmpCommand,
+ RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpCorrelationId,
+ RadrootsSimplexSmpMessageFlags, RadrootsSimplexSmpSendCommand,
+ };
+ use radroots_simplex_smp_transport::prelude::RadrootsSimplexSmpTransportBlock;
+
+ #[test]
+ fn synthetic_policy_accepts_only_rr_owned_fixtures() {
+ let policy = RadrootsSimplexInteropFixturePolicy::default();
+ policy.assert_fixture_id(synthetic_fixture_id()).unwrap();
+ policy
+ .assert_queue_uri(&synthetic_invitation_queue())
+ .unwrap();
+ policy.assert_queue_uri(&synthetic_reply_queue()).unwrap();
+
+ let error = policy.assert_fixture_id("copied-from-refs");
+ assert!(error.is_err());
+ }
+
+ #[test]
+ fn synthetic_stack_roundtrip_exercises_smp_agent_and_chat_layers() {
+ let correlation_id = RadrootsSimplexSmpCorrelationId::new([7_u8; 24]);
+ let send_command = RadrootsSimplexSmpCommand::Send(RadrootsSimplexSmpSendCommand {
+ flags: RadrootsSimplexSmpMessageFlags::notifications_enabled(),
+ message_body: b"rr-synth-body".to_vec(),
+ });
+ let transmission = RadrootsSimplexSmpCommandTransmission {
+ authorization: b"rr-synth-auth".to_vec(),
+ correlation_id: Some(correlation_id),
+ entity_id: b"rr-synth-queue".to_vec(),
+ command: send_command.clone(),
+ };
+ let block = RadrootsSimplexSmpTransportBlock::from_current_command_transmissions(&[
+ transmission.clone(),
+ ])
+ .unwrap();
+ let decoded = RadrootsSimplexSmpTransportBlock::decode(&block.encode().unwrap())
+ .unwrap()
+ .decode_command_transmissions(RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION)
+ .unwrap();
+ assert_eq!(decoded, vec![transmission]);
+
+ let scope = RadrootsSimplexSmpQueueAuthorizationScope::new(
+ b"rr-synth-session".to_vec(),
+ correlation_id,
+ b"rr-synth-queue".to_vec(),
+ )
+ .unwrap();
+ let auth = RadrootsSimplexSmpQueueAuthorizationMaterial::for_command(
+ &scope,
+ &send_command,
+ RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
+ b"rr-synth-queue-key".to_vec(),
+ b"rr-synth-server-key".to_vec(),
+ )
+ .unwrap();
+ assert_eq!(auth.nonce, [7_u8; 24]);
+
+ let chat_messages = synthetic_chat_messages();
+ let compressed_chat = encode_compressed_batch(&chat_messages).unwrap();
+ let decoded_chat = decode_messages(&compressed_chat).unwrap();
+ assert_eq!(decoded_chat, chat_messages);
+
+ let frame = RadrootsSimplexAgentMessageFrame {
+ header: RadrootsSimplexAgentMessageHeader {
+ message_id: 1,
+ previous_message_hash: synthetic_connection_id().as_bytes().to_vec(),
+ },
+ message: RadrootsSimplexAgentMessage::UserMessage(compressed_chat.clone()),
+ padding: Vec::new(),
+ };
+ let encoded_frame = encode_agent_message_frame(&frame).unwrap();
+ let decoded_frame = decode_agent_message_frame(&encoded_frame).unwrap();
+ assert_eq!(decoded_frame.header, frame.header);
+ assert_eq!(decoded_frame.message, frame.message);
+
+ let decrypted = RadrootsSimplexAgentDecryptedMessage::Message(frame.clone());
+ let encoded_decrypted = encode_decrypted_message(&decrypted).unwrap();
+ let envelope =
+ RadrootsSimplexAgentEnvelope::Message(RadrootsSimplexAgentEncryptedPayload {
+ ratchet_header: None,
+ ciphertext: encoded_decrypted.clone(),
+ });
+ let decoded_envelope = decode_envelope(&encode_envelope(&envelope).unwrap()).unwrap();
+ let RadrootsSimplexAgentEnvelope::Message(payload) = decoded_envelope else {
+ panic!("expected message envelope");
+ };
+ let decoded_decrypted = decode_decrypted_message(&payload.ciphertext).unwrap();
+ let RadrootsSimplexAgentDecryptedMessage::Message(decoded_frame_from_envelope) =
+ decoded_decrypted
+ else {
+ panic!("expected message frame");
+ };
+ let RadrootsSimplexAgentMessage::UserMessage(encoded_chat_again) =
+ decoded_frame_from_envelope.message
+ else {
+ panic!("expected user message");
+ };
+ assert_eq!(decode_messages(&encoded_chat_again).unwrap(), chat_messages);
+ }
+
+ #[test]
+ fn synthetic_runtime_flow_stays_fixture_owned() {
+ let mut runtime: RadrootsSimplexAgentRuntime =
+ RadrootsSimplexAgentRuntimeBuilder::new().build().unwrap();
+ let created = runtime
+ .create_connection(
+ synthetic_invitation_queue(),
+ b"rr-synth-e2e".to_vec(),
+ false,
+ 10,
+ )
+ .unwrap();
+ let events = runtime.drain_events(8);
+ let invitation = events
+ .into_iter()
+ .find_map(|event| match event {
+ RadrootsSimplexAgentRuntimeEvent::InvitationReady { invitation, .. } => {
+ Some(invitation)
+ }
+ _ => None,
+ })
+ .expect("invitation event");
+
+ let joined = runtime
+ .join_connection(invitation, synthetic_reply_queue(), 20)
+ .unwrap();
+ runtime
+ .allow_connection(&joined, b"rr-synth-info".to_vec(), 30)
+ .unwrap();
+ let message_id = runtime
+ .send_message(&joined, b"rr-synth-chat".to_vec(), 40)
+ .unwrap();
+ assert_eq!(message_id, 1);
+ runtime.reconnect_connection(&joined, 50).unwrap();
+ assert!(!runtime.retry_pending(50 + 5_000, 64).is_empty());
+ assert!(created.starts_with("conn-"));
+ }
+
+ #[cfg(feature = "std")]
+ #[test]
+ fn local_upstream_contract_is_opt_in() {
+ let Some(target) = RadrootsSimplexInteropLocalUpstream::from_env() else {
+ return;
+ };
+ target.assert_reachable().unwrap();
+ }
+}
diff --git a/crates/simplex-interop-tests/src/policy.rs b/crates/simplex-interop-tests/src/policy.rs
@@ -0,0 +1,111 @@
+use alloc::string::{String, ToString};
+use core::fmt;
+use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueUri;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexInteropFixturePolicy {
+ pub namespace_prefix: &'static str,
+}
+
+impl Default for RadrootsSimplexInteropFixturePolicy {
+ fn default() -> Self {
+ Self {
+ namespace_prefix: "rr-synth/",
+ }
+ }
+}
+
+impl RadrootsSimplexInteropFixturePolicy {
+ pub fn assert_fixture_id(&self, id: &str) -> Result<(), RadrootsSimplexInteropPolicyError> {
+ if id.starts_with(self.namespace_prefix) {
+ return Ok(());
+ }
+ Err(RadrootsSimplexInteropPolicyError::InvalidFixtureId(
+ id.into(),
+ ))
+ }
+
+ pub fn assert_queue_uri(
+ &self,
+ queue_uri: &RadrootsSimplexSmpQueueUri,
+ ) -> Result<(), RadrootsSimplexInteropPolicyError> {
+ for host in &queue_uri.server.hosts {
+ if host.ends_with(".invalid") || host.ends_with(".example") || host.ends_with(".test") {
+ continue;
+ }
+ return Err(RadrootsSimplexInteropPolicyError::InvalidFixtureHost(
+ host.clone(),
+ ));
+ }
+ Ok(())
+ }
+}
+
+#[cfg(feature = "std")]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsSimplexInteropLocalUpstream {
+ pub host: String,
+ pub port: u16,
+}
+
+#[cfg(feature = "std")]
+impl RadrootsSimplexInteropLocalUpstream {
+ pub fn from_env() -> Option<Self> {
+ let host = std::env::var("RADROOTS_SIMPLEX_INTEROP_SMP_HOST").ok()?;
+ let port = std::env::var("RADROOTS_SIMPLEX_INTEROP_SMP_PORT")
+ .ok()?
+ .parse::<u16>()
+ .ok()?;
+ Some(Self { host, port })
+ }
+
+ pub fn assert_reachable(&self) -> Result<(), RadrootsSimplexInteropPolicyError> {
+ use std::net::{TcpStream, ToSocketAddrs};
+ use std::time::Duration;
+
+ let mut addrs = (self.host.as_str(), self.port)
+ .to_socket_addrs()
+ .map_err(|source| {
+ RadrootsSimplexInteropPolicyError::LocalUpstreamIo(source.to_string())
+ })?;
+ let Some(addr) = addrs.next() else {
+ return Err(RadrootsSimplexInteropPolicyError::LocalUpstreamIo(
+ "no socket addresses resolved".into(),
+ ));
+ };
+ TcpStream::connect_timeout(&addr, Duration::from_millis(500)).map_err(|source| {
+ RadrootsSimplexInteropPolicyError::LocalUpstreamIo(source.to_string())
+ })?;
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RadrootsSimplexInteropPolicyError {
+ InvalidFixtureId(String),
+ InvalidFixtureHost(String),
+ LocalUpstreamIo(String),
+}
+
+impl fmt::Display for RadrootsSimplexInteropPolicyError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidFixtureId(id) => {
+ write!(
+ f,
+ "interop fixture id `{id}` is outside the rr-synth namespace"
+ )
+ }
+ Self::InvalidFixtureHost(host) => {
+ write!(
+ f,
+ "interop fixture host `{host}` is not in a synthetic domain"
+ )
+ }
+ Self::LocalUpstreamIo(message) => write!(f, "{message}"),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsSimplexInteropPolicyError {}