commit 5c201a9d09da2068d93b6e006f9cf2fe55b5e7dd
parent 520e2820b8f629a2d2ccda229066a071279dfab9
Author: triesap <tyson@radroots.org>
Date: Mon, 13 Jul 2026 06:20:03 +0000
cli: show transport operation capabilities
- add grouped deliver and fetch capabilities to status views
- render capability columns in terminal transport tables
- propagate SDK sync transport capability summaries
- assert operation capability JSON in transport and sync tests
Diffstat:
5 files changed, 93 insertions(+), 4 deletions(-)
diff --git a/src/out/terminal/renderers/runtime.rs b/src/out/terminal/renderers/runtime.rs
@@ -195,6 +195,12 @@ fn push_transport_status_table(document: &mut TerminalDocument, result: &Value,
common::bool_path(transport, &["usable_for_delivery"])
.map(|value| if value { "yes" } else { "no" }.to_owned())
.unwrap_or_default(),
+ common::bool_path(transport, &["capabilities", "deliver"])
+ .map(|value| if value { "yes" } else { "no" }.to_owned())
+ .unwrap_or_default(),
+ common::bool_path(transport, &["capabilities", "fetch"])
+ .map(|value| if value { "yes" } else { "no" }.to_owned())
+ .unwrap_or_default(),
common::string(transport, &["endpoint_uri"]).unwrap_or_default(),
])
})
@@ -207,6 +213,8 @@ fn push_transport_status_table(document: &mut TerminalDocument, result: &Value,
TerminalTableColumn::new("Configured", 10, 10),
TerminalTableColumn::new("Implementation", 14, 24),
TerminalTableColumn::new("Usable", 6, 6),
+ TerminalTableColumn::new("Deliver", 7, 7),
+ TerminalTableColumn::new("Fetch", 5, 5),
TerminalTableColumn::new("Endpoint", 8, 32),
],
rows,
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -42,7 +42,7 @@ use crate::runtime::sdk::{
use crate::view::runtime::{
SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView, SyncStatusView,
SyncTransportStatusView, SyncTransportTargetView, SyncWatchFrameView, SyncWatchView,
- TransportTargetFailureView,
+ TransportOperationCapabilitiesView, TransportTargetFailureView,
};
const SYNC_SOURCE: &str = "local replica ยท local first";
@@ -613,6 +613,10 @@ fn sdk_transport_status_view(
configured: status.configured,
implementation: status.implementation.clone(),
usable_for_delivery: status.usable_for_delivery,
+ capabilities: TransportOperationCapabilitiesView {
+ deliver: status.capabilities.deliver,
+ fetch: status.capabilities.fetch,
+ },
message: status.message.clone(),
}
}
@@ -1015,6 +1019,10 @@ fn sync_transport_status_view(status: RadrootsTransportStatus) -> SyncTransportS
configured: status.configured,
implementation: transport_implementation_label(status.implementation).to_owned(),
usable_for_delivery: status.usable_for_delivery,
+ capabilities: TransportOperationCapabilitiesView {
+ deliver: status.capabilities.deliver,
+ fetch: status.capabilities.fetch,
+ },
message: status.message,
}
}
@@ -1774,7 +1782,8 @@ mod tests {
PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt,
PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind,
SyncEventStoreStatus, SyncOutboxStatus, SyncStatusReceipt, SyncStatusSource,
- SyncTransportProfileSummary, SyncTransportStatusSummary, SyncTransportTargetSummary,
+ SyncTransportOperationCapabilitiesSummary, SyncTransportProfileSummary,
+ SyncTransportStatusSummary, SyncTransportTargetSummary,
};
use radroots_secret_vault::RadrootsSecretBackend;
use radroots_transport::{
@@ -1868,6 +1877,8 @@ mod tests {
assert!(!view.transport_statuses[0].configured);
assert_eq!(view.transport_statuses[0].implementation, "real");
assert!(!view.transport_statuses[0].usable_for_delivery);
+ assert!(!view.transport_statuses[0].capabilities.deliver);
+ assert!(!view.transport_statuses[0].capabilities.fetch);
assert!(view.target_transport_endpoints.is_empty());
assert_eq!(
view.actions,
@@ -1900,6 +1911,8 @@ mod tests {
assert_eq!(view.transport_statuses.len(), 2);
assert_eq!(view.transport_statuses[0].transport, "nostr");
assert!(view.transport_statuses[0].usable_for_delivery);
+ assert!(view.transport_statuses[0].capabilities.deliver);
+ assert!(!view.transport_statuses[0].capabilities.fetch);
assert_eq!(view.transport_statuses[1].transport, "reticulum");
assert!(view.transport_statuses[1].configured);
assert_eq!(
@@ -1907,6 +1920,8 @@ mod tests {
"preview_unavailable"
);
assert!(!view.transport_statuses[1].usable_for_delivery);
+ assert!(!view.transport_statuses[1].capabilities.deliver);
+ assert!(!view.transport_statuses[1].capabilities.fetch);
assert_eq!(
view.target_transport_endpoints,
vec!["wss://relay.example.com"]
@@ -1944,6 +1959,8 @@ mod tests {
assert!(!view.transport_statuses[0].configured);
assert_eq!(view.transport_statuses[0].implementation, "real");
assert!(!view.transport_statuses[0].usable_for_delivery);
+ assert!(!view.transport_statuses[0].capabilities.deliver);
+ assert!(!view.transport_statuses[0].capabilities.fetch);
assert_eq!(view.transport_statuses[1].transport, "reticulum");
assert!(view.transport_statuses[1].configured);
assert_eq!(
@@ -1951,6 +1968,8 @@ mod tests {
"preview_unavailable"
);
assert!(!view.transport_statuses[1].usable_for_delivery);
+ assert!(!view.transport_statuses[1].capabilities.deliver);
+ assert!(!view.transport_statuses[1].capabilities.fetch);
assert!(view.target_transport_endpoints.is_empty());
assert_eq!(view.actions, vec!["radroots transport profile get"]);
}
@@ -1997,6 +2016,8 @@ mod tests {
"preview_unavailable"
);
assert!(!view.transport_statuses[0].usable_for_delivery);
+ assert!(!view.transport_statuses[0].capabilities.deliver);
+ assert!(!view.transport_statuses[0].capabilities.fetch);
assert_eq!(view.fetched_count, None);
assert!(view.target_transport_endpoints.is_empty());
assert!(
@@ -2097,6 +2118,10 @@ mod tests {
configured: true,
implementation: "real".to_owned(),
usable_for_delivery: true,
+ capabilities: SyncTransportOperationCapabilitiesSummary {
+ deliver: true,
+ fetch: false,
+ },
message: "Nostr relay transport is configured for delivery".to_owned(),
},
SyncTransportStatusSummary {
@@ -2106,6 +2131,10 @@ mod tests {
configured: true,
implementation: "preview_unavailable".to_owned(),
usable_for_delivery: false,
+ capabilities: SyncTransportOperationCapabilitiesSummary {
+ deliver: false,
+ fetch: false,
+ },
message: RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned(),
},
],
@@ -2126,6 +2155,8 @@ mod tests {
"preview_unavailable"
);
assert!(!view.transport_statuses[1].usable_for_delivery);
+ assert!(!view.transport_statuses[1].capabilities.deliver);
+ assert!(!view.transport_statuses[1].capabilities.fetch);
}
#[test]
@@ -2482,6 +2513,10 @@ mod tests {
configured: true,
implementation: "real".to_owned(),
usable_for_delivery: true,
+ capabilities: SyncTransportOperationCapabilitiesSummary {
+ deliver: true,
+ fetch: false,
+ },
message: "Nostr relay transport is configured for delivery".to_owned(),
}],
},
@@ -2532,6 +2567,10 @@ mod tests {
configured: true,
implementation: "preview_unavailable".to_owned(),
usable_for_delivery: false,
+ capabilities: SyncTransportOperationCapabilitiesSummary {
+ deliver: false,
+ fetch: false,
+ },
message: RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned(),
}],
},
diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs
@@ -18,8 +18,9 @@ use crate::runtime::RuntimeError;
use crate::runtime::config::{RuntimeConfig, TransportProfileKind};
use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy};
use crate::view::runtime::{
- TransportOutboxPushView, TransportOutboxStatusView, TransportProfileSummaryView,
- TransportProfileView, TransportRuntimeStatusView, TransportStatusView,
+ TransportOperationCapabilitiesView, TransportOutboxPushView, TransportOutboxStatusView,
+ TransportProfileSummaryView, TransportProfileView, TransportRuntimeStatusView,
+ TransportStatusView,
};
const TRANSPORT_SOURCE: &str = "transport profile config";
@@ -551,6 +552,10 @@ fn transport_runtime_status_view(status: RadrootsTransportStatus) -> TransportRu
configured: status.configured,
implementation: transport_implementation_label(status.implementation).to_owned(),
usable_for_delivery: status.usable_for_delivery,
+ capabilities: TransportOperationCapabilitiesView {
+ deliver: status.capabilities.deliver,
+ fetch: status.capabilities.fetch,
+ },
message: status.message,
}
}
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -3318,10 +3318,17 @@ pub struct TransportRuntimeStatusView {
pub configured: bool,
pub implementation: String,
pub usable_for_delivery: bool,
+ pub capabilities: TransportOperationCapabilitiesView,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
+pub struct TransportOperationCapabilitiesView {
+ pub deliver: bool,
+ pub fetch: bool,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct TransportOutboxStatusView {
pub state: String,
pub source: String,
@@ -3569,6 +3576,7 @@ pub struct SyncTransportStatusView {
pub configured: bool,
pub implementation: String,
pub usable_for_delivery: bool,
+ pub capabilities: TransportOperationCapabilitiesView,
pub message: String,
}
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -107,6 +107,11 @@ fn assert_no_removed_transport_status_fields(status: &Value) {
}
}
+fn assert_transport_capabilities(status: &Value, deliver: bool, fetch: bool) {
+ assert_eq!(status["capabilities"]["deliver"], deliver);
+ assert_eq!(status["capabilities"]["fetch"], fetch);
+}
+
fn write_proxy_transport_config(sandbox: &RadrootsCliSandbox, body: &str) -> PathBuf {
sandbox.write_app_config(
format!("[transport]\nprofile = \"proxy\"\n\n[transport.proxy]\n{body}").as_str(),
@@ -1445,6 +1450,7 @@ fn transport_profile_reticulum_preview_output_is_transport_specific() {
result["transport_statuses"][0]["usable_for_delivery"],
false
);
+ assert_transport_capabilities(&result["transport_statuses"][0], false, false);
assert_eq!(
result["transport_statuses"][0]["message"],
RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE
@@ -1492,6 +1498,7 @@ fn transport_profile_local_only_output_uses_profile_and_local_status_fields() {
assert_eq!(local["implementation"], "real");
assert_eq!(local["configured"], true);
assert_eq!(local["usable_for_delivery"], false);
+ assert_transport_capabilities(local, false, false);
assert_eq!(
local["message"],
"Local-only profile writes only to local state"
@@ -1550,6 +1557,7 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() {
value["result"]["transport_statuses"][0]["usable_for_delivery"],
true
);
+ assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false);
assert_eq!(
value["result"]["transport_statuses"][1]["transport"],
"reticulum"
@@ -1563,6 +1571,7 @@ fn transport_profile_set_hybrid_persists_nostr_and_reticulum_preview_config() {
value["result"]["transport_statuses"][1]["usable_for_delivery"],
false
);
+ assert_transport_capabilities(&value["result"]["transport_statuses"][1], false, false);
assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]);
assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][1]);
assert_eq!(
@@ -1623,10 +1632,12 @@ fn transport_status_hybrid_output_reports_nostr_and_reticulum_rows() {
assert_eq!(nostr["implementation"], "real");
assert_eq!(nostr["configured"], true);
assert_eq!(nostr["usable_for_delivery"], true);
+ assert_transport_capabilities(nostr, true, false);
assert_eq!(reticulum["transport"], "reticulum");
assert_eq!(reticulum["implementation"], "preview_unavailable");
assert_eq!(reticulum["configured"], true);
assert_eq!(reticulum["usable_for_delivery"], false);
+ assert_transport_capabilities(reticulum, false, false);
assert_eq!(reticulum["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
assert_no_removed_transport_status_fields(nostr);
assert_no_removed_transport_status_fields(reticulum);
@@ -1652,6 +1663,7 @@ fn transport_status_nostr_output_uses_canonical_transport_row() {
assert_eq!(nostr["implementation"], "real");
assert_eq!(nostr["configured"], true);
assert_eq!(nostr["usable_for_delivery"], true);
+ assert_transport_capabilities(nostr, true, false);
assert_no_removed_transport_status_fields(nostr);
}
@@ -1680,6 +1692,7 @@ fn transport_status_reticulum_preview_output_reports_unusable_preview_state() {
assert_eq!(active["implementation"], "preview_unavailable");
assert_eq!(active["configured"], true);
assert_eq!(active["usable_for_delivery"], false);
+ assert_transport_capabilities(active, false, false);
assert_eq!(active["message"], RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE);
assert!(
!active["message"]
@@ -2004,6 +2017,10 @@ fn transport_source_boundary_rejects_removed_relay_and_publish_proxy_surfaces()
"pub configured: bool,",
"pub implementation: String,",
"pub usable_for_delivery: bool,",
+ "pub capabilities: TransportOperationCapabilitiesView,",
+ "pub struct TransportOperationCapabilitiesView",
+ "pub deliver: bool,",
+ "pub fetch: bool,",
"pub message: String,",
] {
assert!(
@@ -2194,6 +2211,7 @@ fn sync_transport_status_source_boundary_rejects_retired_relay_shaped_generic_ou
"pub configured: bool,",
"pub implementation: String,",
"pub usable_for_delivery: bool,",
+ "pub capabilities: TransportOperationCapabilitiesView,",
"pub message: String,",
"pub target_transport_endpoints: Vec<String>,",
"pub attempted_transport_endpoints: Vec<String>,",
@@ -2211,6 +2229,15 @@ fn sync_transport_status_source_boundary_rejects_retired_relay_shaped_generic_ou
"pub struct SyncTransportStatusView",
"pub struct TransportTargetFailureView",
);
+ for required in [
+ "pub capabilities: TransportOperationCapabilitiesView,",
+ "pub message: String,",
+ ] {
+ assert!(
+ sync_status_view_source.contains(required),
+ "sync transport status view must retain canonical field `{required}`"
+ );
+ }
for forbidden in [
"pub transport_kind:",
"pub implementation_state:",
@@ -2583,6 +2610,7 @@ fn transport_profile_set_proxy_persists_token_file_without_printing_token_materi
value["result"]["transport_statuses"][0]["usable_for_delivery"],
true
);
+ assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false);
assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]);
assert_eq!(value["result"]["proxy_token_source"], "token_file");
assert_eq!(
@@ -2646,6 +2674,7 @@ fn transport_profile_set_proxy_persists_token_secret_id_without_printing_token_m
value["result"]["transport_statuses"][0]["usable_for_delivery"],
true
);
+ assert_transport_capabilities(&value["result"]["transport_statuses"][0], true, false);
assert_no_removed_transport_status_fields(&value["result"]["transport_statuses"][0]);
assert_eq!(value["result"]["proxy_token_source"], "token_secret_id");
assert!(value["result"]["proxy_token_file"].is_null());