cli

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

commit 40090ccc3eea57a933fe4e8485644145e4650ea7
parent 7fe415d87616db3338d797c4133fc586710eb1a4
Author: triesap <tyson@radroots.org>
Date:   Wed, 29 Apr 2026 16:12:00 +0000

cli: harden order decision failures

- add validation failure as an explicit command disposition
- classify invalid and already-decided decisions through the domain view
- include lifecycle correlation fields in decision error detail
- cover already-decided detail output through operation tests

Diffstat:
Msrc/domain/runtime.rs | 3+++
Msrc/operation_adapter.rs | 4++++
Msrc/operation_core.rs | 1+
Msrc/operation_farm.rs | 1+
Msrc/operation_order.rs | 35++++++++++++++++++++---------------
5 files changed, 29 insertions(+), 15 deletions(-)

diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -12,6 +12,7 @@ use serde::Serialize; pub enum CommandDisposition { Success, NotFound, + ValidationFailed, Unconfigured, ExternalUnavailable, Unsupported, @@ -23,6 +24,7 @@ impl CommandDisposition { match self { Self::Success => ExitCode::SUCCESS, Self::NotFound => ExitCode::from(4), + Self::ValidationFailed => ExitCode::from(10), Self::Unconfigured => ExitCode::from(3), Self::ExternalUnavailable => ExitCode::from(4), Self::Unsupported => ExitCode::from(5), @@ -1259,6 +1261,7 @@ impl OrderDecisionView { pub fn disposition(&self) -> CommandDisposition { match self.state.as_str() { "missing" => CommandDisposition::NotFound, + "invalid" | "already_decided" => CommandDisposition::ValidationFailed, "unconfigured" => CommandDisposition::Unconfigured, "unavailable" => CommandDisposition::ExternalUnavailable, "error" => CommandDisposition::InternalError, diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs @@ -413,6 +413,10 @@ impl OperationAdapterError { operation_id: operation_id.to_owned(), message, }, + CommandDisposition::ValidationFailed => Self::ValidationFailed { + operation_id: operation_id.to_owned(), + message, + }, CommandDisposition::Unconfigured => Self::unconfigured(operation_id, message), CommandDisposition::ExternalUnavailable => Self::unavailable(operation_id, message), CommandDisposition::Unsupported => Self::InvalidInput { diff --git a/src/operation_core.rs b/src/operation_core.rs @@ -553,6 +553,7 @@ fn local_backup_result( view.reason.clone().unwrap_or_else(|| match disposition { CommandDisposition::Success => "store backup succeeded".to_owned(), CommandDisposition::NotFound => "store backup target was not found".to_owned(), + CommandDisposition::ValidationFailed => "store backup validation failed".to_owned(), CommandDisposition::Unconfigured => "store backup is unconfigured".to_owned(), CommandDisposition::ExternalUnavailable => "store backup is unavailable".to_owned(), CommandDisposition::Unsupported => "store backup is unsupported".to_owned(), diff --git a/src/operation_farm.rs b/src/operation_farm.rs @@ -227,6 +227,7 @@ fn farm_publish_result( view.reason.clone().unwrap_or_else(|| match disposition { CommandDisposition::Success => "farm publish succeeded".to_owned(), CommandDisposition::NotFound => "farm publish target was not found".to_owned(), + CommandDisposition::ValidationFailed => "farm publish validation failed".to_owned(), CommandDisposition::Unconfigured => "farm publish is unconfigured".to_owned(), CommandDisposition::ExternalUnavailable => "farm publish is unavailable".to_owned(), CommandDisposition::Unsupported => "farm publish is unsupported".to_owned(), diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -227,22 +227,21 @@ fn decision_result<R>( where R: OperationResultData, { - if matches!(view.state.as_str(), "already_decided" | "invalid") { - let message = view.reason.clone().unwrap_or_else(|| { - format!( - "order decision failed validation with state `{}`", - view.state - ) - }); - return Err(OperationAdapterError::validation_failed_with_detail( - operation_id, - message, - order_decision_error_detail(view), - )); - } - match view.disposition() { CommandDisposition::Success => serialized_target_result::<R, _>(view), + CommandDisposition::ValidationFailed => { + let message = view.reason.clone().unwrap_or_else(|| { + format!( + "order decision failed validation with state `{}`", + view.state + ) + }); + Err(OperationAdapterError::validation_failed_with_detail( + operation_id, + message, + order_decision_error_detail(view), + )) + } disposition => { let message = view .reason @@ -279,6 +278,7 @@ fn order_decision_error_detail(view: &OrderDecisionView) -> Value { "state": &view.state, "order_id": &view.order_id, "listing_addr": &view.listing_addr, + "listing_event_id": &view.listing_event_id, "request_event_id": &view.request_event_id, "root_event_id": &view.root_event_id, "prev_event_id": &view.prev_event_id, @@ -295,6 +295,8 @@ fn order_decision_error_detail(view: &OrderDecisionView) -> Value { "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, + "idempotency_key": &view.idempotency_key, + "signer_mode": &view.signer_mode, "issues": &view.issues, "actions": &view.actions, }) @@ -587,8 +589,11 @@ mod tests { let detail = output_error.detail.expect("validation detail"); assert_eq!(detail["state"], "already_decided"); assert_eq!(detail["operation_id"], "order.accept"); + assert_eq!(detail["listing_event_id"], "l".repeat(64)); assert_eq!(detail["event_id"], "d".repeat(64)); assert_eq!(detail["event_kind"], 3423); + assert_eq!(detail["idempotency_key"], "idem_test"); + assert_eq!(detail["signer_mode"], "local"); assert_eq!(detail["actions"][0], "radroots order status get ord_test"); } @@ -807,7 +812,7 @@ mod tests { fetched_count: 2, decoded_count: 2, skipped_count: 0, - idempotency_key: None, + idempotency_key: Some("idem_test".to_owned()), signer_mode: Some("local".to_owned()), reason: Some( "order accept refused because order `ord_test` already has a visible `accepted` seller decision"