commit 49e72b10f1f636fb2c757c3c92514c7d5feacedb
parent 0dd47689eb437531474926b4d8fa50f19c35c61a
Author: triesap <tyson@radroots.org>
Date: Sat, 9 May 2026 15:39:55 +0000
cli: render failure detail in human output
- use structured error detail as the human display source when result is null
- print failure state publish context reason and recovery actions from detail
- keep successful human output concise and non-json
- cover structured not_implemented detail rendering in target cli tests
Diffstat:
2 files changed, 53 insertions(+), 7 deletions(-)
diff --git a/src/main.rs b/src/main.rs
@@ -575,16 +575,22 @@ fn render_human_envelope(
writeln!(handle, "error: {}", error.code)?;
writeln!(handle, "message: {}", error.message)?;
}
- if let Some(mode) = human_publish_mode(&envelope.result) {
+ let display = human_display_source(envelope);
+ if !envelope.errors.is_empty()
+ && let Some(state) = human_state(display)
+ {
+ writeln!(handle, "state: {state}")?;
+ }
+ if let Some(mode) = human_publish_mode(display) {
writeln!(handle, "publish_mode: {mode}")?;
}
- if let Some(state) = human_publish_state(&envelope.result) {
+ if let Some(state) = human_publish_state(display) {
writeln!(handle, "publish_state: {state}")?;
}
- if let Some(reason) = human_reason(&envelope.result) {
+ if let Some(reason) = human_reason(display) {
writeln!(handle, "reason: {reason}")?;
}
- let actions = human_actions(envelope);
+ let actions = human_actions(envelope, display);
if !actions.is_empty() {
writeln!(handle, "next:")?;
for action in actions {
@@ -594,6 +600,21 @@ fn render_human_envelope(
Ok(())
}
+fn human_display_source(envelope: &OutputEnvelope) -> &Value {
+ if !envelope.result.is_null() {
+ return &envelope.result;
+ }
+ envelope
+ .errors
+ .first()
+ .and_then(|error| error.detail.as_ref())
+ .unwrap_or(&envelope.result)
+}
+
+fn human_state(result: &Value) -> Option<&str> {
+ human_string_path(result, &["state"])
+}
+
fn human_publish_mode(result: &Value) -> Option<&str> {
human_string_path(result, &["publish", "mode"])
.or_else(|| human_string_path(result, &["checks", "publish", "mode"]))
@@ -615,9 +636,8 @@ fn human_reason(result: &Value) -> Option<&str> {
.or_else(|| human_string_path(result, &["checks", "account", "reason"]))
}
-fn human_actions(envelope: &OutputEnvelope) -> Vec<String> {
- let mut actions = envelope
- .result
+fn human_actions(envelope: &OutputEnvelope, display: &Value) -> Vec<String> {
+ let mut actions = display
.get("actions")
.and_then(Value::as_array)
.into_iter()
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -2208,6 +2208,32 @@ fn human_failure_output_preserves_error_code_and_message() {
}
#[test]
+fn human_failure_output_renders_structured_error_detail() {
+ let output = radroots()
+ .args([
+ "--format",
+ "human",
+ "order",
+ "event",
+ "watch",
+ "ord_missing",
+ ])
+ .output()
+ .expect("run order event watch");
+
+ assert_eq!(output.status.code(), Some(3));
+ let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
+
+ assert!(stdout.starts_with("order.event.watch: error\n"));
+ assert!(stdout.contains("request_id: req_order_event_watch_"));
+ assert!(stdout.contains("error: not_implemented"));
+ assert!(stdout.contains("state: not_implemented"));
+ assert!(stdout.contains("reason: relay-backed order event watch is not implemented"));
+ assert!(stdout.contains("- radroots order status get ord_missing"));
+ assert!(serde_json::from_str::<Value>(&stdout).is_err());
+}
+
+#[test]
fn request_ids_are_invocation_unique_and_preserve_caller_fields() {
let first = radroots()
.args([