cli

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

commit ad99087e5f470951d93e97c13855eef09bc4e5a1
parent 7317ad8e30d2369c9d4bb4d1e183ad8926fde289
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 00:42:57 +0000

cli: fix signer and myc failure exits

Diffstat:
Msrc/commands/identity.rs | 3+++
Msrc/commands/myc.rs | 3+++
Msrc/commands/signer.rs | 3+++
Msrc/domain/runtime.rs | 12++++++++++++
Mtests/myc_status.rs | 38++++++++++++++++++++++++++++++++++++--
Mtests/signer_status.rs | 34++++++++++++++++++++++++++++++++++
6 files changed, 91 insertions(+), 2 deletions(-)

diff --git a/src/commands/identity.rs b/src/commands/identity.rs @@ -46,5 +46,8 @@ pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> { CommandDisposition::ExternalUnavailable => { CommandOutput::external_unavailable(CommandView::IdentityShow(view)) } + CommandDisposition::InternalError => { + CommandOutput::internal_error(CommandView::IdentityShow(view)) + } }) } diff --git a/src/commands/myc.rs b/src/commands/myc.rs @@ -11,5 +11,8 @@ pub fn status(config: &RuntimeConfig) -> CommandOutput { CommandDisposition::ExternalUnavailable => { CommandOutput::external_unavailable(CommandView::MycStatus(view)) } + CommandDisposition::InternalError => { + CommandOutput::internal_error(CommandView::MycStatus(view)) + } } } diff --git a/src/commands/signer.rs b/src/commands/signer.rs @@ -12,5 +12,8 @@ pub fn status(config: &RuntimeConfig) -> CommandOutput { CommandDisposition::ExternalUnavailable => { CommandOutput::external_unavailable(CommandView::SignerStatus(view)) } + CommandDisposition::InternalError => { + CommandOutput::internal_error(CommandView::SignerStatus(view)) + } } } diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs @@ -30,6 +30,13 @@ impl CommandOutput { } } + pub fn internal_error(view: CommandView) -> Self { + Self { + disposition: CommandDisposition::InternalError, + view, + } + } + pub fn exit_code(&self) -> ExitCode { self.disposition.exit_code() } @@ -44,6 +51,7 @@ pub enum CommandDisposition { Success, Unconfigured, ExternalUnavailable, + InternalError, } impl CommandDisposition { @@ -52,6 +60,7 @@ impl CommandDisposition { Self::Success => ExitCode::SUCCESS, Self::Unconfigured => ExitCode::from(3), Self::ExternalUnavailable => ExitCode::from(4), + Self::InternalError => ExitCode::from(1), } } } @@ -154,7 +163,9 @@ impl SignerStatusView { pub fn disposition(&self) -> CommandDisposition { match self.state.as_str() { "unconfigured" => CommandDisposition::Unconfigured, + "degraded" => CommandDisposition::ExternalUnavailable, "unavailable" => CommandDisposition::ExternalUnavailable, + "error" => CommandDisposition::InternalError, _ => CommandDisposition::Success, } } @@ -187,6 +198,7 @@ impl MycStatusView { pub fn disposition(&self) -> CommandDisposition { match self.state.as_str() { "unconfigured" => CommandDisposition::Unconfigured, + "degraded" => CommandDisposition::ExternalUnavailable, "unavailable" => CommandDisposition::ExternalUnavailable, _ => CommandDisposition::Success, } diff --git a/tests/myc_status.rs b/tests/myc_status.rs @@ -74,7 +74,41 @@ fn myc_status_reports_unavailable_for_invalid_status_payload() { } #[test] -fn signer_status_reports_myc_backend_details_when_configured() { +fn myc_status_reports_degraded_service_as_external_unavailable() { + let _guard = myc_test_guard(); + let dir = tempdir().expect("tempdir"); + let executable = write_fake_myc( + dir.path(), + successful_status_script(sample_status_payload(false).to_string()).as_str(), + ); + + let output = Command::cargo_bin("radroots") + .expect("binary") + .args([ + "--json", + "--myc-executable", + executable.to_str().expect("executable path"), + "myc", + "status", + ]) + .output() + .expect("run myc status"); + + assert_eq!(output.status.code(), Some(4)); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + let json: Value = serde_json::from_str(stdout.as_str()).expect("json output"); + assert_eq!(json["state"], "degraded"); + assert_eq!(json["service_status"], "degraded"); + assert_eq!(json["ready"], false); + assert!( + json["reason"] + .as_str() + .is_some_and(|value| value.contains("transport quorum is below target")) + ); +} + +#[test] +fn signer_status_reports_degraded_myc_backend_as_external_unavailable() { let _guard = myc_test_guard(); let dir = tempdir().expect("tempdir"); let executable = write_fake_myc( @@ -96,7 +130,7 @@ fn signer_status_reports_myc_backend_details_when_configured() { .output() .expect("run signer status"); - assert!(output.status.success()); + assert_eq!(output.status.code(), Some(4)); let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); let json: Value = serde_json::from_str(stdout.as_str()).expect("json output"); assert_eq!(json["backend"], "myc"); diff --git a/tests/signer_status.rs b/tests/signer_status.rs @@ -1,3 +1,4 @@ +use std::fs; use std::process::Command; use assert_cmd::prelude::*; @@ -77,3 +78,36 @@ fn signer_status_reports_local_unconfigured_when_identity_is_missing() { ); assert_eq!(json["local"], Value::Null); } + +#[test] +fn signer_status_reports_internal_error_for_invalid_identity_file() { + let dir = tempdir().expect("tempdir"); + let identity_path = dir.path().join("invalid-identity.json"); + fs::write(&identity_path, "{ not valid json").expect("write invalid identity"); + + let output = Command::cargo_bin("radroots") + .expect("binary") + .args([ + "--json", + "--identity-path", + identity_path.to_str().expect("identity path"), + "--signer-backend", + "local", + "signer", + "status", + ]) + .output() + .expect("run signer status"); + + assert_eq!(output.status.code(), Some(1)); + let stdout = String::from_utf8(output.stdout).expect("utf8 stdout"); + let json: Value = serde_json::from_str(stdout.as_str()).expect("json output"); + assert_eq!(json["backend"], "local"); + assert_eq!(json["state"], "error"); + assert!( + json["reason"] + .as_str() + .is_some_and(|value| value.contains("invalid identity JSON")) + ); + assert_eq!(json["local"], Value::Null); +}