commit 3295e3f051cbef9ede766a36cdd6aa423ff68d77
parent d6e4a59e0c073a27634b013185bc1e57c4e4d998
Author: triesap <tyson@radroots.org>
Date: Wed, 17 Jun 2026 15:33:36 -0700
order: migrate status to SDK projection
- route public order status through the CLI SDK adapter
- map SDK order status receipts into the CLI status view
- keep relay status private for non-MVP preflight paths
- update tests for local projection status semantics
Diffstat:
5 files changed, 499 insertions(+), 54 deletions(-)
diff --git a/src/ops/exec/order.rs b/src/ops/exec/order.rs
@@ -554,7 +554,7 @@ impl OperationService<OrderStatusGetRequest> for OrderOperationService<'_> {
key: required_order_key(&request)?,
};
let view = crate::runtime::order::status(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
})?;
status_result::<OrderStatusGetResult>(request.operation_id(), &view)
}
@@ -1996,7 +1996,7 @@ mod tests {
}
#[test]
- fn order_status_get_requires_relay_configuration() {
+ fn order_status_get_uses_local_sdk_projection_without_relay() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(OrderOperationService::new(&config));
@@ -2005,26 +2005,24 @@ mod tests {
OrderStatusGetRequest::from_data(data(&[("order_id", "ord_pending")])),
)
.expect("order status request");
- let error = service.execute(status).expect_err("status unconfigured");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "operation_unavailable");
- assert!(output_error.message.contains("configured relay"));
- let detail = output_error.detail.as_ref().expect("status detail");
- assert_eq!(detail["state"], "unconfigured");
- assert_eq!(detail["order_id"], "ord_pending");
- assert_eq!(detail["fetched_count"], 0);
- assert_eq!(detail["decoded_count"], 0);
- assert_eq!(detail["skipped_count"], 0);
- let envelope = crate::out::envelope::OutputEnvelope::failure(
- "order.status.get",
- output_error,
- OperationContext::default().envelope_context("req_order_status"),
- );
+ let envelope = service
+ .execute(status)
+ .expect("status result")
+ .to_envelope(OperationContext::default().envelope_context("req_order_status"))
+ .expect("status envelope");
+
+ assert_eq!(envelope.operation_id, "order.status.get");
+ assert_eq!(envelope.result["state"], "missing");
+ assert_eq!(envelope.result["source"], "SDK local order projection");
assert_eq!(
- envelope.next_actions[0].command.as_deref(),
- Some("radroots --relay wss://relay.example.com order status get ord_pending")
+ envelope.result["actor_context_source"],
+ "sdk_local_projection"
);
+ assert_eq!(envelope.result["order_id"], "ord_pending");
+ assert_eq!(envelope.result["fetched_count"], 0);
+ assert_eq!(envelope.result["decoded_count"], 0);
+ assert_eq!(envelope.result["skipped_count"], 0);
+ assert!(envelope.next_actions.is_empty());
}
#[test]
diff --git a/src/registry/mod.rs b/src/registry/mod.rs
@@ -194,7 +194,6 @@ pub fn network_requirement(operation_id: &str) -> NetworkRequirement {
| "listing.update"
| "listing.archive"
| "order.submit"
- | "order.status.get"
| "order.event.list"
| "validation.receipt.get"
| "validation.receipt.list"
@@ -577,7 +576,6 @@ mod tests {
"order.revision.decline",
"order.fulfillment.update",
"order.receipt.record",
- "order.status.get",
"order.event.list",
"validation.receipt.get",
"validation.receipt.list",
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -71,6 +71,10 @@ use radroots_sdk::client::{
use radroots_sdk::config::{
RadrootsSdkConfig, SdkEnvironment, SdkTransportMode, SignerConfig as SdkSignerConfig,
};
+use radroots_sdk::{
+ OrderFulfillmentStatusKind, OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind,
+ OrderStatusReceipt, OrderStatusRequest, SdkOrderStatusIssue,
+};
use radroots_sql_core::SqliteExecutor;
use radroots_trade::order::{
RadrootsListingInventoryAccountingIssue, RadrootsListingInventoryAccountingProjection,
@@ -110,6 +114,7 @@ use crate::runtime::sync::{
RelayIngestScope, freshness_for_scope, freshness_requires_refresh, market_refresh,
relay_provenance_relays_for_scope,
};
+use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession};
use crate::view::runtime::{
OrderAppRecordExportView, OrderAppRecordListView, OrderAppRecordSummaryView,
OrderCancellationView, OrderDecisionView, OrderDraftItemView, OrderEventListEntryView,
@@ -137,6 +142,7 @@ const ORDER_PAYMENT_SOURCE: &str = "direct Nostr relay payment publish · local
const ORDER_SETTLEMENT_SOURCE: &str = "direct Nostr relay settlement publish · local key";
const ORDER_EVENT_LIST_SOURCE: &str = "direct Nostr relay fetch · selected seller identity";
const ORDER_STATUS_SOURCE: &str = "direct Nostr relay status fetch · active order reducer";
+const ORDER_STATUS_SDK_SOURCE: &str = "SDK local order projection";
const ORDER_EVENT_LIST_RELAY_ACTION: &str =
"radroots --relay wss://relay.example.com order event list";
const ORDER_BUYER_ACTOR_SOURCE_RESOLVED_ACCOUNT: &str = "resolved_account";
@@ -145,6 +151,7 @@ const ORDER_APP_RECORD_LIST_LIMIT: u32 = 500;
const ORDER_ACTOR_CONTEXT_ORDER_DRAFT: &str = "order_draft";
const ORDER_ACTOR_CONTEXT_RESOLVED_ACCOUNT: &str = "resolved_account";
const ORDER_ACTOR_CONTEXT_NETWORK_ONLY: &str = "network_only";
+const ORDER_ACTOR_CONTEXT_SDK_LOCAL: &str = "sdk_local_projection";
const ORDERS_DIR: &str = "orders/drafts";
const APP_ORDER_ALREADY_SUBMITTED_ISSUE: &str = "app_order_already_submitted";
const APP_ORDER_SIGNED_EVIDENCE_CONFLICT_ISSUE: &str = "app_order_signed_evidence_conflict";
@@ -1352,7 +1359,7 @@ pub fn decide(
}
if resolution.requests.len() == 1 {
let request = resolution.requests[0].clone();
- let status_view = status(
+ let status_view = relay_status(
config,
&OrderStatusArgs {
key: args.key.clone(),
@@ -2219,6 +2226,16 @@ pub fn settlement_decision(
pub fn status(
config: &RuntimeConfig,
args: &OrderStatusArgs,
+) -> Result<OrderStatusView, CliSdkAdapterError> {
+ let request = OrderStatusRequest::parse(args.key.as_str())?;
+ let session = CliSdkSession::connect(config)?;
+ let receipt = session.block_on(session.sdk().orders().status(request))?;
+ Ok(sdk_order_status_view(receipt))
+}
+
+fn relay_status(
+ config: &RuntimeConfig,
+ args: &OrderStatusArgs,
) -> Result<OrderStatusView, RuntimeError> {
if config.relay.urls.is_empty() {
return Ok(OrderStatusView {
@@ -2311,6 +2328,271 @@ pub fn status(
Ok(view)
}
+fn sdk_order_status_view(receipt: OrderStatusReceipt) -> OrderStatusView {
+ let state = sdk_order_status_state(receipt.status).to_owned();
+ let reducer_issues = receipt
+ .issues
+ .iter()
+ .map(sdk_order_status_issue_view)
+ .collect::<Vec<_>>();
+ let reason = sdk_order_status_reason(receipt.status, receipt.order_id.as_str());
+ let fulfillment = sdk_order_status_fulfillment_view(&receipt, reducer_issues.as_slice());
+ let lifecycle = sdk_order_status_lifecycle_view(&receipt, reducer_issues.as_slice());
+ let payment = Some(sdk_order_status_payment_view(&receipt, reducer_issues.as_slice()));
+
+ OrderStatusView {
+ state,
+ source: ORDER_STATUS_SDK_SOURCE.to_owned(),
+ order_id: receipt.order_id.to_string(),
+ actor_context_source: ORDER_ACTOR_CONTEXT_SDK_LOCAL.to_owned(),
+ request_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ decision_event_id: sdk_event_id_string(receipt.decision_event_id.as_ref()),
+ agreement_event_id: sdk_order_status_agreement_event_id(&receipt),
+ listing_event_id: None,
+ listing_addr: None,
+ buyer_pubkey: None,
+ seller_pubkey: None,
+ economics: None,
+ last_event_id: sdk_event_id_string(receipt.last_event_id.as_ref()),
+ revision: None,
+ inventory: None,
+ fulfillment,
+ lifecycle: Some(lifecycle),
+ payment,
+ reducer_issues,
+ target_relays: Vec::new(),
+ connected_relays: Vec::new(),
+ failed_relays: Vec::new(),
+ fetched_count: 0,
+ decoded_count: receipt.event_count,
+ skipped_count: 0,
+ reason,
+ actions: Vec::new(),
+ }
+}
+
+fn sdk_order_status_state(status: OrderStatusKind) -> &'static str {
+ match status {
+ OrderStatusKind::Missing => "missing",
+ OrderStatusKind::Requested => "requested",
+ OrderStatusKind::Accepted => "accepted",
+ OrderStatusKind::Declined => "declined",
+ OrderStatusKind::Cancelled => "cancelled",
+ OrderStatusKind::Completed => "completed",
+ OrderStatusKind::Disputed => "disputed",
+ OrderStatusKind::Invalid => "invalid",
+ _ => "unknown",
+ }
+}
+
+fn sdk_order_status_reason(status: OrderStatusKind, order_id: &str) -> Option<String> {
+ match status {
+ OrderStatusKind::Missing => {
+ Some(format!("no local SDK order events matched `{order_id}`"))
+ }
+ OrderStatusKind::Invalid => Some(format!(
+ "local SDK order events for `{order_id}` failed reducer validation"
+ )),
+ _ => None,
+ }
+}
+
+fn sdk_order_status_agreement_event_id(receipt: &OrderStatusReceipt) -> Option<String> {
+ match receipt.status {
+ OrderStatusKind::Accepted
+ | OrderStatusKind::Cancelled
+ | OrderStatusKind::Completed
+ | OrderStatusKind::Disputed => sdk_event_id_string(receipt.decision_event_id.as_ref()),
+ _ => None,
+ }
+}
+
+fn sdk_order_status_fulfillment_view(
+ receipt: &OrderStatusReceipt,
+ issues: &[OrderIssueView],
+) -> Option<OrderStatusFulfillmentView> {
+ let fulfillment_issues = issues
+ .iter()
+ .filter(|issue| {
+ issue.code.starts_with("fulfillment_") || issue.code == "forked_fulfillments"
+ })
+ .cloned()
+ .collect::<Vec<_>>();
+ if !fulfillment_issues.is_empty() {
+ return Some(OrderStatusFulfillmentView {
+ state: "invalid".to_owned(),
+ event_id: sdk_event_id_string(receipt.fulfillment_event_id.as_ref()),
+ root_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ prev_event_id: sdk_event_id_string(receipt.decision_event_id.as_ref()),
+ terminal: false,
+ inventory_released: false,
+ issues: fulfillment_issues,
+ });
+ }
+ let fulfillment_status = receipt.fulfillment_status?;
+ Some(OrderStatusFulfillmentView {
+ state: sdk_fulfillment_status_state(fulfillment_status).to_owned(),
+ event_id: sdk_event_id_string(receipt.fulfillment_event_id.as_ref()),
+ root_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ prev_event_id: sdk_event_id_string(receipt.decision_event_id.as_ref()),
+ terminal: matches!(
+ fulfillment_status,
+ OrderFulfillmentStatusKind::Delivered | OrderFulfillmentStatusKind::SellerCancelled
+ ),
+ inventory_released: matches!(
+ fulfillment_status,
+ OrderFulfillmentStatusKind::SellerCancelled
+ ),
+ issues: Vec::new(),
+ })
+}
+
+fn sdk_order_status_payment_view(
+ receipt: &OrderStatusReceipt,
+ issues: &[OrderIssueView],
+) -> OrderStatusPaymentView {
+ let payment_issues = issues
+ .iter()
+ .filter(|issue| {
+ issue.code.starts_with("payment_") || issue.code.starts_with("settlement_")
+ })
+ .cloned()
+ .collect::<Vec<_>>();
+ OrderStatusPaymentView {
+ state: sdk_payment_state(receipt.payment_state).to_owned(),
+ settlement_state: sdk_settlement_state(receipt.settlement_state).to_owned(),
+ payment_event_id: None,
+ settlement_event_id: None,
+ agreement_event_id: sdk_order_status_agreement_event_id(receipt),
+ quote_id: None,
+ quote_version: None,
+ economics_digest: None,
+ amount: None,
+ currency: None,
+ method: None,
+ reference: None,
+ paid_at: None,
+ reason: None,
+ issues: payment_issues,
+ }
+}
+
+fn sdk_order_status_lifecycle_view(
+ receipt: &OrderStatusReceipt,
+ issues: &[OrderIssueView],
+) -> OrderStatusLifecycleView {
+ let cancellation = receipt.cancellation_event_id.as_ref().map(|event_id| {
+ OrderStatusLifecycleCancellationView {
+ event_id: event_id.to_string(),
+ root_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ prev_event_id: sdk_event_id_string(receipt.decision_event_id.as_ref()),
+ reason: None,
+ }
+ });
+ let receipt_view = receipt.receipt_event_id.as_ref().map(|event_id| {
+ OrderStatusLifecycleReceiptView {
+ event_id: event_id.to_string(),
+ root_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ prev_event_id: sdk_event_id_string(receipt.fulfillment_event_id.as_ref()),
+ received: matches!(receipt.status, OrderStatusKind::Completed),
+ issue: None,
+ received_at: None,
+ }
+ });
+
+ OrderStatusLifecycleView {
+ phase: sdk_order_status_lifecycle_phase(receipt).to_owned(),
+ terminal: receipt.lifecycle_terminal,
+ event_id: sdk_event_id_string(receipt.last_event_id.as_ref()),
+ root_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
+ prev_event_id: None,
+ cancellation,
+ receipt: receipt_view,
+ settlement_required: !matches!(
+ receipt.settlement_state,
+ OrderSettlementStateKind::NotRequired
+ ),
+ settlement_reason: None,
+ issues: issues.to_vec(),
+ }
+}
+
+fn sdk_order_status_lifecycle_phase(receipt: &OrderStatusReceipt) -> &'static str {
+ match receipt.status {
+ OrderStatusKind::Missing => "missing",
+ OrderStatusKind::Requested => "requested",
+ OrderStatusKind::Accepted => match receipt.fulfillment_status {
+ Some(OrderFulfillmentStatusKind::Preparing)
+ | Some(OrderFulfillmentStatusKind::OutForDelivery) => "fulfillment_in_progress",
+ Some(
+ OrderFulfillmentStatusKind::ReadyForPickup
+ | OrderFulfillmentStatusKind::Delivered
+ | OrderFulfillmentStatusKind::SellerCancelled,
+ ) => "fulfilled",
+ Some(OrderFulfillmentStatusKind::AcceptedNotFulfilled) | None => "accepted",
+ Some(_) => "accepted",
+ },
+ OrderStatusKind::Declined => "declined",
+ OrderStatusKind::Cancelled => "cancelled",
+ OrderStatusKind::Completed => "completed",
+ OrderStatusKind::Disputed => "disputed",
+ OrderStatusKind::Invalid => "invalid",
+ _ => "unknown",
+ }
+}
+
+fn sdk_fulfillment_status_state(status: OrderFulfillmentStatusKind) -> &'static str {
+ match status {
+ OrderFulfillmentStatusKind::AcceptedNotFulfilled => "accepted_not_fulfilled",
+ OrderFulfillmentStatusKind::Preparing => "preparing",
+ OrderFulfillmentStatusKind::ReadyForPickup => "ready_for_pickup",
+ OrderFulfillmentStatusKind::OutForDelivery => "out_for_delivery",
+ OrderFulfillmentStatusKind::Delivered => "delivered",
+ OrderFulfillmentStatusKind::SellerCancelled => "seller_cancelled",
+ _ => "unknown",
+ }
+}
+
+fn sdk_payment_state(state: OrderPaymentStateKind) -> &'static str {
+ match state {
+ OrderPaymentStateKind::NotRecorded => "not_recorded",
+ OrderPaymentStateKind::Recorded => "recorded",
+ OrderPaymentStateKind::Settled => "settled",
+ OrderPaymentStateKind::Rejected => "rejected",
+ OrderPaymentStateKind::Invalid => "invalid",
+ _ => "unknown",
+ }
+}
+
+fn sdk_settlement_state(state: OrderSettlementStateKind) -> &'static str {
+ match state {
+ OrderSettlementStateKind::NotRequired => "not_required",
+ OrderSettlementStateKind::Pending => "pending",
+ OrderSettlementStateKind::Accepted => "accepted",
+ OrderSettlementStateKind::Rejected => "rejected",
+ OrderSettlementStateKind::Invalid => "invalid",
+ _ => "unknown",
+ }
+}
+
+fn sdk_order_status_issue_view(issue: &SdkOrderStatusIssue) -> OrderIssueView {
+ let code = issue.code();
+ OrderIssueView {
+ code: code.clone(),
+ field: "sdk_order_status".to_owned(),
+ message: format!("SDK order status reported `{code}`"),
+ event_ids: issue
+ .event_ids
+ .iter()
+ .map(RadrootsEventId::to_string)
+ .collect(),
+ }
+}
+
+fn sdk_event_id_string(event_id: Option<&RadrootsEventId>) -> Option<String> {
+ event_id.map(RadrootsEventId::to_string)
+}
+
enum OrderStatusRecord {
Request {
listing_event_id: Option<String>,
@@ -12739,6 +13021,10 @@ mod tests {
use radroots_nostr::prelude::{radroots_event_from_nostr, radroots_nostr_build_event};
use radroots_runtime_paths::RadrootsMigrationReport;
use radroots_secret_vault::RadrootsSecretBackend;
+ use radroots_sdk::{
+ OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind, OrderStatusReceipt,
+ SdkOrderStatusIssue, SdkOrderStatusIssueKind, SdkOrderStatusSource,
+ };
use radroots_trade::order::{
RadrootsListingInventoryAccountingInputs, RadrootsListingInventoryBinAvailability,
RadrootsOrderCancellationRecord, RadrootsOrderDecisionRecord,
@@ -12780,7 +13066,7 @@ mod tests {
order_status_reduction_from_receipt_with_context, order_submit_dry_run_view,
order_submit_existing_request_view_from_receipt,
order_submit_listing_provenance_preflight_view, proposed_accept_decision_record,
- resolve_local_order_fulfillment_signing_identity,
+ resolve_local_order_fulfillment_signing_identity, sdk_order_status_view,
seller_order_request_resolution_from_receipt,
};
use crate::cli::global::{
@@ -14552,6 +14838,108 @@ mod tests {
}
#[test]
+ fn sdk_order_status_view_reports_found_local_projection() {
+ let request_event_id = test_event_id_char('1');
+ let decision_event_id = test_event_id_char('2');
+ let receipt = OrderStatusReceipt {
+ order_id: test_order_id("ord_AAAAAAAAAAAAAAAAAAAAAg"),
+ source: SdkOrderStatusSource::LocalEventStore,
+ found: true,
+ event_count: 2,
+ limit_applied: 500,
+ status: OrderStatusKind::Accepted,
+ fulfillment_status: None,
+ payment_state: OrderPaymentStateKind::NotRecorded,
+ settlement_state: OrderSettlementStateKind::NotRequired,
+ lifecycle_terminal: false,
+ event_ids: vec![request_event_id.clone(), decision_event_id.clone()],
+ request_event_id: Some(request_event_id.clone()),
+ decision_event_id: Some(decision_event_id.clone()),
+ fulfillment_event_id: None,
+ cancellation_event_id: None,
+ receipt_event_id: None,
+ last_event_id: Some(decision_event_id.clone()),
+ issues: Vec::new(),
+ };
+
+ let view = sdk_order_status_view(receipt);
+
+ assert_eq!(view.state, "accepted");
+ assert_eq!(view.source, "SDK local order projection");
+ assert_eq!(view.actor_context_source, "sdk_local_projection");
+ assert_eq!(
+ view.request_event_id.as_deref(),
+ Some(request_event_id.as_str())
+ );
+ assert_eq!(
+ view.decision_event_id.as_deref(),
+ Some(decision_event_id.as_str())
+ );
+ assert_eq!(
+ view.agreement_event_id.as_deref(),
+ Some(decision_event_id.as_str())
+ );
+ assert_eq!(view.last_event_id.as_deref(), Some(decision_event_id.as_str()));
+ assert_eq!(view.fetched_count, 0);
+ assert_eq!(view.decoded_count, 2);
+ assert_eq!(view.skipped_count, 0);
+ assert!(view.target_relays.is_empty());
+ assert!(view.connected_relays.is_empty());
+ assert!(view.failed_relays.is_empty());
+ assert!(view.reducer_issues.is_empty());
+ let lifecycle = view.lifecycle.expect("lifecycle");
+ assert_eq!(lifecycle.phase, "accepted");
+ assert!(!lifecycle.terminal);
+ assert!(!lifecycle.settlement_required);
+ }
+
+ #[test]
+ fn sdk_order_status_view_maps_stable_issue_codes() {
+ let request_event_id = test_event_id_char('1');
+ let fork_event_id = test_event_id_char('3');
+ let receipt = OrderStatusReceipt {
+ order_id: test_order_id("ord_AAAAAAAAAAAAAAAAAAAAAg"),
+ source: SdkOrderStatusSource::LocalEventStore,
+ found: true,
+ event_count: 2,
+ limit_applied: 500,
+ status: OrderStatusKind::Invalid,
+ fulfillment_status: None,
+ payment_state: OrderPaymentStateKind::NotRecorded,
+ settlement_state: OrderSettlementStateKind::NotRequired,
+ lifecycle_terminal: false,
+ event_ids: vec![request_event_id, fork_event_id.clone()],
+ request_event_id: None,
+ decision_event_id: None,
+ fulfillment_event_id: None,
+ cancellation_event_id: None,
+ receipt_event_id: None,
+ last_event_id: Some(fork_event_id.clone()),
+ issues: vec![SdkOrderStatusIssue {
+ kind: SdkOrderStatusIssueKind::MultipleRequests,
+ event_ids: vec![fork_event_id.clone()],
+ }],
+ };
+
+ let view = sdk_order_status_view(receipt);
+
+ assert_eq!(view.state, "invalid");
+ assert_eq!(
+ view.reason.as_deref(),
+ Some(
+ "local SDK order events for `ord_AAAAAAAAAAAAAAAAAAAAAg` failed reducer validation"
+ )
+ );
+ assert_eq!(view.reducer_issues.len(), 1);
+ assert_eq!(view.reducer_issues[0].code, "multiple_requests");
+ assert_eq!(view.reducer_issues[0].field, "sdk_order_status");
+ assert_eq!(
+ view.reducer_issues[0].event_ids,
+ vec![fork_event_id.to_string()]
+ );
+ }
+
+ #[test]
fn order_status_from_receipt_reports_requested() {
let fixture = order_status_fixture();
let receipt = DirectRelayFetchReceipt {
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -2225,15 +2225,20 @@ fn local_order_failure_envelopes_are_structured_and_actionable() {
assert_no_daemon_runtime_reference(&submit, &submit_args);
let status_args = ["--format", "json", "order", "status", "get", "ord_missing"];
- let (status_output, status) = sandbox.json_output(&status_args);
- assert!(!status_output.status.success());
- assert_eq!(status["errors"][0]["code"], "operation_unavailable");
- assert_eq!(status["errors"][0]["detail"]["state"], "unconfigured");
- assert_eq!(status["errors"][0]["detail"]["order_id"], "ord_missing");
- assert_eq!(status["errors"][0]["detail"]["fetched_count"], 0);
- assert_eq!(
- status["next_actions"][0]["command"],
- "radroots --relay wss://relay.example.com order status get ord_missing"
+ let status = sandbox.json_success(&status_args);
+ assert_eq!(status["operation_id"], "order.status.get");
+ assert_eq!(status["result"]["state"], "missing");
+ assert_eq!(status["result"]["source"], "SDK local order projection");
+ assert_eq!(
+ status["result"]["actor_context_source"],
+ "sdk_local_projection"
+ );
+ assert_eq!(status["result"]["order_id"], "ord_missing");
+ assert_eq!(status["result"]["fetched_count"], 0);
+ assert_eq!(status["result"]["decoded_count"], 0);
+ assert_eq!(
+ status["result"]["reason"],
+ "no local SDK order events matched `ord_missing`"
);
assert_no_daemon_runtime_reference(&status, &status_args);
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -3386,19 +3386,6 @@ fn online_requires_relay_for_external_network_operations() {
["--format", "json", "--online", "order", "event", "list"].as_slice(),
),
(
- "order.status.get",
- [
- "--format",
- "json",
- "--online",
- "order",
- "status",
- "get",
- "ord_missing",
- ]
- .as_slice(),
- ),
- (
"order.cancel",
[
"--format",
@@ -3519,6 +3506,75 @@ fn online_requires_relay_for_external_network_operations() {
}
#[test]
+fn order_status_get_uses_sdk_local_projection_without_relay_fetch() {
+ let sandbox = RadrootsCliSandbox::new();
+ let local = sandbox.json_success(&[
+ "--format",
+ "json",
+ "--online",
+ "order",
+ "status",
+ "get",
+ "ord_missing",
+ ]);
+
+ assert_eq!(local["operation_id"], "order.status.get");
+ assert_eq!(local["result"]["state"], "missing");
+ assert_eq!(local["result"]["source"], "SDK local order projection");
+ assert_eq!(
+ local["result"]["actor_context_source"],
+ "sdk_local_projection"
+ );
+ assert_eq!(local["result"]["fetched_count"], 0);
+ assert_eq!(local["result"]["decoded_count"], 0);
+
+ let listener = TcpListener::bind("127.0.0.1:0").expect("bind closed relay");
+ let closed_relay = format!("ws://{}", listener.local_addr().expect("relay addr"));
+ drop(listener);
+ let with_closed_relay = sandbox.json_success(&[
+ "--format",
+ "json",
+ "--relay",
+ closed_relay.as_str(),
+ "order",
+ "status",
+ "get",
+ "ord_missing",
+ ]);
+
+ assert_eq!(with_closed_relay["operation_id"], "order.status.get");
+ assert_eq!(with_closed_relay["result"]["state"], "missing");
+ assert_eq!(
+ with_closed_relay["result"]["source"],
+ "SDK local order projection"
+ );
+ assert_eq!(with_closed_relay["result"]["fetched_count"], 0);
+ assert_eq!(with_closed_relay["result"]["decoded_count"], 0);
+}
+
+#[test]
+fn order_status_get_invalid_order_id_uses_sdk_error_contract() {
+ let sandbox = RadrootsCliSandbox::new();
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "order",
+ "status",
+ "get",
+ "bad order id",
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(value["operation_id"], "order.status.get");
+ assert_eq!(value["result"], Value::Null);
+ assert_eq!(value["errors"][0]["code"], "invalid_order_id");
+ assert_eq!(value["errors"][0]["exit_code"], 2);
+ assert_eq!(value["errors"][0]["detail"]["class"], "request");
+ assert_eq!(value["errors"][0]["detail"]["retryable"], false);
+ assert_eq!(value["errors"][0]["detail"]["detail"]["value"], "bad order id");
+}
+
+#[test]
fn radrootsd_sync_push_failure_exposes_nostr_relay_recovery_action() {
let sandbox = RadrootsCliSandbox::new();
let (json_output, value) = sandbox.json_output(&[
@@ -6911,26 +6967,26 @@ fn order_status_and_event_list_use_draft_context_after_account_override_drift()
.as_str()
.expect("drift account id");
- let status_relay = RelayFetchServer::with_events(vec![event.clone()]);
let status = sandbox.json_success(&[
"--format",
"json",
"--account-id",
drift_account_id,
- "--relay",
- status_relay.endpoint(),
"order",
"status",
"get",
order_id,
]);
- status_relay.join();
assert_eq!(status["operation_id"], "order.status.get");
- assert_eq!(status["result"]["actor_context_source"], "order_draft");
- assert_eq!(status["result"]["state"], "requested");
- assert_eq!(status["result"]["request_event_id"], event.id.to_string());
- assert_eq!(status["result"]["buyer_pubkey"], buyer.public_key_hex());
+ assert_eq!(status["result"]["source"], "SDK local order projection");
+ assert_eq!(
+ status["result"]["actor_context_source"],
+ "sdk_local_projection"
+ );
+ assert_eq!(status["result"]["state"], "missing");
+ assert_eq!(status["result"]["fetched_count"], 0);
+ assert_eq!(status["result"]["decoded_count"], 0);
let event_list_relay = RelayFetchServer::with_events(vec![event]);
let events = sandbox.json_success(&[