cli

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

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:
Msrc/domain/runtime.rs | 3+++
Msrc/operation_adapter.rs | 22++++++++++++++++++++++
Msrc/operation_core.rs | 31++++++++++---------------------
Msrc/operation_farm.rs | 32++++++++++----------------------
Msrc/operation_listing.rs | 23+----------------------
Msrc/operation_order.rs | 38+++++++-------------------------------
Mtests/target_cli.rs | 33+++++++++++++++++++++++++++++++++
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();