cli

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

commit 1b4b602f38885f829ac14266500de0be64171927
parent b77059c19a55db5fe3b540ff804cc47584349e5e
Author: triesap <tyson@radroots.org>
Date:   Fri,  8 May 2026 03:28:52 +0000

farm: gate setup publish actions

- reuse publish readiness for setup action truth
- withhold publish actions for watch-only farms
- preserve executable relay publish actions
- cover create and update action output

Diffstat:
Msrc/runtime/farm.rs | 41++++++++++++++++++++---------------------
Mtests/signer_runtime_modes.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+), 21 deletions(-)

diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -62,7 +62,7 @@ pub fn init(config: &RuntimeConfig, args: &FarmCreateArgs) -> Result<FarmSetupVi &selected_account, &document, Some("The farm draft is local until you publish it.".to_owned()), - farm_setup_actions(config, &document), + farm_setup_actions(config, &document, Some(&selected_account)), config, ) } @@ -95,7 +95,7 @@ pub fn init_preflight( ), )), reason: Some("dry run requested; farm draft was not written".to_owned()), - actions: farm_setup_actions(config, &document), + actions: farm_setup_actions(config, &document, Some(&selected_account)), }) } @@ -136,7 +136,7 @@ pub fn set(config: &RuntimeConfig, args: &FarmUpdateArgs) -> Result<FarmSetView, account_pubkey, )), reason: None, - actions: farm_update_actions(config, &resolved.document), + actions: farm_update_actions(config, &resolved.document, configured_account.as_ref()), }) } @@ -179,7 +179,7 @@ pub fn set_preflight( account_pubkey, )), reason: Some("dry run requested; farm draft was not written".to_owned()), - actions: farm_update_actions(config, &resolved.document), + actions: farm_update_actions(config, &resolved.document, configured_account.as_ref()), }) } @@ -1727,31 +1727,30 @@ fn save_draft_view( }) } -fn farm_update_actions(config: &RuntimeConfig, document: &FarmConfigDocument) -> Vec<String> { - farm_setup_actions(config, document) +fn farm_update_actions( + config: &RuntimeConfig, + document: &FarmConfigDocument, + account: Option<&AccountRecordView>, +) -> Vec<String> { + farm_setup_actions(config, document, account) } -fn farm_setup_actions(config: &RuntimeConfig, document: &FarmConfigDocument) -> Vec<String> { +fn farm_setup_actions( + config: &RuntimeConfig, + document: &FarmConfigDocument, + account: Option<&AccountRecordView>, +) -> Vec<String> { let mut actions = vec!["radroots farm readiness check".to_owned()]; - if farm_config::missing_fields(document).is_empty() && farm_publish_action_available(config) { + if farm_config::missing_fields(document).is_empty() + && account + .map(|account| farm_publish_readiness(config, account).executable) + .unwrap_or(false) + { actions.push("radroots farm publish".to_owned()); } actions } -fn farm_publish_action_available(config: &RuntimeConfig) -> bool { - match config.publish.mode { - PublishMode::NostrRelay => { - !config.relay.urls.is_empty() && matches!(config.signer.backend, SignerBackend::Local) - } - PublishMode::Radrootsd => { - config.rpc.bridge_bearer_token.is_some() - && resolve_radrootsd_signer_session_id(config, &FarmPublishArgs::default()) - .is_some() - } - } -} - fn missing_blocks_listing_defaults(missing: &[FarmMissingField]) -> bool { missing.iter().any(|field| { matches!( diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -1132,6 +1132,102 @@ fn local_farm_publish_fails_without_configured_relay() { } #[test] +fn farm_setup_actions_offer_publish_only_when_relay_publish_executable() { + let sandbox = RadrootsCliSandbox::new(); + sandbox.json_success(&["--format", "json", "account", "create"]); + + let unconfigured = sandbox.json_success(&[ + "--format", + "json", + "farm", + "create", + "--name", + "Green Farm", + "--location", + "farmstand", + "--country", + "US", + "--delivery-method", + "pickup", + ]); + + assert_action_present(&unconfigured, "radroots farm readiness check"); + assert_action_absent(&unconfigured, "radroots farm publish"); + + let configured = sandbox.json_success(&[ + "--format", + "json", + "--relay", + "ws://127.0.0.1:9", + "farm", + "profile", + "update", + "--field", + "name", + "--value", + "Green Farm Updated", + ]); + + assert_action_present(&configured, "radroots farm readiness check"); + assert_action_present(&configured, "radroots farm publish"); +} + +#[test] +fn farm_setup_actions_withhold_publish_for_watch_only_account() { + let sandbox = RadrootsCliSandbox::new(); + let public_identity = identity_public(51); + let public_identity_file = + write_public_identity_profile(&sandbox, "farm-watch-only", &public_identity); + sandbox.json_success(&[ + "--format", + "json", + "--approval-token", + "approve", + "account", + "import", + "--default", + public_identity_file.to_string_lossy().as_ref(), + ]); + + let created = sandbox.json_success(&[ + "--format", + "json", + "--relay", + "ws://127.0.0.1:9", + "farm", + "create", + "--name", + "Watch Farm", + "--location", + "farmstand", + "--country", + "US", + "--delivery-method", + "pickup", + ]); + + assert_action_present(&created, "radroots farm readiness check"); + assert_action_absent(&created, "radroots farm publish"); + + let updated = sandbox.json_success(&[ + "--format", + "json", + "--relay", + "ws://127.0.0.1:9", + "farm", + "profile", + "update", + "--field", + "name", + "--value", + "Watch Farm Updated", + ]); + + assert_action_present(&updated, "radroots farm readiness check"); + assert_action_absent(&updated, "radroots farm publish"); +} + +#[test] fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publish() { let sandbox = RadrootsCliSandbox::new(); sandbox.json_success(&["--format", "json", "account", "create"]); @@ -1778,3 +1874,28 @@ fn assert_relay_url(value: &Value, relay_url: &str) { "expected relay url `{actual}` to match `{relay_url}`" ); } + +fn assert_action_present(value: &Value, action: &str) { + assert!( + action_list(value).iter().any(|entry| *entry == action), + "expected action `{action}` in `{}`", + value["result"]["actions"] + ); +} + +fn assert_action_absent(value: &Value, action: &str) { + assert!( + action_list(value).iter().all(|entry| *entry != action), + "did not expect action `{action}` in `{}`", + value["result"]["actions"] + ); +} + +fn action_list(value: &Value) -> Vec<&str> { + value["result"]["actions"] + .as_array() + .expect("actions") + .iter() + .map(|entry| entry.as_str().expect("action")) + .collect() +}