commit 2549678030119ee3de648b744c3ec6c23224135f
parent 1fde769e6d5114e42f275be5a8214bce594457c3
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 10:25:11 +0000
cli: return not found for missing order submit
- add a not found command disposition for mutation targets
- centralize command disposition mapping through the operation adapter
- keep order get missing responses as successful read views
- verify target cli and signer runtime mode integration tests
Diffstat:
7 files changed, 86 insertions(+), 96 deletions(-)
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -11,6 +11,7 @@ use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandDisposition {
Success,
+ NotFound,
Unconfigured,
ExternalUnavailable,
Unsupported,
@@ -21,6 +22,7 @@ impl CommandDisposition {
pub fn exit_code(self) -> ExitCode {
match self {
Self::Success => ExitCode::SUCCESS,
+ Self::NotFound => ExitCode::from(4),
Self::Unconfigured => ExitCode::from(3),
Self::ExternalUnavailable => ExitCode::from(4),
Self::Unsupported => ExitCode::from(5),
@@ -1164,6 +1166,7 @@ pub struct OrderSubmitView {
impl OrderSubmitView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
+ "missing" => CommandDisposition::NotFound,
"unconfigured" => CommandDisposition::Unconfigured,
"unavailable" => CommandDisposition::ExternalUnavailable,
"error" => CommandDisposition::InternalError,
diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs
@@ -6,6 +6,7 @@ use std::io::ErrorKind;
use serde::Serialize;
use serde_json::{Map, Value, json};
+use crate::domain::runtime::CommandDisposition;
use crate::operation_registry::{OPERATION_REGISTRY, OperationSpec, get_operation};
use crate::output_contract::{
CliExitCode, EnvelopeActor, EnvelopeContext, NextAction, OUTPUT_SCHEMA_VERSION, OutputEnvelope,
@@ -392,6 +393,27 @@ impl OperationAdapterError {
}
}
+ pub fn from_command_disposition(
+ operation_id: &str,
+ disposition: CommandDisposition,
+ message: String,
+ ) -> Self {
+ match disposition {
+ CommandDisposition::Success => Self::Runtime(message),
+ CommandDisposition::NotFound => Self::NotFound {
+ 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 {
+ operation_id: operation_id.to_owned(),
+ message,
+ },
+ CommandDisposition::InternalError => Self::Runtime(message),
+ }
+ }
+
pub fn unconfigured(operation_id: &str, message: String) -> Self {
classify_runtime_failure(
operation_id,
diff --git a/src/operation_core.rs b/src/operation_core.rs
@@ -529,28 +529,17 @@ fn local_backup_result(
CommandDisposition::Success => {
serialized_operation_result::<StoreBackupCreateResult, _>(view)
}
- CommandDisposition::Unconfigured => Err(OperationAdapterError::unconfigured(
+ disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "store backup is unconfigured".to_owned()),
- )),
- CommandDisposition::ExternalUnavailable => Err(OperationAdapterError::unavailable(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "store backup is unavailable".to_owned()),
- )),
- CommandDisposition::Unsupported => Err(invalid_input(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "store backup is unsupported".to_owned()),
- )),
- CommandDisposition::InternalError => Err(OperationAdapterError::Runtime(
- view.reason
- .clone()
- .unwrap_or_else(|| "store backup failed".to_owned()),
+ disposition,
+ 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::Unconfigured => "store backup is unconfigured".to_owned(),
+ CommandDisposition::ExternalUnavailable => "store backup is unavailable".to_owned(),
+ CommandDisposition::Unsupported => "store backup is unsupported".to_owned(),
+ CommandDisposition::InternalError => "store backup failed".to_owned(),
+ }),
)),
}
}
diff --git a/src/operation_farm.rs b/src/operation_farm.rs
@@ -228,29 +228,17 @@ fn farm_publish_result(
) -> Result<OperationResult<FarmPublishResult>, OperationAdapterError> {
match view.disposition() {
CommandDisposition::Success => serialized_operation_result::<FarmPublishResult, _>(view),
- CommandDisposition::Unconfigured => Err(OperationAdapterError::unconfigured(
+ disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "farm publish is unconfigured".to_owned()),
- )),
- CommandDisposition::ExternalUnavailable => Err(OperationAdapterError::unavailable(
- operation_id,
- view.reason
- .clone()
- .unwrap_or_else(|| "farm publish is unavailable".to_owned()),
- )),
- CommandDisposition::Unsupported => Err(OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message: view
- .reason
- .clone()
- .unwrap_or_else(|| "farm publish is unsupported".to_owned()),
- }),
- CommandDisposition::InternalError => Err(OperationAdapterError::Runtime(
- view.reason
- .clone()
- .unwrap_or_else(|| "farm publish failed".to_owned()),
+ disposition,
+ 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::Unconfigured => "farm publish is unconfigured".to_owned(),
+ CommandDisposition::ExternalUnavailable => "farm publish is unavailable".to_owned(),
+ CommandDisposition::Unsupported => "farm publish is unsupported".to_owned(),
+ CommandDisposition::InternalError => "farm publish failed".to_owned(),
+ }),
)),
}
}
diff --git a/src/operation_listing.rs b/src/operation_listing.rs
@@ -233,7 +233,7 @@ where
{
match view.disposition() {
CommandDisposition::Success => serialized_operation_result::<R, _>(view),
- disposition => Err(disposition_error(
+ disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
disposition,
view.reason.clone().unwrap_or_else(|| {
@@ -246,27 +246,6 @@ where
}
}
-fn disposition_error(
- operation_id: &str,
- disposition: CommandDisposition,
- message: String,
-) -> OperationAdapterError {
- match disposition {
- CommandDisposition::Success => OperationAdapterError::Runtime(message),
- CommandDisposition::Unconfigured => {
- OperationAdapterError::unconfigured(operation_id, message)
- }
- CommandDisposition::ExternalUnavailable => {
- OperationAdapterError::unavailable(operation_id, message)
- }
- CommandDisposition::Unsupported => OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::InternalError => OperationAdapterError::Runtime(message),
- }
-}
-
fn json_operation_result<R>(value: Value) -> Result<OperationResult<R>, OperationAdapterError>
where
R: OperationResultData,
diff --git a/src/operation_order.rs b/src/operation_order.rs
@@ -131,7 +131,7 @@ where
{
match view.disposition() {
CommandDisposition::Success => serialized_target_result::<R, _>(view),
- disposition => Err(disposition_error(
+ disposition => Err(OperationAdapterError::from_command_disposition(
operation_id,
disposition,
view.reason
@@ -141,27 +141,6 @@ where
}
}
-fn disposition_error(
- operation_id: &str,
- disposition: CommandDisposition,
- message: String,
-) -> OperationAdapterError {
- match disposition {
- CommandDisposition::Success => OperationAdapterError::Runtime(message),
- CommandDisposition::Unconfigured => {
- OperationAdapterError::unconfigured(operation_id, message)
- }
- CommandDisposition::ExternalUnavailable => {
- OperationAdapterError::unavailable(operation_id, message)
- }
- CommandDisposition::Unsupported => OperationAdapterError::InvalidInput {
- operation_id: operation_id.to_owned(),
- message,
- },
- CommandDisposition::InternalError => OperationAdapterError::Runtime(message),
- }
-}
-
fn required_order_key<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError>
where
P: OperationRequestPayload + OperationRequestData,
@@ -290,7 +269,7 @@ mod tests {
}
#[test]
- fn order_submit_with_approval_preserves_missing_order_truth() {
+ fn order_submit_with_approval_returns_not_found_for_missing_order() {
let dir = tempdir().expect("tempdir");
let config = sample_config(dir.path());
let service = OperationAdapter::new(OrderOperationService::new(&config));
@@ -301,15 +280,12 @@ mod tests {
OrderSubmitRequest::from_data(data(&[("order_id", "ord_missing")])),
)
.expect("order submit request");
- let envelope = service
- .execute(submit)
- .expect("order submit result")
- .to_envelope(context.envelope_context("req_order_submit"))
- .expect("order submit envelope");
+ let error = service.execute(submit).expect_err("missing order error");
+ let output_error = error.to_output_error();
- assert_eq!(envelope.operation_id, "order.submit");
- assert_eq!(envelope.result["state"], "missing");
- assert_eq!(envelope.result["actions"][0], "radroots order list");
+ assert_eq!(output_error.code, "not_found");
+ assert_eq!(output_error.exit_code, 4);
+ assert!(output_error.message.contains("ord_missing"));
}
#[test]
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -597,6 +597,39 @@ fn assert_required_approval_token_rejected(
}
#[test]
+fn order_submit_missing_order_returns_not_found_while_read_view_stays_successful() {
+ let sandbox = RadrootsCliSandbox::new();
+
+ let get = sandbox.json_success(&[
+ "--format",
+ "json",
+ "order",
+ "get",
+ "ord_missing_submit_target",
+ ]);
+ assert_eq!(get["operation_id"], "order.get");
+ assert_eq!(get["result"]["state"], "missing");
+ assert_eq!(get["errors"].as_array().expect("errors").len(), 0);
+
+ let (output, submit) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--approval-token",
+ "approve",
+ "order",
+ "submit",
+ "ord_missing_submit_target",
+ ]);
+
+ assert_eq!(output.status.code(), Some(4));
+ assert_eq!(submit["operation_id"], "order.submit");
+ assert_eq!(submit["errors"][0]["code"], "not_found");
+ assert_eq!(submit["errors"][0]["exit_code"], 4);
+ assert_eq!(submit["errors"][0]["detail"]["class"], "resource");
+ assert_no_removed_command_reference(&submit, &["order", "submit"]);
+}
+
+#[test]
fn buyer_target_flow_acceptance_uses_target_operations() {
let sandbox = RadrootsCliSandbox::new();