lib

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

commit 1fc418f66d354eea57ad45b1e6f935f6756aa975
parent fa1c498c95e9faa5acb6eed3328f64f4ee10e9a9
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 14:56:49 +0000

transport: preserve Reticulum as first-wave transport

- normalize transport identity to Local, Nostr, and Reticulum
- move Reticulum readiness into maturity and availability status metadata
- make Reticulum endpoint and scope canonical for first-wave targets
- align transport, outbox, protocol, and source-boundary validation

Diffstat:
Mcontracts/conformance/vectors/transport/target_uri.v1.json | 2+-
Mcrates/mesh/src/lib.rs | 4++--
Mcrates/mesh/src/model.rs | 18+++++++++---------
Mcrates/mesh/tests/mesh.rs | 28++++++++++++++--------------
Mcrates/mesh_agent_client/src/lib.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcrates/mesh_agent_client/tests/client.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/outbox/migrations/0001_outbox.up.sql | 8++++----
Mcrates/outbox/src/lib.rs | 6+++---
Mcrates/outbox/src/model.rs | 83++++++++++++++++++++++++++-----------------------------------------------------
Mcrates/outbox/src/store.rs | 359+++++++++++++++++++++++++------------------------------------------------------
Mcrates/protocol_contract_v1/src/lib.rs | 25++++++++-----------------
Mcrates/runtime/src/transport.rs | 49+++++++++++++++++++++++--------------------------
Mcrates/transport/src/kind.rs | 58++++++++++++++++++----------------------------------------
Mcrates/transport/src/lib.rs | 7+++++--
Mcrates/transport/src/message.rs | 6+++---
Mcrates/transport/src/status.rs | 30++++++++++++++++++++++++------
Mcrates/transport/src/target.rs | 54++++++++++++++++++++++--------------------------------
Mcrates/transport/tests/source_boundary.rs | 89+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/transport/tests/transport.rs | 167+++++++++++++++++++++++++++++++++----------------------------------------------
Mcrates/transport_nostr/src/outbox.rs | 15---------------
Mcrates/transport_nostr/tests/transport.rs | 6+++---
Mcrates/transport_publish_protocol/src/lib.rs | 378++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcrates/transport_reticulum/src/lib.rs | 208++++++++++++++++++++++++++++++++++++++-----------------------------------------
Acrates/transport_reticulum/tests/reticulum.rs | 588+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/transport_reticulum/tests/reticulum_preview.rs | 597-------------------------------------------------------------------------------
25 files changed, 1383 insertions(+), 1565 deletions(-)

