cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 30c822b014b853085938db1071d7bbbd632ed74f
parent b1a29e8a80a241d2b8eaa698d594849819849d87
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Apr 2026 20:00:38 +0000

test: cover order decision preflight edges

Diffstat:
Msrc/runtime/order.rs | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 240 insertions(+), 5 deletions(-)

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -2752,8 +2752,9 @@ mod tests { SellerOrderRequestResolution, accepted_order_decision_payload_from_request, collect_issues, declined_order_decision_payload_from_request, inspect_document, next_order_id, order_decision_dry_run_view, order_decision_preflight_view_from_status, - order_history_entry_from_event, order_history_from_receipt, order_request_filter, - order_status_from_receipt, seller_order_request_resolution_from_receipt, + order_decision_view_from_resolution, order_history_entry_from_event, + order_history_from_receipt, order_request_filter, order_status_from_receipt, + seller_order_request_resolution_from_receipt, }; use crate::runtime::config::{ AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, @@ -3334,6 +3335,72 @@ mod tests { } #[test] + fn order_decline_dry_run_view_preserves_ready_preflight_without_publish_fields() { + let dir = tempdir().expect("tempdir"); + let mut config = sample_config(dir.path()); + config.output.dry_run = true; + config.relay.urls = vec!["ws://relay.test".to_owned()]; + let fixture = order_status_fixture(); + let resolution = request_resolution_for_fixture(&fixture); + let request = resolution.requests[0].clone(); + let status_view = order_status_from_receipt( + fixture.order_id.as_str(), + DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone()], + }, + ); + let args = OrderDecisionArgs { + key: fixture.order_id.clone(), + decision: OrderDecisionArg::Decline, + reason: Some(" out of stock ".to_owned()), + idempotency_key: Some("idem_decline_dry_run".to_owned()), + }; + + let view = order_decision_dry_run_view(&config, &args, &request, &status_view); + + assert_eq!(view.state, "dry_run"); + assert_eq!(view.decision, "declined"); + assert_eq!(view.dry_run, true); + assert_eq!( + view.reason.as_deref(), + Some( + "dry run requested; seller order decision publication skipped with reason `out of stock`" + ) + ); + assert_eq!( + view.request_event_id.as_deref(), + Some(request.request_event_id.as_str()) + ); + assert_eq!( + view.root_event_id.as_deref(), + Some(request.request_event_id.as_str()) + ); + assert_eq!( + view.prev_event_id.as_deref(), + Some(request.request_event_id.as_str()) + ); + assert_eq!( + view.listing_addr.as_deref(), + Some(fixture.listing_addr.as_str()) + ); + assert_eq!(view.event_id, None); + assert_eq!(view.event_kind, None); + assert!(view.acknowledged_relays.is_empty()); + assert_eq!(view.target_relays, vec!["ws://relay.test"]); + assert_eq!(view.connected_relays, vec!["ws://relay.test"]); + assert_eq!(view.fetched_count, 1); + assert_eq!(view.decoded_count, 1); + assert_eq!(view.skipped_count, 0); + assert_eq!( + view.idempotency_key.as_deref(), + Some("idem_decline_dry_run") + ); + } + + #[test] fn order_status_from_receipt_reports_accepted() { let fixture = order_status_fixture(); let decision_event = signed_order_decision_event( @@ -3501,6 +3568,9 @@ mod tests { reason: "out of stock".to_owned(), }, ); + let mut expected_event_ids = + vec![accepted_event.id.to_string(), declined_event.id.to_string()]; + expected_event_ids.sort(); let receipt = DirectRelayFetchReceipt { target_relays: vec!["ws://relay.test".to_owned()], connected_relays: vec!["ws://relay.test".to_owned()], @@ -3517,9 +3587,17 @@ mod tests { assert_eq!(view.state, "invalid"); assert_eq!(view.decoded_count, 3); assert!(view.decision_event_id.is_none()); - assert!(view.reducer_issues.iter().any(|issue| { - issue.field == "decision_event_id" && issue.message.contains("ConflictingDecisions") - })); + let issue = view + .reducer_issues + .iter() + .find(|issue| issue.code == "conflicting_decisions") + .expect("conflicting decision issue"); + assert_eq!(issue.field, "decision_event_id"); + assert_eq!( + issue.message, + "active order reducer reported conflicting decisions" + ); + assert_eq!(issue.event_ids, expected_event_ids); } #[test] @@ -3595,7 +3673,131 @@ mod tests { assert!(resolution.requests.is_empty()); } + #[test] + fn seller_order_request_resolution_reports_invalid_same_order_candidate() { + let fixture = order_status_fixture(); + let invalid_event = signed_malformed_order_request_event( + &fixture.buyer, + fixture.order_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + "2".repeat(64).as_str(), + ); + let invalid_event_id = invalid_event.id.to_string(); + let receipt = DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone(), invalid_event], + }; + + let resolution = seller_order_request_resolution_from_receipt( + fixture.seller_pubkey.as_str(), + fixture.order_id.as_str(), + receipt, + ) + .expect("seller order request resolution"); + + assert_eq!(resolution.fetched_count, 2); + assert_eq!(resolution.decoded_count, 1); + assert_eq!(resolution.skipped_count, 1); + assert_eq!(resolution.requests.len(), 1); + assert_eq!(resolution.candidate_issues.len(), 1); + assert_eq!( + resolution.candidate_issues[0].code, + "invalid_request_candidate" + ); + assert_eq!( + resolution.candidate_issues[0].event_ids, + vec![invalid_event_id.clone()] + ); + + let dir = tempdir().expect("tempdir"); + let mut config = sample_config(dir.path()); + config.output.dry_run = true; + config.relay.urls = vec!["ws://relay.test".to_owned()]; + let args = OrderDecisionArgs { + key: fixture.order_id.clone(), + decision: OrderDecisionArg::Accept, + reason: None, + idempotency_key: None, + }; + let view = order_decision_view_from_resolution( + &config, + &args, + fixture.seller_pubkey.clone(), + resolution, + ); + + assert_eq!(view.state, "invalid"); + assert_eq!(view.issues[0].code, "invalid_request_candidate"); + assert_eq!(view.issues[0].event_ids, vec![invalid_event_id]); + assert!(view.event_id.is_none()); + } + + #[test] + fn seller_order_request_resolution_reports_multiple_same_order_candidates_invalid() { + let fixture = order_status_fixture(); + let second_request_event = signed_order_request_event( + &fixture.buyer, + fixture.order_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + "2".repeat(64).as_str(), + ); + let mut expected_event_ids = vec![ + fixture.request_event.id.to_string(), + second_request_event.id.to_string(), + ]; + expected_event_ids.sort(); + let receipt = DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone(), second_request_event], + }; + + let resolution = seller_order_request_resolution_from_receipt( + fixture.seller_pubkey.as_str(), + fixture.order_id.as_str(), + receipt, + ) + .expect("seller order request resolution"); + + assert_eq!(resolution.fetched_count, 2); + assert_eq!(resolution.decoded_count, 2); + assert_eq!(resolution.skipped_count, 0); + assert_eq!(resolution.requests.len(), 2); + + let dir = tempdir().expect("tempdir"); + let mut config = sample_config(dir.path()); + config.output.dry_run = true; + config.relay.urls = vec!["ws://relay.test".to_owned()]; + let args = OrderDecisionArgs { + key: fixture.order_id.clone(), + decision: OrderDecisionArg::Accept, + reason: None, + idempotency_key: None, + }; + let view = order_decision_view_from_resolution( + &config, + &args, + fixture.seller_pubkey.clone(), + resolution, + ); + + assert_eq!(view.state, "invalid"); + assert_eq!(view.issues.len(), 1); + assert_eq!(view.issues[0].code, "multiple_request_candidates"); + assert_eq!(view.issues[0].field, "request_event_id"); + assert_eq!(view.issues[0].event_ids, expected_event_ids); + assert!(view.event_id.is_none()); + } + struct OrderStatusFixture { + buyer: RadrootsIdentity, seller: RadrootsIdentity, order_id: String, listing_addr: String, @@ -3622,6 +3824,7 @@ mod tests { ); OrderStatusFixture { + buyer, seller, order_id, listing_addr, @@ -3771,6 +3974,38 @@ mod tests { .expect("signed order decision") } + fn signed_malformed_order_request_event( + buyer: &RadrootsIdentity, + order_id: &str, + listing_addr: &str, + buyer_pubkey: &str, + seller_pubkey: &str, + listing_event_id: &str, + ) -> radroots_nostr::prelude::RadrootsNostrEvent { + let payload = RadrootsTradeOrderRequested { + order_id: order_id.to_owned(), + listing_addr: listing_addr.to_owned(), + buyer_pubkey: buyer_pubkey.to_owned(), + seller_pubkey: seller_pubkey.to_owned(), + items: vec![RadrootsTradeOrderItem { + bin_id: "bin-1".to_owned(), + bin_count: 2, + }], + }; + let parts = active_trade_order_request_event_build( + &RadrootsNostrEventPtr { + id: listing_event_id.to_owned(), + relays: None, + }, + &payload, + ) + .expect("order request parts"); + radroots_nostr_build_event(parts.kind, "not-json".to_owned(), parts.tags) + .expect("nostr event builder") + .sign_with_keys(buyer.keys()) + .expect("signed malformed order request") + } + fn signed_order_request_event( buyer: &RadrootsIdentity, order_id: &str,