lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 13+++++++++++++
MCargo.toml | 2++
Acrates/simplex-interop-tests/Cargo.toml | 35+++++++++++++++++++++++++++++++++++
Acrates/simplex-interop-tests/src/fixtures.rs | 39+++++++++++++++++++++++++++++++++++++++
Acrates/simplex-interop-tests/src/lib.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/simplex-interop-tests/src/policy.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 {}