commit 4a7b84d31254d9b2b8c2de1659c2115b768e3587
parent 9feb085c6cbf3471d35394712cd927dae9ebcc84
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Jul 2026 19:50:58 +0000
transport: report empty Nostr targets as misconfigured
Diffstat:
| M | src/runtime/sync.rs | | | 127 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
1 file changed, 119 insertions(+), 8 deletions(-)
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -891,6 +891,7 @@ fn sync_transport_target_view(
fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusView> {
let profile_id = config.transport.profile.as_str();
+ let nostr_targets_configured = !config.transport.nostr_relay_urls.is_empty();
match config.transport.profile {
TransportProfileKind::LocalOnly => vec![sync_transport_status_view(
RadrootsTransportStatus::new(
@@ -903,6 +904,7 @@ fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusVie
TransportProfileKind::Nostr => {
vec![sync_transport_status_view(nostr_transport_status(
profile_id,
+ nostr_targets_configured,
))]
}
TransportProfileKind::ReticulumPreview => {
@@ -911,7 +913,10 @@ fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusVie
)]
}
TransportProfileKind::Hybrid => vec![
- sync_transport_status_view(nostr_transport_status(profile_id)),
+ sync_transport_status_view(nostr_transport_status(
+ profile_id,
+ nostr_targets_configured,
+ )),
sync_transport_status_view(reticulum_preview_transport_status(profile_id)),
],
TransportProfileKind::Proxy => {
@@ -939,15 +944,23 @@ fn sync_transport_statuses(config: &RuntimeConfig) -> Vec<SyncTransportStatusVie
}
}
-fn nostr_transport_status(profile_id: &str) -> RadrootsTransportStatus {
+fn nostr_transport_status(profile_id: &str, targets_configured: bool) -> RadrootsTransportStatus {
RadrootsTransportStatus::new(
RadrootsTransportKind::Nostr,
- RadrootsTransportImplementationState::Available,
- RadrootsTransportReadinessState::Ready,
+ if targets_configured {
+ RadrootsTransportImplementationState::Available
+ } else {
+ RadrootsTransportImplementationState::Misconfigured
+ },
+ if targets_configured {
+ RadrootsTransportReadinessState::Ready
+ } else {
+ RadrootsTransportReadinessState::Misconfigured
+ },
)
.with_profile_id(profile_id)
- .with_publish_usable(true)
- .with_fetch_usable(true)
+ .with_publish_usable(targets_configured)
+ .with_fetch_usable(targets_configured)
}
fn reticulum_preview_transport_status(profile_id: &str) -> RadrootsTransportStatus {
@@ -1811,6 +1824,42 @@ mod tests {
}
#[test]
+ fn sync_pull_empty_nostr_profile_reports_nostr_misconfigured() {
+ let dir = tempdir().expect("tempdir");
+ let config = nostr_config(dir.path(), Vec::new());
+ crate::runtime::store::init(&config).expect("store init");
+
+ let view = pull_with_fetcher(&config, |_, _| {
+ panic!("empty Nostr profile sync pull must not fetch")
+ })
+ .expect("sync pull empty Nostr profile");
+
+ assert_eq!(view.state, "unconfigured");
+ assert_eq!(view.configured_transport_target_count, 0);
+ assert!(view.configured_transport_targets.is_empty());
+ assert_eq!(view.transport_statuses.len(), 1);
+ assert_eq!(view.transport_statuses[0].transport_kind, "nostr");
+ assert_eq!(
+ view.transport_statuses[0].profile_id.as_deref(),
+ Some("nostr")
+ );
+ assert_eq!(
+ view.transport_statuses[0].implementation_state,
+ "misconfigured"
+ );
+ assert_eq!(view.transport_statuses[0].readiness, "misconfigured");
+ assert!(!view.transport_statuses[0].publish_usable);
+ assert!(!view.transport_statuses[0].fetch_usable);
+ assert!(view.target_transport_endpoints.is_empty());
+ assert_eq!(
+ view.actions,
+ vec![
+ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"
+ ]
+ );
+ }
+
+ #[test]
fn sync_pull_hybrid_reports_profile_targets_and_nostr_fetch_detail() {
let dir = tempdir().expect("tempdir");
let config = hybrid_config(dir.path(), vec!["wss://relay.example.com".to_owned()]);
@@ -1847,6 +1896,44 @@ mod tests {
}
#[test]
+ fn sync_pull_empty_hybrid_profile_reports_nostr_misconfigured() {
+ let dir = tempdir().expect("tempdir");
+ let config = hybrid_config(dir.path(), Vec::new());
+ crate::runtime::store::init(&config).expect("store init");
+
+ let view = pull_with_fetcher(&config, |_, _| {
+ panic!("empty Hybrid profile sync pull must not fetch")
+ })
+ .expect("sync pull empty Hybrid profile");
+
+ assert_eq!(view.state, "unavailable");
+ assert_eq!(view.configured_transport_target_count, 1);
+ assert_eq!(view.configured_transport_targets.len(), 1);
+ assert_eq!(
+ view.configured_transport_targets[0].transport_kind,
+ "reticulum"
+ );
+ assert_eq!(view.transport_statuses.len(), 2);
+ assert_eq!(view.transport_statuses[0].transport_kind, "nostr");
+ assert_eq!(
+ view.transport_statuses[0].profile_id.as_deref(),
+ Some("hybrid")
+ );
+ assert_eq!(
+ view.transport_statuses[0].implementation_state,
+ "misconfigured"
+ );
+ assert_eq!(view.transport_statuses[0].readiness, "misconfigured");
+ assert!(!view.transport_statuses[0].publish_usable);
+ assert!(!view.transport_statuses[0].fetch_usable);
+ assert_eq!(view.transport_statuses[1].transport_kind, "reticulum");
+ assert_eq!(view.transport_statuses[1].readiness, "preview_unavailable");
+ assert!(!view.transport_statuses[1].fetch_usable);
+ assert!(view.target_transport_endpoints.is_empty());
+ assert_eq!(view.actions, vec!["radroots transport profile get"]);
+ }
+
+ #[test]
fn sync_inspect_hybrid_uses_profile_target_count() {
let dir = tempdir().expect("tempdir");
let config = hybrid_config(dir.path(), vec!["wss://relay.example.com".to_owned()]);
@@ -2100,12 +2187,26 @@ mod tests {
#[test]
fn sync_push_empty_queue_reports_ready_sdk_state() {
let dir = tempdir().expect("tempdir");
- let config = sample_config(dir.path(), Vec::new());
+ let config = sample_config(dir.path(), vec!["wss://relay.example.com".to_owned()]);
let view = sdk_push_view(
&config,
PushOutboxReceipt::default(),
- sdk_status_receipt(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, None, &[]),
+ sdk_status_receipt(
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ None,
+ None,
+ &["wss://relay.example.com"],
+ ),
);
assert_eq!(view.state, "ready");
@@ -2226,6 +2327,10 @@ mod tests {
last_error: Option<String>,
relays: &[&str],
) -> SyncStatusReceipt {
+ assert!(
+ !relays.is_empty(),
+ "sdk_status_receipt must not build ready Nostr status without transport targets"
+ );
SyncStatusReceipt {
source: SyncStatusSource::SdkCanonicalStores,
observed_at_ms: 1_700_000_030_000,
@@ -2879,6 +2984,12 @@ mod tests {
config
}
+ fn nostr_config(root: &Path, relays: Vec<String>) -> RuntimeConfig {
+ let mut config = sample_config(root, relays);
+ config.transport.profile = TransportProfileKind::Nostr;
+ config
+ }
+
fn reticulum_preview_config(root: &Path) -> RuntimeConfig {
let mut config = sample_config(root, Vec::new());
config.transport.profile = TransportProfileKind::ReticulumPreview;