cli

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

commit 6a6f576e617e1fcca6c3fdbbd631cd627d4b621d
parent 6da69db2f79f812657ce33b6b335aa06ecd0a85b
Author: triesap <tyson@radroots.org>
Date:   Fri, 10 Jul 2026 02:08:54 +0000

cli: consume sdk outcome labels

Route transport, target, and validation receipt outcome labels through
SDK-owned helpers instead of serde JSON conversion.

Remove duplicated target and relay outcome renderers that wildcarded
known variants to unknown across sync, farm, listing, order, and
validation receipt views.

Add CLI source guards and filtered-test coverage for the SDK outcome
label contract.

Diffstat:
Msrc/runtime/farm.rs | 25+++----------------------
Msrc/runtime/listing.rs | 25+++----------------------
Msrc/runtime/order.rs | 23++---------------------
Msrc/runtime/sdk.rs | 293+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/runtime/sync.rs | 25++-----------------------
Msrc/runtime/validation_receipt.rs | 14+++-----------
6 files changed, 292 insertions(+), 113 deletions(-)

diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -34,8 +34,8 @@ use crate::runtime::farm_config::{ }; use crate::runtime::local_events::append_local_work; use crate::runtime::sdk::{ - CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy, - sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, + CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_outcome_kind_label, + sdk_target_policy, sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, }; use crate::runtime::signer::ActorWriteBindingError; use crate::view::runtime::{ @@ -1294,30 +1294,11 @@ fn sdk_push_failed_transport_targets( reason: target .message .clone() - .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()), + .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), }) .collect() } -fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str { - match kind { - PushOutboxTargetOutcomeKind::Accepted => "accepted", - PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted", - PushOutboxTargetOutcomeKind::Blocked => "blocked", - PushOutboxTargetOutcomeKind::RateLimited => "rate_limited", - PushOutboxTargetOutcomeKind::Invalid => "invalid", - PushOutboxTargetOutcomeKind::PowRequired => "pow_required", - PushOutboxTargetOutcomeKind::Restricted => "restricted", - PushOutboxTargetOutcomeKind::AuthRequired => "auth_required", - PushOutboxTargetOutcomeKind::Error => "error", - PushOutboxTargetOutcomeKind::Timeout => "timeout", - PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed", - PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected", - PushOutboxTargetOutcomeKind::Unknown => "unknown", - _ => "unknown", - } -} - fn profile_not_submitted_component( idempotency_key: Option<String>, args: &FarmPublishArgs, diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -48,8 +48,8 @@ use crate::runtime::local_events::{ shared_local_events_db_path, }; use crate::runtime::sdk::{ - CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy, - sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, + CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_outcome_kind_label, + sdk_target_policy, sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, }; use crate::runtime::sync::{ RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness, @@ -2079,30 +2079,11 @@ fn sdk_push_failed_transport_targets( reason: target .message .clone() - .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()), + .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), }) .collect() } -fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str { - match kind { - PushOutboxTargetOutcomeKind::Accepted => "accepted", - PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted", - PushOutboxTargetOutcomeKind::Blocked => "blocked", - PushOutboxTargetOutcomeKind::RateLimited => "rate_limited", - PushOutboxTargetOutcomeKind::Invalid => "invalid", - PushOutboxTargetOutcomeKind::PowRequired => "pow_required", - PushOutboxTargetOutcomeKind::Restricted => "restricted", - PushOutboxTargetOutcomeKind::AuthRequired => "auth_required", - PushOutboxTargetOutcomeKind::Error => "error", - PushOutboxTargetOutcomeKind::Timeout => "timeout", - PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed", - PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected", - PushOutboxTargetOutcomeKind::Unknown => "unknown", - _ => "unknown", - } -} - pub fn update( config: &RuntimeConfig, args: &ListingMutationArgs, diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -83,7 +83,7 @@ use crate::runtime::local_events::{ }; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, fetch_relay_events_via_shared_transport, - sdk_transport_outcome_kind_label, + sdk_target_outcome_kind_label, sdk_transport_outcome_kind_label, }; use crate::runtime::sync::{RelayIngestScope, relay_provenance_relays_for_scope}; use crate::view::runtime::{ @@ -5098,30 +5098,11 @@ fn sdk_push_failed_transport_targets( reason: target .message .clone() - .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()), + .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), }) .collect() } -fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str { - match kind { - PushOutboxTargetOutcomeKind::Accepted => "accepted", - PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted", - PushOutboxTargetOutcomeKind::Blocked => "blocked", - PushOutboxTargetOutcomeKind::RateLimited => "rate_limited", - PushOutboxTargetOutcomeKind::Invalid => "invalid", - PushOutboxTargetOutcomeKind::PowRequired => "pow_required", - PushOutboxTargetOutcomeKind::Restricted => "restricted", - PushOutboxTargetOutcomeKind::AuthRequired => "auth_required", - PushOutboxTargetOutcomeKind::Error => "error", - PushOutboxTargetOutcomeKind::Timeout => "timeout", - PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed", - PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected", - PushOutboxTargetOutcomeKind::Unknown => "unknown", - _ => "unknown", - } -} - fn validate_bound_order_buyer_account( config: &RuntimeConfig, loaded: &LoadedOrderDraft, diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -17,11 +17,12 @@ use radroots_nostr_connect::prelude::{ }; use radroots_sdk::{ HybridProfile, MeshScopeId, NostrProfile, NostrRelayUrlPolicy, ProxyProfile, - PushOutboxTransportOutcomeKind, RadrootsClient, RadrootsClientBuilder, RadrootsSdkError, - RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, - RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, - RadrootsSdkStorageConfig, ReticulumPreviewAgentEndpoint, - ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, ReticulumPreviewProfile, TargetPolicy, + PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, RadrootsClient, + RadrootsClientBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner, + RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, + RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig, + ReticulumPreviewAgentEndpoint, ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, + ReticulumPreviewProfile, TargetPolicy, TradeValidationReceiptNostrRelayOutcomeKind, TradeValidationReceiptNostrRelayTransportOutcomeKind, TransportProfile, }; use radroots_transport_nostr::{ @@ -53,21 +54,23 @@ pub enum CliSdkAdapterError { } pub fn sdk_transport_outcome_kind_label(kind: PushOutboxTransportOutcomeKind) -> String { - sdk_enum_label(kind) + kind.as_str().to_owned() +} + +pub fn sdk_target_outcome_kind_label(kind: PushOutboxTargetOutcomeKind) -> String { + kind.as_str().to_owned() } pub fn sdk_validation_transport_outcome_kind_label( kind: TradeValidationReceiptNostrRelayTransportOutcomeKind, ) -> String { - sdk_enum_label(kind) + kind.as_str().to_owned() } -fn sdk_enum_label<T: serde::Serialize>(value: T) -> String { - match serde_json::to_value(value) { - Ok(serde_json::Value::String(value)) => value, - Ok(value) => panic!("SDK enum serialized to non-string JSON value `{value}`"), - Err(error) => panic!("SDK enum serialization failed: {error}"), - } +pub fn sdk_validation_relay_outcome_kind_label( + kind: TradeValidationReceiptNostrRelayOutcomeKind, +) -> String { + kind.as_str().to_owned() } #[derive(Debug, Clone, PartialEq, Eq)] @@ -778,7 +781,11 @@ mod tests { use std::time::Duration; use radroots_authority::RadrootsEventSigner; - use radroots_sdk::{ProxyAuth, SdkStorageKind, StorageStatusRequest}; + use radroots_sdk::{ + ProxyAuth, PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, SdkStorageKind, + StorageStatusRequest, TradeValidationReceiptNostrRelayOutcomeKind, + TradeValidationReceiptNostrRelayTransportOutcomeKind, + }; use radroots_secret_vault::RadrootsSecretBackend; use tempfile::tempdir; @@ -970,6 +977,29 @@ mod tests { ".fetch_events(", ]; + const SDK_OUTCOME_LABEL_SOURCE_DISALLOWED_TOKENS: &[(&str, &str)] = &[ + ( + concat!("sdk_enum", "_label"), + "serde-backed SDK enum label extraction", + ), + ( + concat!("serde_json::to_value", "(kind)"), + "serde-backed SDK enum label extraction", + ), + ( + concat!("panic!", "(\"SDK enum"), + "production SDK enum label panic", + ), + ( + concat!("fn sdk_target", "_outcome_kind("), + "local SDK target outcome label helper", + ), + ( + concat!("fn sdk_relay", "_outcome_kind("), + "local SDK validation receipt relay label helper", + ), + ]; + const MIGRATED_CLI_PATH_GUARDS: &[MigratedCliPathGuard] = &[ MigratedCliPathGuard { label: "listing publish", @@ -1480,6 +1510,184 @@ mod tests { } #[test] + fn sync_order_transport_label_helpers_cover_public_outcomes() { + for (kind, label) in [ + (PushOutboxTargetOutcomeKind::Accepted, "accepted"), + ( + PushOutboxTargetOutcomeKind::DuplicateAccepted, + "duplicate_accepted", + ), + (PushOutboxTargetOutcomeKind::Blocked, "blocked"), + (PushOutboxTargetOutcomeKind::RateLimited, "rate_limited"), + (PushOutboxTargetOutcomeKind::Invalid, "invalid"), + (PushOutboxTargetOutcomeKind::PowRequired, "pow_required"), + (PushOutboxTargetOutcomeKind::Restricted, "restricted"), + (PushOutboxTargetOutcomeKind::AuthRequired, "auth_required"), + (PushOutboxTargetOutcomeKind::Muted, "muted"), + (PushOutboxTargetOutcomeKind::Unsupported, "unsupported"), + ( + PushOutboxTargetOutcomeKind::PaymentRequired, + "payment_required", + ), + (PushOutboxTargetOutcomeKind::Error, "error"), + (PushOutboxTargetOutcomeKind::Timeout, "timeout"), + ( + PushOutboxTargetOutcomeKind::ConnectionFailed, + "connection_failed", + ), + ( + PushOutboxTargetOutcomeKind::TargetUriRejected, + "target_uri_rejected", + ), + ( + PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted, + "skipped_already_accepted", + ), + ( + PushOutboxTargetOutcomeKind::DeferredUntilImplemented, + "deferred_until_implemented", + ), + ( + PushOutboxTargetOutcomeKind::PreviewUnavailable, + "preview_unavailable", + ), + (PushOutboxTargetOutcomeKind::Unknown, "unknown"), + ] { + assert_eq!(sdk_target_outcome_kind_label(kind), label); + } + + for (kind, label) in [ + (PushOutboxTransportOutcomeKind::Accepted, "accepted"), + ( + PushOutboxTransportOutcomeKind::DuplicateAccepted, + "duplicate_accepted", + ), + (PushOutboxTransportOutcomeKind::Delivered, "delivered"), + (PushOutboxTransportOutcomeKind::Forwarded, "forwarded"), + ( + PushOutboxTransportOutcomeKind::StoredByGateway, + "stored_by_gateway", + ), + (PushOutboxTransportOutcomeKind::Seen, "seen"), + ( + PushOutboxTransportOutcomeKind::DeferredUntilImplemented, + "deferred_until_implemented", + ), + (PushOutboxTransportOutcomeKind::Rejected, "rejected"), + ( + PushOutboxTransportOutcomeKind::RouteUnavailable, + "route_unavailable", + ), + ( + PushOutboxTransportOutcomeKind::PayloadTooLarge, + "payload_too_large", + ), + ( + PushOutboxTransportOutcomeKind::PolicyDenied, + "policy_denied", + ), + (PushOutboxTransportOutcomeKind::Timeout, "timeout"), + ( + PushOutboxTransportOutcomeKind::ConnectionFailed, + "connection_failed", + ), + ( + PushOutboxTransportOutcomeKind::TransportUnavailable, + "transport_unavailable", + ), + ] { + assert_eq!(sdk_transport_outcome_kind_label(kind), label); + } + + for (kind, label) in [ + (TradeValidationReceiptNostrRelayOutcomeKind::Eose, "eose"), + ( + TradeValidationReceiptNostrRelayOutcomeKind::Closed, + "closed", + ), + ( + TradeValidationReceiptNostrRelayOutcomeKind::Notice, + "notice", + ), + ] { + assert_eq!(sdk_validation_relay_outcome_kind_label(kind), label); + } + + for (kind, label) in [ + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Accepted, + "accepted", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::DuplicateAccepted, + "duplicate_accepted", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Blocked, + "blocked", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::RateLimited, + "rate_limited", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Invalid, + "invalid", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::PowRequired, + "pow_required", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Restricted, + "restricted", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::AuthRequired, + "auth_required", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Muted, + "muted", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Unsupported, + "unsupported", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::PaymentRequired, + "payment_required", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Error, + "error", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Timeout, + "timeout", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::ConnectionFailed, + "connection_failed", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::RelayUrlRejected, + "relay_url_rejected", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::SkippedAlreadyAccepted, + "skipped_already_accepted", + ), + ( + TradeValidationReceiptNostrRelayTransportOutcomeKind::Unknown, + "unknown", + ), + ] { + assert_eq!(sdk_validation_transport_outcome_kind_label(kind), label); + } + } + + #[test] fn cli_direct_rr_rs_dependencies_are_classified() { let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); let manifest = fs::read_to_string(&manifest_path).expect("read manifest"); @@ -1556,6 +1764,35 @@ mod tests { } #[test] + fn sync_order_transport_cli_production_sources_use_sdk_outcome_label_contract() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut files = Vec::new(); + collect_rs_files(manifest_dir.join("src").as_path(), &mut files); + files.sort(); + + let findings = files + .iter() + .flat_map(|file| { + let source = fs::read_to_string(file).expect("read cli source"); + let relative_path = relative_source_path(manifest_dir, file.as_path()); + match production_source_without_tests(&relative_path, &source) { + Ok(production_source) => sdk_outcome_label_contract_findings( + &relative_path, + production_source.as_str(), + ), + Err(error) => vec![error], + } + }) + .collect::<Vec<_>>(); + + assert!( + findings.is_empty(), + "CLI production sources violate the SDK outcome label contract:\n{}", + findings.join("\n") + ); + } + + #[test] fn cli_account_create_sources_reject_implicit_identity_ingestion() { let account_source = rust_code_without_non_code( "src/runtime/account.rs", @@ -1938,6 +2175,34 @@ mod tests { .collect() } + fn sdk_outcome_label_contract_findings(label: &str, source: &str) -> Vec<String> { + let mut findings = SDK_OUTCOME_LABEL_SOURCE_DISALLOWED_TOKENS + .iter() + .flat_map(|(token, reason)| { + source.match_indices(token).map(move |(index, _)| { + format!( + "{label}:{} uses forbidden SDK outcome label token `{token}` for {reason}", + line_number(source, index) + ) + }) + }) + .collect::<Vec<_>>(); + + let wildcard_unknown = concat!("_ =>", " \"unknown\""); + for enum_name in [ + "PushOutboxTargetOutcomeKind::", + "TradeValidationReceiptNostrRelayOutcomeKind::", + ] { + if source.contains(enum_name) && source.contains(wildcard_unknown) { + findings.push(format!( + "{label} degrades known `{enum_name}` variants through wildcard unknown label rendering" + )); + } + } + + findings + } + fn production_source_without_tests(path: &str, source: &str) -> Result<String, String> { let code_source = rust_code_without_non_code(path, source)?; let mut production_source = String::with_capacity(code_source.len()); diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -37,7 +37,7 @@ use crate::runtime::RuntimeError; use crate::runtime::config::{RuntimeConfig, TransportProfileKind}; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, fetch_relay_events_via_shared_transport, - sdk_nostr_relay_url_policy, sdk_transport_outcome_kind_label, + sdk_nostr_relay_url_policy, sdk_target_outcome_kind_label, sdk_transport_outcome_kind_label, }; use crate::view::runtime::{ SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView, SyncStatusView, @@ -844,7 +844,7 @@ fn sdk_push_failed_transport_targets( reason: target .message .clone() - .unwrap_or_else(|| sdk_target_outcome_kind(target.outcome_kind).to_owned()), + .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), }) .collect() } @@ -856,27 +856,6 @@ fn sdk_target_accepted(kind: PushOutboxTargetOutcomeKind) -> bool { ) } -fn sdk_target_outcome_kind(kind: PushOutboxTargetOutcomeKind) -> &'static str { - match kind { - PushOutboxTargetOutcomeKind::Accepted => "accepted", - PushOutboxTargetOutcomeKind::DuplicateAccepted => "duplicate_accepted", - PushOutboxTargetOutcomeKind::Blocked => "blocked", - PushOutboxTargetOutcomeKind::RateLimited => "rate_limited", - PushOutboxTargetOutcomeKind::Invalid => "invalid", - PushOutboxTargetOutcomeKind::PowRequired => "pow_required", - PushOutboxTargetOutcomeKind::Restricted => "restricted", - PushOutboxTargetOutcomeKind::AuthRequired => "auth_required", - PushOutboxTargetOutcomeKind::Error => "error", - PushOutboxTargetOutcomeKind::Timeout => "timeout", - PushOutboxTargetOutcomeKind::ConnectionFailed => "connection_failed", - PushOutboxTargetOutcomeKind::TargetUriRejected => "target_uri_rejected", - PushOutboxTargetOutcomeKind::DeferredUntilImplemented => "deferred_until_implemented", - PushOutboxTargetOutcomeKind::PreviewUnavailable => "preview_unavailable", - PushOutboxTargetOutcomeKind::Unknown => "unknown", - _ => "unknown", - } -} - fn usize_from_i64(value: i64) -> usize { usize::try_from(value.max(0)).unwrap_or(usize::MAX) } diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs @@ -19,7 +19,8 @@ use serde_json::Value; use crate::runtime::config::RuntimeConfig; use crate::runtime::sdk::{ - CliSdkAdapterError, CliSdkSession, sdk_validation_transport_outcome_kind_label, + CliSdkAdapterError, CliSdkSession, sdk_validation_relay_outcome_kind_label, + sdk_validation_transport_outcome_kind_label, }; use crate::view::runtime::{CommandDisposition, TransportTargetFailureView}; @@ -1256,20 +1257,11 @@ fn sdk_relay_failures( reason: relay .message .clone() - .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()), + .unwrap_or_else(|| sdk_validation_relay_outcome_kind_label(relay.outcome_kind)), }) .collect() } -fn sdk_relay_outcome_kind(kind: TradeValidationReceiptNostrRelayOutcomeKind) -> &'static str { - match kind { - TradeValidationReceiptNostrRelayOutcomeKind::Eose => "eose", - TradeValidationReceiptNostrRelayOutcomeKind::Closed => "closed", - TradeValidationReceiptNostrRelayOutcomeKind::Notice => "notice", - _ => "unknown", - } -} - fn summary_view( event: &radroots_events::RadrootsNostrEvent, receipt: &RadrootsTradeValidationReceipt,