cli

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

commit 91ce165c44c6c3904c4af74dbc1d58d2cd2a2797
parent 5a8a172feb2f2fecf95cb2bb9e195b5d9a282d79
Author: triesap <tyson@radroots.org>
Date:   Thu,  7 May 2026 03:41:49 +0000

cli: align health and publish preflight

Diffstat:
Msrc/main.rs | 2+-
Msrc/operation_core.rs | 30++++++++++++++++++++++++++++--
Mtests/target_cli.rs | 33+++++++++++++++++++++++++++++++++
3 files changed, 62 insertions(+), 3 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -353,8 +353,8 @@ fn validate_request_contract( config: &RuntimeConfig, ) -> Result<(), OperationAdapterError> { validate_pre_runtime_request_contract(request)?; - validate_signer_mode_contract(request, config)?; validate_publish_mode_contract(request, config)?; + validate_signer_mode_contract(request, config)?; validate_network_contract(request, config)?; Ok(()) } diff --git a/src/operation_core.rs b/src/operation_core.rs @@ -102,8 +102,9 @@ impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> { let store = map_runtime(crate::runtime::local::status(self.config))?; let account = map_runtime(resolve_account_resolution(self.config))?; let publish = publish_runtime_view(self.config, true, &account); + let state = health_status_state(&store.state, &publish); json_operation_result::<HealthStatusGetResult>(json!({ - "state": if store.state == "ready" { "ready" } else { "needs_attention" }, + "state": state, "store": store, "account_resolution": account_resolution_view(&account), "publish": publish, @@ -130,8 +131,9 @@ impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> { Some(map_runtime(unresolved_account_reason(self.config))?) }; let publish = publish_runtime_view(self.config, true, &account); + let state = health_check_state(&store.state, account.resolved_account.is_some(), &publish); json_operation_result::<HealthCheckRunResult>(json!({ - "state": if store.state == "ready" && account.resolved_account.is_some() { "ready" } else { "needs_attention" }, + "state": state, "checks": { "workspace": { "state": "ready", @@ -800,6 +802,30 @@ fn nostr_relay_publish_readiness( ("ready", true, None) } +fn health_status_state(store_state: &str, publish: &PublishRuntimeView) -> &'static str { + if store_state == "ready" && publish_runtime_ready(publish) { + "ready" + } else { + "needs_attention" + } +} + +fn health_check_state( + store_state: &str, + account_ready: bool, + publish: &PublishRuntimeView, +) -> &'static str { + if store_state == "ready" && account_ready && publish_runtime_ready(publish) { + "ready" + } else { + "needs_attention" + } +} + +fn publish_runtime_ready(publish: &PublishRuntimeView) -> bool { + !publish.signed_write_required || publish.executable +} + fn required_string<P>( request: &OperationRequest<P>, key: &str, diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -229,6 +229,7 @@ fn health_surfaces_publish_state_under_deferred_signer_mode() { let value = sandbox.json_success(&["--format", "json", "health", "status", "get"]); assert_eq!(value["operation_id"], "health.status.get"); + assert_eq!(value["result"]["state"], "needs_attention"); assert_eq!(value["result"]["publish"]["mode"], "radrootsd"); assert_eq!(value["result"]["publish"]["executable"], false); assert_eq!( @@ -253,6 +254,7 @@ fn health_status_distinguishes_relay_ready_from_missing_signed_write_account() { ]); assert_eq!(value["operation_id"], "health.status.get"); + assert_eq!(value["result"]["state"], "needs_attention"); assert_eq!(value["result"]["publish"]["relay"]["ready"], true); assert_eq!(value["result"]["publish"]["signed_write_required"], true); assert_eq!(value["result"]["publish"]["state"], "unconfigured"); @@ -271,6 +273,7 @@ fn health_check_exposes_publish_readiness() { let value = sandbox.json_success(&["--format", "json", "health", "check", "run"]); assert_eq!(value["operation_id"], "health.check.run"); + assert_eq!(value["result"]["state"], "needs_attention"); assert_eq!(value["result"]["checks"]["publish"]["mode"], "radrootsd"); assert_eq!(value["result"]["checks"]["publish"]["state"], "unavailable"); assert_eq!(value["result"]["checks"]["publish"]["executable"], false); @@ -280,6 +283,7 @@ fn health_check_exposes_publish_readiness() { #[test] fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() { let sandbox = RadrootsCliSandbox::new(); + sandbox.json_success(&["--format", "json", "workspace", "init"]); sandbox.json_success(&["--format", "json", "account", "create"]); let value = sandbox.json_success(&[ @@ -293,6 +297,7 @@ fn health_check_marks_relay_publish_ready_with_secret_backed_local_account() { ]); assert_eq!(value["operation_id"], "health.check.run"); + assert_eq!(value["result"]["state"], "ready"); assert_eq!(value["result"]["checks"]["publish"]["mode"], "nostr_relay"); assert_eq!(value["result"]["checks"]["publish"]["state"], "ready"); assert_eq!(value["result"]["checks"]["publish"]["executable"], true); @@ -335,6 +340,34 @@ fn radrootsd_publish_mode_fails_closed_for_direct_relay_publish_paths() { } #[test] +fn radrootsd_publish_mode_takes_precedence_over_deferred_signer_mode() { + let sandbox = RadrootsCliSandbox::new(); + let missing_listing = sandbox.root().join("missing-listing.toml"); + sandbox.write_app_config("[publish]\nmode = \"radrootsd\"\n\n[signer]\nmode = \"myc\"\n"); + + let (output, value) = sandbox.json_output(&[ + "--format", + "json", + "--approval-token", + "approve", + "listing", + "publish", + missing_listing.to_string_lossy().as_ref(), + ]); + + assert!(!output.status.success()); + assert_eq!(output.status.code(), Some(3)); + assert_eq!(value["operation_id"], "listing.publish"); + assert_eq!(value["errors"][0]["code"], "operation_unavailable"); + assert_eq!(value["errors"][0]["detail"]["class"], "operation"); + assert_eq!(value["errors"][0]["detail"]["publish"]["mode"], "radrootsd"); + assert_eq!( + value["errors"][0]["detail"]["publish"]["provider"]["state"], + "unavailable" + ); +} + +#[test] fn radrootsd_publish_mode_fails_closed_for_listing_update() { let sandbox = RadrootsCliSandbox::new(); let missing_listing = sandbox.root().join("missing-listing.toml");