commit 2894c333555a208a0c4449d1ea952c1a8bf33c59
parent 1bf34ff31134165995d533e9e4854a18b6417f10
Author: triesap <tyson@radroots.org>
Date: Wed, 29 Apr 2026 22:29:42 +0000
tests: cover order fulfillment cli
Diffstat:
3 files changed, 394 insertions(+), 1 deletion(-)
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -5031,8 +5031,10 @@ mod tests {
order_history_from_receipt, order_request_filter, order_status_from_receipt,
order_status_from_receipt_with_context, order_status_reduction_from_receipt_with_context,
order_submit_dry_run_view, order_submit_existing_request_view_from_receipt,
- proposed_accept_decision_record, seller_order_request_resolution_from_receipt,
+ proposed_accept_decision_record, resolve_local_order_fulfillment_signing_identity,
+ seller_order_request_resolution_from_receipt,
};
+ use crate::runtime::accounts;
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
@@ -5040,6 +5042,7 @@ mod tests {
SignerBackend, SignerConfig, Verbosity,
};
use crate::runtime::direct_relay::DirectRelayFetchReceipt;
+ use crate::runtime::signer::ActorWriteBindingError;
use crate::runtime_args::{
OrderDecisionArg, OrderDecisionArgs, OrderFulfillmentArgs, OrderSubmitArgs,
};
@@ -6324,6 +6327,203 @@ mod tests {
}
#[test]
+ fn order_fulfillment_preflight_rejects_missing_order() {
+ let dir = tempdir().expect("tempdir");
+ let mut config = sample_config(dir.path());
+ config.relay.urls = vec!["ws://relay.test".to_owned()];
+ let fixture = order_status_fixture();
+ 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::new(),
+ },
+ );
+ let args = fulfillment_args_for_fixture(&fixture, "ready_for_pickup");
+
+ let view =
+ order_fulfillment_preflight_view_from_status(&config, &args, &status_view, None, None)
+ .expect("missing fulfillment preflight");
+
+ assert_eq!(view.state, "missing");
+ assert_eq!(view.event_id, None);
+ assert!(
+ view.reason
+ .as_deref()
+ .expect("reason")
+ .contains("no active order events")
+ );
+ assert_eq!(
+ view.actions,
+ vec![format!("radroots order status get {}", fixture.order_id)]
+ );
+ }
+
+ #[test]
+ fn order_fulfillment_preflight_rejects_requested_order() {
+ let dir = tempdir().expect("tempdir");
+ let mut config = sample_config(dir.path());
+ config.relay.urls = vec!["ws://relay.test".to_owned()];
+ let fixture = order_status_fixture();
+ 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 = fulfillment_args_for_fixture(&fixture, "ready_for_pickup");
+
+ let view =
+ order_fulfillment_preflight_view_from_status(&config, &args, &status_view, None, None)
+ .expect("requested fulfillment preflight");
+
+ assert_eq!(view.state, "requested");
+ let request_event_id = fixture.request_event.id.to_string();
+ assert_eq!(
+ view.request_event_id.as_deref(),
+ Some(request_event_id.as_str())
+ );
+ assert!(view.event_id.is_none());
+ assert!(
+ view.reason
+ .as_deref()
+ .expect("reason")
+ .contains("has no accepted seller decision")
+ );
+ }
+
+ #[test]
+ fn order_fulfillment_preflight_rejects_declined_order() {
+ let dir = tempdir().expect("tempdir");
+ let mut config = sample_config(dir.path());
+ config.relay.urls = vec!["ws://relay.test".to_owned()];
+ let fixture = order_status_fixture();
+ let decision_event = signed_order_decision_event(
+ &fixture.seller,
+ &fixture.request_event,
+ fixture.order_id.as_str(),
+ fixture.listing_addr.as_str(),
+ fixture.buyer_pubkey.as_str(),
+ fixture.seller_pubkey.as_str(),
+ RadrootsTradeOrderDecision::Declined {
+ reason: "out of stock".to_owned(),
+ },
+ );
+ 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(), decision_event.clone()],
+ },
+ );
+ let args = fulfillment_args_for_fixture(&fixture, "ready_for_pickup");
+
+ let view =
+ order_fulfillment_preflight_view_from_status(&config, &args, &status_view, None, None)
+ .expect("declined fulfillment preflight");
+
+ assert_eq!(view.state, "declined");
+ let decision_event_id = decision_event.id.to_string();
+ assert_eq!(
+ view.decision_event_id.as_deref(),
+ Some(decision_event_id.as_str())
+ );
+ assert!(view.event_id.is_none());
+ assert!(
+ view.reason
+ .as_deref()
+ .expect("reason")
+ .contains("was declined")
+ );
+ }
+
+ #[test]
+ fn order_fulfillment_preflight_rejects_invalid_order_state() {
+ let dir = tempdir().expect("tempdir");
+ let mut config = sample_config(dir.path());
+ config.relay.urls = vec!["ws://relay.test".to_owned()];
+ let fixture = order_status_fixture();
+ let accepted_event = signed_order_decision_event(
+ &fixture.seller,
+ &fixture.request_event,
+ fixture.order_id.as_str(),
+ fixture.listing_addr.as_str(),
+ fixture.buyer_pubkey.as_str(),
+ fixture.seller_pubkey.as_str(),
+ RadrootsTradeOrderDecision::Accepted {
+ inventory_commitments: vec![RadrootsTradeInventoryCommitment {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }],
+ },
+ );
+ let declined_event = signed_order_decision_event(
+ &fixture.seller,
+ &fixture.request_event,
+ fixture.order_id.as_str(),
+ fixture.listing_addr.as_str(),
+ fixture.buyer_pubkey.as_str(),
+ fixture.seller_pubkey.as_str(),
+ RadrootsTradeOrderDecision::Declined {
+ reason: "out of stock".to_owned(),
+ },
+ );
+ 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(),
+ accepted_event,
+ declined_event,
+ ],
+ },
+ );
+ let args = fulfillment_args_for_fixture(&fixture, "ready_for_pickup");
+
+ let view =
+ order_fulfillment_preflight_view_from_status(&config, &args, &status_view, None, None)
+ .expect("invalid fulfillment preflight");
+
+ assert_eq!(view.state, "invalid");
+ assert!(view.event_id.is_none());
+ assert_eq!(view.issues.len(), 1);
+ assert_eq!(view.issues[0].code, "conflicting_decisions");
+ assert!(
+ view.reason
+ .as_deref()
+ .expect("reason")
+ .contains("failed reducer validation")
+ );
+ }
+
+ #[test]
+ fn order_fulfillment_signing_rejects_selected_non_seller_account() {
+ let dir = tempdir().expect("tempdir");
+ let config = sample_config(dir.path());
+ accounts::create_or_migrate_default_account(&config).expect("create selected account");
+ let fixture = order_status_fixture();
+
+ let error = resolve_local_order_fulfillment_signing_identity(
+ &config,
+ fixture.seller_pubkey.as_str(),
+ )
+ .expect_err("non seller account rejected");
+
+ let ActorWriteBindingError::Unconfigured(reason) = error;
+ assert!(reason.contains("cannot sign order seller_pubkey"));
+ }
+
+ #[test]
fn order_status_from_receipt_rejects_wrong_decision_counterparty() {
let fixture = order_status_fixture();
let wrong_buyer = RadrootsIdentity::generate();
@@ -7018,6 +7218,17 @@ mod tests {
.expect("seller order request resolution")
}
+ fn fulfillment_args_for_fixture(
+ fixture: &OrderStatusFixture,
+ state: &str,
+ ) -> OrderFulfillmentArgs {
+ OrderFulfillmentArgs {
+ key: fixture.order_id.clone(),
+ state: state.to_owned(),
+ idempotency_key: None,
+ }
+ }
+
fn sample_config(root: &Path) -> RuntimeConfig {
let data = root.join("data");
let logs = root.join("logs");
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -800,6 +800,102 @@ fn local_order_event_list_attempts_configured_direct_relay() {
}
#[test]
+fn local_order_fulfillment_update_requires_configured_relay_before_publish_preflight() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--approval-token",
+ "approve",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_missing_relay",
+ "--state",
+ "ready_for_pickup",
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(output.status.code(), Some(3));
+ assert_eq!(value["operation_id"], "order.fulfillment.update");
+ assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "operation");
+ assert_contains(
+ &value["errors"][0]["message"],
+ "requires at least one configured relay",
+ );
+ assert_no_removed_command_reference(&value, &["order", "fulfillment", "update"]);
+ assert_no_daemon_runtime_reference(&value, &["order", "fulfillment", "update"]);
+}
+
+#[test]
+fn local_order_fulfillment_update_requires_selected_seller_account_before_relay_fetch() {
+ let sandbox = RadrootsCliSandbox::new();
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--relay",
+ "ws://127.0.0.1:9",
+ "--approval-token",
+ "approve",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_no_account",
+ "--state",
+ "ready_for_pickup",
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(output.status.code(), Some(3));
+ assert_eq!(value["operation_id"], "order.fulfillment.update");
+ assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["class"], "operation");
+ assert_contains(
+ &value["errors"][0]["message"],
+ "requires a selected seller account",
+ );
+ assert_no_removed_command_reference(&value, &["order", "fulfillment", "update"]);
+ assert_no_daemon_runtime_reference(&value, &["order", "fulfillment", "update"]);
+}
+
+#[test]
+fn local_order_fulfillment_update_dry_run_attempts_configured_direct_relay() {
+ let sandbox = RadrootsCliSandbox::new();
+ sandbox.json_success(&["--format", "json", "account", "create"]);
+ let relay = "ws://127.0.0.1:9";
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--dry-run",
+ "--relay",
+ relay,
+ "order",
+ "fulfillment",
+ "update",
+ "ord_direct_fulfillment",
+ "--state",
+ "ready_for_pickup",
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(value["dry_run"], true);
+ assert_direct_relay_connection_failure(
+ &value,
+ "order.fulfillment.update",
+ &["order", "fulfillment", "update"],
+ );
+ assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
+ assert_eq!(value["errors"][0]["detail"]["target_relays"][0], relay);
+}
+
+#[test]
fn watch_only_farm_publish_dry_run_fails_as_account_watch_only() {
let sandbox = RadrootsCliSandbox::new();
let public_identity = identity_public(13);
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -476,6 +476,21 @@ fn offline_forbids_external_network_operations() {
"order.submit",
["--format", "json", "--offline", "order", "submit"].as_slice(),
),
+ (
+ "order.fulfillment.update",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_offline_fulfillment",
+ "--state",
+ "ready_for_pickup",
+ ]
+ .as_slice(),
+ ),
] {
let output = radroots()
.args(args)
@@ -544,6 +559,22 @@ fn offline_rejects_order_decision_dry_run() {
]
.as_slice(),
),
+ (
+ "order.fulfillment.update",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--dry-run",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_offline_decision",
+ "--state",
+ "ready_for_pickup",
+ ]
+ .as_slice(),
+ ),
] {
let output = radroots()
.args(args)
@@ -643,6 +674,21 @@ fn online_requires_relay_for_external_network_operations() {
]
.as_slice(),
),
+ (
+ "order.fulfillment.update",
+ [
+ "--format",
+ "json",
+ "--online",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_missing",
+ "--state",
+ "ready_for_pickup",
+ ]
+ .as_slice(),
+ ),
] {
let output = radroots()
.args(args)
@@ -1099,6 +1145,18 @@ fn required_approval_token_rejects_absent_empty_and_whitespace_values() {
"order.decline",
&["order", "decline", "--reason", "out_of_stock"],
);
+ assert_required_approval_token_rejected(
+ &sandbox,
+ "order.fulfillment.update",
+ &[
+ "order",
+ "fulfillment",
+ "update",
+ "ord_pending_fulfillment",
+ "--state",
+ "ready_for_pickup",
+ ],
+ );
}
fn assert_required_approval_token_rejected(
@@ -1125,6 +1183,34 @@ fn assert_required_approval_token_rejected(
}
#[test]
+fn order_fulfillment_update_requires_state_before_approval() {
+ let sandbox = RadrootsCliSandbox::new();
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_missing_state",
+ ]);
+
+ assert_eq!(output.status.code(), Some(2));
+ assert_eq!(value["operation_id"], "order.fulfillment.update");
+ assert_eq!(value["result"], Value::Null);
+ assert_eq!(value["errors"][0]["code"], "invalid_input");
+ assert_eq!(value["errors"][0]["exit_code"], 2);
+ assert!(
+ value["errors"][0]["message"]
+ .as_str()
+ .expect("message")
+ .contains("state")
+ );
+ assert_no_removed_command_reference(&value, &["order", "fulfillment", "update"]);
+ assert_no_daemon_runtime_reference(&value, &["order", "fulfillment", "update"]);
+}
+
+#[test]
fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful() {
let sandbox = RadrootsCliSandbox::new();