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:
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"