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:
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()
+}