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:
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");