commit c2ee5ccab8eb19f8d996184a584997407f9e5bea
parent 4a4e7690529e855eeb5492e9cfa8757ec6827d2c
Author: triesap <tyson@radroots.org>
Date: Thu, 18 Jun 2026 20:42:11 -0700
sdk: harden farm order runtime guards
Diffstat:
3 files changed, 142 insertions(+), 5 deletions(-)
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -11,8 +11,10 @@ use radroots_events::{
kinds::{KIND_FARM, KIND_PROFILE},
};
use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState};
+use radroots_relay_transport::RadrootsMockRelayPublishAdapter;
use radroots_sdk::{
- FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmPreparePublishRequest, RadrootsSdk,
+ FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmPreparePublishRequest,
+ PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsSdk,
RadrootsSdkError, RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction,
RadrootsSdkTimestamp, SdkMutationState, SdkRelayTargetPolicy, SdkRelayTargetSet,
SdkRelayUrlPolicy,
@@ -349,6 +351,63 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() {
}
#[tokio::test]
+async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() {
+ let (_tempdir, sdk) = directory_sdk().await;
+ let enqueue_request = FarmEnqueuePublishRequest::new(
+ farmer_actor(),
+ farm(FARM_D_D_TAG, "Sync Farm"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("target relays");
+ let enqueue_receipt = sdk
+ .farms()
+ .enqueue_publish(enqueue_request, &FixtureSigner::new(FARMER))
+ .await
+ .expect("enqueue");
+ let adapter = RadrootsMockRelayPublishAdapter::new();
+
+ let push_receipt = sdk
+ .sync()
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(1))
+ .await
+ .expect("push");
+
+ assert_eq!(push_receipt.attempted_events, 1);
+ assert_eq!(push_receipt.published_events, 1);
+ assert_eq!(push_receipt.retryable_events, 0);
+ assert_eq!(push_receipt.terminal_events, 0);
+ assert_eq!(push_receipt.events.len(), 1);
+ let event = &push_receipt.events[0];
+ assert_eq!(event.event_id, enqueue_receipt.signed_event_id);
+ assert_eq!(event.outbox_event_id, enqueue_receipt.outbox_event_id);
+ assert_eq!(event.final_state, PushOutboxEventState::Published);
+ assert_eq!(event.attempted_count, 1);
+ assert_eq!(event.accepted_count, 1);
+ assert_eq!(event.retryable_count, 0);
+ assert_eq!(event.terminal_count, 0);
+ assert_eq!(event.quorum, 1);
+ assert!(event.quorum_met);
+ assert_eq!(event.relays.len(), 1);
+ assert_eq!(event.relays[0].relay_url, RELAY);
+ assert_eq!(
+ event.relays[0].outcome_kind,
+ PushOutboxRelayOutcomeKind::Accepted
+ );
+ assert_eq!(adapter.captured_raw_events().len(), 1);
+
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ .await
+ .expect("outbox");
+ let stored = outbox
+ .get_event(enqueue_receipt.outbox_event_id)
+ .await
+ .expect("stored")
+ .expect("stored");
+ assert_eq!(stored.state, RadrootsOutboxEventState::Published);
+}
+
+#[tokio::test]
async fn farm_enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() {
let (_tempdir, sdk) = directory_sdk().await;
let first = FarmEnqueuePublishRequest::new(
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -19,6 +19,7 @@ use radroots_nostr::prelude::{
radroots_nostr_build_event, radroots_nostr_sign_frozen_draft,
};
use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState};
+use radroots_relay_transport::RadrootsMockRelayPublishAdapter;
use radroots_sdk::protocol::events::RadrootsNostrEventPtr;
use radroots_sdk::protocol::order::{
RadrootsListingAddress, RadrootsOrderDecision, RadrootsOrderDecisionOutcome,
@@ -30,7 +31,8 @@ use radroots_sdk::protocol::wire::WireEventParts;
use radroots_sdk::{
ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, ORDER_SUBMIT_OPERATION_KIND,
OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind, OrderStatusRequest,
- OrderSubmitEnqueueRequest, OrderSubmitPrepareRequest, RadrootsSdk, RadrootsSdkError,
+ OrderSubmitEnqueueRequest, OrderSubmitPrepareRequest, PushOutboxEventState,
+ PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsSdk, RadrootsSdkError,
RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction, RadrootsSdkTimestamp,
SdkMutationState, SdkOrderStatusIssue, SdkOrderStatusIssueKind, SdkOrderStatusSource,
SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy,
@@ -543,6 +545,64 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() {
}
#[tokio::test]
+async fn order_submit_enqueue_pushes_queued_event_with_mock_relay_sync() {
+ let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
+ let enqueue_request = OrderSubmitEnqueueRequest::new(
+ buyer_actor(),
+ listing_event_ptr(),
+ order_request("order-submit-sync"),
+ SdkRelayTargetPolicy::UseConfiguredRelays,
+ )
+ .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public)
+ .expect("target relays");
+ let enqueue_receipt = sdk
+ .orders()
+ .enqueue_submit(enqueue_request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX))
+ .await
+ .expect("enqueue");
+ let adapter = RadrootsMockRelayPublishAdapter::new();
+
+ let push_receipt = sdk
+ .sync()
+ .push_outbox_with_adapter(&adapter, PushOutboxRequest::new().with_limit(1))
+ .await
+ .expect("push");
+
+ assert_eq!(push_receipt.attempted_events, 1);
+ assert_eq!(push_receipt.published_events, 1);
+ assert_eq!(push_receipt.retryable_events, 0);
+ assert_eq!(push_receipt.terminal_events, 0);
+ assert_eq!(push_receipt.events.len(), 1);
+ let event = &push_receipt.events[0];
+ assert_eq!(event.event_id, enqueue_receipt.signed_event_id);
+ assert_eq!(event.outbox_event_id, enqueue_receipt.outbox_event_id);
+ assert_eq!(event.final_state, PushOutboxEventState::Published);
+ assert_eq!(event.attempted_count, 1);
+ assert_eq!(event.accepted_count, 1);
+ assert_eq!(event.retryable_count, 0);
+ assert_eq!(event.terminal_count, 0);
+ assert_eq!(event.quorum, 1);
+ assert!(event.quorum_met);
+ assert_eq!(event.relays.len(), 1);
+ assert_eq!(event.relays[0].relay_url, RELAY);
+ assert_eq!(
+ event.relays[0].outcome_kind,
+ PushOutboxRelayOutcomeKind::Accepted
+ );
+ assert_eq!(adapter.captured_raw_events().len(), 1);
+
+ let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path)
+ .await
+ .expect("outbox");
+ let stored = outbox
+ .get_event(enqueue_receipt.outbox_event_id)
+ .await
+ .expect("stored")
+ .expect("stored");
+ assert_eq!(stored.state, RadrootsOutboxEventState::Published);
+}
+
+#[tokio::test]
async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_conflict() {
let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
let first = OrderSubmitEnqueueRequest::new(
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -70,10 +70,25 @@ fn sdk_manifest_does_not_depend_on_app_or_cli_crates() {
#[test]
fn farm_runtime_stays_on_product_runtime_boundary() {
+ product_runtime_file_stays_on_boundary("src/farms_runtime.rs");
+}
+
+#[test]
+fn order_runtime_stays_on_product_runtime_boundary() {
+ product_runtime_file_stays_on_boundary("src/orders_runtime.rs");
+}
+
+#[test]
+fn migrated_runtime_tests_stay_on_product_runtime_boundary() {
+ for file in ["tests/farms_runtime.rs", "tests/orders_runtime.rs"] {
+ product_runtime_file_stays_on_boundary(file);
+ }
+}
+
+fn product_runtime_file_stays_on_boundary(relative_path: &str) {
let source = read_source(
Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("src")
- .join("farms_runtime.rs")
+ .join(relative_path)
.as_path(),
);
@@ -84,10 +99,13 @@ fn farm_runtime_stays_on_product_runtime_boundary() {
"radrootsd",
"publish_with_identity",
"publish_parts_via_relay",
+ "publish_listing_via_radrootsd",
+ "publish_order_request_via_radrootsd",
+ "publish_farm_via_radrootsd",
] {
assert!(
!source.contains(forbidden),
- "farms_runtime.rs must not use legacy SDK client or transport concept `{forbidden}`"
+ "{relative_path} must not use legacy SDK client or transport concept `{forbidden}`"
);
}
}