commit 982502a94010d80a5a8b119941d50813828ce607
parent 49e72b10f1f636fb2c757c3c92514c7d5feacedb
Author: triesap <tyson@radroots.org>
Date: Sat, 9 May 2026 15:41:54 +0000
cli: add sync push mode recovery action
- attach nostr_relay recovery detail to radrootsd sync push mode failures
- expose the recovery action through json ndjson and human output
- keep recovery actions narrow to parser-valid sync push invocation
- cover mode-mismatch envelopes and human rendering in target cli tests
Diffstat:
2 files changed, 92 insertions(+), 4 deletions(-)
diff --git a/src/main.rs b/src/main.rs
@@ -476,13 +476,18 @@ fn validate_publish_mode_contract(
if matches!(config.publish.mode, PublishMode::Radrootsd)
&& requires_nostr_relay_publish_mode(spec.operation_id)
{
+ let message = format!(
+ "`{}` cannot run with publish mode `radrootsd`; radrootsd publish transport is only implemented for farm and listing publish operations",
+ spec.cli_path
+ );
+ let actions = nostr_relay_publish_mode_recovery_actions(spec.operation_id);
return Err(OperationAdapterError::operation_unavailable_with_detail(
spec.operation_id,
- format!(
- "`{}` cannot run with publish mode `radrootsd`; radrootsd publish transport is only implemented for farm and listing publish operations",
- spec.cli_path
- ),
+ message.clone(),
json!({
+ "state": "unavailable",
+ "reason": message,
+ "actions": actions,
"publish": {
"mode": config.publish.mode.as_str(),
"source": config.publish.source.as_str(),
@@ -500,6 +505,14 @@ fn validate_publish_mode_contract(
Ok(())
}
+fn nostr_relay_publish_mode_recovery_actions(operation_id: &str) -> Vec<String> {
+ if operation_id == "sync.push" {
+ vec!["radroots --publish-mode nostr_relay sync push".to_owned()]
+ } else {
+ Vec::new()
+ }
+}
+
fn is_publish_mode_routed_operation(operation_id: &str) -> bool {
matches!(
operation_id,
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -2863,6 +2863,81 @@ fn online_requires_relay_for_external_network_operations() {
}
#[test]
+fn radrootsd_sync_push_failure_exposes_nostr_relay_recovery_action() {
+ let sandbox = RadrootsCliSandbox::new();
+ let (json_output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--publish-mode",
+ "radrootsd",
+ "sync",
+ "push",
+ ]);
+
+ assert_eq!(json_output.status.code(), Some(3));
+ assert_eq!(value["operation_id"], "sync.push");
+ assert_eq!(value["result"], Value::Null);
+ assert_eq!(value["errors"][0]["code"], "operation_unavailable");
+ assert_eq!(value["errors"][0]["detail"]["state"], "unavailable");
+ assert_eq!(value["errors"][0]["detail"]["publish"]["mode"], "radrootsd");
+ assert_eq!(
+ value["errors"][0]["detail"]["publish"]["state"],
+ "unavailable"
+ );
+ assert_eq!(
+ value["errors"][0]["detail"]["actions"][0],
+ "radroots --publish-mode nostr_relay sync push"
+ );
+ assert_eq!(value["next_actions"][0]["kind"], "cli_command");
+ assert_eq!(
+ value["next_actions"][0]["command"],
+ "radroots --publish-mode nostr_relay sync push"
+ );
+
+ let ndjson_output = sandbox
+ .command()
+ .args([
+ "--format",
+ "ndjson",
+ "--publish-mode",
+ "radrootsd",
+ "sync",
+ "push",
+ ])
+ .output()
+ .expect("run sync push ndjson");
+ let frames = ndjson_from_stdout(&ndjson_output);
+ let terminal = frames.last().expect("terminal frame");
+
+ assert_eq!(ndjson_output.status.code(), Some(3));
+ assert_eq!(terminal["operation_id"], "sync.push");
+ assert_eq!(terminal["frame_type"], "error");
+ assert_eq!(
+ terminal["payload"]["next_actions"][0]["command"],
+ "radroots --publish-mode nostr_relay sync push"
+ );
+
+ let human_output = sandbox
+ .command()
+ .args(["--publish-mode", "radrootsd", "sync", "push"])
+ .output()
+ .expect("run sync push human");
+ let stdout = String::from_utf8(human_output.stdout).expect("utf8 stdout");
+
+ assert_eq!(human_output.status.code(), Some(3));
+ assert!(stdout.starts_with("sync.push: error\n"));
+ assert!(stdout.contains("error: operation_unavailable"));
+ assert!(stdout.contains("state: unavailable"));
+ assert!(stdout.contains("publish_mode: radrootsd"));
+ assert!(stdout.contains("publish_state: unavailable"));
+ assert!(
+ stdout.contains("reason: `radroots sync push` cannot run with publish mode `radrootsd`")
+ );
+ assert!(stdout.contains("- radroots --publish-mode nostr_relay sync push"));
+ assert!(serde_json::from_str::<Value>(&stdout).is_err());
+}
+
+#[test]
fn online_order_event_watch_returns_deferred_without_relay_preflight() {
let sandbox = RadrootsCliSandbox::new();
let (output, value) = sandbox.json_output(&[