sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit ba79ad1f6641b3ec86ffad50c1672f49134e78ab
parent 2dd31d71067eed4281d22bbfbaff5125223fd56f
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 17:21:03 -0700

sdk: add runtime error taxonomy

- Add structured SDK runtime errors for actor authorization, signer mismatch, relay targets, idempotency, order status, and sync setup.
- Route known authority, listing, outbox, relay transport, order status, and sync failures through product-matchable variants.
- Preserve redacted display text with pubkey and digest prefixes plus relay URL userinfo redaction.
- Update runtime and all-features tests to match structured failures without parsing strings.

Diffstat:
Mcrates/sdk/src/error.rs | 291++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/sdk/src/listings_runtime.rs | 23+++++++++++++++++------
Mcrates/sdk/src/orders_runtime.rs | 14++++++--------
Mcrates/sdk/src/runtime_targets.rs | 17++++++++++-------
Mcrates/sdk/src/sync_runtime.rs | 7++++---
Mcrates/sdk/tests/listings_runtime.rs | 12++++++++----
Mcrates/sdk/tests/orders_runtime.rs | 20+++++++++++++++++---
Mcrates/sdk/tests/runtime_foundation.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/sdk/tests/sync_runtime.rs | 10++++++++--
9 files changed, 392 insertions(+), 55 deletions(-)

diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -13,6 +13,7 @@ pub enum RadrootsSdkRecoveryAction { #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsSdkPartialLocalMutationFailure { OutboxEnqueue, + OutboxIdempotencyConflict, } #[cfg(feature = "runtime")] @@ -30,17 +31,80 @@ pub struct RadrootsSdkPartialLocalMutationError { #[cfg(feature = "runtime")] #[derive(Debug)] pub enum RadrootsSdkError { - Io { path: PathBuf, message: String }, + Io { + path: PathBuf, + message: String, + }, ClockBeforeUnixEpoch, - TimestampOutOfRange { value: u64 }, - Authority { message: String }, - EventStore { message: String }, - InvalidRequest { message: String }, - ListingDraft { message: String }, - ListingMutation { message: String }, - Outbox { message: String }, - RelayTransport { message: String }, - Projection { message: String }, + TimestampOutOfRange { + value: u64, + }, + UnauthorizedActor { + operation: String, + reason: String, + }, + SignerPubkeyMismatch { + operation: String, + expected_pubkey_prefix: String, + signer_pubkey_prefix: String, + }, + EmptyTargetRelays { + operation: String, + }, + RelayTargetLimitExceeded { + max: usize, + actual: usize, + }, + InvalidRelayUrl { + url: String, + reason: String, + }, + IdempotencyConflict { + operation_kind: String, + expected_pubkey_prefix: String, + existing_digest_prefix: String, + new_digest_prefix: String, + }, + OrderStatusLimitInvalid { + limit: u32, + min: u32, + max: u32, + }, + InvalidOrderId { + value: String, + message: String, + }, + ProductSyncUnsupported { + operation: &'static str, + required_feature: &'static str, + }, + ProductSyncRelaySetupFailure { + message: String, + }, + Authority { + message: String, + }, + EventStore { + message: String, + }, + InvalidRequest { + message: String, + }, + ListingDraft { + message: String, + }, + ListingMutation { + message: String, + }, + Outbox { + message: String, + }, + RelayTransport { + message: String, + }, + Projection { + message: String, + }, PartialLocalMutation(RadrootsSdkPartialLocalMutationError), } @@ -65,6 +129,50 @@ impl RadrootsSdkError { failure: RadrootsSdkPartialLocalMutationFailure::OutboxEnqueue, }) } + + pub fn partial_outbox_idempotency_conflict_mutation( + event_id: impl Into<String>, + operation_kind: impl Into<String>, + idempotency_digest_prefix: impl Into<String>, + ) -> Self { + Self::PartialLocalMutation(RadrootsSdkPartialLocalMutationError { + event_id: Some(event_id.into()), + operation_kind: operation_kind.into(), + idempotency_digest_prefix: Some(idempotency_digest_prefix.into()), + stored: true, + queued: false, + recovery: RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey, + failure: RadrootsSdkPartialLocalMutationFailure::OutboxIdempotencyConflict, + }) + } + + pub(crate) fn empty_target_relays(operation: impl Into<String>) -> Self { + Self::EmptyTargetRelays { + operation: operation.into(), + } + } + + pub(crate) fn relay_target_limit_exceeded(max: usize, actual: usize) -> Self { + Self::RelayTargetLimitExceeded { max, actual } + } + + pub(crate) fn invalid_relay_url(url: impl Into<String>, reason: impl Into<String>) -> Self { + Self::InvalidRelayUrl { + url: redacted_relay_url(url.into()), + reason: reason.into(), + } + } + + pub(crate) fn order_status_limit_invalid(limit: u32, min: u32, max: u32) -> Self { + Self::OrderStatusLimitInvalid { limit, min, max } + } + + pub(crate) fn invalid_order_id(value: impl Into<String>, message: impl Into<String>) -> Self { + Self::InvalidOrderId { + value: value.into(), + message: message.into(), + } + } } #[cfg(feature = "runtime")] @@ -81,6 +189,55 @@ impl fmt::Display for RadrootsSdkError { "sdk timestamp {value} exceeds Nostr u32 created_at range" ) } + Self::UnauthorizedActor { operation, reason } => { + write!(f, "sdk unauthorized actor for {operation}: {reason}") + } + Self::SignerPubkeyMismatch { + operation, + expected_pubkey_prefix, + signer_pubkey_prefix, + } => write!( + f, + "sdk signer pubkey mismatch for {operation}: expected_pubkey_prefix={expected_pubkey_prefix}, signer_pubkey_prefix={signer_pubkey_prefix}" + ), + Self::EmptyTargetRelays { operation } => { + write!(f, "sdk empty target relays for {operation}") + } + Self::RelayTargetLimitExceeded { max, actual } => { + write!( + f, + "sdk relay target limit exceeded: max={max}, actual={actual}" + ) + } + Self::InvalidRelayUrl { url, reason } => { + write!(f, "sdk invalid relay URL `{url}`: {reason}") + } + Self::IdempotencyConflict { + operation_kind, + expected_pubkey_prefix, + existing_digest_prefix, + new_digest_prefix, + } => write!( + f, + "sdk idempotency conflict for {operation_kind}: expected_pubkey_prefix={expected_pubkey_prefix}, existing_digest_prefix={existing_digest_prefix}, new_digest_prefix={new_digest_prefix}" + ), + Self::OrderStatusLimitInvalid { limit, min, max } => write!( + f, + "sdk order status limit invalid: limit={limit}, min={min}, max={max}" + ), + Self::InvalidOrderId { value, message } => { + write!(f, "sdk invalid order id `{value}`: {message}") + } + Self::ProductSyncUnsupported { + operation, + required_feature, + } => write!( + f, + "sdk product sync operation {operation} requires feature `{required_feature}`" + ), + Self::ProductSyncRelaySetupFailure { message } => { + write!(f, "sdk product sync relay setup failed: {message}") + } Self::Authority { message } => write!(f, "sdk authority error: {message}"), Self::EventStore { message } => write!(f, "sdk event store error: {message}"), Self::InvalidRequest { message } => write!(f, "sdk invalid request: {message}"), @@ -117,8 +274,36 @@ impl std::error::Error for RadrootsSdkError {} #[cfg(feature = "runtime")] impl From<radroots_authority::RadrootsAuthorityError> for RadrootsSdkError { fn from(error: radroots_authority::RadrootsAuthorityError) -> Self { - Self::Authority { - message: error.to_string(), + match error { + radroots_authority::RadrootsAuthorityError::ActorRoleUnsatisfied { + contract_id, + required_role, + } => Self::UnauthorizedActor { + operation: contract_id, + reason: format!("missing role {required_role:?}"), + }, + radroots_authority::RadrootsAuthorityError::ActorPubkeyMismatch { + expected_pubkey, + actor_pubkey, + } => Self::UnauthorizedActor { + operation: "event authorization".to_owned(), + reason: format!( + "actor_pubkey_prefix={} expected_pubkey_prefix={}", + redacted_prefix(actor_pubkey.as_str()), + redacted_prefix(expected_pubkey.as_str()) + ), + }, + radroots_authority::RadrootsAuthorityError::SignerPubkeyMismatch { + expected_pubkey, + signer_pubkey, + } => Self::SignerPubkeyMismatch { + operation: "event signing".to_owned(), + expected_pubkey_prefix: redacted_prefix(expected_pubkey.as_str()), + signer_pubkey_prefix: redacted_prefix(signer_pubkey.as_str()), + }, + error => Self::Authority { + message: error.to_string(), + }, } } } @@ -135,8 +320,16 @@ impl From<radroots_event_store::RadrootsEventStoreError> for RadrootsSdkError { #[cfg(feature = "runtime")] impl From<radroots_trade::listing::RadrootsListingDraftError> for RadrootsSdkError { fn from(error: radroots_trade::listing::RadrootsListingDraftError) -> Self { - Self::ListingDraft { - message: error.to_string(), + match error { + radroots_trade::listing::RadrootsListingDraftError::ActorRoleUnsatisfied { + required_role, + } => Self::UnauthorizedActor { + operation: "listing.prepare_publish".to_owned(), + reason: format!("missing role {required_role:?}"), + }, + error => Self::ListingDraft { + message: error.to_string(), + }, } } } @@ -153,8 +346,25 @@ impl From<radroots_trade::listing::RadrootsListingMutationError> for RadrootsSdk #[cfg(feature = "runtime")] impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError { fn from(error: radroots_outbox::RadrootsOutboxError) -> Self { - Self::Outbox { - message: error.to_string(), + match error { + radroots_outbox::RadrootsOutboxError::EmptyTargetRelays => { + Self::empty_target_relays("outbox enqueue") + } + radroots_outbox::RadrootsOutboxError::IdempotencyConflict { + operation_kind, + expected_pubkey, + existing_digest, + new_digest, + .. + } => Self::IdempotencyConflict { + operation_kind, + expected_pubkey_prefix: redacted_prefix(expected_pubkey.as_str()), + existing_digest_prefix: redacted_prefix(existing_digest.as_str()), + new_digest_prefix: redacted_prefix(new_digest.as_str()), + }, + error => Self::Outbox { + message: error.to_string(), + }, } } } @@ -162,8 +372,53 @@ impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError { #[cfg(feature = "runtime")] impl From<radroots_relay_transport::RadrootsRelayTransportError> for RadrootsSdkError { fn from(error: radroots_relay_transport::RadrootsRelayTransportError) -> Self { - Self::RelayTransport { - message: error.to_string(), + match error { + radroots_relay_transport::RadrootsRelayTransportError::RelayUrlParse { + url, + reason, + } => Self::invalid_relay_url(url, reason), + radroots_relay_transport::RadrootsRelayTransportError::WsRequiresLocalPolicy { + url, + } => Self::invalid_relay_url(url, "ws relay URL requires localhost policy"), + radroots_relay_transport::RadrootsRelayTransportError::UnsupportedRelayScheme { + url, + scheme, + } => Self::invalid_relay_url(url, format!("unsupported scheme `{scheme}`")), + radroots_relay_transport::RadrootsRelayTransportError::RelayUrlUserinfo { url } => { + Self::invalid_relay_url(url, "relay URL must not include userinfo") + } + radroots_relay_transport::RadrootsRelayTransportError::EmptyRelayHost { url } => { + Self::invalid_relay_url(url, "relay URL must include a host") + } + radroots_relay_transport::RadrootsRelayTransportError::RelayUrlQueryOrFragment { + url, + } => Self::invalid_relay_url(url, "relay URL must not include query or fragment"), + radroots_relay_transport::RadrootsRelayTransportError::EmptyTargetSet => { + Self::empty_target_relays("relay publish") + } + #[cfg(feature = "runtime")] + radroots_relay_transport::RadrootsRelayTransportError::Outbox(error) => error.into(), + error => Self::RelayTransport { + message: error.to_string(), + }, } } } + +#[cfg(feature = "runtime")] +fn redacted_prefix(value: &str) -> String { + value.chars().take(12).collect() +} + +#[cfg(feature = "runtime")] +fn redacted_relay_url(value: String) -> String { + let Some((scheme, rest)) = value.split_once("://") else { + return value; + }; + let authority = rest.split('/').next().unwrap_or(rest); + let Some((_, after_userinfo)) = authority.rsplit_once('@') else { + return value; + }; + let path = rest.strip_prefix(authority).unwrap_or_default(); + format!("{scheme}://<redacted>@{after_userinfo}{path}") +} diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -227,12 +227,23 @@ impl<'sdk> ListingsClient<'sdk> { ._outbox .enqueue_signed_operation(outbox_input) .await - .map_err(|_| { - RadrootsSdkError::partial_outbox_enqueue_mutation( - signed_event_id.as_str(), - LISTING_PUBLISH_OPERATION_KIND, - partial_failure_digest_prefix.as_str(), - ) + .map_err(|error| { + if matches!( + error, + radroots_outbox::RadrootsOutboxError::IdempotencyConflict { .. } + ) { + RadrootsSdkError::partial_outbox_idempotency_conflict_mutation( + signed_event_id.as_str(), + LISTING_PUBLISH_OPERATION_KIND, + partial_failure_digest_prefix.as_str(), + ) + } else { + RadrootsSdkError::partial_outbox_enqueue_mutation( + signed_event_id.as_str(), + LISTING_PUBLISH_OPERATION_KIND, + partial_failure_digest_prefix.as_str(), + ) + } })?; let idempotency_digest_prefix = digest_prefix(outbox_receipt.idempotency_digest.as_str()); Ok(ListingEnqueueReceipt { diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -36,9 +36,7 @@ impl OrderStatusRequest { pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> { RadrootsOrderId::parse(order_id) .map(Self::new) - .map_err(|error| RadrootsSdkError::InvalidRequest { - message: format!("order_id is invalid: {error}"), - }) + .map_err(|error| RadrootsSdkError::invalid_order_id(order_id, error.to_string())) } pub fn with_limit(mut self, limit: u32) -> Self { @@ -48,11 +46,11 @@ impl OrderStatusRequest { fn validate(&self) -> Result<(), RadrootsSdkError> { if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT { - return Err(RadrootsSdkError::InvalidRequest { - message: format!( - "order status limit must be between 1 and {ORDER_STATUS_MAX_LIMIT}" - ), - }); + return Err(RadrootsSdkError::order_status_limit_invalid( + self.limit, + 1, + ORDER_STATUS_MAX_LIMIT, + )); } Ok(()) } diff --git a/crates/sdk/src/runtime_targets.rs b/crates/sdk/src/runtime_targets.rs @@ -101,12 +101,15 @@ impl SdkRelayTargetSet { fn from_normalized_set(normalized: BTreeSet<String>) -> Result<Self, RadrootsSdkError> { if normalized.is_empty() { - return Err(invalid_request("relay target set must not be empty")); + return Err(RadrootsSdkError::empty_target_relays( + "sdk relay target set", + )); } if normalized.len() > SDK_RELAY_TARGET_MAX_COUNT { - return Err(invalid_request(format!( - "relay target set must contain at most {SDK_RELAY_TARGET_MAX_COUNT} relays" - ))); + return Err(RadrootsSdkError::relay_target_limit_exceeded( + SDK_RELAY_TARGET_MAX_COUNT, + normalized.len(), + )); } Ok(Self { relays: normalized.into_iter().collect(), @@ -186,11 +189,11 @@ fn normalized_relay_url( value: &str, policy: SdkRelayUrlPolicy, ) -> Result<String, RadrootsSdkError> { - let relay = RadrootsRelayUrl::parse(value, policy.relay_transport_policy()) - .map_err(|error| invalid_request(format!("invalid relay target: {error}")))?; + let relay = RadrootsRelayUrl::parse(value, policy.relay_transport_policy())?; let normalized = relay.into_string(); if normalized.starts_with("ws://") && !is_local_ws_relay(normalized.as_str()) { - return Err(invalid_request( + return Err(RadrootsSdkError::invalid_relay_url( + normalized, "ws relay targets are limited to localhost, 127.0.0.1, or [::1]", )); } diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -192,7 +192,7 @@ impl<'sdk> SyncClient<'sdk> { #[cfg(feature = "relay-runtime")] { if self.sdk.relay_urls().is_empty() { - return Err(RadrootsSdkError::InvalidRequest { + return Err(RadrootsSdkError::ProductSyncRelaySetupFailure { message: "sync push requires configured relay URLs".to_owned(), }); } @@ -204,8 +204,9 @@ impl<'sdk> SyncClient<'sdk> { #[cfg(not(feature = "relay-runtime"))] { let _ = request; - Err(RadrootsSdkError::RelayTransport { - message: "sync push requires the relay-runtime feature".to_owned(), + Err(RadrootsSdkError::ProductSyncUnsupported { + operation: "sync.push_outbox", + required_feature: "relay-runtime", }) } } diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -19,8 +19,8 @@ use radroots_events::{ use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState}; use radroots_sdk::{ ListingEnqueuePublishRequest, ListingPreparePublishRequest, RadrootsSdk, RadrootsSdkError, - RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkMutationState, SdkRelayTargetPolicy, - SdkRelayTargetSet, SdkRelayUrlPolicy, + RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, + SdkMutationState, SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, }; const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -213,7 +213,7 @@ async fn prepare_publish_rejects_non_seller_actor() { .prepare_publish(request) .expect_err("non seller"); - assert!(matches!(error, RadrootsSdkError::ListingDraft { .. })); + assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. })); } #[tokio::test] @@ -288,7 +288,10 @@ async fn enqueue_publish_returns_sanitized_signer_errors() { .expect_err("signer error"); let message = error.to_string(); - assert!(matches!(error, RadrootsSdkError::Authority { .. })); + assert!(matches!( + error, + RadrootsSdkError::SignerPubkeyMismatch { .. } + )); assert!(!message.contains("raw")); assert!(!message.contains("ffff")); } @@ -329,6 +332,7 @@ async fn enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() && partial.event_id.is_some() && partial.operation_kind == "listing.publish.v1" && partial.idempotency_digest_prefix.is_some() + && partial.failure == RadrootsSdkPartialLocalMutationFailure::OutboxIdempotencyConflict && partial.recovery == RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey )); assert!(!error.to_string().contains("idem-d")); diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -229,15 +229,29 @@ async fn order_status_rejects_invalid_limits_before_querying() { .await .expect_err("too large"); - assert!(matches!(zero, RadrootsSdkError::InvalidRequest { .. })); - assert!(matches!(too_large, RadrootsSdkError::InvalidRequest { .. })); + assert!(matches!( + zero, + RadrootsSdkError::OrderStatusLimitInvalid { + limit: 0, + min: 1, + max: ORDER_STATUS_MAX_LIMIT + } + )); + assert!(matches!( + too_large, + RadrootsSdkError::OrderStatusLimitInvalid { + limit, + min: 1, + max: ORDER_STATUS_MAX_LIMIT + } if limit == ORDER_STATUS_MAX_LIMIT + 1 + )); } #[test] fn order_status_parse_rejects_invalid_order_ids() { let error = OrderStatusRequest::parse("bad order id").expect_err("invalid order id"); - assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. })); + assert!(matches!(error, RadrootsSdkError::InvalidOrderId { .. })); } #[tokio::test] diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -45,10 +45,24 @@ async fn sdk_builder_rejects_ws_relay_without_localhost_policy() { assert!(matches!( result, - Err(RadrootsSdkError::InvalidRequest { .. }) + Err(RadrootsSdkError::InvalidRelayUrl { .. }) )); } +#[test] +fn invalid_relay_url_errors_redact_userinfo() { + let error = SdkRelayTargetSet::new( + ["wss://user:password@relay.example.com"], + SdkRelayUrlPolicy::Public, + ) + .expect_err("invalid relay"); + let message = error.to_string(); + + assert!(matches!(error, RadrootsSdkError::InvalidRelayUrl { .. })); + assert!(message.contains("<redacted>@relay.example.com")); + assert!(!message.contains("password")); +} + #[tokio::test] async fn sdk_builder_allows_only_local_ws_targets_with_localhost_policy() { let sdk = RadrootsSdk::builder() @@ -70,7 +84,7 @@ async fn sdk_builder_allows_only_local_ws_targets_with_localhost_policy() { assert!(matches!( result, - Err(RadrootsSdkError::InvalidRequest { .. }) + Err(RadrootsSdkError::InvalidRelayUrl { .. }) )); } @@ -166,7 +180,7 @@ fn relay_target_set_validates_normalizes_dedupes_sorts_and_caps() { assert!(matches!( SdkRelayTargetSet::new(Vec::<String>::new(), SdkRelayUrlPolicy::Public), - Err(RadrootsSdkError::InvalidRequest { .. }) + Err(RadrootsSdkError::EmptyTargetRelays { .. }) )); let too_many = (0..=SDK_RELAY_TARGET_MAX_COUNT) @@ -174,7 +188,10 @@ fn relay_target_set_validates_normalizes_dedupes_sorts_and_caps() { .collect::<Vec<_>>(); assert!(matches!( SdkRelayTargetSet::new(too_many, SdkRelayUrlPolicy::Public), - Err(RadrootsSdkError::InvalidRequest { .. }) + Err(RadrootsSdkError::RelayTargetLimitExceeded { + max: SDK_RELAY_TARGET_MAX_COUNT, + actual + }) if actual == SDK_RELAY_TARGET_MAX_COUNT + 1 )); } @@ -199,3 +216,31 @@ fn idempotency_key_validation_is_bounded_and_debug_redacted() { Err(RadrootsSdkError::InvalidRequest { .. }) )); } + +#[test] +fn outbox_idempotency_conflict_maps_to_structured_sdk_error() { + let error = RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::IdempotencyConflict { + operation_kind: "listing.publish.v1".to_owned(), + expected_pubkey: "a".repeat(64), + idempotency_key: "secret-idempotency-key".to_owned(), + existing_digest: "b".repeat(64), + new_digest: "c".repeat(64), + }); + let message = error.to_string(); + + assert!(matches!( + error, + RadrootsSdkError::IdempotencyConflict { + operation_kind, + expected_pubkey_prefix, + existing_digest_prefix, + new_digest_prefix, + } if operation_kind == "listing.publish.v1" + && expected_pubkey_prefix == "aaaaaaaaaaaa" + && existing_digest_prefix == "bbbbbbbbbbbb" + && new_digest_prefix == "cccccccccccc" + )); + assert!(!message.contains("secret-idempotency-key")); + assert!(!message.contains(&"b".repeat(64))); + assert!(!message.contains(&"c".repeat(64))); +} diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -221,7 +221,10 @@ async fn product_push_outbox_without_relay_runtime_returns_structured_error() { .await .expect_err("unsupported product push"); - assert!(matches!(error, RadrootsSdkError::RelayTransport { .. })); + assert!(matches!( + error, + RadrootsSdkError::ProductSyncUnsupported { .. } + )); } #[cfg(feature = "relay-runtime")] @@ -250,7 +253,10 @@ async fn product_push_outbox_requires_configured_relays() { .await .expect_err("missing configured relays"); - assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. })); + assert!(matches!( + error, + RadrootsSdkError::ProductSyncRelaySetupFailure { .. } + )); } #[tokio::test]