cli

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

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:
Msrc/main.rs | 34+++++++++++++++++++++++++++-------
Mtests/target_cli.rs | 26++++++++++++++++++++++++++
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([