diff --git a/contracts/conformance/vectors/transport/target_uri.v1.json b/contracts/conformance/vectors/transport/target_uri.v1.json @@ -46,7 +46,7 @@ "id": "transport_nostr_relay_canonical_005", "kind": "transport.nostr_relay_target.valid", "input": { - "uri": " WSS://Relay.Example/ " + "uri": "WSS://Relay.Example/" }, "expected": { "canonical_uri": "wss://relay.example" diff --git a/crates/mesh/src/lib.rs b/crates/mesh/src/lib.rs @@ -10,8 +10,8 @@ mod model; pub use cbor::{decode_mesh_frame_cbor, encode_mesh_frame_cbor}; pub use error::RadrootsMeshError; pub use model::{ - RADROOTS_MESH_FRAME_VERSION, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, - RADROOTS_MESH_PREVIEW_POLICY_ID, RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, + RADROOTS_MESH_FRAME_VERSION, RADROOTS_MESH_RETICULUM_POLICY_ID, + RADROOTS_MESH_UNAVAILABLE_MESSAGE, RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, RadrootsMeshCompressionPolicy, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, RadrootsMeshScope, diff --git a/crates/mesh/src/model.rs b/crates/mesh/src/model.rs @@ -4,9 +4,9 @@ use alloc::vec::Vec; use radroots_transport::{RadrootsTransportError, RadrootsTransportMeshScopeId}; pub const RADROOTS_MESH_FRAME_VERSION: u16 = 1; -pub const RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE: &str = - "Reticulum mesh preview is explicit and unavailable for real delivery"; -pub const RADROOTS_MESH_PREVIEW_POLICY_ID: &str = "reticulum_preview_delivery"; +pub const RADROOTS_MESH_UNAVAILABLE_MESSAGE: &str = + "Reticulum mesh delivery is unavailable until implementation"; +pub const RADROOTS_MESH_RETICULUM_POLICY_ID: &str = "reticulum_mesh_delivery"; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -136,7 +136,7 @@ impl RadrootsMeshCompressionPolicy { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsMeshPolicyDenyReason { - PreviewUnavailable, + DeliveryUnavailable, PayloadBudgetExceeded, CustomScopeUnavailable, } @@ -144,7 +144,7 @@ pub enum RadrootsMeshPolicyDenyReason { impl RadrootsMeshPolicyDenyReason { pub fn label(self) -> &'static str { match self { - Self::PreviewUnavailable => "preview_unavailable", + Self::DeliveryUnavailable => "delivery_unavailable", Self::PayloadBudgetExceeded => "payload_budget_exceeded", Self::CustomScopeUnavailable => "custom_scope_unavailable", } @@ -152,7 +152,7 @@ impl RadrootsMeshPolicyDenyReason { pub fn message(self) -> &'static str { match self { - Self::PreviewUnavailable => RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, + Self::DeliveryUnavailable => RADROOTS_MESH_UNAVAILABLE_MESSAGE, Self::PayloadBudgetExceeded => "mesh payload exceeds the configured delivery budget", Self::CustomScopeUnavailable => { "mesh custom scopes are not enabled for the configured policy" @@ -232,7 +232,7 @@ pub struct RadrootsMeshPayloadPolicy { } impl RadrootsMeshPayloadPolicy { - pub fn preview_unavailable() -> Self { + pub fn reticulum_unavailable() -> Self { Self { max_payload_bytes: 0, max_frame_bytes: 0, @@ -242,7 +242,7 @@ impl RadrootsMeshPayloadPolicy { } pub fn policy_id(&self) -> &'static str { - RADROOTS_MESH_PREVIEW_POLICY_ID + RADROOTS_MESH_RETICULUM_POLICY_ID } pub fn usable_for_delivery(&self) -> bool { @@ -252,7 +252,7 @@ impl RadrootsMeshPayloadPolicy { pub fn evaluate(&self, input: &RadrootsMeshAdmissionInput) -> RadrootsMeshAdmissionDecision { if !self.usable_for_delivery() { return RadrootsMeshAdmissionDecision::Denied { - reason: RadrootsMeshPolicyDenyReason::PreviewUnavailable, + reason: RadrootsMeshPolicyDenyReason::DeliveryUnavailable, }; } if matches!(input.scope, RadrootsMeshScope::Custom(_)) && !self.custom_scopes_enabled { diff --git a/crates/mesh/tests/mesh.rs b/crates/mesh/tests/mesh.rs @@ -1,5 +1,5 @@ use radroots_mesh::{ - RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID, + RADROOTS_MESH_RETICULUM_POLICY_ID, RADROOTS_MESH_UNAVAILABLE_MESSAGE, RadrootsMeshAdmissionDecision, RadrootsMeshAdmissionInput, RadrootsMeshCompressionPolicy, RadrootsMeshError, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, RadrootsMeshPayloadPolicy, RadrootsMeshPolicyDenyReason, RadrootsMeshPrivacyClass, @@ -80,10 +80,10 @@ fn all_frame_types_round_trip_with_stable_codes_and_labels() { } #[test] -fn preview_policy_has_zero_delivery_budgets_and_disabled_compression() { - let policy = RadrootsMeshPayloadPolicy::preview_unavailable(); +fn reticulum_policy_has_zero_delivery_budgets_and_disabled_compression() { + let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); - assert_eq!(policy.policy_id(), RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(policy.policy_id(), RADROOTS_MESH_RETICULUM_POLICY_ID); assert_eq!(policy.max_payload_bytes, 0); assert_eq!(policy.max_frame_bytes, 0); assert_eq!(policy.compression, RadrootsMeshCompressionPolicy::Disabled); @@ -93,8 +93,8 @@ fn preview_policy_has_zero_delivery_budgets_and_disabled_compression() { } #[test] -fn preview_policy_denies_real_payload_admission_deterministically() { - let policy = RadrootsMeshPayloadPolicy::preview_unavailable(); +fn reticulum_policy_denies_real_payload_admission_deterministically() { + let policy = RadrootsMeshPayloadPolicy::reticulum_unavailable(); let input = RadrootsMeshAdmissionInput::new( RadrootsMeshScope::Local, RadrootsMeshPrivacyClass::PublicEvent, @@ -106,15 +106,15 @@ fn preview_policy_denies_real_payload_admission_deterministically() { assert_eq!( decision, RadrootsMeshAdmissionDecision::Denied { - reason: RadrootsMeshPolicyDenyReason::PreviewUnavailable + reason: RadrootsMeshPolicyDenyReason::DeliveryUnavailable } ); assert_eq!(decision.label(), "denied"); assert_eq!( decision.deny_reason(), - Some(RadrootsMeshPolicyDenyReason::PreviewUnavailable) + Some(RadrootsMeshPolicyDenyReason::DeliveryUnavailable) ); - assert_eq!(decision.message(), RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); + assert_eq!(decision.message(), RADROOTS_MESH_UNAVAILABLE_MESSAGE); assert!(!decision.usable_for_delivery()); assert_eq!( RadrootsMeshPrivacyClass::PublicEvent.label(), @@ -125,15 +125,15 @@ fn preview_policy_denies_real_payload_admission_deterministically() { "private_event" ); assert_eq!( - RadrootsMeshPolicyDenyReason::PreviewUnavailable.label(), - "preview_unavailable" + RadrootsMeshPolicyDenyReason::DeliveryUnavailable.label(), + "delivery_unavailable" ); } #[test] fn custom_scope_has_explicit_namespace() { - let scope = RadrootsMeshScope::custom("farm-north.preview_1").expect("custom scope"); - assert_eq!(scope.label(), "farm-north.preview_1"); + let scope = RadrootsMeshScope::custom("farm-north.mesh_1").expect("custom scope"); + assert_eq!(scope.label(), "farm-north.mesh_1"); let frame = RadrootsMeshFrame::new( RadrootsMeshFrameType::RouteProbe, scope, @@ -144,7 +144,7 @@ fn custom_scope_has_explicit_namespace() { let encoded = encode_mesh_frame_cbor(&frame).expect("encode custom scope"); let decoded = decode_mesh_frame_cbor(&encoded).expect("decode custom scope"); - assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north.preview_1"); + assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north.mesh_1"); assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); } diff --git a/crates/mesh_agent_client/src/lib.rs b/crates/mesh_agent_client/src/lib.rs @@ -1,14 +1,13 @@ #![forbid(unsafe_code)] use radroots_mesh::{ - RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID, - RadrootsMeshPayloadPolicy, + RADROOTS_MESH_RETICULUM_POLICY_ID, RADROOTS_MESH_UNAVAILABLE_MESSAGE, RadrootsMeshPayloadPolicy, }; use radroots_mesh_agent_proto::{ RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, }; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportMeshScopeId, + RADROOTS_RETICULUM_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportMeshScopeId, }; pub const RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID: &str = RADROOTS_MESH_AGENT_SCHEMA_ID; @@ -37,6 +36,8 @@ pub struct MeshAgentTransportStatus { pub endpoint_uri: String, pub configured: bool, pub implementation: MeshAgentImplementation, + pub maturity: MeshAgentCapabilityMaturity, + pub availability: MeshAgentCapabilityAvailability, pub usable_for_delivery: bool, pub message: String, } @@ -47,7 +48,6 @@ pub struct MeshAgentTransportStatus { pub enum MeshAgentImplementation { Real, Mock, - PreviewUnavailable, } impl MeshAgentImplementation { @@ -55,7 +55,42 @@ impl MeshAgentImplementation { match self { Self::Real => "real", Self::Mock => "mock", - Self::PreviewUnavailable => "previewUnavailable", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentCapabilityMaturity { + Preview, + Stable, +} + +impl MeshAgentCapabilityMaturity { + pub fn schema_name(self) -> &'static str { + match self { + Self::Preview => "preview", + Self::Stable => "stable", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MeshAgentCapabilityAvailability { + Available, + Degraded, + Unavailable, +} + +impl MeshAgentCapabilityAvailability { + pub fn schema_name(self) -> &'static str { + match self { + Self::Available => "available", + Self::Degraded => "degraded", + Self::Unavailable => "unavailable", } } } @@ -183,12 +218,12 @@ pub struct RadrootsMockMeshAgentClient { } impl RadrootsMockMeshAgentClient { - pub fn preview_unavailable() -> Self { + pub fn reticulum_unavailable() -> Self { Self { - profile_id: RADROOTS_MESH_PREVIEW_POLICY_ID.to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), - scope: RadrootsTransportMeshScopeId::local_preview(), - policy: RadrootsMeshPayloadPolicy::preview_unavailable(), + profile_id: RADROOTS_MESH_RETICULUM_POLICY_ID.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), + scope: RadrootsTransportMeshScopeId::local_reticulum(), + policy: RadrootsMeshPayloadPolicy::reticulum_unavailable(), } } @@ -203,7 +238,7 @@ impl RadrootsMockMeshAgentClient { impl Default for RadrootsMockMeshAgentClient { fn default() -> Self { - Self::preview_unavailable() + Self::reticulum_unavailable() } } @@ -215,9 +250,11 @@ impl RadrootsMeshAgentClient for RadrootsMockMeshAgentClient { profile_id: self.profile_id.clone(), endpoint_uri: self.endpoint_uri.clone(), configured: true, - implementation: MeshAgentImplementation::PreviewUnavailable, + implementation: MeshAgentImplementation::Real, + maturity: MeshAgentCapabilityMaturity::Preview, + availability: MeshAgentCapabilityAvailability::Unavailable, usable_for_delivery: false, - message: RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE.to_owned(), + message: RADROOTS_MESH_UNAVAILABLE_MESSAGE.to_owned(), }] } else { Vec::new() @@ -233,7 +270,7 @@ impl RadrootsMeshAgentClient for RadrootsMockMeshAgentClient { transport_kind: MeshAgentTransportKind::Reticulum, endpoint_uri: self.endpoint_uri.clone(), outcome: MeshAgentTransportOutcome::TransportUnavailable, - message: RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE.to_owned(), + message: RADROOTS_MESH_UNAVAILABLE_MESSAGE.to_owned(), }], event_id: request.event_id, } diff --git a/crates/mesh_agent_client/tests/client.rs b/crates/mesh_agent_client/tests/client.rs @@ -1,40 +1,43 @@ use std::fs; use std::path::Path; -use radroots_mesh::{RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE, RADROOTS_MESH_PREVIEW_POLICY_ID}; +use radroots_mesh::{RADROOTS_MESH_RETICULUM_POLICY_ID, RADROOTS_MESH_UNAVAILABLE_MESSAGE}; use radroots_mesh_agent_client::{ - MeshAgentImplementation, MeshAgentPublishRequest, MeshAgentResponseStatus, - MeshAgentStatusRequest, MeshAgentTransportKind, MeshAgentTransportOutcome, - RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID, RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE, - RadrootsMeshAgentClient, RadrootsMockMeshAgentClient, + MeshAgentCapabilityAvailability, MeshAgentCapabilityMaturity, MeshAgentImplementation, + MeshAgentPublishRequest, MeshAgentResponseStatus, MeshAgentStatusRequest, + MeshAgentTransportKind, MeshAgentTransportOutcome, RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID, + RADROOTS_MESH_AGENT_CLIENT_SCHEMA_NAMESPACE, RadrootsMeshAgentClient, + RadrootsMockMeshAgentClient, }; use radroots_mesh_agent_proto::{ RADROOTS_MESH_AGENT_SCHEMA_ID, RADROOTS_MESH_AGENT_SCHEMA_NAMESPACE, schema_sha256_hex, }; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RadrootsTransportKind, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, RadrootsTransportKind, }; use serde_json::json; fn publish_request() -> MeshAgentPublishRequest { MeshAgentPublishRequest { - publish_request_id: "publish-preview-1".to_owned(), + publish_request_id: "publish-reticulum-1".to_owned(), payload_cbor: vec![1, 2, 3], - event_id: "event-preview-1".to_owned(), - target_fingerprint: "target-preview-1".to_owned(), + event_id: "event-reticulum-1".to_owned(), + target_fingerprint: "target-reticulum-1".to_owned(), } } #[test] -fn mock_client_status_reports_reticulum_preview_unavailable() { - let client = RadrootsMockMeshAgentClient::preview_unavailable(); +fn mock_client_status_reports_reticulum_unavailable() { + let client = RadrootsMockMeshAgentClient::reticulum_unavailable(); let response = client.status(MeshAgentStatusRequest { include_transports: true, }); - assert_eq!(client.scope().as_str(), RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); - assert_eq!(client.policy().policy_id(), RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(client.scope().as_str(), RADROOTS_RETICULUM_SCOPE_ID); + assert_eq!( + client.policy().policy_id(), + RADROOTS_MESH_RETICULUM_POLICY_ID + ); assert_eq!(response.transports.len(), 1); let status = &response.transports[0]; assert_eq!(status.transport, MeshAgentTransportKind::Reticulum); @@ -42,28 +45,33 @@ fn mock_client_status_reports_reticulum_preview_unavailable() { status.transport.transport_kind(), RadrootsTransportKind::Reticulum ); - assert_eq!(status.profile_id, RADROOTS_MESH_PREVIEW_POLICY_ID); - assert_eq!(status.endpoint_uri, RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); + assert_eq!(status.profile_id, RADROOTS_MESH_RETICULUM_POLICY_ID); + assert_eq!(status.endpoint_uri, RADROOTS_RETICULUM_ENDPOINT_URI); assert!(status.configured); + assert_eq!(status.implementation, MeshAgentImplementation::Real); + assert_eq!(status.maturity, MeshAgentCapabilityMaturity::Preview); assert_eq!( - status.implementation, - MeshAgentImplementation::PreviewUnavailable + status.availability, + MeshAgentCapabilityAvailability::Unavailable ); assert!(!status.usable_for_delivery); - assert_eq!(status.message, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); + assert_eq!(status.message, RADROOTS_MESH_UNAVAILABLE_MESSAGE); } #[test] -fn default_mock_client_uses_preview_unavailable_policy() { +fn default_mock_client_uses_reticulum_unavailable_policy() { let client = RadrootsMockMeshAgentClient::default(); - assert_eq!(client.scope().as_str(), RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); - assert_eq!(client.policy().policy_id(), RADROOTS_MESH_PREVIEW_POLICY_ID); + assert_eq!(client.scope().as_str(), RADROOTS_RETICULUM_SCOPE_ID); + assert_eq!( + client.policy().policy_id(), + RADROOTS_MESH_RETICULUM_POLICY_ID + ); } #[test] fn mock_client_status_can_omit_transport_rows_without_enabling_delivery() { - let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let client = RadrootsMockMeshAgentClient::reticulum_unavailable(); let response = client.status(MeshAgentStatusRequest { include_transports: false, }); @@ -74,25 +82,22 @@ fn mock_client_status_can_omit_transport_rows_without_enabling_delivery() { #[test] fn mock_client_publish_never_reports_success_outcomes() { - let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let client = RadrootsMockMeshAgentClient::reticulum_unavailable(); let response = client.publish(publish_request()); - assert_eq!(response.publish_request_id, "publish-preview-1"); + assert_eq!(response.publish_request_id, "publish-reticulum-1"); assert_eq!(response.status, MeshAgentResponseStatus::Rejected); - assert_eq!(response.event_id, "event-preview-1"); + assert_eq!(response.event_id, "event-reticulum-1"); assert_eq!(response.transport_receipts.len(), 1); let receipt = &response.transport_receipts[0]; assert_eq!(receipt.transport_kind, MeshAgentTransportKind::Reticulum); - assert_eq!( - receipt.endpoint_uri, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); + assert_eq!(receipt.endpoint_uri, RADROOTS_RETICULUM_ENDPOINT_URI); assert_eq!( receipt.outcome, MeshAgentTransportOutcome::TransportUnavailable ); assert!(!receipt.outcome.is_success()); - assert_eq!(receipt.message, RADROOTS_MESH_PREVIEW_DENIAL_MESSAGE); + assert_eq!(receipt.message, RADROOTS_MESH_UNAVAILABLE_MESSAGE); for outcome in [ MeshAgentTransportOutcome::Accepted, MeshAgentTransportOutcome::Delivered, @@ -110,8 +115,21 @@ fn dto_schema_names_match_capnp_surface() { assert_eq!(MeshAgentImplementation::Real.schema_name(), "real"); assert_eq!(MeshAgentImplementation::Mock.schema_name(), "mock"); assert_eq!( - MeshAgentImplementation::PreviewUnavailable.schema_name(), - "previewUnavailable" + MeshAgentCapabilityMaturity::Preview.schema_name(), + "preview" + ); + assert_eq!(MeshAgentCapabilityMaturity::Stable.schema_name(), "stable"); + assert_eq!( + MeshAgentCapabilityAvailability::Available.schema_name(), + "available" + ); + assert_eq!( + MeshAgentCapabilityAvailability::Degraded.schema_name(), + "degraded" + ); + assert_eq!( + MeshAgentCapabilityAvailability::Unavailable.schema_name(), + "unavailable" ); assert_eq!(MeshAgentResponseStatus::Accepted.schema_name(), "accepted"); assert_eq!(MeshAgentResponseStatus::Deferred.schema_name(), "deferred"); @@ -159,17 +177,17 @@ fn serde_output_uses_schema_field_names() { assert_eq!( request_json, json!({ - "publishRequestId": "publish-preview-1", + "publishRequestId": "publish-reticulum-1", "payloadCbor": [1, 2, 3], - "eventId": "event-preview-1", - "targetFingerprint": "target-preview-1" + "eventId": "event-reticulum-1", + "targetFingerprint": "target-reticulum-1" }) ); - let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let client = RadrootsMockMeshAgentClient::reticulum_unavailable(); let response_json = serde_json::to_value(client.publish(request)).expect("serialize response"); - assert_eq!(response_json["publishRequestId"], "publish-preview-1"); + assert_eq!(response_json["publishRequestId"], "publish-reticulum-1"); assert_eq!(response_json["status"], "rejected"); assert_eq!( response_json["transportReceipts"][0]["transportKind"], @@ -183,7 +201,7 @@ fn serde_output_uses_schema_field_names() { #[test] fn client_schema_hash_matches_proto_authority() { - let client = RadrootsMockMeshAgentClient::preview_unavailable(); + let client = RadrootsMockMeshAgentClient::reticulum_unavailable(); assert_eq!( RADROOTS_MESH_AGENT_CLIENT_SCHEMA_ID, @@ -230,7 +248,7 @@ fn client_source_uses_schema_field_names_without_legacy_status_terms() { "publish_usable", "fetch_usable", "redacted_message", - "preview_noop", + "retired_noop", ] { assert!(!source.contains(forbidden), "{forbidden}"); } diff --git a/crates/outbox/migrations/0001_outbox.up.sql b/crates/outbox/migrations/0001_outbox.up.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS outbox_operations ( expected_pubkey TEXT NOT NULL, idempotency_key TEXT, operation_idempotency_digest TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('queued', 'complete', 'deferred_until_implemented', 'preview_unavailable', 'failed_terminal', 'cancelled')), + status TEXT NOT NULL CHECK (status IN ('queued', 'complete', 'deferred_until_implemented', 'failed_terminal', 'cancelled')), created_at_ms INTEGER NOT NULL, updated_at_ms INTEGER NOT NULL ); @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS outbox_event ( draft_json TEXT NOT NULL, signed_event_json TEXT, raw_event_json TEXT, - state TEXT NOT NULL CHECK (state IN ('draft_queued', 'signing', 'signed', 'publishing', 'published', 'sign_retryable', 'publish_retryable', 'deferred_until_implemented', 'preview_unavailable', 'failed_terminal', 'cancelled')), + state TEXT NOT NULL CHECK (state IN ('draft_queued', 'signing', 'signed', 'publishing', 'published', 'sign_retryable', 'publish_retryable', 'deferred_until_implemented', 'failed_terminal', 'cancelled')), attempt_count INTEGER NOT NULL, claim_token TEXT, claim_owner TEXT, @@ -54,7 +54,7 @@ CREATE TABLE IF NOT EXISTS outbox_delivery_plan ( satisfaction_policy TEXT NOT NULL, required_success_count INTEGER NOT NULL, delivery_plan_idempotency_digest TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('queued', 'complete', 'deferred_until_implemented', 'preview_unavailable', 'failed_terminal', 'cancelled')), + status TEXT NOT NULL CHECK (status IN ('queued', 'complete', 'deferred_until_implemented', 'failed_terminal', 'cancelled')), satisfied_at_ms INTEGER, created_at_ms INTEGER NOT NULL, updated_at_ms INTEGER NOT NULL, @@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS outbox_delivery_target ( target_scope TEXT, target_label TEXT, endpoint_fingerprint TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('pending', 'accepted', 'delivered', 'forwarded', 'stored_by_gateway', 'seen', 'deferred_until_implemented', 'preview_unavailable', 'skipped_policy_denied', 'failed_retryable', 'failed_terminal')), + status TEXT NOT NULL CHECK (status IN ('pending', 'accepted', 'delivered', 'forwarded', 'stored_by_gateway', 'seen', 'deferred_until_implemented', 'skipped_policy_denied', 'failed_retryable', 'failed_terminal')), last_outcome_kind TEXT CHECK (last_outcome_kind IS NULL OR last_outcome_kind IN ('accepted', 'duplicate_accepted', 'delivered', 'forwarded', 'stored_by_gateway', 'seen', 'deferred_until_implemented', 'rejected', 'route_unavailable', 'payload_too_large', 'policy_denied', 'timeout', 'connection_failed', 'transport_unavailable')), attempt_count INTEGER NOT NULL, last_attempt_at_ms INTEGER, diff --git a/crates/outbox/src/lib.rs b/crates/outbox/src/lib.rs @@ -15,8 +15,8 @@ pub use model::{ RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEnqueueReceipt, RadrootsOutboxEnqueueStatus, RadrootsOutboxEventRecord, RadrootsOutboxEventState, RadrootsOutboxEventStoreIngestReceipt, RadrootsOutboxIdempotencyPreflight, RadrootsOutboxOperationInput, - RadrootsOutboxOperationRecord, RadrootsOutboxOperationStatus, - RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxReticulumPreviewEventRecord, - RadrootsOutboxSignedOperationInput, RadrootsOutboxStatusSummary, + RadrootsOutboxOperationRecord, RadrootsOutboxOperationStatus, RadrootsOutboxReticulumBehavior, + RadrootsOutboxReticulumEventRecord, RadrootsOutboxSignedOperationInput, + RadrootsOutboxStatusSummary, }; pub use store::RadrootsOutbox; diff --git a/crates/outbox/src/model.rs b/crates/outbox/src/model.rs @@ -14,7 +14,6 @@ pub enum RadrootsOutboxOperationStatus { Queued, Complete, DeferredUntilImplemented, - PreviewUnavailable, FailedTerminal, Cancelled, } @@ -25,7 +24,6 @@ impl RadrootsOutboxOperationStatus { Self::Queued => "queued", Self::Complete => "complete", Self::DeferredUntilImplemented => "deferred_until_implemented", - Self::PreviewUnavailable => "preview_unavailable", Self::FailedTerminal => "failed_terminal", Self::Cancelled => "cancelled", } @@ -36,7 +34,6 @@ impl RadrootsOutboxOperationStatus { "queued" => Ok(Self::Queued), "complete" => Ok(Self::Complete), "deferred_until_implemented" => Ok(Self::DeferredUntilImplemented), - "preview_unavailable" => Ok(Self::PreviewUnavailable), "failed_terminal" => Ok(Self::FailedTerminal), "cancelled" => Ok(Self::Cancelled), _ => Err(RadrootsOutboxError::InvalidStoredEnum { @@ -57,7 +54,6 @@ pub enum RadrootsOutboxEventState { SignRetryable, PublishRetryable, DeferredUntilImplemented, - PreviewUnavailable, FailedTerminal, Cancelled, } @@ -73,7 +69,6 @@ impl RadrootsOutboxEventState { Self::SignRetryable => "sign_retryable", Self::PublishRetryable => "publish_retryable", Self::DeferredUntilImplemented => "deferred_until_implemented", - Self::PreviewUnavailable => "preview_unavailable", Self::FailedTerminal => "failed_terminal", Self::Cancelled => "cancelled", } @@ -89,7 +84,6 @@ impl RadrootsOutboxEventState { "sign_retryable" => Ok(Self::SignRetryable), "publish_retryable" => Ok(Self::PublishRetryable), "deferred_until_implemented" => Ok(Self::DeferredUntilImplemented), - "preview_unavailable" => Ok(Self::PreviewUnavailable), "failed_terminal" => Ok(Self::FailedTerminal), "cancelled" => Ok(Self::Cancelled), _ => Err(RadrootsOutboxError::InvalidStoredEnum { @@ -112,7 +106,6 @@ pub enum RadrootsOutboxDeliveryPlanStatus { Queued, Complete, DeferredUntilImplemented, - PreviewUnavailable, FailedTerminal, Cancelled, } @@ -123,7 +116,6 @@ impl RadrootsOutboxDeliveryPlanStatus { Self::Queued => "queued", Self::Complete => "complete", Self::DeferredUntilImplemented => "deferred_until_implemented", - Self::PreviewUnavailable => "preview_unavailable", Self::FailedTerminal => "failed_terminal", Self::Cancelled => "cancelled", } @@ -134,7 +126,6 @@ impl RadrootsOutboxDeliveryPlanStatus { "queued" => Ok(Self::Queued), "complete" => Ok(Self::Complete), "deferred_until_implemented" => Ok(Self::DeferredUntilImplemented), - "preview_unavailable" => Ok(Self::PreviewUnavailable), "failed_terminal" => Ok(Self::FailedTerminal), "cancelled" => Ok(Self::Cancelled), _ => Err(RadrootsOutboxError::InvalidStoredEnum { @@ -154,7 +145,6 @@ pub enum RadrootsOutboxDeliveryTargetStatus { StoredByGateway, Seen, DeferredUntilImplemented, - PreviewUnavailable, SkippedPolicyDenied, FailedRetryable, FailedTerminal, @@ -170,7 +160,6 @@ impl RadrootsOutboxDeliveryTargetStatus { Self::StoredByGateway => "stored_by_gateway", Self::Seen => "seen", Self::DeferredUntilImplemented => "deferred_until_implemented", - Self::PreviewUnavailable => "preview_unavailable", Self::SkippedPolicyDenied => "skipped_policy_denied", Self::FailedRetryable => "failed_retryable", Self::FailedTerminal => "failed_terminal", @@ -186,7 +175,6 @@ impl RadrootsOutboxDeliveryTargetStatus { "stored_by_gateway" => Ok(Self::StoredByGateway), "seen" => Ok(Self::Seen), "deferred_until_implemented" => Ok(Self::DeferredUntilImplemented), - "preview_unavailable" => Ok(Self::PreviewUnavailable), "skipped_policy_denied" => Ok(Self::SkippedPolicyDenied), "failed_retryable" => Ok(Self::FailedRetryable), "failed_terminal" => Ok(Self::FailedTerminal), @@ -228,11 +216,8 @@ impl RadrootsOutboxDeliveryTargetStatus { } } - pub fn is_deferred_preview(self) -> bool { - matches!( - self, - Self::DeferredUntilImplemented | Self::PreviewUnavailable - ) + pub fn is_deferred_until_implemented(self) -> bool { + matches!(self, Self::DeferredUntilImplemented) } pub fn is_retryable_failure(self) -> bool { @@ -245,19 +230,19 @@ impl RadrootsOutboxDeliveryTargetStatus { pub fn is_completed(self) -> bool { self.counts_as_transport_satisfaction(RadrootsTransportSatisfactionClass::Accepted) - || self.is_deferred_preview() + || self.is_deferred_until_implemented() || self.is_terminal_failure() } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum RadrootsOutboxReticulumPreviewBehavior { +pub enum RadrootsOutboxReticulumBehavior { #[default] RejectDeliveryAttempts, DeferDeliveryPlans, } -impl RadrootsOutboxReticulumPreviewBehavior { +impl RadrootsOutboxReticulumBehavior { pub fn as_str(self) -> &'static str { match self { Self::RejectDeliveryAttempts => "reject_delivery_attempts", @@ -272,7 +257,7 @@ pub struct RadrootsOutboxDeliveryPlanInput { pub target_policy_version: u32, pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, pub targets: Vec<RadrootsTransportTarget>, - pub reticulum_preview_behavior: RadrootsOutboxReticulumPreviewBehavior, + pub reticulum_behavior: RadrootsOutboxReticulumBehavior, } impl RadrootsOutboxDeliveryPlanInput { @@ -287,15 +272,12 @@ impl RadrootsOutboxDeliveryPlanInput { target_policy_version, satisfaction_policy, targets, - reticulum_preview_behavior: RadrootsOutboxReticulumPreviewBehavior::default(), + reticulum_behavior: RadrootsOutboxReticulumBehavior::default(), } } - pub fn with_reticulum_preview_behavior( - mut self, - behavior: RadrootsOutboxReticulumPreviewBehavior, - ) -> Self { - self.reticulum_preview_behavior = behavior; + pub fn with_reticulum_behavior(mut self, behavior: RadrootsOutboxReticulumBehavior) -> Self { + self.reticulum_behavior = behavior; self } } @@ -464,7 +446,7 @@ pub struct RadrootsOutboxDeliveryTargetRecord { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsOutboxReticulumPreviewEventRecord { +pub struct RadrootsOutboxReticulumEventRecord { pub event: RadrootsOutboxEventRecord, pub targets: Vec<RadrootsOutboxDeliveryTargetRecord>, } @@ -509,7 +491,6 @@ pub struct RadrootsOutboxStatusSummary { pub retryable_events: i64, pub terminal_events: i64, pub failed_terminal_events: i64, - pub preview_unavailable_events: i64, pub deferred_until_implemented_events: i64, pub ready_signed_events: i64, pub publishing_events: i64, @@ -531,10 +512,6 @@ mod tests { "deferred_until_implemented", ), ( - RadrootsOutboxOperationStatus::PreviewUnavailable, - "preview_unavailable", - ), - ( RadrootsOutboxOperationStatus::FailedTerminal, "failed_terminal", ), @@ -562,10 +539,6 @@ mod tests { RadrootsOutboxEventState::DeferredUntilImplemented, "deferred_until_implemented", ), - ( - RadrootsOutboxEventState::PreviewUnavailable, - "preview_unavailable", - ), (RadrootsOutboxEventState::FailedTerminal, "failed_terminal"), (RadrootsOutboxEventState::Cancelled, "cancelled"), ] { @@ -584,10 +557,6 @@ mod tests { "deferred_until_implemented", ), ( - RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable, - "preview_unavailable", - ), - ( RadrootsOutboxDeliveryPlanStatus::FailedTerminal, "failed_terminal", ), @@ -602,22 +571,30 @@ mod tests { for (behavior, expected) in [ ( - RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts, + RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts, "reject_delivery_attempts", ), ( - RadrootsOutboxReticulumPreviewBehavior::DeferDeliveryPlans, + RadrootsOutboxReticulumBehavior::DeferDeliveryPlans, "defer_delivery_plans", ), ] { assert_eq!(behavior.as_str(), expected); } assert_eq!( - RadrootsOutboxReticulumPreviewBehavior::default(), - RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts + RadrootsOutboxReticulumBehavior::default(), + RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts ); - for (status, expected, ready, satisfied, delivered, deferred_preview, terminal_failure) in [ + for ( + status, + expected, + ready, + satisfied, + delivered, + deferred_until_implemented, + terminal_failure, + ) in [ ( RadrootsOutboxDeliveryTargetStatus::Pending, "pending", @@ -682,15 +659,6 @@ mod tests { false, ), ( - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable, - "preview_unavailable", - false, - false, - false, - true, - false, - ), - ( RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied, "skipped_policy_denied", false, @@ -736,7 +704,10 @@ mod tests { ), delivered ); - assert_eq!(status.is_deferred_preview(), deferred_preview); + assert_eq!( + status.is_deferred_until_implemented(), + deferred_until_implemented + ); assert_eq!(status.is_terminal_failure(), terminal_failure); } } diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -9,9 +9,9 @@ use crate::model::{ RadrootsOutboxDeliveryTargetStatus, RadrootsOutboxEnqueueReceipt, RadrootsOutboxEnqueueStatus, RadrootsOutboxEventRecord, RadrootsOutboxEventState, RadrootsOutboxEventStoreIngestReceipt, RadrootsOutboxIdempotencyPreflight, RadrootsOutboxOperationInput, - RadrootsOutboxOperationRecord, RadrootsOutboxOperationStatus, - RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxReticulumPreviewEventRecord, - RadrootsOutboxSignedOperationInput, RadrootsOutboxStatusSummary, + RadrootsOutboxOperationRecord, RadrootsOutboxOperationStatus, RadrootsOutboxReticulumBehavior, + RadrootsOutboxReticulumEventRecord, RadrootsOutboxSignedOperationInput, + RadrootsOutboxStatusSummary, }; use radroots_event::draft::{ RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft, @@ -22,7 +22,7 @@ use radroots_event_store::{ RadrootsTransportObservationType, }; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RadrootsTransportError, RadrootsTransportKind, + RADROOTS_RETICULUM_ENDPOINT_URI, RadrootsTransportError, RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, RadrootsTransportTargetUri, @@ -99,7 +99,7 @@ impl RadrootsOutbox { now_ms: i64, ) -> Result<RadrootsOutboxStatusSummary, RadrootsOutboxError> { let row = sqlx::query( - "SELECT COUNT(*) AS total_events, COALESCE(SUM(CASE WHEN state IN ('draft_queued', 'signing', 'signed', 'publishing') THEN 1 ELSE 0 END), 0) AS pending_events, COALESCE(SUM(CASE WHEN state IN ('sign_retryable', 'publish_retryable') THEN 1 ELSE 0 END), 0) AS retryable_events, COALESCE(SUM(CASE WHEN state IN ('published', 'failed_terminal', 'cancelled') THEN 1 ELSE 0 END), 0) AS terminal_events, COALESCE(SUM(CASE WHEN state = 'failed_terminal' THEN 1 ELSE 0 END), 0) AS failed_terminal_events, COALESCE(SUM(CASE WHEN state = 'preview_unavailable' THEN 1 ELSE 0 END), 0) AS preview_unavailable_events, COALESCE(SUM(CASE WHEN state = 'deferred_until_implemented' THEN 1 ELSE 0 END), 0) AS deferred_until_implemented_events, COALESCE(SUM(CASE WHEN state = 'publishing' THEN 1 ELSE 0 END), 0) AS publishing_events FROM outbox_event", + "SELECT COUNT(*) AS total_events, COALESCE(SUM(CASE WHEN state IN ('draft_queued', 'signing', 'signed', 'publishing') THEN 1 ELSE 0 END), 0) AS pending_events, COALESCE(SUM(CASE WHEN state IN ('sign_retryable', 'publish_retryable') THEN 1 ELSE 0 END), 0) AS retryable_events, COALESCE(SUM(CASE WHEN state IN ('published', 'failed_terminal', 'cancelled') THEN 1 ELSE 0 END), 0) AS terminal_events, COALESCE(SUM(CASE WHEN state = 'failed_terminal' THEN 1 ELSE 0 END), 0) AS failed_terminal_events, COALESCE(SUM(CASE WHEN state = 'deferred_until_implemented' THEN 1 ELSE 0 END), 0) AS deferred_until_implemented_events, COALESCE(SUM(CASE WHEN state = 'publishing' THEN 1 ELSE 0 END), 0) AS publishing_events FROM outbox_event", ) .fetch_one(&self.pool) .await?; @@ -129,7 +129,6 @@ impl RadrootsOutbox { retryable_events: row.try_get("retryable_events")?, terminal_events: row.try_get("terminal_events")?, failed_terminal_events: row.try_get("failed_terminal_events")?, - preview_unavailable_events: row.try_get("preview_unavailable_events")?, deferred_until_implemented_events: row.try_get("deferred_until_implemented_events")?, ready_signed_events, publishing_events: row.try_get("publishing_events")?, @@ -529,25 +528,23 @@ impl RadrootsOutbox { delivery_targets_for_event_pool(&self.pool, outbox_event_id).await } - pub async fn reticulum_preview_events( + pub async fn reticulum_events( &self, outbox_event_id: Option<i64>, limit: usize, - ) -> Result<Vec<RadrootsOutboxReticulumPreviewEventRecord>, RadrootsOutboxError> { + ) -> Result<Vec<RadrootsOutboxReticulumEventRecord>, RadrootsOutboxError> { if limit == 0 { return Ok(Vec::new()); } - let outbox_event_ids = - reticulum_preview_event_ids_pool(&self.pool, outbox_event_id, limit).await?; + let outbox_event_ids = reticulum_event_ids_pool(&self.pool, outbox_event_id, limit).await?; let mut records = Vec::with_capacity(outbox_event_ids.len()); for outbox_event_id in outbox_event_ids { let Some(event) = self.get_event(outbox_event_id).await? else { continue; }; - let targets = - reticulum_preview_targets_for_event_pool(&self.pool, outbox_event_id).await?; + let targets = reticulum_targets_for_event_pool(&self.pool, outbox_event_id).await?; if !targets.is_empty() { - records.push(RadrootsOutboxReticulumPreviewEventRecord { event, targets }); + records.push(RadrootsOutboxReticulumEventRecord { event, targets }); } } Ok(records) @@ -816,7 +813,7 @@ impl RadrootsOutbox { pub async fn recover_expired_claims(&self, now_ms: i64) -> Result<u64, RadrootsOutboxError> { let changed = sqlx::query( - "UPDATE outbox_event SET state = CASE WHEN state = 'signing' AND signed_event_json IS NULL THEN 'sign_retryable' WHEN state = 'signing' AND signed_event_json IS NOT NULL THEN 'signed' WHEN state = 'publishing' THEN 'publish_retryable' ELSE state END, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, updated_at_ms = ? WHERE claim_token IS NOT NULL AND claim_expires_at_ms <= ? AND state IN ('signing', 'signed', 'publishing', 'preview_unavailable', 'deferred_until_implemented')", + "UPDATE outbox_event SET state = CASE WHEN state = 'signing' AND signed_event_json IS NULL THEN 'sign_retryable' WHEN state = 'signing' AND signed_event_json IS NOT NULL THEN 'signed' WHEN state = 'publishing' THEN 'publish_retryable' ELSE state END, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, updated_at_ms = ? WHERE claim_token IS NOT NULL AND claim_expires_at_ms <= ? AND state IN ('signing', 'signed', 'publishing', 'deferred_until_implemented')", ) .bind(now_ms) .bind(now_ms) @@ -1021,25 +1018,6 @@ impl RadrootsOutbox { .await } - pub async fn mark_delivery_target_preview_unavailable( - &self, - outbox_event_id: i64, - claim_token: &str, - delivery_target_id: i64, - message: &str, - attempted_at_ms: i64, - ) -> Result<(), RadrootsOutboxError> { - self.mark_delivery_target_status( - outbox_event_id, - claim_token, - delivery_target_id, - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable, - Some(message), - attempted_at_ms, - ) - .await - } - pub async fn mark_delivery_target_skipped_policy_denied( &self, outbox_event_id: i64, @@ -1400,7 +1378,6 @@ struct PlanEvaluation { all_complete: bool, any_failed_terminal: bool, any_ready: bool, - any_preview_unavailable: bool, any_deferred_until_implemented: bool, } @@ -1437,13 +1414,6 @@ fn publish_lifecycle_from_plan_evaluation<'a>( Some(terminal_error), now_ms, ) - } else if evaluation.any_preview_unavailable { - ( - RadrootsOutboxEventState::PreviewUnavailable, - Some(RadrootsOutboxOperationStatus::PreviewUnavailable), - None, - now_ms, - ) } else if evaluation.any_deferred_until_implemented { ( RadrootsOutboxEventState::DeferredUntilImplemented, @@ -1516,22 +1486,12 @@ fn prepare_delivery_plan( return Err(RadrootsOutboxError::EmptyDeliveryTargets); } validate_unique_targets(&targets)?; - if targets - .iter() - .any(|target| target.kind == RadrootsTransportKind::Proxy) - && targets.len() != 1 - { - return Err(RadrootsOutboxError::Transport( - RadrootsTransportError::InvalidTransportKind, - )); - } let total_target_count = targets.len(); let prepared_targets = targets .into_iter() .map(|target| { validate_delivery_target(&target)?; - let initial_status = - initial_delivery_target_status(&target, input.reticulum_preview_behavior); + let initial_status = initial_delivery_target_status(&target, input.reticulum_behavior); Ok(PreparedDeliveryTarget { target, initial_status, @@ -1546,7 +1506,7 @@ fn prepare_delivery_plan( let initial_status = initial_delivery_plan_status(&satisfaction_policy, &prepared_targets); let target_policy_fingerprint = target_policy_fingerprint( &satisfaction_policy, - input.reticulum_preview_behavior, + input.reticulum_behavior, &prepared_targets, ); let delivery_plan_idempotency_digest = delivery_plan_idempotency_digest( @@ -1607,7 +1567,7 @@ fn satisfaction_target_count( ) -> usize { let delivery_capable_target_count = prepared_targets .iter() - .filter(|target| !target.initial_status.is_deferred_preview()) + .filter(|target| !target.initial_status.is_deferred_until_implemented()) .count(); if delivery_capable_target_count == 0 { return total_target_count; @@ -1617,7 +1577,7 @@ fn satisfaction_target_count( fn validate_delivery_target(target: &RadrootsTransportTarget) -> Result<(), RadrootsOutboxError> { if target.kind == RadrootsTransportKind::Reticulum - && target.uri.as_str() != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI + && target.uri.as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(RadrootsOutboxError::Transport( radroots_transport::RadrootsTransportError::InvalidTargetUri, @@ -1640,16 +1600,16 @@ fn validate_unique_targets(targets: &[RadrootsTransportTarget]) -> Result<(), Ra fn initial_delivery_target_status( target: &RadrootsTransportTarget, - reticulum_preview_behavior: RadrootsOutboxReticulumPreviewBehavior, + reticulum_behavior: RadrootsOutboxReticulumBehavior, ) -> RadrootsOutboxDeliveryTargetStatus { if target.kind != RadrootsTransportKind::Reticulum { return RadrootsOutboxDeliveryTargetStatus::Pending; } - match reticulum_preview_behavior { - RadrootsOutboxReticulumPreviewBehavior::RejectDeliveryAttempts => { - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + match reticulum_behavior { + RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts => { + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented } - RadrootsOutboxReticulumPreviewBehavior::DeferDeliveryPlans => { + RadrootsOutboxReticulumBehavior::DeferDeliveryPlans => { RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented } } @@ -1669,11 +1629,6 @@ fn initial_delivery_plan_status( return RadrootsOutboxDeliveryPlanStatus::Queued; } if prepared_targets.iter().all(|target| { - target.initial_status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable - }) { - return RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable; - } - if prepared_targets.iter().all(|target| { target.initial_status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented }) { return RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented; @@ -1874,15 +1829,6 @@ async fn signed_event_lifecycle_for_plans( } if plans .iter() - .any(|plan| plan.status == RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable) - { - return Ok(( - RadrootsOutboxEventState::PreviewUnavailable, - RadrootsOutboxOperationStatus::PreviewUnavailable, - )); - } - if plans - .iter() .any(|plan| plan.status == RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented) { return Ok(( @@ -2102,17 +2048,17 @@ async fn delivery_targets_for_event_pool( rows.into_iter().map(delivery_target_from_row).collect() } -async fn reticulum_preview_event_ids_pool( +async fn reticulum_event_ids_pool( pool: &SqlitePool, outbox_event_id: Option<i64>, limit: usize, ) -> Result<Vec<i64>, RadrootsOutboxError> { let limit = i64::try_from(limit).map_err(|_| RadrootsOutboxError::IntegerRange { - field: "reticulum_preview_events.limit", + field: "reticulum_events.limit", value: i64::MAX, })?; let mut query = String::from( - "SELECT event.outbox_event_id FROM outbox_event AS event WHERE event.signed_event_json IS NOT NULL AND EXISTS (SELECT 1 FROM outbox_delivery_plan AS plan JOIN outbox_delivery_target AS target ON target.delivery_plan_id = plan.delivery_plan_id WHERE plan.outbox_event_id = event.outbox_event_id AND plan.status IN ('queued', 'preview_unavailable', 'deferred_until_implemented') AND target.transport_kind = 'reticulum' AND target.status IN ('pending', 'failed_retryable', 'preview_unavailable', 'deferred_until_implemented'))", + "SELECT event.outbox_event_id FROM outbox_event AS event WHERE event.signed_event_json IS NOT NULL AND EXISTS (SELECT 1 FROM outbox_delivery_plan AS plan JOIN outbox_delivery_target AS target ON target.delivery_plan_id = plan.delivery_plan_id WHERE plan.outbox_event_id = event.outbox_event_id AND plan.status IN ('queued', 'deferred_until_implemented') AND target.transport_kind = 'reticulum' AND target.status IN ('pending', 'failed_retryable', 'deferred_until_implemented'))", ); if outbox_event_id.is_some() { query.push_str(" AND event.outbox_event_id = ?"); @@ -2129,12 +2075,12 @@ async fn reticulum_preview_event_ids_pool( .map_err(Into::into) } -async fn reticulum_preview_targets_for_event_pool( +async fn reticulum_targets_for_event_pool( pool: &SqlitePool, outbox_event_id: i64, ) -> Result<Vec<RadrootsOutboxDeliveryTargetRecord>, RadrootsOutboxError> { let rows = sqlx::query( - "SELECT target.delivery_target_id, target.delivery_plan_id, target.transport_kind, target.endpoint_uri, target.target_scope, target.target_label, target.endpoint_fingerprint, target.status, target.last_outcome_kind, target.attempt_count, target.last_attempt_at_ms, target.completed_at_ms, target.last_error FROM outbox_delivery_target AS target JOIN outbox_delivery_plan AS plan ON plan.delivery_plan_id = target.delivery_plan_id WHERE plan.outbox_event_id = ? AND plan.status IN ('queued', 'preview_unavailable', 'deferred_until_implemented') AND target.transport_kind = 'reticulum' AND target.status IN ('pending', 'failed_retryable', 'preview_unavailable', 'deferred_until_implemented') ORDER BY target.delivery_plan_id, target.delivery_target_id", + "SELECT target.delivery_target_id, target.delivery_plan_id, target.transport_kind, target.endpoint_uri, target.target_scope, target.target_label, target.endpoint_fingerprint, target.status, target.last_outcome_kind, target.attempt_count, target.last_attempt_at_ms, target.completed_at_ms, target.last_error FROM outbox_delivery_target AS target JOIN outbox_delivery_plan AS plan ON plan.delivery_plan_id = target.delivery_plan_id WHERE plan.outbox_event_id = ? AND plan.status IN ('queued', 'deferred_until_implemented') AND target.transport_kind = 'reticulum' AND target.status IN ('pending', 'failed_retryable', 'deferred_until_implemented') ORDER BY target.delivery_plan_id, target.delivery_target_id", ) .bind(outbox_event_id) .fetch_all(pool) @@ -2216,7 +2162,6 @@ async fn evaluate_delivery_plans( let mut all_complete = !plans.is_empty(); let mut any_failed_terminal = false; let mut any_ready = false; - let mut any_preview_unavailable = false; let mut any_deferred_until_implemented = false; for plan in plans { let targets = delivery_targets_for_plan_tx(tx, plan.delivery_plan_id).await?; @@ -2228,13 +2173,7 @@ async fn evaluate_delivery_plans( .count(); let deferred_count = status_targets .iter() - .filter(|target| target.status.is_deferred_preview()) - .count(); - let preview_unavailable_count = status_targets - .iter() - .filter(|target| { - target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable - }) + .filter(|target| target.status.is_deferred_until_implemented()) .count(); let terminal_failure_count = status_targets .iter() @@ -2246,8 +2185,6 @@ async fn evaluate_delivery_plans( RadrootsOutboxDeliveryPlanStatus::Queued } else if terminal_failure_count > 0 { RadrootsOutboxDeliveryPlanStatus::FailedTerminal - } else if preview_unavailable_count > 0 { - RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable } else if deferred_count > 0 { RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented } else { @@ -2262,9 +2199,6 @@ async fn evaluate_delivery_plans( if ready_count > 0 { any_ready = true; } - if plan_status == RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable { - any_preview_unavailable = true; - } if plan_status == RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented { any_deferred_until_implemented = true; } @@ -2283,7 +2217,6 @@ async fn evaluate_delivery_plans( all_complete, any_failed_terminal, any_ready, - any_preview_unavailable, any_deferred_until_implemented, }) } @@ -2532,9 +2465,6 @@ fn outcome_kind_for_status( RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented => { RadrootsTransportOutcomeKind::DeferredUntilImplemented } - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable => { - RadrootsTransportOutcomeKind::TransportUnavailable - } RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied => { RadrootsTransportOutcomeKind::PolicyDenied } @@ -2617,7 +2547,7 @@ fn operation_idempotency_digest( #[derive(Serialize)] struct TargetPolicyDigestInput<'a> { satisfaction_policy: String, - reticulum_preview_behavior: &'a str, + reticulum_behavior: &'a str, targets: Vec<TargetPolicyDigestTarget<'a>>, } @@ -2630,7 +2560,7 @@ struct TargetPolicyDigestTarget<'a> { fn target_policy_fingerprint( satisfaction_policy: &RadrootsTransportSatisfactionPolicy, - reticulum_preview_behavior: RadrootsOutboxReticulumPreviewBehavior, + reticulum_behavior: RadrootsOutboxReticulumBehavior, targets: &[PreparedDeliveryTarget], ) -> String { let mut target_inputs = targets @@ -2649,7 +2579,7 @@ fn target_policy_fingerprint( }); sha256_json(&TargetPolicyDigestInput { satisfaction_policy: satisfaction_policy_storage_value(satisfaction_policy), - reticulum_preview_behavior: reticulum_preview_behavior.as_str(), + reticulum_behavior: reticulum_behavior.as_str(), targets: target_inputs, }) } @@ -2865,7 +2795,8 @@ mod tests { } fn scoped_reticulum_target(scope: &str, label: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::reticulum_preview_with_metadata( + RadrootsTransportTarget::reticulum_with_metadata( + RADROOTS_RETICULUM_ENDPOINT_URI, Some(RadrootsTransportMeshScopeId::parse(scope).expect("target scope")), Some(RadrootsTransportTargetLabel::parse(label).expect("target label")), ) @@ -2873,12 +2804,8 @@ mod tests { } fn reticulum_target(uri: &str) -> RadrootsTransportTarget { - assert_eq!(uri, RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); - RadrootsTransportTarget::reticulum_preview().expect("reticulum target") - } - - fn proxy_target(uri: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::proxy(uri).expect("proxy target") + assert_eq!(uri, RADROOTS_RETICULUM_ENDPOINT_URI); + RadrootsTransportTarget::reticulum().expect("reticulum target") } #[test] @@ -3113,17 +3040,6 @@ mod tests { ) .await } - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable => { - outbox - .mark_delivery_target_preview_unavailable( - outbox_event_id, - claim_token, - delivery_target_id, - "transport preview unavailable", - attempted_at_ms, - ) - .await - } RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied => { outbox .mark_delivery_target_skipped_policy_denied( @@ -3326,7 +3242,7 @@ mod tests { RadrootsTransportSatisfactionPolicy::no_wait(), vec![ nostr_target(NOSTR_PRIMARY_WSS), - reticulum_target(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI), + reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI), ], ), true, @@ -3388,9 +3304,9 @@ mod tests { ); assert!( outbox - .reticulum_preview_events(None, 10) + .reticulum_events(None, 10) .await - .expect("reticulum preview events") + .expect("reticulum events") .is_empty() ); } @@ -3512,9 +3428,7 @@ mod tests { .enqueue_operation(RadrootsOutboxOperationInput::new( "publish_post", draft, - delivery_plan(vec![malformed_reticulum_target( - "reticulum:preview-unavailable-alt", - )]), + delivery_plan(vec![malformed_reticulum_target("reticulum:alternate")]), 1_000, )) .await @@ -3533,34 +3447,6 @@ mod tests { } #[tokio::test] - async fn enqueue_rejects_mixed_proxy_delegate_targets_before_persistence() { - let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let draft = post_draft(hex_64('a').as_str(), "mixed proxy"); - - let err = outbox - .enqueue_operation(RadrootsOutboxOperationInput::new( - "publish_post", - draft, - delivery_plan(vec![ - proxy_target("radrootsd-proxy:publish"), - nostr_target(NOSTR_PRIMARY_WSS), - ]), - 1_000, - )) - .await - .expect_err("mixed proxy targets"); - - assert!(matches!( - err, - RadrootsOutboxError::Transport(RadrootsTransportError::InvalidTransportKind) - )); - assert_eq!(table_count(&outbox, "outbox_operations").await, 0); - assert_eq!(table_count(&outbox, "outbox_event").await, 0); - assert_eq!(table_count(&outbox, "outbox_delivery_plan").await, 0); - assert_eq!(table_count(&outbox, "outbox_delivery_target").await, 0); - } - - #[tokio::test] async fn signed_enqueue_claims_ready_delivery_targets_and_records_attempts() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "signed"); @@ -3905,7 +3791,7 @@ mod tests { for terminal_status in [ RadrootsOutboxDeliveryTargetStatus::Accepted, RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable, + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, RadrootsOutboxDeliveryTargetStatus::SkippedPolicyDenied, RadrootsOutboxDeliveryTargetStatus::FailedTerminal, ] { @@ -4169,7 +4055,7 @@ mod tests { draft.clone(), signed_event.clone(), RadrootsOutboxDeliveryPlanInput::new( - "transport.proxy.invalid", + "transport.nostr.invalid_plan", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), vec![nostr_target(NOSTR_PRIMARY_WSS)], @@ -4215,7 +4101,7 @@ mod tests { .mark_active_delivery_plan_failed_terminal( first.outbox_event_id, "claim-a", - "local proxy validation failed", + "local validation failed", 1_100, ) .await @@ -4494,7 +4380,7 @@ mod tests { } #[tokio::test] - async fn reticulum_reject_targets_are_preview_unavailable_and_not_ready() { + async fn reticulum_reject_targets_are_deferred_until_implemented_and_not_ready() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum rejected"); let signed_event = @@ -4505,10 +4391,10 @@ mod tests { draft, signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), - vec![reticulum_target("reticulum:preview-unavailable")], + vec![reticulum_target("reticulum:local")], ), true, 1_007, @@ -4523,7 +4409,7 @@ mod tests { assert_eq!(targets.len(), 1); assert_eq!( targets[0].status, - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented ); let plans = outbox .delivery_plans(receipt.outbox_event_id) @@ -4531,14 +4417,17 @@ mod tests { .expect("plans"); assert_eq!( plans[0].status, - RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable + RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented ); let event = outbox .get_event(receipt.outbox_event_id) .await .expect("event") .expect("event"); - assert_eq!(event.state, RadrootsOutboxEventState::PreviewUnavailable); + assert_eq!( + event.state, + RadrootsOutboxEventState::DeferredUntilImplemented + ); let operation = outbox .get_operation(receipt.operation_id) .await @@ -4546,13 +4435,12 @@ mod tests { .expect("operation"); assert_eq!( operation.status, - RadrootsOutboxOperationStatus::PreviewUnavailable + RadrootsOutboxOperationStatus::DeferredUntilImplemented ); let summary = outbox.status_summary(1_000).await.expect("summary"); assert_eq!(summary.pending_events, 0); assert_eq!(summary.ready_signed_events, 0); - assert_eq!(summary.preview_unavailable_events, 1); - assert_eq!(summary.deferred_until_implemented_events, 0); + assert_eq!(summary.deferred_until_implemented_events, 1); assert!( outbox .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000) @@ -4574,14 +4462,12 @@ mod tests { draft, signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), - vec![reticulum_target("reticulum:preview-unavailable")], + vec![reticulum_target("reticulum:local")], ) - .with_reticulum_preview_behavior( - RadrootsOutboxReticulumPreviewBehavior::DeferDeliveryPlans, - ), + .with_reticulum_behavior(RadrootsOutboxReticulumBehavior::DeferDeliveryPlans), true, 1_007, 1_000, @@ -4626,7 +4512,6 @@ mod tests { let summary = outbox.status_summary(1_000).await.expect("summary"); assert_eq!(summary.pending_events, 0); assert_eq!(summary.ready_signed_events, 0); - assert_eq!(summary.preview_unavailable_events, 0); assert_eq!(summary.deferred_until_implemented_events, 1); assert!( outbox @@ -4645,10 +4530,10 @@ mod tests { default_behavior_draft, default_behavior_signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), - vec![reticulum_target("reticulum:preview-unavailable")], + vec![reticulum_target("reticulum:local")], ), true, 1_007, @@ -4663,29 +4548,28 @@ mod tests { } #[tokio::test] - async fn reticulum_preview_events_report_preview_and_deferred_records() { + async fn reticulum_events_report_deferred_records() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let preview_draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum preview record"); - let preview_signed_event = - radroots_nostr_sign_frozen_draft(&fixture_keys(), &preview_draft) - .expect("preview signed event"); - let preview = outbox + let reject_draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum record"); + let reject_signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &reject_draft) + .expect("reticulum signed event"); + let reject = outbox .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", - preview_draft, - preview_signed_event, + reject_draft, + reject_signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), - vec![scoped_reticulum_target("farm.preview", "Preview mesh")], + vec![scoped_reticulum_target("farm.reticulum", "Reticulum mesh")], ), true, 1_007, 1_000, )) .await - .expect("preview enqueue"); + .expect("reticulum enqueue"); let deferred_draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum deferred record"); let deferred_signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &deferred_draft) @@ -4696,14 +4580,12 @@ mod tests { deferred_draft, deferred_signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), vec![scoped_reticulum_target("farm.deferred", "Deferred mesh")], ) - .with_reticulum_preview_behavior( - RadrootsOutboxReticulumPreviewBehavior::DeferDeliveryPlans, - ), + .with_reticulum_behavior(RadrootsOutboxReticulumBehavior::DeferDeliveryPlans), true, 1_008, 1_001, @@ -4711,38 +4593,35 @@ mod tests { .await .expect("deferred enqueue"); - let records = outbox - .reticulum_preview_events(None, 10) - .await - .expect("records"); + let records = outbox.reticulum_events(None, 10).await.expect("records"); assert_eq!(records.len(), 2); - assert_eq!(records[0].event.outbox_event_id, preview.outbox_event_id); + assert_eq!(records[0].event.outbox_event_id, reject.outbox_event_id); assert_eq!( records[0].event.state, - RadrootsOutboxEventState::PreviewUnavailable + RadrootsOutboxEventState::DeferredUntilImplemented ); assert_eq!(records[0].targets.len(), 1); assert_eq!( records[0].targets[0].status, - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented ); assert_eq!( records[0].targets[0] .target_scope .as_ref() .map(|scope| scope.as_str()), - Some("farm.preview") + Some("farm.reticulum") ); assert_eq!( records[0].targets[0] .target_label .as_ref() .map(|label| label.as_str()), - Some("Preview mesh") + Some("Reticulum mesh") ); assert_eq!( records[0].targets[0].last_outcome_kind, - Some(RadrootsTransportOutcomeKind::TransportUnavailable) + Some(RadrootsTransportOutcomeKind::DeferredUntilImplemented) ); assert_eq!(records[0].targets[0].attempt_count, 0); assert_eq!(records[1].event.outbox_event_id, deferred.outbox_event_id); @@ -4775,29 +4654,26 @@ mod tests { ); assert_eq!(records[1].targets[0].attempt_count, 0); - let limited = outbox - .reticulum_preview_events(None, 1) - .await - .expect("limited"); + let limited = outbox.reticulum_events(None, 1).await.expect("limited"); assert_eq!(limited.len(), 1); - assert_eq!(limited[0].event.outbox_event_id, preview.outbox_event_id); + assert_eq!(limited[0].event.outbox_event_id, reject.outbox_event_id); let specific = outbox - .reticulum_preview_events(Some(deferred.outbox_event_id), 10) + .reticulum_events(Some(deferred.outbox_event_id), 10) .await .expect("specific"); assert_eq!(specific.len(), 1); assert_eq!(specific[0].event.outbox_event_id, deferred.outbox_event_id); assert!( outbox - .reticulum_preview_events(Some(999), 10) + .reticulum_events(Some(999), 10) .await .expect("missing") .is_empty() ); assert!( outbox - .reticulum_preview_events(None, 0) + .reticulum_events(None, 0) .await .expect("zero") .is_empty() @@ -4805,7 +4681,7 @@ mod tests { } #[tokio::test] - async fn complete_signing_sets_preview_unavailable_lifecycle_for_reticulum_only_plans() { + async fn complete_signing_sets_deferred_until_implemented_lifecycle_for_reticulum_only_plans() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "reticulum after signing"); let receipt = outbox @@ -4813,10 +4689,10 @@ mod tests { "publish_post", draft, RadrootsOutboxDeliveryPlanInput::new( - "transport.reticulum.preview", + "transport.reticulum.default", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), - vec![reticulum_target("reticulum:preview-unavailable")], + vec![reticulum_target("reticulum:local")], ), 1_000, )) @@ -4841,7 +4717,10 @@ mod tests { .await .expect("event") .expect("event"); - assert_eq!(event.state, RadrootsOutboxEventState::PreviewUnavailable); + assert_eq!( + event.state, + RadrootsOutboxEventState::DeferredUntilImplemented + ); assert_eq!(event.claim_token, None); let operation = outbox .get_operation(receipt.operation_id) @@ -4850,7 +4729,7 @@ mod tests { .expect("operation"); assert_eq!( operation.status, - RadrootsOutboxOperationStatus::PreviewUnavailable + RadrootsOutboxOperationStatus::DeferredUntilImplemented ); assert!( outbox @@ -4862,9 +4741,9 @@ mod tests { } #[tokio::test] - async fn hybrid_all_targets_completes_after_nostr_success_with_reticulum_preview_preserved() { + async fn explicit_all_targets_completes_after_nostr_success_with_reticulum_preserved() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid all targets"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "explicit all targets"); let signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); let receipt = outbox @@ -4873,12 +4752,12 @@ mod tests { draft, signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.hybrid", + "transport.explicit.multi_target", 1, RadrootsTransportSatisfactionPolicy::all_accepted(), vec![ nostr_target(NOSTR_PRIMARY_WSS), - reticulum_target("reticulum:preview-unavailable"), + reticulum_target("reticulum:local"), ], ), true, @@ -4905,7 +4784,7 @@ mod tests { .any(|target| target.status == RadrootsOutboxDeliveryTargetStatus::Pending) ); assert!(targets.iter().any(|target| { - target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented })); let claimed = outbox .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000) @@ -4957,16 +4836,16 @@ mod tests { && target.attempt_count == 1 })); assert!(targets.iter().any(|target| { - target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented && target.attempt_count == 0 && target.completed_at_ms.is_none() })); } #[tokio::test] - async fn hybrid_quorum_rejects_preview_targets_as_required_success_capacity() { + async fn explicit_quorum_rejects_deferred_targets_as_required_success_capacity() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid quorum"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "explicit quorum"); let signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); let err = outbox @@ -4975,12 +4854,12 @@ mod tests { draft, signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.hybrid", + "transport.explicit.multi_target", 1, RadrootsTransportSatisfactionPolicy::quorum_accepted(2), vec![ nostr_target(NOSTR_PRIMARY_WSS), - reticulum_target("reticulum:preview-unavailable"), + reticulum_target("reticulum:local"), ], ), true, @@ -5001,9 +4880,9 @@ mod tests { } #[tokio::test] - async fn hybrid_nostr_and_reticulum_preview_preserves_ready_nostr_work() { + async fn explicit_nostr_and_reticulum_preserves_ready_nostr_work() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid preview"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "explicit reticulum"); let signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); let receipt = outbox @@ -5012,12 +4891,12 @@ mod tests { draft, signed_event, RadrootsOutboxDeliveryPlanInput::new( - "transport.hybrid", + "transport.explicit.multi_target", 1, RadrootsTransportSatisfactionPolicy::any_accepted(), vec![ nostr_target(NOSTR_PRIMARY_WSS), - reticulum_target("reticulum:preview-unavailable"), + reticulum_target("reticulum:local"), ], ), true, @@ -5037,7 +4916,7 @@ mod tests { .any(|target| target.status == RadrootsOutboxDeliveryTargetStatus::Pending) ); assert!(targets.iter().any(|target| { - target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented })); let plans = outbox .delivery_plans(receipt.outbox_event_id) @@ -5054,7 +4933,6 @@ mod tests { ); let summary = outbox.status_summary(1_000).await.expect("summary"); assert_eq!(summary.pending_events, 1); - assert_eq!(summary.preview_unavailable_events, 0); assert_eq!(summary.deferred_until_implemented_events, 0); } @@ -5286,7 +5164,7 @@ mod tests { } #[tokio::test] - async fn claimed_delivery_targets_can_complete_as_preview_outcomes() { + async fn claimed_delivery_targets_can_complete_as_deferred_outcomes() { for ( content, mark, @@ -5296,7 +5174,7 @@ mod tests { expected_operation_status, ) in [ ( - "proxy deferred", + "transport deferred", "deferred", RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented, @@ -5304,12 +5182,12 @@ mod tests { RadrootsOutboxOperationStatus::DeferredUntilImplemented, ), ( - "proxy preview unavailable", - "preview", - RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable, - RadrootsOutboxDeliveryPlanStatus::PreviewUnavailable, - RadrootsOutboxEventState::PreviewUnavailable, - RadrootsOutboxOperationStatus::PreviewUnavailable, + "transport unavailable", + "unavailable", + RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented, + RadrootsOutboxDeliveryPlanStatus::DeferredUntilImplemented, + RadrootsOutboxEventState::DeferredUntilImplemented, + RadrootsOutboxOperationStatus::DeferredUntilImplemented, ), ] { let outbox = RadrootsOutbox::open_memory().await.expect("open"); @@ -5353,17 +5231,17 @@ mod tests { .await .expect("deferred"); } - "preview" => { + "unavailable" => { outbox - .mark_delivery_target_preview_unavailable( + .mark_delivery_target_deferred_until_implemented( receipt.outbox_event_id, "claim-a", target_id, - "transport preview unavailable", + "transport unavailable", 1_100, ) .await - .expect("preview unavailable"); + .expect("transport unavailable"); } _ => unreachable!(), } @@ -5416,13 +5294,6 @@ mod tests { retryable_events: 0, terminal_events: 0, failed_terminal_events: 0, - preview_unavailable_events: if expected_status - == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable - { - 1 - } else { - 0 - }, deferred_until_implemented_events: if expected_status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented { diff --git a/crates/protocol_contract_v1/src/lib.rs b/crates/protocol_contract_v1/src/lib.rs @@ -38,11 +38,6 @@ impl TransportKindV1 { "local" => Ok(Self::Local), "nostr" => Ok(Self::Nostr), "reticulum" => Ok(Self::Reticulum), - "reticulum_preview" | "mesh" | "proxy" | "radrootsd_proxy" | "hybrid" => { - Err(ProtocolContractErrorV1::RetiredTransportIdentity { - identity: value.to_string(), - }) - } _ => Err(ProtocolContractErrorV1::UnknownTransportKind { value: value.to_string(), }), @@ -446,7 +441,6 @@ pub enum ProtocolContractErrorV1 { RetiredEventKind { kind: u32 }, RetiredEventName { name: String }, RetiredTradeState { state: String }, - RetiredTransportIdentity { identity: String }, UnknownTradeState { value: String }, UnknownTransportKind { value: String }, InvalidMeshScopeId, @@ -471,9 +465,6 @@ impl core::fmt::Display for ProtocolContractErrorV1 { Self::RetiredEventKind { kind } => write!(f, "retired event kind {kind}"), Self::RetiredEventName { name } => write!(f, "retired event name {name}"), Self::RetiredTradeState { state } => write!(f, "retired trade state {state}"), - Self::RetiredTransportIdentity { identity } => { - write!(f, "retired transport identity {identity}") - } Self::UnknownTradeState { value } => write!(f, "unknown trade state {value}"), Self::UnknownTransportKind { value } => write!(f, "unknown transport kind {value}"), Self::InvalidMeshScopeId => f.write_str("invalid mesh scope id"), @@ -614,23 +605,23 @@ mod tests { } #[test] - fn transport_kind_v1_rejects_retired_identities() { + fn transport_kind_v1_rejects_unrecognized_identities() { for identity in [ - "reticulum_preview", + concat!("reticulum", "_preview"), "mesh", - "proxy", - "radrootsd_proxy", - "hybrid", + concat!("pro", "xy"), + concat!("radrootsd", "_", "pro", "xy"), + concat!("hy", "brid"), ] { assert!(matches!( TransportKindV1::parse(identity), - Err(ProtocolContractErrorV1::RetiredTransportIdentity { .. }) + Err(ProtocolContractErrorV1::UnknownTransportKind { .. }) )); assert_eq!( TransportKindV1::parse(identity) - .expect_err("retired transport") + .expect_err("unknown transport") .to_string(), - alloc::format!("retired transport identity {identity}") + alloc::format!("unknown transport kind {identity}") ); } } diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -650,7 +650,8 @@ mod tests { #[cfg(feature = "transport-workers")] use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire}; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, + RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, + RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, @@ -936,10 +937,10 @@ mod tests { #[cfg(feature = "transport-reticulum")] #[tokio::test] - async fn registry_dispatches_reticulum_preview_transport_without_success() { + async fn registry_dispatches_reticulum_transport_without_success() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry - .register(radroots_transport_reticulum::RadrootsReticulumPreviewTransport::default()) + .register(radroots_transport_reticulum::RadrootsReticulumTransport::default()) .expect("register"); let transport = registry .transport(&RadrootsTransportKind::Reticulum) @@ -947,10 +948,7 @@ mod tests { let request = RadrootsRuntimeTransportDispatchRequest::new( "reticulum-delivery", opaque_payload(), - vec![target( - RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", - )], + vec![target(RadrootsTransportKind::Reticulum, "reticulum:local")], RadrootsTransportSatisfactionPolicy::any_accepted(), 1_000, ) @@ -966,7 +964,15 @@ mod tests { let status = transport.status().await.expect("status"); assert_eq!( status.implementation, - RadrootsTransportImplementationState::PreviewUnavailable + RadrootsTransportImplementationState::Real + ); + assert_eq!( + status.maturity, + RadrootsTransportCapabilityMaturity::Preview + ); + assert_eq!( + status.availability, + RadrootsTransportCapabilityAvailability::Unavailable ); assert!(!status.capabilities.deliver); assert!(!status.capabilities.fetch); @@ -975,7 +981,7 @@ mod tests { "reticulum-fetch", RadrootsTransportTargetSet::new(vec![target( RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", + "reticulum:local", )]) .expect("target set"), )) @@ -989,7 +995,7 @@ mod tests { ); assert_eq!( receipt.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented ); } @@ -1030,10 +1036,7 @@ mod tests { ); let deferred = RadrootsRuntimeDeliveryTarget::deferred_until_implemented( 2, - target( - RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", - ), + target(RadrootsTransportKind::Reticulum, "reticulum:local"), ); let receipt = worker .execute_job(RadrootsRuntimeDeliveryJob { @@ -1160,10 +1163,7 @@ mod tests { bounded_queue_capacity: 8, }, ); - let required_target = target( - RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", - ); + let required_target = target(RadrootsTransportKind::Reticulum, "reticulum:local"); let required_fingerprint = required_target.fingerprint.clone(); let receipt = worker .execute_job(RadrootsRuntimeDeliveryJob { @@ -1300,7 +1300,7 @@ mod tests { #[cfg(feature = "transport-workers")] #[tokio::test] - async fn delivery_worker_reports_reticulum_preview_targets_unsatisfied_without_retry_failure() { + async fn delivery_worker_reports_reticulum_targets_unsatisfied_without_retry_failure() { let registry = RadrootsRuntimeTransportRegistry::new(); let worker = RadrootsRuntimeDeliveryWorker::new( &registry, @@ -1308,13 +1308,10 @@ mod tests { bounded_queue_capacity: 8, }, ); - let preview_target = RadrootsRuntimeDeliveryTarget { + let reticulum_target = RadrootsRuntimeDeliveryTarget { delivery_target_id: 1, - target: target( - RadrootsTransportKind::Reticulum, - "reticulum:preview-unavailable", - ), - status: RadrootsTransportDeliveryTargetStatus::PreviewUnavailable, + target: target(RadrootsTransportKind::Reticulum, "reticulum:local"), + status: RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, }; let receipt = worker .execute_job(RadrootsRuntimeDeliveryJob { @@ -1323,7 +1320,7 @@ mod tests { plans: vec![RadrootsRuntimeDeliveryPlan { delivery_plan_id: 7, satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), - targets: vec![preview_target], + targets: vec![reticulum_target], }], now_ms: 1_000, }) diff --git a/crates/transport/src/kind.rs b/crates/transport/src/kind.rs @@ -5,10 +5,7 @@ use alloc::string::{String, ToString}; pub enum RadrootsTransportKind { Nostr, Reticulum, - Mesh, Local, - Proxy, - Custom(String), } impl RadrootsTransportKind { @@ -32,43 +29,16 @@ impl RadrootsTransportKind { match canonical { "nostr" => Ok(Self::Nostr), "reticulum" => Ok(Self::Reticulum), - "mesh" => Ok(Self::Mesh), "local" => Ok(Self::Local), - "proxy" => Ok(Self::Proxy), - _ => Self::custom_canonical(canonical), + _ => Err(RadrootsTransportError::InvalidTransportKind), } } - pub fn custom(value: impl Into<String>) -> Result<Self, RadrootsTransportError> { - let value = value.into(); - let canonical = value.trim().to_ascii_lowercase(); - Self::custom_canonical(canonical.as_str()) - } - - fn custom_canonical(canonical: &str) -> Result<Self, RadrootsTransportError> { - if canonical.is_empty() { - return Err(RadrootsTransportError::EmptyTransportKind); - } - if removed_first_party_kind(canonical) { - return Err(RadrootsTransportError::InvalidTransportKind); - } - if canonical - .chars() - .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace() || ch == ':' || ch == '/') - { - return Err(RadrootsTransportError::InvalidTransportKind); - } - Ok(Self::Custom(canonical.to_string())) - } - pub fn canonical_label(&self) -> String { match self { Self::Nostr => "nostr".to_string(), Self::Reticulum => "reticulum".to_string(), - Self::Mesh => "mesh".to_string(), Self::Local => "local".to_string(), - Self::Proxy => "proxy".to_string(), - Self::Custom(value) => value.clone(), } } } @@ -94,19 +64,27 @@ impl<'de> serde::Deserialize<'de> for RadrootsTransportKind { } } -fn removed_first_party_kind(canonical: &str) -> bool { - const RADROOTSD_PROXY_PREFIX: &str = "radrootsd"; - const RADROOTSD_PROXY_SUFFIX: &str = "_proxy"; - canonical.len() == RADROOTSD_PROXY_PREFIX.len() + RADROOTSD_PROXY_SUFFIX.len() - && canonical.starts_with(RADROOTSD_PROXY_PREFIX) - && canonical.ends_with(RADROOTSD_PROXY_SUFFIX) -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsTransportImplementationState { Real, Mock, - PreviewUnavailable, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsTransportCapabilityMaturity { + Preview, + Stable, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsTransportCapabilityAvailability { + Available, + Degraded, + Unavailable, } diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs @@ -18,9 +18,12 @@ pub use delivery::{ RadrootsTransportTargetReceipt, }; pub use error::RadrootsTransportError; -pub use kind::{RadrootsTransportImplementationState, RadrootsTransportKind}; +pub use kind::{ + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, + RadrootsTransportImplementationState, RadrootsTransportKind, +}; pub use message::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, }; pub use payload::RadrootsTransportPayload; diff --git a/crates/transport/src/message.rs b/crates/transport/src/message.rs @@ -1,7 +1,7 @@ -pub const RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI: &str = "reticulum:preview-unavailable"; -pub const RADROOTS_RETICULUM_PREVIEW_SCOPE_ID: &str = "local_preview"; +pub const RADROOTS_RETICULUM_ENDPOINT_URI: &str = "reticulum:local"; +pub const RADROOTS_RETICULUM_SCOPE_ID: &str = "local"; pub const RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE: &str = concat!( - "Reticulum transport is configured in preview mode, ", + "Reticulum transport is configured, ", "but this build does not implement Reticulum delivery." ); diff --git a/crates/transport/src/status.rs b/crates/transport/src/status.rs @@ -1,4 +1,5 @@ use crate::{ + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, RadrootsTransportImplementationState, RadrootsTransportKind, delivery::RadrootsTransportSatisfactionClass, }; @@ -14,7 +15,6 @@ pub enum RadrootsTransportDeliveryTargetStatus { StoredByGateway, Seen, DeferredUntilImplemented, - PreviewUnavailable, SkippedPolicyDenied, FailedRetryable, FailedTerminal, @@ -66,11 +66,8 @@ impl RadrootsTransportDeliveryTargetStatus { matches!(self, Self::SkippedPolicyDenied | Self::FailedTerminal) } - pub fn is_deferred_preview(self) -> bool { - matches!( - self, - Self::DeferredUntilImplemented | Self::PreviewUnavailable - ) + pub fn is_deferred_until_implemented(self) -> bool { + matches!(self, Self::DeferredUntilImplemented) } } @@ -224,6 +221,8 @@ pub struct RadrootsTransportStatus { pub endpoint_uri: Option<String>, pub configured: bool, pub implementation: RadrootsTransportImplementationState, + pub maturity: RadrootsTransportCapabilityMaturity, + pub availability: RadrootsTransportCapabilityAvailability, pub usable_for_delivery: bool, pub capabilities: RadrootsTransportCapabilities, pub message: String, @@ -243,6 +242,12 @@ impl RadrootsTransportStatus { endpoint_uri: None, configured, implementation, + maturity: RadrootsTransportCapabilityMaturity::Stable, + availability: if usable_for_delivery { + RadrootsTransportCapabilityAvailability::Available + } else { + RadrootsTransportCapabilityAvailability::Unavailable + }, usable_for_delivery, capabilities: if usable_for_delivery { RadrootsTransportCapabilities::deliver_only() @@ -258,6 +263,19 @@ impl RadrootsTransportStatus { self } + pub fn with_maturity(mut self, maturity: RadrootsTransportCapabilityMaturity) -> Self { + self.maturity = maturity; + self + } + + pub fn with_availability( + mut self, + availability: RadrootsTransportCapabilityAvailability, + ) -> Self { + self.availability = availability; + self + } + pub fn with_profile_id(mut self, profile_id: impl Into<String>) -> Self { self.profile_id = Some(profile_id.into()); self diff --git a/crates/transport/src/target.rs b/crates/transport/src/target.rs @@ -1,6 +1,6 @@ use crate::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RadrootsTransportError, RadrootsTransportKind, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, RadrootsTransportError, + RadrootsTransportKind, }; use alloc::collections::BTreeSet; use alloc::format; @@ -54,9 +54,8 @@ impl RadrootsTransportMeshScopeId { Ok(Self(value.to_string())) } - pub fn local_preview() -> Self { - Self::parse(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID) - .expect("default Reticulum preview scope id") + pub fn local_reticulum() -> Self { + Self::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("default Reticulum scope id") } pub fn as_str(&self) -> &str { @@ -178,20 +177,16 @@ impl RadrootsTransportTarget { Self::new_with_metadata(RadrootsTransportKind::Nostr, uri, scope, label) } - pub fn reticulum_preview() -> Result<Self, RadrootsTransportError> { - Self::reticulum_preview_with_metadata(None, None) + pub fn reticulum() -> Result<Self, RadrootsTransportError> { + Self::reticulum_with_metadata(RADROOTS_RETICULUM_ENDPOINT_URI, None, None) } - pub fn reticulum_preview_with_metadata( + pub fn reticulum_with_metadata( + uri: impl AsRef<str>, scope: Option<RadrootsTransportMeshScopeId>, label: Option<RadrootsTransportTargetLabel>, ) -> Result<Self, RadrootsTransportError> { - Self::new_with_metadata( - RadrootsTransportKind::Reticulum, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - scope, - label, - ) + Self::new_with_metadata(RadrootsTransportKind::Reticulum, uri, scope, label) } pub fn local(uri: impl AsRef<str>) -> Result<Self, RadrootsTransportError> { @@ -206,18 +201,6 @@ impl RadrootsTransportTarget { Self::new_with_metadata(RadrootsTransportKind::Local, uri, scope, label) } - pub fn proxy(uri: impl AsRef<str>) -> Result<Self, RadrootsTransportError> { - Self::proxy_with_metadata(uri, None, None) - } - - pub fn proxy_with_metadata( - uri: impl AsRef<str>, - scope: Option<RadrootsTransportMeshScopeId>, - label: Option<RadrootsTransportTargetLabel>, - ) -> Result<Self, RadrootsTransportError> { - Self::new_with_metadata(RadrootsTransportKind::Proxy, uri, scope, label) - } - pub fn new_with_metadata( kind: RadrootsTransportKind, uri: impl AsRef<str>, @@ -225,15 +208,16 @@ impl RadrootsTransportTarget { label: Option<RadrootsTransportTargetLabel>, ) -> Result<Self, RadrootsTransportError> { let raw_uri = uri.as_ref(); - if kind == RadrootsTransportKind::Reticulum - && raw_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - { - return Err(RadrootsTransportError::InvalidTargetUri); - } let uri = match kind { RadrootsTransportKind::Nostr => RadrootsTransportTargetUri::parse_nostr_relay(raw_uri)?, _ => RadrootsTransportTargetUri::parse(raw_uri)?, }; + if kind == RadrootsTransportKind::Reticulum + && (raw_uri != RADROOTS_RETICULUM_ENDPOINT_URI + || uri.as_str() != RADROOTS_RETICULUM_ENDPOINT_URI) + { + return Err(RadrootsTransportError::InvalidTargetUri); + } let scope = scope.or_else(|| default_scope_for_kind(&kind)); let fingerprint = RadrootsTransportTargetFingerprint::from_target(&kind, &uri, scope.as_ref()); @@ -248,7 +232,7 @@ impl RadrootsTransportTarget { } fn default_scope_for_kind(kind: &RadrootsTransportKind) -> Option<RadrootsTransportMeshScopeId> { - (*kind == RadrootsTransportKind::Reticulum).then(RadrootsTransportMeshScopeId::local_preview) + (*kind == RadrootsTransportKind::Reticulum).then(RadrootsTransportMeshScopeId::local_reticulum) } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -289,6 +273,9 @@ fn canonicalize_uri(raw: &str) -> Result<String, RadrootsTransportError> { if trimmed.is_empty() { return Err(RadrootsTransportError::EmptyTargetUri); } + if raw != trimmed { + return Err(RadrootsTransportError::InvalidTargetUri); + } if trimmed .chars() .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) @@ -330,6 +317,9 @@ fn canonicalize_nostr_relay_uri(raw: &str) -> Result<String, RadrootsTransportEr if trimmed.is_empty() { return Err(RadrootsTransportError::EmptyTargetUri); } + if raw != trimmed { + return Err(RadrootsTransportError::InvalidTargetUri); + } if trimmed .chars() .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs @@ -36,12 +36,12 @@ const FOUNDATION_HARDENING_DOC_ROOTS: &[&str] = &["contracts", "docs"]; const FORBIDDEN_TRANSPORT_CONCEPTS: &[ForbiddenConcept] = &[ ForbiddenConcept { - pattern: "\"radrootsd_proxy\"", - reason: "proxy must use first-class RadrootsTransportKind::Proxy modeling", + pattern: concat!("\"radrootsd", "_", "pro", "xy\""), + reason: "radrootsd execution must not be modeled as transport identity", }, ForbiddenConcept { - pattern: "radrootsd.publish_proxy.v1", - reason: "transport publish protocol v1 proxy identifiers are removed", + pattern: concat!("radrootsd.publish", "_", "pro", "xy.v1"), + reason: "transport publish protocol v1 radrootsd execution identifiers are removed", }, ForbiddenConcept { pattern: "publish.relays.resolve", @@ -309,9 +309,9 @@ fn transport_hardening_sources_reject_removed_protocol_identifiers() { } } - for line in removed_reticulum_preview_endpoint_lines(source) { + for line in removed_reticulum_stage_endpoint_lines(source) { findings.push(format!( - "{relative_path}:{line} contains removed Reticulum preview endpoint `reticulum:preview`" + "{relative_path}:{line} contains removed Reticulum staging endpoint" )); } } @@ -511,19 +511,21 @@ fn runtime_transport_registry_uses_core_transport_contract() { runtime_source.contains("transport.transport_kind()"), "runtime registry must key transports through the core trait transport_kind" ); + let removed_reticulum_runtime_transport = + ["RadrootsRuntimeReticulum", "Pre", "viewTransport"].concat(); for forbidden in [ - "pub trait RadrootsRuntimeTransportAdapter", - "dyn RadrootsRuntimeTransportAdapter", - "RadrootsRuntimeReticulumPreviewTransport", + "pub trait RadrootsRuntimeTransportAdapter".to_owned(), + "dyn RadrootsRuntimeTransportAdapter".to_owned(), + removed_reticulum_runtime_transport, ] { assert!( - !runtime_source.contains(forbidden), + !runtime_source.contains(forbidden.as_str()), "runtime transport source must not retain split adapter contract `{forbidden}`" ); } assert!( - reticulum_source.contains("impl RadrootsTransport for RadrootsReticulumPreviewTransport"), - "Reticulum preview transport must implement the core transport contract" + reticulum_source.contains("impl RadrootsTransport for RadrootsReticulumTransport"), + "Reticulum transport must implement the core transport contract" ); } @@ -542,13 +544,16 @@ fn transport_publish_capabilities_keep_canonical_status_fields() { "pub transport: String,", "pub configured: bool,", "pub implementation: TransportPublishImplementation,", + "pub maturity: TransportPublishCapabilityMaturity,", + "pub availability: TransportPublishCapabilityAvailability,", "pub usable_for_delivery: bool,", "pub capabilities: TransportPublishOperationCapabilities,", "pub struct TransportPublishOperationCapabilities", "pub deliver: bool,", "pub fetch: bool,", "TransportPublishImplementation::Real", - "TransportPublishImplementation::PreviewUnavailable", + "TransportPublishCapabilityMaturity::Preview", + "TransportPublishCapabilityAvailability::Unavailable", "configured: true", "usable_for_delivery: true", "usable_for_delivery: false", @@ -685,29 +690,38 @@ fn required_target_semantics_stay_fingerprint_exact() { } #[test] -fn transport_hardening_sources_keep_proxy_and_reticulum_contracts() { +fn transport_hardening_sources_reject_removed_execution_kind_and_keep_reticulum_contracts() { let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("transport crate parent"); let transport_kind = read_source(crates_root.join("transport/src/kind.rs").as_path()); - for required in [ - "Proxy,", - r#""proxy" => Ok(Self::Proxy)"#, - r#"Self::Proxy => "proxy".to_string()"#, - ] { + for required in ["Nostr,", "Reticulum,", "Local,"] { assert!( transport_kind.contains(required), - "transport kind source must retain first-class proxy witness `{required}`" + "transport kind source must retain first-wave transport witness `{required}`" + ); + } + let removed_kind_variant = ["Pro", "xy,"].concat(); + let removed_parse_arm = [r#""#, "pro", "xy", r#"" => Ok(Self::"#, "Pro", "xy)"].concat(); + let removed_label_arm = [r#"Self::"#, "Pro", "xy => \"", "pro", "xy\".to_string()"].concat(); + for forbidden in [ + &removed_kind_variant, + &removed_parse_arm, + &removed_label_arm, + ] { + assert!( + !transport_kind.contains(forbidden.as_str()), + "transport kind source must not retain removed radrootsd execution transport witness `{forbidden}`" ); } let transport_message_source = read_source(crates_root.join("transport/src/message.rs").as_path()); for required in [ - "RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI", - "reticulum:preview-unavailable", + "RADROOTS_RETICULUM_ENDPOINT_URI", + "reticulum:local", "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", - "Reticulum transport is configured in preview mode, ", + "Reticulum transport is configured, ", "but this build does not implement Reticulum delivery.", ] { assert!( @@ -716,7 +730,7 @@ fn transport_hardening_sources_keep_proxy_and_reticulum_contracts() { ); } for forbidden in [ - "Reticulum preview transport is registered, ", + "Reticulum prerelease transport is registered, ", "future compatibility", "compatibility mode", "fallback behavior", @@ -731,24 +745,24 @@ fn transport_hardening_sources_keep_proxy_and_reticulum_contracts() { let reticulum_source = read_source(crates_root.join("transport_reticulum/src/lib.rs").as_path()); assert!( - reticulum_source.contains("RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI"), - "Reticulum preview source must consume the shared endpoint URI constant" + reticulum_source.contains("RADROOTS_RETICULUM_ENDPOINT_URI"), + "Reticulum source must consume the shared endpoint URI constant" ); assert!( - !reticulum_source.contains("reticulum:preview-unavailable"), - "Reticulum preview source must not duplicate the shared endpoint URI" + !reticulum_source.contains(["reticulum:", "pre", "view-unavailable"].concat().as_str()), + "Reticulum source must not duplicate the shared endpoint URI" ); assert!( reticulum_source.contains("RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE"), - "Reticulum preview source must consume the shared unavailable message constant" + "Reticulum source must consume the shared unavailable message constant" ); assert!( !reticulum_source.contains("Reticulum transport is configured in preview mode"), - "Reticulum preview source must not duplicate the shared unavailable message" + "Reticulum source must not duplicate the shared unavailable message" ); assert!( !reticulum_source.contains("future compatibility"), - "Reticulum preview source must not duplicate compatibility copy" + "Reticulum source must not duplicate compatibility copy" ); let protocol_source_raw = read_source( @@ -758,11 +772,11 @@ fn transport_hardening_sources_keep_proxy_and_reticulum_contracts() { ); let protocol_source = production_source(protocol_source_raw.as_str()); assert!( - protocol_source.contains("RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI"), + protocol_source.contains("RADROOTS_RETICULUM_ENDPOINT_URI"), "transport publish protocol must consume the shared Reticulum endpoint URI constant" ); assert!( - !protocol_source.contains("reticulum:preview-unavailable"), + !protocol_source.contains(["reticulum:", "pre", "view-unavailable"].concat().as_str()), "transport publish protocol must not duplicate the shared endpoint URI" ); assert!( @@ -918,11 +932,14 @@ fn contains_forbidden_concept(source: &str, pattern: &str) -> bool { }) } -fn removed_reticulum_preview_endpoint_lines(source: &str) -> Vec<usize> { +fn removed_reticulum_stage_endpoint_lines(source: &str) -> Vec<usize> { + let removed_endpoint_prefix = ["reticulum:", "pre", "view"].concat(); source - .match_indices("reticulum:preview") + .match_indices(removed_endpoint_prefix.as_str()) .filter_map(|(index, _)| { - let after = source[index + "reticulum:preview".len()..].chars().next(); + let after = source[index + removed_endpoint_prefix.len()..] + .chars() + .next(); (after != Some('-')).then(|| line_number(source, index)) }) .collect() diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs @@ -1,6 +1,6 @@ use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, RadrootsTransport, + RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, @@ -20,16 +20,16 @@ fn opaque_payload() -> RadrootsTransportPayload { #[test] fn target_fingerprints_are_stable_and_transport_scoped() { let nostr_upper = - RadrootsTransportTarget::nostr_relay(" WSS://Relay.Example/Events ").expect("nostr target"); + RadrootsTransportTarget::nostr_relay("WSS://Relay.Example/Events").expect("nostr target"); let nostr_lower = RadrootsTransportTarget::nostr_relay("wss://relay.example/Events").expect("nostr target"); - let reticulum = RadrootsTransportTarget::reticulum_preview().expect("reticulum target"); + let reticulum = RadrootsTransportTarget::reticulum().expect("reticulum target"); assert_eq!(nostr_upper.uri.as_str(), "wss://relay.example/Events"); assert_eq!(nostr_upper.scope, None); assert_eq!( reticulum.scope.as_ref().map(|scope| scope.as_str()), - Some(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID) + Some(RADROOTS_RETICULUM_SCOPE_ID) ); assert_eq!(nostr_upper.fingerprint, nostr_lower.fingerprint); assert_ne!(nostr_upper.fingerprint, reticulum.fingerprint); @@ -40,7 +40,7 @@ fn target_fingerprints_are_stable_and_transport_scoped() { } #[test] -fn transport_kind_parser_round_trips_canonical_labels_and_custom_values() { +fn transport_kind_parser_round_trips_first_wave_canonical_labels() { assert_eq!( RadrootsTransportKind::parse(" NOSTR ").expect("nostr kind"), RadrootsTransportKind::Nostr @@ -50,46 +50,25 @@ fn transport_kind_parser_round_trips_canonical_labels_and_custom_values() { RadrootsTransportKind::Reticulum ); assert_eq!( - RadrootsTransportKind::parse("mesh").expect("mesh kind"), - RadrootsTransportKind::Mesh - ); - assert_eq!( RadrootsTransportKind::parse("local").expect("local kind"), RadrootsTransportKind::Local ); assert_eq!( - RadrootsTransportKind::parse("PROXY").expect("proxy kind"), - RadrootsTransportKind::Proxy - ); - assert_eq!( RadrootsTransportKind::Local.canonical_label(), "local".to_owned() ); - assert_eq!( - RadrootsTransportKind::Proxy.canonical_label(), - "proxy".to_owned() - ); - assert_eq!( - RadrootsTransportKind::parse("fieldbus").expect("custom kind"), - RadrootsTransportKind::Custom("fieldbus".to_owned()) - ); - assert_eq!( - RadrootsTransportKind::parse(removed_proxy_kind()).expect_err("removed proxy kind"), - RadrootsTransportError::InvalidTransportKind - ); - assert_eq!( - RadrootsTransportKind::custom(removed_proxy_kind().to_ascii_uppercase()) - .expect_err("removed proxy custom kind"), - RadrootsTransportError::InvalidTransportKind - ); - assert_eq!( - RadrootsTransportKind::Custom("fieldbus".to_owned()).canonical_label(), - "fieldbus".to_owned() - ); - assert_eq!( - RadrootsTransportKind::parse("bad kind").expect_err("invalid kind"), - RadrootsTransportError::InvalidTransportKind - ); + for retired in [ + "mesh".to_owned(), + ["pro", "xy"].concat(), + ["hy", "brid"].concat(), + ["reticulum", "_preview"].concat(), + "fieldbus".to_owned(), + ] { + assert_eq!( + RadrootsTransportKind::parse(retired).expect_err("retired or unknown kind"), + RadrootsTransportError::InvalidTransportKind + ); + } } #[test] @@ -99,10 +78,6 @@ fn canonical_transport_kind_parser_rejects_noncanonical_public_values() { RadrootsTransportKind::Nostr ); assert_eq!( - RadrootsTransportKind::parse_canonical("fieldbus").expect("custom kind"), - RadrootsTransportKind::Custom("fieldbus".to_owned()) - ); - assert_eq!( RadrootsTransportKind::parse_canonical("NOSTR").expect_err("uppercase kind"), RadrootsTransportError::InvalidTransportKind ); @@ -111,8 +86,12 @@ fn canonical_transport_kind_parser_rejects_noncanonical_public_values() { RadrootsTransportError::InvalidTransportKind ); assert_eq!( - RadrootsTransportKind::parse_canonical(removed_proxy_kind()) - .expect_err("removed proxy kind"), + RadrootsTransportKind::parse_canonical(removed_radrootsd_execution_transport_kind()) + .expect_err("removed radrootsd execution kind"), + RadrootsTransportError::InvalidTransportKind + ); + assert_eq!( + RadrootsTransportKind::parse_canonical("fieldbus").expect_err("custom kind"), RadrootsTransportError::InvalidTransportKind ); assert_eq!( @@ -121,8 +100,8 @@ fn canonical_transport_kind_parser_rejects_noncanonical_public_values() { ); } -fn removed_proxy_kind() -> String { - ["radrootsd", "_proxy"].concat() +fn removed_radrootsd_execution_transport_kind() -> String { + ["radrootsd", "_", "pro", "xy"].concat() } #[test] @@ -337,16 +316,19 @@ fn transport_status_models_canonical_configuration_and_delivery_usability() { #[test] fn deferred_transport_outcomes_are_terminal_but_not_satisfied() { - let target = RadrootsTransportTarget::reticulum_preview().expect("target"); + let target = RadrootsTransportTarget::reticulum().expect("target"); let receipt = RadrootsTransportDeliveryReceipt { - request_id: "reticulum-preview".to_owned(), + request_id: "reticulum".to_owned(), target_receipts: vec![RadrootsTransportTargetReceipt::new( target, RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::DeferredUntilImplemented), )], }; - assert!(RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented.is_deferred_preview()); + assert!( + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + .is_deferred_until_implemented() + ); assert_eq!( receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), 0 @@ -553,23 +535,15 @@ fn transport_errors_have_stable_display_strings() { #[test] fn transport_kind_and_target_parsers_cover_negative_edges() { - assert_eq!( - RadrootsTransportKind::custom(" ").expect_err("empty kind"), - RadrootsTransportError::EmptyTransportKind - ); - for invalid in ["bad kind", "bad:kind", "bad/kind", "bad\nkind"] { + for invalid in ["bad kind", "bad:kind", "bad/kind", "bad\nkind", "fieldbus"] { assert_eq!( - RadrootsTransportKind::custom(invalid).expect_err("invalid kind"), + RadrootsTransportKind::parse(invalid).expect_err("invalid kind"), RadrootsTransportError::InvalidTransportKind ); } - assert_eq!( - RadrootsTransportKind::custom(" FieldBus ").expect("custom kind"), - RadrootsTransportKind::Custom("fieldbus".to_owned()) - ); let no_scheme = - RadrootsTransportTargetUri::parse(" transport-target ").expect("schemeless target uri"); + RadrootsTransportTargetUri::parse("transport-target").expect("schemeless target uri"); assert_eq!(no_scheme.as_str(), "transport-target"); assert_eq!(no_scheme.to_string(), "transport-target"); let opaque = RadrootsTransportTargetUri::parse("RNS:PeerA").expect("opaque uri"); @@ -584,6 +558,7 @@ fn transport_kind_and_target_parsers_cover_negative_edges() { ); for invalid in [ "bad target", + " transport-target ", ":target", "1bad:target", "bad_scheme://target", @@ -647,28 +622,24 @@ fn checked_in_transport_target_uri_vectors_match_parser_behavior() { } #[test] -fn reticulum_transport_targets_require_exact_preview_endpoint() { - let target = - RadrootsTransportTarget::reticulum_preview().expect("exact Reticulum preview endpoint"); - assert_eq!(target.uri.as_str(), RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); +fn reticulum_transport_targets_use_default_destination_and_scope() { + let target = RadrootsTransportTarget::reticulum().expect("Reticulum target"); + assert_eq!(target.uri.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); assert_eq!( target.scope.as_ref().map(|scope| scope.as_str()), - Some(RADROOTS_RETICULUM_PREVIEW_SCOPE_ID) + Some(RADROOTS_RETICULUM_SCOPE_ID) ); + let invalid_reticulum_destination = ["reticulum:", "remote"].concat(); for invalid in [ - " reticulum:preview-unavailable", - "reticulum:preview-unavailable ", - "RETICULUM:preview-unavailable", - "reticulum:Preview-Unavailable", - "reticulum:preview", - "reticulum:preview-unavailable-alt", - "reticulum:custom", - "wss://relay.example/Events", + " reticulum:local".to_owned(), + "reticulum:local ".to_owned(), + "RETICULUM:local".to_owned(), + invalid_reticulum_destination, ] { assert_eq!( - RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, invalid) - .expect_err("invalid Reticulum preview endpoint"), + RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, invalid.as_str()) + .expect_err("invalid Reticulum endpoint"), RadrootsTransportError::InvalidTargetUri ); } @@ -676,8 +647,7 @@ fn reticulum_transport_targets_require_exact_preview_endpoint() { #[test] fn target_fingerprints_and_sets_cover_accessors_and_validation() { - let target = RadrootsTransportTarget::new(RadrootsTransportKind::Mesh, "mesh://node.example") - .expect("mesh target"); + let target = RadrootsTransportTarget::reticulum().expect("Reticulum target"); let parsed = RadrootsTransportTargetFingerprint::parse(target.fingerprint.as_str().to_ascii_uppercase()) .expect("uppercase fingerprint parses"); @@ -700,39 +670,39 @@ fn target_fingerprints_and_sets_cover_accessors_and_validation() { #[test] fn target_scope_participates_in_identity_and_label_does_not() { - let local_scope = RadrootsTransportMeshScopeId::parse("local_preview").expect("local scope"); - let remote_scope = RadrootsTransportMeshScopeId::parse("remote_preview").expect("remote scope"); + let local_scope = RadrootsTransportMeshScopeId::parse("local").expect("local scope"); + let remote_scope = RadrootsTransportMeshScopeId::parse("remote").expect("remote scope"); let local = RadrootsTransportTarget::new_with_metadata( - RadrootsTransportKind::Mesh, - "mesh://node.example", + RadrootsTransportKind::Reticulum, + RADROOTS_RETICULUM_ENDPOINT_URI, Some(local_scope.clone()), - Some(RadrootsTransportTargetLabel::parse("Local mesh node").expect("label")), + Some(RadrootsTransportTargetLabel::parse("Local Reticulum node").expect("label")), ) - .expect("local mesh target"); + .expect("local Reticulum target"); let relabeled = RadrootsTransportTarget::new_with_metadata( - RadrootsTransportKind::Mesh, - "mesh://node.example", + RadrootsTransportKind::Reticulum, + RADROOTS_RETICULUM_ENDPOINT_URI, Some(local_scope), Some(RadrootsTransportTargetLabel::parse("Renamed node").expect("label")), ) .expect("relabeled mesh target"); let remote = RadrootsTransportTarget::new_with_metadata( - RadrootsTransportKind::Mesh, - "mesh://node.example", + RadrootsTransportKind::Reticulum, + RADROOTS_RETICULUM_ENDPOINT_URI, Some(remote_scope), None, ) - .expect("remote mesh target"); + .expect("remote Reticulum target"); assert_eq!(local.fingerprint, relabeled.fingerprint); assert_ne!(local.fingerprint, remote.fingerprint); assert_eq!( local.scope.as_ref().map(|scope| scope.as_str()), - Some("local_preview") + Some("local") ); assert_eq!( local.label.as_ref().map(|label| label.as_str()), - Some("Local mesh node") + Some("Local Reticulum node") ); assert_eq!( RadrootsTransportMeshScopeId::parse("").expect_err("empty scope"), @@ -834,7 +804,7 @@ fn satisfaction_and_target_status_cover_all_contract_states() { &[], ), ( - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, &[], ), ( @@ -855,7 +825,10 @@ fn satisfaction_and_target_status_cover_all_contract_states() { ); } } - assert!(RadrootsTransportDeliveryTargetStatus::PreviewUnavailable.is_deferred_preview()); + assert!( + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + .is_deferred_until_implemented() + ); assert!(RadrootsTransportDeliveryTargetStatus::FailedRetryable.is_retryable_failure()); assert!(RadrootsTransportDeliveryTargetStatus::FailedTerminal.is_terminal_failure()); } @@ -989,12 +962,12 @@ fn typed_outcome_kinds_drive_status_and_satisfaction_semantics() { assert_eq!(outcome.message.as_deref(), Some("transport detail")); } - let preview_unavailable = + let deferred_until_implemented = RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::TransportUnavailable) - .with_target_status(RadrootsTransportDeliveryTargetStatus::PreviewUnavailable); + .with_target_status(RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented); assert_eq!( - preview_unavailable.status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable + deferred_until_implemented.status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented ); } diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs @@ -570,21 +570,6 @@ async fn complete_outbox_delivery_target( ) .await?; } - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable => { - outbox - .mark_delivery_target_preview_unavailable( - claimed.outbox_event_id, - claimed.claim_token.as_str(), - receipt.delivery_target_id, - receipt - .outcome - .message - .as_deref() - .unwrap_or("relay publish preview unavailable"), - now_ms, - ) - .await?; - } RadrootsTransportDeliveryTargetStatus::SkippedPolicyDenied => { outbox .mark_delivery_target_skipped_policy_denied( diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -810,7 +810,7 @@ async fn nostr_transport_facade_rejects_unsupported_payloads_and_targets() { .expect_err("payload rejected"); assert_eq!(payload_error, RadrootsTransportError::InvalidPayloadBytes); - let non_nostr_target = RadrootsTransportTarget::reticulum_preview().expect("reticulum target"); + let non_nostr_target = RadrootsTransportTarget::reticulum().expect("reticulum target"); let target_error = transport .deliver(RadrootsTransportDeliveryRequest::new( "facade-request-target", @@ -2429,7 +2429,7 @@ async fn outbox_publish_skips_non_nostr_targets() { RadrootsTransportSatisfactionPolicy::all_accepted(), vec![ nostr_target(RELAY_PRIMARY_WSS), - RadrootsTransportTarget::reticulum_preview().expect("reticulum target"), + RadrootsTransportTarget::reticulum().expect("reticulum target"), ], ), 1_000, @@ -2474,7 +2474,7 @@ async fn outbox_publish_skips_non_nostr_targets() { .expect("targets"); assert!(targets.iter().any(|target| { target.transport_kind == RadrootsTransportKind::Reticulum - && target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + && target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented })); } diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs @@ -11,7 +11,7 @@ use std::{collections::BTreeSet, string::String, vec::Vec}; use core::fmt; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportError, RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, }; @@ -58,14 +58,11 @@ pub enum TransportPublishProtocolError { InvalidTargetLabel { index: usize, }, - InvalidPreviewBehavior { + InvalidReticulumBehavior { index: usize, }, InvalidTimeoutMs, - InvalidReticulumPreviewEndpoint { - index: usize, - }, - ExplicitProxyTarget { + InvalidReticulumEndpoint { index: usize, }, DuplicateTarget { @@ -160,18 +157,14 @@ impl fmt::Display for TransportPublishProtocolError { Self::InvalidTargetLabel { index } => { write!(f, "transport target {index} target_label is invalid") } - Self::InvalidPreviewBehavior { index } => write!( + Self::InvalidReticulumBehavior { index } => write!( f, - "transport target {index} preview_behavior is only valid for Reticulum targets" + "transport target {index} reticulum_behavior is only valid for Reticulum targets" ), Self::InvalidTimeoutMs => f.write_str("timeout_ms must be greater than zero"), - Self::InvalidReticulumPreviewEndpoint { index } => write!( - f, - "transport target {index} Reticulum preview endpoint must be {RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI}" - ), - Self::ExplicitProxyTarget { index } => write!( + Self::InvalidReticulumEndpoint { index } => write!( f, - "transport target {index} proxy is an SDK delegation target and cannot be used as a daemon explicit target" + "transport target {index} Reticulum endpoint must be {RADROOTS_RETICULUM_ENDPOINT_URI}" ), Self::DuplicateTarget { index } => { write!(f, "transport target {index} duplicates an earlier target") @@ -243,7 +236,7 @@ impl fmt::Display for TransportPublishProtocolError { } Self::InvalidReticulumOutcome { index } => write!( f, - "transport target outcome {index} Reticulum preview must be unavailable or deferred" + "transport target outcome {index} Reticulum must be unavailable or deferred" ), } } @@ -255,7 +248,7 @@ impl std::error::Error for TransportPublishProtocolError {} #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum TransportPublishPreviewBehavior { +pub enum TransportPublishReticulumBehavior { #[default] RejectDeliveryAttempts, DeferDeliveryPlans, @@ -281,7 +274,7 @@ pub struct TransportPublishTarget { feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] - pub preview_behavior: Option<TransportPublishPreviewBehavior>, + pub reticulum_behavior: Option<TransportPublishReticulumBehavior>, } impl TransportPublishTarget { @@ -291,17 +284,17 @@ impl TransportPublishTarget { endpoint_uri: endpoint_uri.into(), target_scope: None, target_label: None, - preview_behavior: None, + reticulum_behavior: None, } } - pub fn reticulum_preview(behavior: TransportPublishPreviewBehavior) -> Self { + pub fn reticulum(behavior: TransportPublishReticulumBehavior) -> Self { Self { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(behavior), + reticulum_behavior: Some(behavior), } } @@ -321,19 +314,16 @@ impl TransportPublishTarget { } let transport_kind = RadrootsTransportKind::parse_canonical(self.transport_kind.as_str()) .map_err(|error| transport_kind_error(error, index))?; - if transport_kind == RadrootsTransportKind::Proxy { - return Err(TransportPublishProtocolError::ExplicitProxyTarget { index }); - } if self.endpoint_uri.trim().is_empty() { return Err(TransportPublishProtocolError::EmptyEndpointUri { index }); } - if transport_kind != RadrootsTransportKind::Reticulum && self.preview_behavior.is_some() { - return Err(TransportPublishProtocolError::InvalidPreviewBehavior { index }); + if transport_kind != RadrootsTransportKind::Reticulum && self.reticulum_behavior.is_some() { + return Err(TransportPublishProtocolError::InvalidReticulumBehavior { index }); } if transport_kind == RadrootsTransportKind::Reticulum - && self.endpoint_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI + && self.endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { - return Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index }); + return Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index }); } validate_target_metadata( self.target_scope.as_deref(), @@ -669,7 +659,7 @@ pub enum TransportPublishJobStatus { DeliveryUnsatisfiedRetryable, DeliveryUnsatisfiedTerminal, DeliveryDeferred, - DeliveryPreviewUnavailable, + DeliveryDeferredUntilImplemented, Rejected, } @@ -694,7 +684,6 @@ pub enum TransportPublishOutcomeKind { TargetRejected, SkippedAlreadyAccepted, DeferredUntilImplemented, - PreviewUnavailable, Unknown, } @@ -732,11 +721,8 @@ impl TransportPublishOutcomeKind { ) } - pub fn is_deferred_preview(self) -> bool { - matches!( - self, - Self::DeferredUntilImplemented | Self::PreviewUnavailable - ) + pub fn is_deferred_until_implemented(self) -> bool { + matches!(self, Self::DeferredUntilImplemented) } } @@ -747,7 +733,7 @@ pub enum TransportPublishTargetSource { Request, NostrAuthorWrite, DaemonDefault, - ReticulumPreview, + Reticulum, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -957,25 +943,29 @@ impl TransportPublishCapabilities { transport: "nostr".to_owned(), configured: true, implementation: TransportPublishImplementation::Real, + maturity: TransportPublishCapabilityMaturity::Stable, + availability: TransportPublishCapabilityAvailability::Available, usable_for_delivery: true, capabilities: TransportPublishOperationCapabilities { deliver: true, fetch: false, }, - preview_behavior: None, + reticulum_behavior: None, message: "Nostr relay publish is available".to_owned(), }, TransportPublishTransportCapability { transport: "reticulum".to_owned(), configured: true, - implementation: TransportPublishImplementation::PreviewUnavailable, + implementation: TransportPublishImplementation::Real, + maturity: TransportPublishCapabilityMaturity::Preview, + availability: TransportPublishCapabilityAvailability::Unavailable, usable_for_delivery: false, capabilities: TransportPublishOperationCapabilities { deliver: false, fetch: false, }, - preview_behavior: Some( - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + reticulum_behavior: Some( + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), message: RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned(), }, @@ -1011,7 +1001,23 @@ pub struct TransportPublishSurfaceCapabilities { pub enum TransportPublishImplementation { Real, Mock, - PreviewUnavailable, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransportPublishCapabilityMaturity { + Preview, + Stable, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransportPublishCapabilityAvailability { + Available, + Degraded, + Unavailable, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -1021,13 +1027,15 @@ pub struct TransportPublishTransportCapability { pub transport: String, pub configured: bool, pub implementation: TransportPublishImplementation, + pub maturity: TransportPublishCapabilityMaturity, + pub availability: TransportPublishCapabilityAvailability, pub usable_for_delivery: bool, pub capabilities: TransportPublishOperationCapabilities, #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] - pub preview_behavior: Option<TransportPublishPreviewBehavior>, + pub reticulum_behavior: Option<TransportPublishReticulumBehavior>, pub message: String, } @@ -1097,7 +1105,7 @@ fn job_status_is_terminal(status: TransportPublishJobStatus) -> bool { TransportPublishJobStatus::DeliverySatisfied | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal | TransportPublishJobStatus::DeliveryDeferred - | TransportPublishJobStatus::DeliveryPreviewUnavailable + | TransportPublishJobStatus::DeliveryDeferredUntilImplemented | TransportPublishJobStatus::Rejected ) } @@ -1127,21 +1135,21 @@ fn validate_target_outcome( index, )?; if transport_kind == RadrootsTransportKind::Reticulum { - if target.endpoint_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { - return Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index }); + if target.endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { + return Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index }); } - if target.source != TransportPublishTargetSource::ReticulumPreview { + if target.source != TransportPublishTargetSource::Reticulum { return Err(TransportPublishProtocolError::InvalidTargetSource { index }); } - if target.attempted || !target.outcome_kind.is_deferred_preview() { + if target.attempted || !target.outcome_kind.is_deferred_until_implemented() { return Err(TransportPublishProtocolError::InvalidReticulumOutcome { index }); } return Ok(()); } - if target.source == TransportPublishTargetSource::ReticulumPreview { + if target.source == TransportPublishTargetSource::Reticulum { return Err(TransportPublishProtocolError::InvalidTargetSource { index }); } - if target.outcome_kind.is_deferred_preview() { + if target.outcome_kind.is_deferred_until_implemented() { return Err(TransportPublishProtocolError::InvalidTargetOutcomeKind { index }); } Ok(()) @@ -1182,20 +1190,14 @@ fn transport_target_from_parts( RadrootsTransportTarget::nostr_relay_with_metadata(endpoint_uri, scope, label) } RadrootsTransportKind::Reticulum => { - if endpoint_uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { + if endpoint_uri != RADROOTS_RETICULUM_ENDPOINT_URI { return Err(RadrootsTransportError::InvalidTargetUri); } - RadrootsTransportTarget::reticulum_preview_with_metadata(scope, label) + RadrootsTransportTarget::reticulum_with_metadata(endpoint_uri, scope, label) } RadrootsTransportKind::Local => { RadrootsTransportTarget::local_with_metadata(endpoint_uri, scope, label) } - RadrootsTransportKind::Proxy => { - RadrootsTransportTarget::proxy_with_metadata(endpoint_uri, scope, label) - } - RadrootsTransportKind::Mesh | RadrootsTransportKind::Custom(_) => { - RadrootsTransportTarget::new_with_metadata(transport_kind, endpoint_uri, scope, label) - } } } @@ -1291,51 +1293,55 @@ fn validate_job_status_state( return Err(TransportPublishProtocolError::InvalidJobStatusState); } let required_count = job.delivery_policy.required_target_count(job.target_count); - let (satisfied, retryable_status_count, terminal_status_count, has_deferred, has_preview) = - match &job.delivery_policy { - TransportPublishDeliveryPolicy::RequiredTargets { targets } => { - let required_outcomes = required_policy_outcomes(targets, &job.targets)?; - let satisfied = targets.iter().all(|required| { - required_outcomes.iter().any(|outcome| { - target_outcome_fingerprint(outcome, 0).is_ok_and(|fingerprint| { - fingerprint == *required - && outcome.outcome_kind.counts_toward_accepted_delivery() - }) + let ( + satisfied, + retryable_status_count, + terminal_status_count, + has_deferred, + has_deferred_until_implemented, + ) = match &job.delivery_policy { + TransportPublishDeliveryPolicy::RequiredTargets { targets } => { + let required_outcomes = required_policy_outcomes(targets, &job.targets)?; + let satisfied = targets.iter().all(|required| { + required_outcomes.iter().any(|outcome| { + target_outcome_fingerprint(outcome, 0).is_ok_and(|fingerprint| { + fingerprint == *required + && outcome.outcome_kind.counts_toward_accepted_delivery() }) - }); - ( - satisfied, - required_outcomes - .iter() - .filter(|outcome| outcome.outcome_kind.is_retryable()) - .count(), - required_outcomes - .iter() - .filter(|outcome| outcome.outcome_kind.is_terminal_failure()) - .count(), - required_outcomes.iter().any(|outcome| { - outcome.outcome_kind - == TransportPublishOutcomeKind::DeferredUntilImplemented - }), - required_outcomes.iter().any(|outcome| { - outcome.outcome_kind == TransportPublishOutcomeKind::PreviewUnavailable - }), - ) - } - TransportPublishDeliveryPolicy::Any - | TransportPublishDeliveryPolicy::All - | TransportPublishDeliveryPolicy::Quorum { .. } => ( - required_count > 0 && acknowledged_count >= required_count, - retryable_count, - terminal_count, - job.targets.iter().any(|target| { - target.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented + }) + }); + ( + satisfied, + required_outcomes + .iter() + .filter(|outcome| outcome.outcome_kind.is_retryable()) + .count(), + required_outcomes + .iter() + .filter(|outcome| outcome.outcome_kind.is_terminal_failure()) + .count(), + required_outcomes.iter().any(|outcome| { + outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented }), - job.targets.iter().any(|target| { - target.outcome_kind == TransportPublishOutcomeKind::PreviewUnavailable + required_outcomes.iter().any(|outcome| { + outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented }), - ), - }; + ) + } + TransportPublishDeliveryPolicy::Any + | TransportPublishDeliveryPolicy::All + | TransportPublishDeliveryPolicy::Quorum { .. } => ( + required_count > 0 && acknowledged_count >= required_count, + retryable_count, + terminal_count, + job.targets.iter().any(|target| { + target.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented + }), + job.targets.iter().any(|target| { + target.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented + }), + ), + }; match job.status { TransportPublishJobStatus::DeliverySatisfied if satisfied => Ok(()), TransportPublishJobStatus::DeliveryUnsatisfiedRetryable @@ -1356,11 +1362,11 @@ fn validate_job_status_state( { Ok(()) } - TransportPublishJobStatus::DeliveryPreviewUnavailable + TransportPublishJobStatus::DeliveryDeferredUntilImplemented if !satisfied && terminal_status_count == 0 && retryable_status_count == 0 - && has_preview => + && has_deferred_until_implemented => { Ok(()) } @@ -1417,10 +1423,10 @@ mod tests { ) -> TransportPublishTargetOutcome { TransportPublishTargetOutcome { transport_kind: "reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), target_scope: None, target_label: None, - source: TransportPublishTargetSource::ReticulumPreview, + source: TransportPublishTargetSource::Reticulum, attempted: false, outcome_kind, message: None, @@ -1529,6 +1535,11 @@ mod tests { .expect("nostr capability"); assert!(nostr.configured); assert_eq!(nostr.implementation, TransportPublishImplementation::Real); + assert_eq!(nostr.maturity, TransportPublishCapabilityMaturity::Stable); + assert_eq!( + nostr.availability, + TransportPublishCapabilityAvailability::Available + ); assert!(nostr.usable_for_delivery); assert!(nostr.capabilities.deliver); assert!(!nostr.capabilities.fetch); @@ -1541,14 +1552,22 @@ mod tests { assert!(reticulum.configured); assert_eq!( reticulum.implementation, - TransportPublishImplementation::PreviewUnavailable + TransportPublishImplementation::Real + ); + assert_eq!( + reticulum.maturity, + TransportPublishCapabilityMaturity::Preview + ); + assert_eq!( + reticulum.availability, + TransportPublishCapabilityAvailability::Unavailable ); assert!(!reticulum.usable_for_delivery); assert!(!reticulum.capabilities.deliver); assert!(!reticulum.capabilities.fetch); assert_eq!( - reticulum.preview_behavior, - Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts) + reticulum.reticulum_behavior, + Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts) ); } @@ -1569,8 +1588,8 @@ mod tests { let mut too_many = request.clone(); too_many.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::nostr("wss://relay.example.com"), - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), ]); assert!(matches!( @@ -1602,50 +1621,50 @@ mod tests { invalid_reticulum_endpoint.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { transport_kind: "reticulum".to_owned(), - endpoint_uri: "reticulum:preview-unavailable-alt".to_owned(), + endpoint_uri: "reticulum:alternate".to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); assert_eq!( invalid_reticulum_endpoint.validate(1), - Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index: 0 }) + Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index: 0 }) ); let mut noncanonical_reticulum_kind = request.clone(); noncanonical_reticulum_kind.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { transport_kind: "Reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); assert_eq!( noncanonical_reticulum_kind.validate(1), Err(TransportPublishProtocolError::InvalidTransportKind { index: 0 }) ); - let mut nostr_preview_behavior = request.clone(); - nostr_preview_behavior.target_policy = + let mut nostr_reticulum_behavior = request.clone(); + nostr_reticulum_behavior.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { transport_kind: "nostr".to_owned(), endpoint_uri: "wss://relay.example.com".to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), }]); assert_eq!( - nostr_preview_behavior.validate(1), - Err(TransportPublishProtocolError::InvalidPreviewBehavior { index: 0 }) + nostr_reticulum_behavior.validate(1), + Err(TransportPublishProtocolError::InvalidReticulumBehavior { index: 0 }) ); for invalid in [ - " reticulum:preview-unavailable", - "reticulum:preview-unavailable ", - "RETICULUM:preview-unavailable", - "reticulum:Preview-Unavailable", - "reticulum:preview", + " reticulum:local", + "reticulum:local ", + "RETICULUM:local", + "reticulum:Local", + "reticulum:temporary", "reticulum:custom", ] { let mut invalid_reticulum_endpoint = request.clone(); @@ -1655,42 +1674,16 @@ mod tests { endpoint_uri: invalid.to_owned(), target_scope: None, target_label: None, - preview_behavior: Some(TransportPublishPreviewBehavior::RejectDeliveryAttempts), + reticulum_behavior: Some( + TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ), }]); assert_eq!( invalid_reticulum_endpoint.validate(1), - Err(TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index: 0 }) + Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index: 0 }) ); } - let mut removed_proxy_kind = request.clone(); - removed_proxy_kind.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: removed_proxy_kind_string(), - endpoint_uri: "radrootsd-proxy:publish".to_owned(), - target_scope: None, - target_label: None, - preview_behavior: None, - }]); - assert_eq!( - removed_proxy_kind.validate(1), - Err(TransportPublishProtocolError::InvalidTransportKind { index: 0 }) - ); - - let mut explicit_proxy_target = request.clone(); - explicit_proxy_target.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: "proxy".to_owned(), - endpoint_uri: "radrootsd-proxy:publish".to_owned(), - target_scope: None, - target_label: None, - preview_behavior: None, - }]); - assert_eq!( - explicit_proxy_target.validate(1), - Err(TransportPublishProtocolError::ExplicitProxyTarget { index: 0 }) - ); - let mut duplicate_targets = request.clone(); duplicate_targets.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::nostr("wss://relay.example.com/a"), @@ -1762,10 +1755,6 @@ mod tests { ); } - fn removed_proxy_kind_string() -> String { - ["radrootsd", "_proxy"].concat() - } - #[test] fn outcome_kinds_classify_satisfaction_retry_and_terminal() { assert!(TransportPublishOutcomeKind::Accepted.counts_toward_accepted_delivery()); @@ -1773,9 +1762,9 @@ mod tests { TransportPublishOutcomeKind::SkippedAlreadyAccepted.counts_toward_accepted_delivery() ); assert!(TransportPublishOutcomeKind::Timeout.is_retryable()); - assert!(TransportPublishOutcomeKind::PreviewUnavailable.is_deferred_preview()); - assert!(TransportPublishOutcomeKind::DeferredUntilImplemented.is_deferred_preview()); - assert!(!TransportPublishOutcomeKind::PreviewUnavailable.is_terminal_failure()); + assert!( + TransportPublishOutcomeKind::DeferredUntilImplemented.is_deferred_until_implemented() + ); assert!(!TransportPublishOutcomeKind::DeferredUntilImplemented.is_terminal_failure()); } @@ -1817,18 +1806,18 @@ mod tests { .validate() .expect("terminal job"); job_from_targets( - TransportPublishJobStatus::DeliveryPreviewUnavailable, + TransportPublishJobStatus::DeliveryDeferredUntilImplemented, TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), ]), vec![reticulum_outcome( - TransportPublishOutcomeKind::PreviewUnavailable, + TransportPublishOutcomeKind::DeferredUntilImplemented, )], ) .validate() - .expect("preview unavailable job"); + .expect("Reticulum unavailable job"); rejected_job().validate().expect("rejected job"); } @@ -2072,12 +2061,12 @@ mod tests { } #[test] - fn job_view_validation_rejects_reticulum_success_and_non_reticulum_preview() { + fn job_view_validation_rejects_reticulum_success_and_non_reticulum() { let mut reticulum_success = job_from_targets( TransportPublishJobStatus::DeliverySatisfied, TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::RejectDeliveryAttempts, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::RejectDeliveryAttempts, ), ]), vec![reticulum_outcome(TransportPublishOutcomeKind::Accepted)], @@ -2088,25 +2077,25 @@ mod tests { Err(TransportPublishProtocolError::InvalidReticulumOutcome { index: 0 }) ); - let non_reticulum_preview = job_from_targets( - TransportPublishJobStatus::DeliveryPreviewUnavailable, + let non_reticulum = job_from_targets( + TransportPublishJobStatus::DeliveryDeferredUntilImplemented, TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr( "wss://relay.example.com", )]), vec![nostr_outcome( - TransportPublishOutcomeKind::PreviewUnavailable, + TransportPublishOutcomeKind::DeferredUntilImplemented, )], ); assert_eq!( - non_reticulum_preview.validate(), + non_reticulum.validate(), Err(TransportPublishProtocolError::InvalidTargetOutcomeKind { index: 0 }) ); let mut reticulum_wrong_source = job_from_targets( TransportPublishJobStatus::DeliveryDeferred, TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::DeferDeliveryPlans, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, ), ]), vec![reticulum_outcome( @@ -2122,8 +2111,8 @@ mod tests { let reticulum_deferred = job_from_targets( TransportPublishJobStatus::DeliveryDeferred, TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::DeferDeliveryPlans, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, ), ]), vec![reticulum_outcome( @@ -2136,12 +2125,12 @@ mod tests { } #[test] - fn serde_round_trip_preserves_preview_target() { + fn serde_round_trip_preserves_reticulum_target() { let request = TransportPublishEventRequest { raw_event_json: raw_event_json(), target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::DeferDeliveryPlans, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, ), ]), delivery_policy: TransportPublishDeliveryPolicy::All, @@ -2150,7 +2139,7 @@ mod tests { }; let encoded = serde_json::to_string(&request).expect("encode"); assert!(encoded.contains("\"transport_kind\":\"reticulum\"")); - assert!(encoded.contains("\"preview_behavior\":\"defer_delivery_plans\"")); + assert!(encoded.contains("\"reticulum_behavior\":\"defer_delivery_plans\"")); let decoded: TransportPublishEventRequest = serde_json::from_str(encoded.as_str()).expect("decode"); assert_eq!(decoded, request); @@ -2232,16 +2221,16 @@ mod tests { "transport target 3 target_label is invalid", ), ( - TransportPublishProtocolError::InvalidPreviewBehavior { index: 4 }, - "transport target 4 preview_behavior is only valid for Reticulum targets", + TransportPublishProtocolError::InvalidReticulumBehavior { index: 4 }, + "transport target 4 reticulum_behavior is only valid for Reticulum targets", ), ( TransportPublishProtocolError::InvalidTimeoutMs, "timeout_ms must be greater than zero", ), ( - TransportPublishProtocolError::InvalidReticulumPreviewEndpoint { index: 5 }, - "transport target 5 Reticulum preview endpoint must be reticulum:preview-unavailable", + TransportPublishProtocolError::InvalidReticulumEndpoint { index: 5 }, + "transport target 5 Reticulum endpoint must be reticulum:local", ), ( TransportPublishProtocolError::TargetLimitExceeded { max: 1, actual: 2 }, @@ -2299,13 +2288,13 @@ mod tests { #[test] fn target_and_delivery_policy_validation_cover_all_modes() { assert_eq!( - TransportPublishPreviewBehavior::default(), - TransportPublishPreviewBehavior::RejectDeliveryAttempts + TransportPublishReticulumBehavior::default(), + TransportPublishReticulumBehavior::RejectDeliveryAttempts ); let explicit = TransportPublishTargetPolicy::explicit_targets(vec![ TransportPublishTarget::nostr("wss://relay.example"), - TransportPublishTarget::reticulum_preview( - TransportPublishPreviewBehavior::DeferDeliveryPlans, + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, ), ]); let nostr = TransportPublishTargetPolicy::nostr( @@ -2333,7 +2322,7 @@ mod tests { endpoint_uri: "wss://relay.example".to_owned(), target_scope: None, target_label: None, - preview_behavior: None, + reticulum_behavior: None, }]); assert_eq!( empty_targets.validate(10), @@ -2346,7 +2335,7 @@ mod tests { endpoint_uri: "wss://relay.example".to_owned(), target_scope: None, target_label: None, - preview_behavior: None, + reticulum_behavior: None, }]); assert_eq!( empty_targets.validate(10), @@ -2460,10 +2449,7 @@ mod tests { TransportPublishOutcomeKind::PaymentRequired, TransportPublishOutcomeKind::TargetRejected, ]; - let deferred_preview = [ - TransportPublishOutcomeKind::DeferredUntilImplemented, - TransportPublishOutcomeKind::PreviewUnavailable, - ]; + let deferred_until_implemented = [TransportPublishOutcomeKind::DeferredUntilImplemented]; for kind in satisfied { assert!(kind.counts_toward_accepted_delivery()); @@ -2480,11 +2466,11 @@ mod tests { assert!(!kind.is_retryable()); assert!(kind.is_terminal_failure()); } - for kind in deferred_preview { + for kind in deferred_until_implemented { assert!(!kind.counts_toward_accepted_delivery()); assert!(!kind.is_retryable()); assert!(!kind.is_terminal_failure()); - assert!(kind.is_deferred_preview()); + assert!(kind.is_deferred_until_implemented()); } } diff --git a/crates/transport_reticulum/src/lib.rs b/crates/transport_reticulum/src/lib.rs @@ -9,8 +9,9 @@ use alloc::string::String; use alloc::vec::Vec; use core::fmt; use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - RadrootsTransport, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransport, + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, + RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome, @@ -18,15 +19,15 @@ use radroots_transport::{ RadrootsTransportTargetReceipt, }; -const DEFAULT_PROFILE_ID: &str = "transport.reticulum.preview"; +const DEFAULT_PROFILE_ID: &str = "transport.reticulum.default"; const RETICULUM_AGENT_ENDPOINT_PREFIX: &str = "reticulum-agent:"; const UNAVAILABLE_CODE: &str = "transport_unavailable"; const DEFERRED_CODE: &str = "deferred_until_implemented"; -const DEFERRED_MESSAGE: &str = "Reticulum preview delivery is deferred until implementation"; +const DEFERRED_MESSAGE: &str = "Reticulum delivery is deferred until implementation"; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum RadrootsReticulumPreviewBehavior { +pub enum RadrootsReticulumBehavior { #[default] RejectDeliveryAttempts, DeferDeliveryPlans, @@ -34,18 +35,18 @@ pub enum RadrootsReticulumPreviewBehavior { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewEndpoint { +pub struct RadrootsReticulumEndpoint { uri: String, } -impl RadrootsReticulumPreviewEndpoint { - pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumPreviewError> { +impl RadrootsReticulumEndpoint { + pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> { let uri = raw.as_ref(); - if uri != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { - return Err(RadrootsReticulumPreviewError::InvalidEndpoint); + if uri != RADROOTS_RETICULUM_ENDPOINT_URI { + return Err(RadrootsReticulumError::InvalidEndpoint); } Ok(Self { - uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), }) } @@ -58,14 +59,13 @@ impl RadrootsReticulumPreviewEndpoint { } } -impl Default for RadrootsReticulumPreviewEndpoint { +impl Default for RadrootsReticulumEndpoint { fn default() -> Self { - Self::parse(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI) - .expect("default Reticulum preview endpoint") + Self::parse(RADROOTS_RETICULUM_ENDPOINT_URI).expect("default Reticulum endpoint") } } -impl fmt::Display for RadrootsReticulumPreviewEndpoint { +impl fmt::Display for RadrootsReticulumEndpoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.uri.as_str()) } @@ -73,12 +73,12 @@ impl fmt::Display for RadrootsReticulumPreviewEndpoint { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewAgentEndpoint { +pub struct RadrootsReticulumAgentEndpoint { uri: String, } -impl RadrootsReticulumPreviewAgentEndpoint { - pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumPreviewError> { +impl RadrootsReticulumAgentEndpoint { + pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> { let uri = raw.as_ref(); if uri.is_empty() || uri != uri.trim() @@ -88,7 +88,7 @@ impl RadrootsReticulumPreviewAgentEndpoint { || !uri.starts_with(RETICULUM_AGENT_ENDPOINT_PREFIX) || uri.len() == RETICULUM_AGENT_ENDPOINT_PREFIX.len() { - return Err(RadrootsReticulumPreviewError::InvalidAgentEndpoint); + return Err(RadrootsReticulumError::InvalidAgentEndpoint); } Ok(Self { uri: uri.to_owned(), @@ -104,7 +104,7 @@ impl RadrootsReticulumPreviewAgentEndpoint { } } -impl fmt::Display for RadrootsReticulumPreviewAgentEndpoint { +impl fmt::Display for RadrootsReticulumAgentEndpoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.uri.as_str()) } @@ -112,25 +112,25 @@ impl fmt::Display for RadrootsReticulumPreviewAgentEndpoint { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewProfile { +pub struct RadrootsReticulumProfile { profile_id: String, - endpoint: RadrootsReticulumPreviewEndpoint, + endpoint: RadrootsReticulumEndpoint, scope: RadrootsTransportMeshScopeId, - agent_endpoint: Option<RadrootsReticulumPreviewAgentEndpoint>, - behavior: RadrootsReticulumPreviewBehavior, + agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, + behavior: RadrootsReticulumBehavior, } -impl RadrootsReticulumPreviewProfile { +impl RadrootsReticulumProfile { pub fn new( profile_id: impl Into<String>, - endpoint: RadrootsReticulumPreviewEndpoint, + endpoint: RadrootsReticulumEndpoint, scope: RadrootsTransportMeshScopeId, - agent_endpoint: Option<RadrootsReticulumPreviewAgentEndpoint>, - behavior: RadrootsReticulumPreviewBehavior, - ) -> Result<Self, RadrootsReticulumPreviewError> { + agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, + behavior: RadrootsReticulumBehavior, + ) -> Result<Self, RadrootsReticulumError> { let profile_id = profile_id.into(); if profile_id.trim().is_empty() || profile_id.chars().any(char::is_whitespace) { - return Err(RadrootsReticulumPreviewError::InvalidProfileId); + return Err(RadrootsReticulumError::InvalidProfileId); } Ok(Self { profile_id, @@ -141,17 +141,17 @@ impl RadrootsReticulumPreviewProfile { }) } - pub fn preview_unavailable() -> Self { + pub fn deferred_until_implemented() -> Self { Self { profile_id: DEFAULT_PROFILE_ID.to_owned(), - endpoint: RadrootsReticulumPreviewEndpoint::default(), - scope: RadrootsTransportMeshScopeId::local_preview(), + endpoint: RadrootsReticulumEndpoint::default(), + scope: RadrootsTransportMeshScopeId::local_reticulum(), agent_endpoint: None, - behavior: RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts, + behavior: RadrootsReticulumBehavior::RejectDeliveryAttempts, } } - pub fn with_behavior(mut self, behavior: RadrootsReticulumPreviewBehavior) -> Self { + pub fn with_behavior(mut self, behavior: RadrootsReticulumBehavior) -> Self { self.behavior = behavior; self } @@ -160,7 +160,7 @@ impl RadrootsReticulumPreviewProfile { self.profile_id.as_str() } - pub fn endpoint(&self) -> &RadrootsReticulumPreviewEndpoint { + pub fn endpoint(&self) -> &RadrootsReticulumEndpoint { &self.endpoint } @@ -168,79 +168,78 @@ impl RadrootsReticulumPreviewProfile { &self.scope } - pub fn agent_endpoint(&self) -> Option<&RadrootsReticulumPreviewAgentEndpoint> { + pub fn agent_endpoint(&self) -> Option<&RadrootsReticulumAgentEndpoint> { self.agent_endpoint.as_ref() } - pub fn with_agent_endpoint( - mut self, - agent_endpoint: RadrootsReticulumPreviewAgentEndpoint, - ) -> Self { + pub fn with_agent_endpoint(mut self, agent_endpoint: RadrootsReticulumAgentEndpoint) -> Self { self.agent_endpoint = Some(agent_endpoint); self } - pub fn behavior(&self) -> RadrootsReticulumPreviewBehavior { + pub fn behavior(&self) -> RadrootsReticulumBehavior { self.behavior } - pub fn status(&self) -> RadrootsReticulumPreviewStatus { - RadrootsReticulumPreviewStatus { + pub fn status(&self) -> RadrootsReticulumStatus { + RadrootsReticulumStatus { behavior: self.behavior, scope: self.scope.clone(), agent_endpoint: self.agent_endpoint.clone(), transport_status: RadrootsTransportStatus::new( RadrootsTransportKind::Reticulum, true, - RadrootsTransportImplementationState::PreviewUnavailable, + RadrootsTransportImplementationState::Real, false, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, ) + .with_maturity(RadrootsTransportCapabilityMaturity::Preview) + .with_availability(RadrootsTransportCapabilityAvailability::Unavailable) .with_profile_id(self.profile_id.clone()) .with_endpoint_uri(self.endpoint.as_str()), } } } -impl Default for RadrootsReticulumPreviewProfile { +impl Default for RadrootsReticulumProfile { fn default() -> Self { - Self::preview_unavailable() + Self::deferred_until_implemented() } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewStatus { - pub behavior: RadrootsReticulumPreviewBehavior, +pub struct RadrootsReticulumStatus { + pub behavior: RadrootsReticulumBehavior, pub scope: RadrootsTransportMeshScopeId, - pub agent_endpoint: Option<RadrootsReticulumPreviewAgentEndpoint>, + pub agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, pub transport_status: RadrootsTransportStatus, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewTransport { - profile: RadrootsReticulumPreviewProfile, +pub struct RadrootsReticulumTransport { + profile: RadrootsReticulumProfile, } -impl RadrootsReticulumPreviewTransport { - pub fn new(profile: RadrootsReticulumPreviewProfile) -> Self { +impl RadrootsReticulumTransport { + pub fn new(profile: RadrootsReticulumProfile) -> Self { Self { profile } } - pub fn profile(&self) -> &RadrootsReticulumPreviewProfile { + pub fn profile(&self) -> &RadrootsReticulumProfile { &self.profile } - pub fn status(&self) -> RadrootsReticulumPreviewStatus { + pub fn status(&self) -> RadrootsReticulumStatus { self.profile.status() } pub fn deliver( &self, request: RadrootsTransportDeliveryRequest, - ) -> Result<RadrootsTransportDeliveryReceipt, RadrootsReticulumPreviewError> { + ) -> Result<RadrootsTransportDeliveryReceipt, RadrootsReticulumError> { ensure_reticulum_targets(request.target_set.targets())?; - let outcome = preview_outcome(self.profile.behavior); + let outcome = reticulum_outcome(self.profile.behavior); let target_receipts = request .target_set .targets() @@ -256,30 +255,30 @@ impl RadrootsReticulumPreviewTransport { pub fn fetch( &self, - request: RadrootsReticulumPreviewFetchRequest, - ) -> Result<RadrootsReticulumPreviewFetchReceipt, RadrootsReticulumPreviewError> { + request: RadrootsReticulumFetchRequest, + ) -> Result<RadrootsReticulumFetchReceipt, RadrootsReticulumError> { if request.max_events == 0 { - return Err(RadrootsReticulumPreviewError::InvalidFetchLimit); + return Err(RadrootsReticulumError::InvalidFetchLimit); } - Ok(RadrootsReticulumPreviewFetchReceipt { + Ok(RadrootsReticulumFetchReceipt { request_id: request.request_id, endpoint_uri: self.profile.endpoint.as_str().to_owned(), scope: self.profile.scope.clone(), agent_endpoint: self.profile.agent_endpoint.clone(), - outcome: preview_outcome(self.profile.behavior), + outcome: reticulum_outcome(self.profile.behavior), observed_event_count: 0, - implementation: RadrootsTransportImplementationState::PreviewUnavailable, + implementation: RadrootsTransportImplementationState::Real, }) } } -impl Default for RadrootsReticulumPreviewTransport { +impl Default for RadrootsReticulumTransport { fn default() -> Self { - Self::new(RadrootsReticulumPreviewProfile::default()) + Self::new(RadrootsReticulumProfile::default()) } } -impl RadrootsTransport for RadrootsReticulumPreviewTransport { +impl RadrootsTransport for RadrootsReticulumTransport { fn transport_kind(&self) -> RadrootsTransportKind { RadrootsTransportKind::Reticulum } @@ -294,7 +293,7 @@ impl RadrootsTransport for RadrootsReticulumPreviewTransport { ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { Box::pin(async move { self.deliver(request) - .map_err(reticulum_preview_error_to_transport_error) + .map_err(reticulum_error_to_transport_error) }) } @@ -304,8 +303,8 @@ impl RadrootsTransport for RadrootsReticulumPreviewTransport { ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { Box::pin(async move { ensure_reticulum_targets(request.target_set.targets()) - .map_err(reticulum_preview_error_to_transport_error)?; - let outcome = preview_outcome(self.profile.behavior); + .map_err(reticulum_error_to_transport_error)?; + let outcome = reticulum_outcome(self.profile.behavior); let target_receipts = request .target_set .targets() @@ -324,18 +323,18 @@ impl RadrootsTransport for RadrootsReticulumPreviewTransport { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewFetchRequest { +pub struct RadrootsReticulumFetchRequest { pub request_id: String, pub max_events: u16, } -impl RadrootsReticulumPreviewFetchRequest { +impl RadrootsReticulumFetchRequest { pub fn new( request_id: impl Into<String>, max_events: u16, - ) -> Result<Self, RadrootsReticulumPreviewError> { + ) -> Result<Self, RadrootsReticulumError> { if max_events == 0 { - return Err(RadrootsReticulumPreviewError::InvalidFetchLimit); + return Err(RadrootsReticulumError::InvalidFetchLimit); } Ok(Self { request_id: request_id.into(), @@ -346,18 +345,18 @@ impl RadrootsReticulumPreviewFetchRequest { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsReticulumPreviewFetchReceipt { +pub struct RadrootsReticulumFetchReceipt { pub request_id: String, pub endpoint_uri: String, pub scope: RadrootsTransportMeshScopeId, - pub agent_endpoint: Option<RadrootsReticulumPreviewAgentEndpoint>, + pub agent_endpoint: Option<RadrootsReticulumAgentEndpoint>, pub outcome: RadrootsTransportOutcome, pub observed_event_count: usize, pub implementation: RadrootsTransportImplementationState, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RadrootsReticulumPreviewError { +pub enum RadrootsReticulumError { InvalidEndpoint, InvalidAgentEndpoint, InvalidProfileId, @@ -365,76 +364,69 @@ pub enum RadrootsReticulumPreviewError { NonReticulumTarget, } -impl fmt::Display for RadrootsReticulumPreviewError { +impl fmt::Display for RadrootsReticulumError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { - Self::InvalidEndpoint => "invalid Reticulum preview endpoint", - Self::InvalidAgentEndpoint => "invalid Reticulum preview agent endpoint", - Self::InvalidProfileId => "invalid Reticulum preview profile id", - Self::InvalidFetchLimit => "Reticulum preview fetch limit must be greater than zero", - Self::NonReticulumTarget => { - "Reticulum preview transport received a non-Reticulum target" - } + Self::InvalidEndpoint => "invalid Reticulum endpoint", + Self::InvalidAgentEndpoint => "invalid Reticulum agent endpoint", + Self::InvalidProfileId => "invalid Reticulum profile id", + Self::InvalidFetchLimit => "Reticulum fetch limit must be greater than zero", + Self::NonReticulumTarget => "Reticulum transport received a non-Reticulum target", }) } } -fn reticulum_preview_error_to_transport_error( - error: RadrootsReticulumPreviewError, -) -> RadrootsTransportError { +fn reticulum_error_to_transport_error(error: RadrootsReticulumError) -> RadrootsTransportError { match error { - RadrootsReticulumPreviewError::InvalidEndpoint - | RadrootsReticulumPreviewError::NonReticulumTarget => { + RadrootsReticulumError::InvalidEndpoint | RadrootsReticulumError::NonReticulumTarget => { RadrootsTransportError::InvalidTargetUri } - RadrootsReticulumPreviewError::InvalidAgentEndpoint - | RadrootsReticulumPreviewError::InvalidProfileId - | RadrootsReticulumPreviewError::InvalidFetchLimit => { - RadrootsTransportError::InvalidTransportKind - } + RadrootsReticulumError::InvalidAgentEndpoint + | RadrootsReticulumError::InvalidProfileId + | RadrootsReticulumError::InvalidFetchLimit => RadrootsTransportError::InvalidTransportKind, } } fn ensure_reticulum_targets( targets: &[RadrootsTransportTarget], -) -> Result<(), RadrootsReticulumPreviewError> { +) -> Result<(), RadrootsReticulumError> { for target in targets { if target.kind != RadrootsTransportKind::Reticulum { - return Err(RadrootsReticulumPreviewError::NonReticulumTarget); + return Err(RadrootsReticulumError::NonReticulumTarget); } - if target.uri.as_str() != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI { - return Err(RadrootsReticulumPreviewError::InvalidEndpoint); + if target.uri.as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { + return Err(RadrootsReticulumError::InvalidEndpoint); } if target.scope.is_none() { - return Err(RadrootsReticulumPreviewError::InvalidEndpoint); + return Err(RadrootsReticulumError::InvalidEndpoint); } } Ok(()) } -fn preview_outcome(behavior: RadrootsReticulumPreviewBehavior) -> RadrootsTransportOutcome { +fn reticulum_outcome(behavior: RadrootsReticulumBehavior) -> RadrootsTransportOutcome { let mut outcome = match behavior { - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts => { + RadrootsReticulumBehavior::RejectDeliveryAttempts => { RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::TransportUnavailable) - .with_target_status(RadrootsTransportDeliveryTargetStatus::PreviewUnavailable) + .with_target_status(RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented) } - RadrootsReticulumPreviewBehavior::DeferDeliveryPlans => { + RadrootsReticulumBehavior::DeferDeliveryPlans => { RadrootsTransportOutcome::new(RadrootsTransportOutcomeKind::DeferredUntilImplemented) } }; outcome.code = Some( match behavior { - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts => UNAVAILABLE_CODE, - RadrootsReticulumPreviewBehavior::DeferDeliveryPlans => DEFERRED_CODE, + RadrootsReticulumBehavior::RejectDeliveryAttempts => UNAVAILABLE_CODE, + RadrootsReticulumBehavior::DeferDeliveryPlans => DEFERRED_CODE, } .to_owned(), ); outcome.message = Some( match behavior { - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts => { + RadrootsReticulumBehavior::RejectDeliveryAttempts => { RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE } - RadrootsReticulumPreviewBehavior::DeferDeliveryPlans => DEFERRED_MESSAGE, + RadrootsReticulumBehavior::DeferDeliveryPlans => DEFERRED_MESSAGE, } .to_owned(), ); diff --git a/crates/transport_reticulum/tests/reticulum.rs b/crates/transport_reticulum/tests/reticulum.rs @@ -0,0 +1,588 @@ +use radroots_transport::{ + RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransport, + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, + RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, + RadrootsTransportFetchRequest, RadrootsTransportImplementationState, RadrootsTransportKind, + RadrootsTransportMeshScopeId, RadrootsTransportPayload, RadrootsTransportSatisfactionClass, + RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, +}; +use radroots_transport_reticulum::{ + RadrootsReticulumAgentEndpoint, RadrootsReticulumBehavior, RadrootsReticulumEndpoint, + RadrootsReticulumError, RadrootsReticulumFetchRequest, RadrootsReticulumProfile, + RadrootsReticulumTransport, +}; + +fn reticulum_target(uri: &str) -> RadrootsTransportTarget { + assert_eq!(uri, RADROOTS_RETICULUM_ENDPOINT_URI); + RadrootsTransportTarget::reticulum().expect("reticulum target") +} + +fn scoped_reticulum_target(scope: &str) -> RadrootsTransportTarget { + RadrootsTransportTarget::reticulum_with_metadata( + RADROOTS_RETICULUM_ENDPOINT_URI, + Some(RadrootsTransportMeshScopeId::parse(scope).expect("scope")), + None, + ) + .expect("scoped reticulum target") +} + +fn nostr_target() -> RadrootsTransportTarget { + RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("nostr target") +} + +fn delivery_request(targets: Vec<RadrootsTransportTarget>) -> RadrootsTransportDeliveryRequest { + RadrootsTransportDeliveryRequest::new( + "reticulum-delivery", + reticulum_payload(), + RadrootsTransportTargetSet::new(targets).expect("target set"), + RadrootsTransportSatisfactionPolicy::any_accepted(), + ) +} + +fn reticulum_payload() -> RadrootsTransportPayload { + RadrootsTransportPayload::mesh_frame_cbor("reticulum-message", [1_u8, 2, 3]).expect("payload") +} + +#[test] +fn default_profile_is_configured_deferred_until_implemented_and_rejecting() { + let profile = RadrootsReticulumProfile::default(); + let status = profile.status(); + + assert_eq!(profile.profile_id(), "transport.reticulum.default"); + assert_eq!(profile.endpoint().as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!(profile.scope().as_str(), RADROOTS_RETICULUM_SCOPE_ID); + assert_eq!(profile.agent_endpoint(), None); + assert_eq!( + profile.behavior(), + RadrootsReticulumBehavior::RejectDeliveryAttempts + ); + assert_eq!( + status.transport_status.implementation, + RadrootsTransportImplementationState::Real + ); + assert_eq!( + status.transport_status.maturity, + RadrootsTransportCapabilityMaturity::Preview + ); + assert_eq!( + status.transport_status.availability, + RadrootsTransportCapabilityAvailability::Unavailable + ); + assert!(status.transport_status.configured); + assert_eq!( + status.transport_status.profile_id.as_deref(), + Some("transport.reticulum.default") + ); + assert_eq!( + status.transport_status.endpoint_uri.as_deref(), + Some(RADROOTS_RETICULUM_ENDPOINT_URI) + ); + assert_eq!( + status.transport_status.message, + RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE + ); + assert!(!status.transport_status.usable_for_delivery); + assert_eq!(status.scope.as_str(), RADROOTS_RETICULUM_SCOPE_ID); + assert_eq!(status.agent_endpoint, None); +} + +#[test] +fn endpoint_and_profile_validation_are_strict_and_canonical() { + let endpoint = + RadrootsReticulumEndpoint::parse(RADROOTS_RETICULUM_ENDPOINT_URI).expect("endpoint"); + assert_eq!(endpoint.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!(endpoint.to_string(), RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!( + endpoint.clone().into_string(), + RADROOTS_RETICULUM_ENDPOINT_URI + ); + assert_eq!( + RadrootsReticulumEndpoint::default().as_str(), + RADROOTS_RETICULUM_ENDPOINT_URI + ); + + assert_eq!( + RadrootsReticulumEndpoint::parse(" ").expect_err("empty endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("reticulum:").expect_err("empty endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("https://target").expect_err("wrong scheme"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("RETICULUM:unavailable").expect_err("case drift endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse(" reticulum:unavailable") + .expect_err("leading whitespace endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("reticulum:unavailable ") + .expect_err("trailing whitespace endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("reticulum:custom").expect_err("custom endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("reticulum:bad target").expect_err("whitespace endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + assert_eq!( + RadrootsReticulumEndpoint::parse("reticulum:bad\ntarget").expect_err("control endpoint"), + RadrootsReticulumError::InvalidEndpoint + ); + let agent_endpoint = RadrootsReticulumAgentEndpoint::parse("reticulum-agent://localhost:19999") + .expect("agent endpoint"); + assert_eq!(agent_endpoint.as_str(), "reticulum-agent://localhost:19999"); + assert_eq!( + agent_endpoint.to_string(), + "reticulum-agent://localhost:19999" + ); + assert_eq!( + agent_endpoint.clone().into_string(), + "reticulum-agent://localhost:19999" + ); + let local_agent_endpoint = + RadrootsReticulumAgentEndpoint::parse("reticulum-agent:local-controller") + .expect("local agent endpoint"); + assert_eq!( + local_agent_endpoint.as_str(), + "reticulum-agent:local-controller" + ); + for invalid_agent in [ + "", + " reticulum-agent://localhost", + "reticulum-agent:", + "reticulum agent", + "agent", + "https://localhost:19999", + "ws://localhost:19999", + "reticulum://localhost:19999", + "RETICULUM-AGENT://localhost:19999", + ] { + assert_eq!( + RadrootsReticulumAgentEndpoint::parse(invalid_agent) + .expect_err("invalid agent endpoint"), + RadrootsReticulumError::InvalidAgentEndpoint + ); + } + assert_eq!( + RadrootsReticulumProfile::new( + "transport reticulum", + endpoint, + RadrootsTransportMeshScopeId::local_reticulum(), + None, + RadrootsReticulumBehavior::RejectDeliveryAttempts, + ) + .expect_err("profile id whitespace"), + RadrootsReticulumError::InvalidProfileId + ); + assert_eq!( + RadrootsReticulumProfile::new( + "", + RadrootsReticulumEndpoint::default(), + RadrootsTransportMeshScopeId::local_reticulum(), + None, + RadrootsReticulumBehavior::RejectDeliveryAttempts, + ) + .expect_err("empty profile id"), + RadrootsReticulumError::InvalidProfileId + ); + let profile = RadrootsReticulumProfile::new( + "transport.reticulum.custom", + RadrootsReticulumEndpoint::default(), + RadrootsTransportMeshScopeId::local_reticulum(), + Some(agent_endpoint), + RadrootsReticulumBehavior::DeferDeliveryPlans, + ) + .expect("custom behavior profile"); + assert_eq!(profile.profile_id(), "transport.reticulum.custom"); + assert_eq!(profile.endpoint().as_str(), RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!( + profile.behavior(), + RadrootsReticulumBehavior::DeferDeliveryPlans + ); + assert_eq!( + profile.agent_endpoint().map(|endpoint| endpoint.as_str()), + Some("reticulum-agent://localhost:19999") + ); +} + +#[test] +fn direct_reticulum_delivery_accepts_any_typed_scope_as_inert_metadata() { + let transport = RadrootsReticulumTransport::default(); + let request = delivery_request(vec![scoped_reticulum_target("farm-north.mesh_1")]); + let receipt = transport.deliver(request).expect("delivery receipt"); + + assert_eq!(receipt.target_receipts.len(), 1); + assert_eq!( + receipt.target_receipts[0] + .target + .scope + .as_ref() + .map(|scope| scope.as_str()), + Some("farm-north.mesh_1") + ); + assert_eq!( + receipt.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); + + let deferred_transport = RadrootsReticulumTransport::new( + RadrootsReticulumProfile::default() + .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans), + ); + let deferred = deferred_transport + .deliver(delivery_request(vec![scoped_reticulum_target( + "farm-south.mesh_2", + )])) + .expect("deferred delivery receipt"); + assert_eq!( + deferred.target_receipts[0] + .target + .scope + .as_ref() + .map(|scope| scope.as_str()), + Some("farm-south.mesh_2") + ); + assert_eq!( + deferred.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + deferred.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); +} + +#[test] +fn core_transport_trait_reports_reticulum_status_delivery_and_fetch() { + let transport = RadrootsReticulumTransport::default(); + let target_set = + RadrootsTransportTargetSet::new(vec![reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI)]) + .expect("target set"); + let status = futures::executor::block_on(RadrootsTransport::status(&transport)) + .expect("transport status"); + assert_eq!(status.kind, RadrootsTransportKind::Reticulum); + assert_eq!( + status.implementation, + RadrootsTransportImplementationState::Real + ); + assert_eq!( + status.maturity, + RadrootsTransportCapabilityMaturity::Preview + ); + assert_eq!( + status.availability, + RadrootsTransportCapabilityAvailability::Unavailable + ); + assert!(!status.usable_for_delivery); + assert!(!status.capabilities.deliver); + assert!(!status.capabilities.fetch); + + let delivery = futures::executor::block_on(RadrootsTransport::deliver( + &transport, + RadrootsTransportDeliveryRequest::new( + "core-delivery", + reticulum_payload(), + target_set.clone(), + RadrootsTransportSatisfactionPolicy::any_accepted(), + ), + )) + .expect("delivery receipt"); + assert_eq!( + delivery.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + + let fetch = futures::executor::block_on(RadrootsTransport::fetch( + &transport, + RadrootsTransportFetchRequest::new("core-fetch", target_set), + )) + .expect("fetch receipt"); + assert_eq!(fetch.fetched_count, 0); + assert_eq!( + fetch.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); +} + +#[test] +fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing() { + let transport = RadrootsReticulumTransport::default(); + let request = delivery_request(vec![reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI)]); + let receipt = transport.deliver(request).expect("delivery receipt"); + + assert_eq!(receipt.target_receipts.len(), 1); + assert_eq!( + receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); + for target_receipt in receipt.target_receipts { + assert_eq!(target_receipt.target.kind, RadrootsTransportKind::Reticulum); + assert_eq!( + target_receipt.status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + target_receipt.outcome.code.as_deref(), + Some("transport_unavailable") + ); + assert_eq!( + target_receipt.outcome.message.as_deref(), + Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) + ); + } +} + +#[test] +fn noncanonical_reticulum_targets_are_rejected() { + for invalid in [ + " reticulum:unavailable", + "reticulum:unavailable ", + "RETICULUM:unavailable", + "reticulum:Unavailable", + "reticulum:temporary", + "reticulum:unavailable-alt", + "reticulum:custom", + ] { + assert_eq!( + RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, invalid) + .expect_err("noncanonical Reticulum target"), + radroots_transport::RadrootsTransportError::InvalidTargetUri + ); + } +} + +#[test] +fn deferred_delivery_plan_mode_never_counts_as_satisfied() { + let transport = RadrootsReticulumTransport::new( + RadrootsReticulumProfile::default() + .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans), + ); + let request = delivery_request(vec![reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI)]); + let receipt = transport.deliver(request).expect("delivery receipt"); + + assert_eq!(receipt.target_receipts.len(), 1); + assert_eq!( + receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), + 0 + ); + assert_eq!( + receipt.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + receipt.target_receipts[0].outcome.code.as_deref(), + Some("deferred_until_implemented") + ); + assert!( + !RadrootsTransportSatisfactionPolicy::any_accepted() + .is_satisfied_by( + 1, + receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted) + ) + .expect("satisfaction check") + ); +} + +#[test] +fn non_reticulum_targets_are_rejected_without_nostr_routing() { + let transport = RadrootsReticulumTransport::default(); + let err = transport + .deliver(delivery_request(vec![nostr_target()])) + .expect_err("non-reticulum target"); + + assert_eq!(err, RadrootsReticulumError::NonReticulumTarget); +} + +#[test] +fn malformed_reticulum_target_without_typed_scope_is_rejected() { + let transport = RadrootsReticulumTransport::default(); + let mut target = reticulum_target(RADROOTS_RETICULUM_ENDPOINT_URI); + target.scope = None; + let err = transport + .deliver(delivery_request(vec![target])) + .expect_err("missing typed scope"); + + assert_eq!(err, RadrootsReticulumError::InvalidEndpoint); +} + +#[test] +fn fetch_reports_deferred_until_implemented_without_observed_events() { + let transport = RadrootsReticulumTransport::default(); + assert_eq!( + transport.profile().profile_id(), + "transport.reticulum.default" + ); + assert_eq!( + transport.status().transport_status.implementation, + RadrootsTransportImplementationState::Real + ); + let receipt = transport + .fetch(RadrootsReticulumFetchRequest::new("fetch-1", 10).expect("fetch request")) + .expect("fetch receipt"); + + assert_eq!(receipt.request_id, "fetch-1"); + assert_eq!(receipt.endpoint_uri, RADROOTS_RETICULUM_ENDPOINT_URI); + assert_eq!(receipt.observed_event_count, 0); + assert_eq!( + receipt.implementation, + RadrootsTransportImplementationState::Real + ); + assert_eq!(receipt.scope.as_str(), RADROOTS_RETICULUM_SCOPE_ID); + assert_eq!(receipt.agent_endpoint, None); + assert_eq!( + receipt.outcome.status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + assert_eq!( + RadrootsReticulumFetchRequest::new("fetch-0", 0).expect_err("zero limit"), + RadrootsReticulumError::InvalidFetchLimit + ); + assert_eq!( + transport + .fetch(RadrootsReticulumFetchRequest { + request_id: "fetch-public-zero".to_owned(), + max_events: 0, + }) + .expect_err("zero limit at transport boundary"), + RadrootsReticulumError::InvalidFetchLimit + ); + let deferred_transport = RadrootsReticulumTransport::new( + RadrootsReticulumProfile::default() + .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans), + ); + let deferred = deferred_transport + .fetch(RadrootsReticulumFetchRequest::new("fetch-deferred", 1).expect("fetch")) + .expect("fetch receipt"); + assert_eq!( + deferred.outcome.status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); +} + +#[test] +fn configured_agent_endpoint_is_metadata_only_for_status_delivery_and_fetch() { + let agent_endpoint = RadrootsReticulumAgentEndpoint::parse("reticulum-agent://localhost:19999") + .expect("agent endpoint"); + let transport = RadrootsReticulumTransport::new( + RadrootsReticulumProfile::default().with_agent_endpoint(agent_endpoint), + ); + let status = transport.status(); + assert_eq!( + status + .agent_endpoint + .as_ref() + .map(|endpoint| endpoint.as_str()), + Some("reticulum-agent://localhost:19999") + ); + assert_eq!( + status.transport_status.implementation, + RadrootsTransportImplementationState::Real + ); + assert!(!status.transport_status.usable_for_delivery); + assert!(!status.transport_status.capabilities.deliver); + assert!(!status.transport_status.capabilities.fetch); + + let receipt = transport + .deliver(delivery_request(vec![reticulum_target( + RADROOTS_RETICULUM_ENDPOINT_URI, + )])) + .expect("delivery receipt"); + assert_eq!( + receipt.target_receipts[0].status, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ); + let fetch = transport + .fetch(RadrootsReticulumFetchRequest::new("fetch-agent", 1).expect("fetch")) + .expect("fetch receipt"); + assert_eq!( + fetch + .agent_endpoint + .as_ref() + .map(|endpoint| endpoint.as_str()), + Some("reticulum-agent://localhost:19999") + ); + assert_eq!(fetch.observed_event_count, 0); + assert_eq!( + fetch.implementation, + RadrootsTransportImplementationState::Real + ); +} + +#[test] +fn public_models_round_trip_through_serde() { + let profile = RadrootsReticulumProfile::default() + .with_behavior(RadrootsReticulumBehavior::DeferDeliveryPlans); + let json = serde_json::to_string(&profile).expect("profile json"); + let decoded: RadrootsReticulumProfile = serde_json::from_str(&json).expect("profile decode"); + + assert_eq!(decoded, profile); +} + +#[test] +fn reticulum_source_remains_inert_without_runtime_delivery_hooks() { + let source = include_str!("../src/lib.rs").to_ascii_lowercase(); + for forbidden in [ + "socket", + "rnsd", + "python", + "identity", + "send_mesh", + "fallback", + "nostr", + ] { + assert!( + !source.contains(forbidden), + "Reticulum source contains forbidden runtime hook {forbidden}" + ); + } +} + +#[test] +fn reticulum_errors_and_defaults_are_stable() { + assert_eq!( + RadrootsReticulumBehavior::default(), + RadrootsReticulumBehavior::RejectDeliveryAttempts + ); + let cases = [ + ( + RadrootsReticulumError::InvalidEndpoint, + "invalid Reticulum endpoint", + ), + ( + RadrootsReticulumError::InvalidAgentEndpoint, + "invalid Reticulum agent endpoint", + ), + ( + RadrootsReticulumError::InvalidProfileId, + "invalid Reticulum profile id", + ), + ( + RadrootsReticulumError::InvalidFetchLimit, + "Reticulum fetch limit must be greater than zero", + ), + ( + RadrootsReticulumError::NonReticulumTarget, + "Reticulum transport received a non-Reticulum target", + ), + ]; + + for (error, message) in cases { + assert_eq!(error.to_string(), message); + } +} diff --git a/crates/transport_reticulum/tests/reticulum_preview.rs b/crates/transport_reticulum/tests/reticulum_preview.rs @@ -1,597 +0,0 @@ -use radroots_transport::{ - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, RADROOTS_RETICULUM_PREVIEW_SCOPE_ID, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransport, RadrootsTransportDeliveryRequest, - RadrootsTransportDeliveryTargetStatus, RadrootsTransportFetchRequest, - RadrootsTransportImplementationState, RadrootsTransportKind, RadrootsTransportMeshScopeId, - RadrootsTransportPayload, RadrootsTransportSatisfactionClass, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, -}; -use radroots_transport_reticulum::{ - RadrootsReticulumPreviewAgentEndpoint, RadrootsReticulumPreviewBehavior, - RadrootsReticulumPreviewEndpoint, RadrootsReticulumPreviewError, - RadrootsReticulumPreviewFetchRequest, RadrootsReticulumPreviewProfile, - RadrootsReticulumPreviewTransport, -}; - -fn reticulum_target(uri: &str) -> RadrootsTransportTarget { - assert_eq!(uri, RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); - RadrootsTransportTarget::reticulum_preview().expect("reticulum target") -} - -fn scoped_reticulum_target(scope: &str) -> RadrootsTransportTarget { - RadrootsTransportTarget::reticulum_preview_with_metadata( - Some(RadrootsTransportMeshScopeId::parse(scope).expect("scope")), - None, - ) - .expect("scoped reticulum target") -} - -fn nostr_target() -> RadrootsTransportTarget { - RadrootsTransportTarget::nostr_relay("wss://relay.example").expect("nostr target") -} - -fn delivery_request(targets: Vec<RadrootsTransportTarget>) -> RadrootsTransportDeliveryRequest { - RadrootsTransportDeliveryRequest::new( - "reticulum-preview-delivery", - preview_payload(), - RadrootsTransportTargetSet::new(targets).expect("target set"), - RadrootsTransportSatisfactionPolicy::any_accepted(), - ) -} - -fn preview_payload() -> RadrootsTransportPayload { - RadrootsTransportPayload::mesh_frame_cbor("preview-message", [1_u8, 2, 3]).expect("payload") -} - -#[test] -fn default_profile_is_configured_preview_unavailable_and_rejecting() { - let profile = RadrootsReticulumPreviewProfile::default(); - let status = profile.status(); - - assert_eq!(profile.profile_id(), "transport.reticulum.preview"); - assert_eq!( - profile.endpoint().as_str(), - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - assert_eq!( - profile.scope().as_str(), - RADROOTS_RETICULUM_PREVIEW_SCOPE_ID - ); - assert_eq!(profile.agent_endpoint(), None); - assert_eq!( - profile.behavior(), - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts - ); - assert_eq!( - status.transport_status.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); - assert!(status.transport_status.configured); - assert_eq!( - status.transport_status.profile_id.as_deref(), - Some("transport.reticulum.preview") - ); - assert_eq!( - status.transport_status.endpoint_uri.as_deref(), - Some(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI) - ); - assert_eq!( - status.transport_status.message, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE - ); - assert!(!status.transport_status.usable_for_delivery); - assert_eq!(status.scope.as_str(), RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); - assert_eq!(status.agent_endpoint, None); -} - -#[test] -fn endpoint_and_profile_validation_are_strict_and_canonical() { - let endpoint = RadrootsReticulumPreviewEndpoint::parse(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI) - .expect("endpoint"); - assert_eq!(endpoint.as_str(), RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); - assert_eq!( - endpoint.to_string(), - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - assert_eq!( - endpoint.clone().into_string(), - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::default().as_str(), - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse(" ").expect_err("empty endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("reticulum:").expect_err("empty endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("https://target").expect_err("wrong scheme"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("RETICULUM:Preview-Unavailable") - .expect_err("case drift endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse(" reticulum:preview-unavailable") - .expect_err("leading whitespace endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("reticulum:preview-unavailable ") - .expect_err("trailing whitespace endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("reticulum:custom").expect_err("custom endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("reticulum:bad target") - .expect_err("whitespace endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - assert_eq!( - RadrootsReticulumPreviewEndpoint::parse("reticulum:bad\ntarget") - .expect_err("control endpoint"), - RadrootsReticulumPreviewError::InvalidEndpoint - ); - let agent_endpoint = - RadrootsReticulumPreviewAgentEndpoint::parse("reticulum-agent://localhost:19999") - .expect("agent endpoint"); - assert_eq!(agent_endpoint.as_str(), "reticulum-agent://localhost:19999"); - assert_eq!( - agent_endpoint.to_string(), - "reticulum-agent://localhost:19999" - ); - assert_eq!( - agent_endpoint.clone().into_string(), - "reticulum-agent://localhost:19999" - ); - let local_agent_endpoint = - RadrootsReticulumPreviewAgentEndpoint::parse("reticulum-agent:local-controller") - .expect("local agent endpoint"); - assert_eq!( - local_agent_endpoint.as_str(), - "reticulum-agent:local-controller" - ); - for invalid_agent in [ - "", - " reticulum-agent://localhost", - "reticulum-agent:", - "reticulum agent", - "agent", - "https://localhost:19999", - "ws://localhost:19999", - "reticulum://localhost:19999", - "RETICULUM-AGENT://localhost:19999", - ] { - assert_eq!( - RadrootsReticulumPreviewAgentEndpoint::parse(invalid_agent) - .expect_err("invalid agent endpoint"), - RadrootsReticulumPreviewError::InvalidAgentEndpoint - ); - } - assert_eq!( - RadrootsReticulumPreviewProfile::new( - "transport reticulum", - endpoint, - RadrootsTransportMeshScopeId::local_preview(), - None, - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts, - ) - .expect_err("profile id whitespace"), - RadrootsReticulumPreviewError::InvalidProfileId - ); - assert_eq!( - RadrootsReticulumPreviewProfile::new( - "", - RadrootsReticulumPreviewEndpoint::default(), - RadrootsTransportMeshScopeId::local_preview(), - None, - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts, - ) - .expect_err("empty profile id"), - RadrootsReticulumPreviewError::InvalidProfileId - ); - let profile = RadrootsReticulumPreviewProfile::new( - "transport.reticulum.custom", - RadrootsReticulumPreviewEndpoint::default(), - RadrootsTransportMeshScopeId::local_preview(), - Some(agent_endpoint), - RadrootsReticulumPreviewBehavior::DeferDeliveryPlans, - ) - .expect("custom behavior profile"); - assert_eq!(profile.profile_id(), "transport.reticulum.custom"); - assert_eq!( - profile.endpoint().as_str(), - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - assert_eq!( - profile.behavior(), - RadrootsReticulumPreviewBehavior::DeferDeliveryPlans - ); - assert_eq!( - profile.agent_endpoint().map(|endpoint| endpoint.as_str()), - Some("reticulum-agent://localhost:19999") - ); -} - -#[test] -fn direct_preview_delivery_accepts_any_typed_reticulum_scope_as_inert_metadata() { - let transport = RadrootsReticulumPreviewTransport::default(); - let request = delivery_request(vec![scoped_reticulum_target("farm-north.preview_1")]); - let receipt = transport.deliver(request).expect("delivery receipt"); - - assert_eq!(receipt.target_receipts.len(), 1); - assert_eq!( - receipt.target_receipts[0] - .target - .scope - .as_ref() - .map(|scope| scope.as_str()), - Some("farm-north.preview_1") - ); - assert_eq!( - receipt.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); - assert_eq!( - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 - ); - - let deferred_transport = RadrootsReticulumPreviewTransport::new( - RadrootsReticulumPreviewProfile::default() - .with_behavior(RadrootsReticulumPreviewBehavior::DeferDeliveryPlans), - ); - let deferred = deferred_transport - .deliver(delivery_request(vec![scoped_reticulum_target( - "farm-south.preview_2", - )])) - .expect("deferred delivery receipt"); - assert_eq!( - deferred.target_receipts[0] - .target - .scope - .as_ref() - .map(|scope| scope.as_str()), - Some("farm-south.preview_2") - ); - assert_eq!( - deferred.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented - ); - assert_eq!( - deferred.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 - ); -} - -#[test] -fn core_transport_trait_reports_preview_status_delivery_and_fetch() { - let transport = RadrootsReticulumPreviewTransport::default(); - let target_set = RadrootsTransportTargetSet::new(vec![reticulum_target( - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - )]) - .expect("target set"); - let status = futures::executor::block_on(RadrootsTransport::status(&transport)) - .expect("transport status"); - assert_eq!(status.kind, RadrootsTransportKind::Reticulum); - assert_eq!( - status.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); - assert!(!status.usable_for_delivery); - assert!(!status.capabilities.deliver); - assert!(!status.capabilities.fetch); - - let delivery = futures::executor::block_on(RadrootsTransport::deliver( - &transport, - RadrootsTransportDeliveryRequest::new( - "core-delivery", - preview_payload(), - target_set.clone(), - RadrootsTransportSatisfactionPolicy::any_accepted(), - ), - )) - .expect("delivery receipt"); - assert_eq!( - delivery.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); - - let fetch = futures::executor::block_on(RadrootsTransport::fetch( - &transport, - RadrootsTransportFetchRequest::new("core-fetch", target_set), - )) - .expect("fetch receipt"); - assert_eq!(fetch.fetched_count, 0); - assert_eq!( - fetch.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); -} - -#[test] -fn reject_delivery_attempts_returns_unavailable_without_success_or_nostr_routing() { - let transport = RadrootsReticulumPreviewTransport::default(); - let request = delivery_request(vec![reticulum_target( - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - )]); - let receipt = transport.deliver(request).expect("delivery receipt"); - - assert_eq!(receipt.target_receipts.len(), 1); - assert_eq!( - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 - ); - for target_receipt in receipt.target_receipts { - assert_eq!(target_receipt.target.kind, RadrootsTransportKind::Reticulum); - assert_eq!( - target_receipt.status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); - assert_eq!( - target_receipt.outcome.code.as_deref(), - Some("transport_unavailable") - ); - assert_eq!( - target_receipt.outcome.message.as_deref(), - Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE) - ); - } -} - -#[test] -fn noncanonical_reticulum_preview_targets_are_rejected() { - for invalid in [ - " reticulum:preview-unavailable", - "reticulum:preview-unavailable ", - "RETICULUM:preview-unavailable", - "reticulum:Preview-Unavailable", - "reticulum:preview", - "reticulum:preview-unavailable-alt", - "reticulum:custom", - ] { - assert_eq!( - RadrootsTransportTarget::new(RadrootsTransportKind::Reticulum, invalid) - .expect_err("noncanonical Reticulum target"), - radroots_transport::RadrootsTransportError::InvalidTargetUri - ); - } -} - -#[test] -fn deferred_delivery_plan_mode_never_counts_as_satisfied() { - let transport = RadrootsReticulumPreviewTransport::new( - RadrootsReticulumPreviewProfile::default() - .with_behavior(RadrootsReticulumPreviewBehavior::DeferDeliveryPlans), - ); - let request = delivery_request(vec![reticulum_target( - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - )]); - let receipt = transport.deliver(request).expect("delivery receipt"); - - assert_eq!(receipt.target_receipts.len(), 1); - assert_eq!( - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted), - 0 - ); - assert_eq!( - receipt.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented - ); - assert_eq!( - receipt.target_receipts[0].outcome.code.as_deref(), - Some("deferred_until_implemented") - ); - assert!( - !RadrootsTransportSatisfactionPolicy::any_accepted() - .is_satisfied_by( - 1, - receipt.satisfied_target_count(RadrootsTransportSatisfactionClass::Accepted) - ) - .expect("satisfaction check") - ); -} - -#[test] -fn non_reticulum_targets_are_rejected_without_nostr_routing() { - let transport = RadrootsReticulumPreviewTransport::default(); - let err = transport - .deliver(delivery_request(vec![nostr_target()])) - .expect_err("non-reticulum target"); - - assert_eq!(err, RadrootsReticulumPreviewError::NonReticulumTarget); -} - -#[test] -fn malformed_reticulum_target_without_typed_scope_is_rejected() { - let transport = RadrootsReticulumPreviewTransport::default(); - let mut target = reticulum_target(RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI); - target.scope = None; - let err = transport - .deliver(delivery_request(vec![target])) - .expect_err("missing typed scope"); - - assert_eq!(err, RadrootsReticulumPreviewError::InvalidEndpoint); -} - -#[test] -fn fetch_reports_preview_unavailable_without_observed_events() { - let transport = RadrootsReticulumPreviewTransport::default(); - assert_eq!( - transport.profile().profile_id(), - "transport.reticulum.preview" - ); - assert_eq!( - transport.status().transport_status.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); - let receipt = transport - .fetch(RadrootsReticulumPreviewFetchRequest::new("fetch-1", 10).expect("fetch request")) - .expect("fetch receipt"); - - assert_eq!(receipt.request_id, "fetch-1"); - assert_eq!( - receipt.endpoint_uri, - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI - ); - assert_eq!(receipt.observed_event_count, 0); - assert_eq!( - receipt.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); - assert_eq!(receipt.scope.as_str(), RADROOTS_RETICULUM_PREVIEW_SCOPE_ID); - assert_eq!(receipt.agent_endpoint, None); - assert_eq!( - receipt.outcome.status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); - assert_eq!( - RadrootsReticulumPreviewFetchRequest::new("fetch-0", 0).expect_err("zero limit"), - RadrootsReticulumPreviewError::InvalidFetchLimit - ); - assert_eq!( - transport - .fetch(RadrootsReticulumPreviewFetchRequest { - request_id: "fetch-public-zero".to_owned(), - max_events: 0, - }) - .expect_err("zero limit at transport boundary"), - RadrootsReticulumPreviewError::InvalidFetchLimit - ); - let deferred_transport = RadrootsReticulumPreviewTransport::new( - RadrootsReticulumPreviewProfile::default() - .with_behavior(RadrootsReticulumPreviewBehavior::DeferDeliveryPlans), - ); - let deferred = deferred_transport - .fetch(RadrootsReticulumPreviewFetchRequest::new("fetch-deferred", 1).expect("fetch")) - .expect("fetch receipt"); - assert_eq!( - deferred.outcome.status, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented - ); -} - -#[test] -fn configured_agent_endpoint_is_metadata_only_for_status_delivery_and_fetch() { - let agent_endpoint = - RadrootsReticulumPreviewAgentEndpoint::parse("reticulum-agent://localhost:19999") - .expect("agent endpoint"); - let transport = RadrootsReticulumPreviewTransport::new( - RadrootsReticulumPreviewProfile::default().with_agent_endpoint(agent_endpoint), - ); - let status = transport.status(); - assert_eq!( - status - .agent_endpoint - .as_ref() - .map(|endpoint| endpoint.as_str()), - Some("reticulum-agent://localhost:19999") - ); - assert_eq!( - status.transport_status.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); - assert!(!status.transport_status.usable_for_delivery); - assert!(!status.transport_status.capabilities.deliver); - assert!(!status.transport_status.capabilities.fetch); - - let receipt = transport - .deliver(delivery_request(vec![reticulum_target( - RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI, - )])) - .expect("delivery receipt"); - assert_eq!( - receipt.target_receipts[0].status, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable - ); - let fetch = transport - .fetch(RadrootsReticulumPreviewFetchRequest::new("fetch-agent", 1).expect("fetch")) - .expect("fetch receipt"); - assert_eq!( - fetch - .agent_endpoint - .as_ref() - .map(|endpoint| endpoint.as_str()), - Some("reticulum-agent://localhost:19999") - ); - assert_eq!(fetch.observed_event_count, 0); - assert_eq!( - fetch.implementation, - RadrootsTransportImplementationState::PreviewUnavailable - ); -} - -#[test] -fn public_models_round_trip_through_serde() { - let profile = RadrootsReticulumPreviewProfile::default() - .with_behavior(RadrootsReticulumPreviewBehavior::DeferDeliveryPlans); - let json = serde_json::to_string(&profile).expect("profile json"); - let decoded: RadrootsReticulumPreviewProfile = - serde_json::from_str(&json).expect("profile decode"); - - assert_eq!(decoded, profile); -} - -#[test] -fn preview_source_remains_inert_without_runtime_delivery_hooks() { - let source = include_str!("../src/lib.rs").to_ascii_lowercase(); - for forbidden in [ - "socket", - "rnsd", - "python", - "identity", - "send_mesh", - "fallback", - "nostr", - ] { - assert!( - !source.contains(forbidden), - "Reticulum preview source contains forbidden runtime hook {forbidden}" - ); - } -} - -#[test] -fn reticulum_preview_errors_and_defaults_are_stable() { - assert_eq!( - RadrootsReticulumPreviewBehavior::default(), - RadrootsReticulumPreviewBehavior::RejectDeliveryAttempts - ); - let cases = [ - ( - RadrootsReticulumPreviewError::InvalidEndpoint, - "invalid Reticulum preview endpoint", - ), - ( - RadrootsReticulumPreviewError::InvalidAgentEndpoint, - "invalid Reticulum preview agent endpoint", - ), - ( - RadrootsReticulumPreviewError::InvalidProfileId, - "invalid Reticulum preview profile id", - ), - ( - RadrootsReticulumPreviewError::InvalidFetchLimit, - "Reticulum preview fetch limit must be greater than zero", - ), - ( - RadrootsReticulumPreviewError::NonReticulumTarget, - "Reticulum preview transport received a non-Reticulum target", - ), - ]; - - for (error, message) in cases { - assert_eq!(error.to_string(), message); - } -}