commit bd2f309e24a184beba4ad8a8b55090edc272572a
parent 15f157e6d7bb2e848d75c0efffa3bb4c9166d70f
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Jul 2026 10:10:24 +0000
transport_publish_protocol: close semantic validation coverage
- Cover target identity, metadata, fingerprint, and source validation boundaries.
- Exercise every persisted job count and terminal-state consistency guard.
- Simplify delivery status precedence and remove redundant deferred predicates.
- Verify stable protocol errors, strict Clippy, and full policy coverage.
Diffstat:
1 file changed, 469 insertions(+), 51 deletions(-)
diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs
@@ -1302,23 +1302,14 @@ fn validate_job_status_state(
return Err(TransportPublishProtocolError::InvalidJobStatusState);
}
let required_count = job.delivery_policy.required_target_count(job.target_count);
- let (
- satisfied,
- retryable_status_count,
- terminal_status_count,
- has_deferred,
- has_deferred_until_implemented,
- ) = match &job.delivery_policy {
+ let (satisfied, retryable_status_count, terminal_status_count, has_deferred) = match &job
+ .delivery_policy
+ {
TransportPublishDeliveryPolicy::RequiredTargets { targets } => {
let required_outcomes = required_policy_outcomes(targets, &job.targets)?;
- let satisfied = targets.iter().all(|required| {
- required_outcomes.iter().any(|outcome| {
- target_outcome_fingerprint(outcome, 0).is_ok_and(|fingerprint| {
- fingerprint == *required
- && outcome.outcome_kind.counts_toward_accepted_delivery()
- })
- })
- });
+ let satisfied = required_outcomes
+ .iter()
+ .all(|outcome| outcome.outcome_kind.counts_toward_accepted_delivery());
(
satisfied,
required_outcomes
@@ -1332,54 +1323,35 @@ fn validate_job_status_state(
required_outcomes.iter().any(|outcome| {
outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented
}),
- required_outcomes.iter().any(|outcome| {
- outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented
- }),
)
}
TransportPublishDeliveryPolicy::Any
| TransportPublishDeliveryPolicy::All
| TransportPublishDeliveryPolicy::Quorum { .. } => (
- required_count > 0 && acknowledged_count >= required_count,
+ acknowledged_count >= required_count,
retryable_count,
terminal_count,
job.targets.iter().any(|target| {
target.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented
}),
- job.targets.iter().any(|target| {
- target.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented
- }),
),
};
- match job.status {
- TransportPublishJobStatus::DeliverySatisfied if satisfied => Ok(()),
- TransportPublishJobStatus::DeliveryUnsatisfiedRetryable
- if !satisfied && retryable_status_count > 0 =>
- {
- Ok(())
- }
- TransportPublishJobStatus::DeliveryUnsatisfiedTerminal
- if !satisfied && retryable_status_count == 0 && terminal_status_count > 0 =>
- {
- Ok(())
- }
- TransportPublishJobStatus::DeliveryDeferred
- if !satisfied
- && terminal_status_count == 0
- && retryable_status_count == 0
- && has_deferred =>
- {
- Ok(())
- }
- TransportPublishJobStatus::DeliveryDeferredUntilImplemented
- if !satisfied
- && terminal_status_count == 0
- && retryable_status_count == 0
- && has_deferred_until_implemented =>
- {
- Ok(())
- }
- _ => Err(TransportPublishProtocolError::InvalidJobStatusState),
+ let status_matches = if satisfied {
+ job.status == TransportPublishJobStatus::DeliverySatisfied
+ } else if retryable_status_count > 0 {
+ job.status == TransportPublishJobStatus::DeliveryUnsatisfiedRetryable
+ } else if terminal_status_count > 0 {
+ job.status == TransportPublishJobStatus::DeliveryUnsatisfiedTerminal
+ } else if has_deferred {
+ job.status == TransportPublishJobStatus::DeliveryDeferred
+ || job.status == TransportPublishJobStatus::DeliveryDeferredUntilImplemented
+ } else {
+ false
+ };
+ if status_matches {
+ Ok(())
+ } else {
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
}
}
@@ -2264,6 +2236,22 @@ mod tests {
"delivery quorum must be greater than zero",
),
(
+ TransportPublishProtocolError::EmptyRequiredTargetSet,
+ "delivery required target set must not be empty",
+ ),
+ (
+ TransportPublishProtocolError::InvalidRequiredTargetFingerprint { index: 1 },
+ "delivery required target 1 fingerprint is invalid",
+ ),
+ (
+ TransportPublishProtocolError::DuplicateRequiredTargetFingerprint { index: 2 },
+ "delivery required target 2 duplicates an earlier fingerprint",
+ ),
+ (
+ TransportPublishProtocolError::RequiredTargetNotInTargetSet { index: 3 },
+ "delivery required target 3 is not in the target set",
+ ),
+ (
TransportPublishProtocolError::EmptyPrincipalId,
"principal id must not be empty",
),
@@ -2272,9 +2260,65 @@ mod tests {
"job id must not be empty",
),
(
+ TransportPublishProtocolError::InvalidJobTargetCount {
+ expected: 1,
+ actual: 2,
+ },
+ "job target_count 2 does not match 1 target outcomes",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobAcknowledgedCount {
+ expected: 1,
+ actual: 2,
+ },
+ "job acknowledged_count 2 does not match 1 target outcomes",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobRetryableCount {
+ expected: 1,
+ actual: 2,
+ },
+ "job retryable_count 2 does not match 1 target outcomes",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobTerminalCount {
+ expected: 1,
+ actual: 2,
+ },
+ "job terminal_count 2 does not match 1 target outcomes",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobTerminalState,
+ "job terminal flag does not match status",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobDeliverySatisfiedState,
+ "job delivery_satisfied flag does not match status",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobCompletedAt,
+ "job completed_at_ms does not match status or request time",
+ ),
+ (
+ TransportPublishProtocolError::InvalidJobStatusState,
+ "job status does not match target outcomes",
+ ),
+ (
TransportPublishProtocolError::InvalidExplicitTargetOutcome { index: 6 },
"transport target outcome 6 does not match explicit target policy",
),
+ (
+ TransportPublishProtocolError::InvalidTargetOutcomeKind { index: 7 },
+ "transport target outcome 7 kind is not valid for its transport",
+ ),
+ (
+ TransportPublishProtocolError::InvalidTargetSource { index: 8 },
+ "transport target outcome 8 source does not match transport kind",
+ ),
+ (
+ TransportPublishProtocolError::InvalidReticulumOutcome { index: 9 },
+ "transport target outcome 9 Reticulum must be unavailable or deferred",
+ ),
];
for (error, message) in cases {
@@ -2538,4 +2582,378 @@ mod tests {
Err(TransportPublishProtocolError::InvalidQuorum)
);
}
+
+ #[test]
+ fn low_level_target_validation_covers_defensive_mapping_boundaries() {
+ assert_eq!(
+ transport_kind_error(RadrootsTransportError::EmptyTransportKind, 1),
+ TransportPublishProtocolError::EmptyTransportKind { index: 1 }
+ );
+ assert_eq!(
+ transport_kind_error(RadrootsTransportError::InvalidTransportKind, 2),
+ TransportPublishProtocolError::InvalidTransportKind { index: 2 }
+ );
+ assert_eq!(
+ target_fingerprint_error(RadrootsTransportError::EmptyTargetUri, 3),
+ TransportPublishProtocolError::EmptyEndpointUri { index: 3 }
+ );
+ assert_eq!(
+ target_fingerprint_error(RadrootsTransportError::InvalidTargetUri, 4),
+ TransportPublishProtocolError::InvalidEndpointUri { index: 4 }
+ );
+ for (error, expected) in [
+ (
+ RadrootsTransportError::EmptyTargetScope,
+ TransportPublishProtocolError::EmptyTargetScope { index: 5 },
+ ),
+ (
+ RadrootsTransportError::InvalidTargetScope,
+ TransportPublishProtocolError::InvalidTargetScope { index: 5 },
+ ),
+ (
+ RadrootsTransportError::EmptyTargetLabel,
+ TransportPublishProtocolError::EmptyTargetLabel { index: 5 },
+ ),
+ (
+ RadrootsTransportError::InvalidTargetLabel,
+ TransportPublishProtocolError::InvalidTargetLabel { index: 5 },
+ ),
+ (
+ RadrootsTransportError::InvalidTargetUri,
+ TransportPublishProtocolError::InvalidEndpointUri { index: 5 },
+ ),
+ ] {
+ assert_eq!(target_metadata_error(error, 5), expected);
+ }
+
+ let invalid_fingerprint: RadrootsTransportTargetFingerprint =
+ serde_json::from_str("\"invalid\"").expect("unchecked serde newtype fixture");
+ assert_eq!(
+ TransportPublishDeliveryPolicy::required_targets(vec![invalid_fingerprint]),
+ Err(TransportPublishProtocolError::InvalidRequiredTargetFingerprint { index: 0 })
+ );
+
+ assert!(
+ transport_target_from_parts(RadrootsTransportKind::Local, "local:publish", None, None,)
+ .is_ok()
+ );
+ assert_eq!(
+ transport_target_from_parts(
+ RadrootsTransportKind::Reticulum,
+ "reticulum:other",
+ None,
+ None,
+ ),
+ Err(RadrootsTransportError::InvalidTargetUri)
+ );
+
+ let valid = nostr_outcome(TransportPublishOutcomeKind::Accepted);
+ assert!(validate_target_outcome(&valid, 0).is_ok());
+ assert!(target_outcome_fingerprint(&valid, 0).is_ok());
+
+ let mut invalid_target = TransportPublishTarget::nostr("wss://relay.example");
+ invalid_target.transport_kind = "Nostr".to_owned();
+ assert_eq!(
+ invalid_target.fingerprint(10),
+ Err(TransportPublishProtocolError::InvalidTransportKind { index: 10 })
+ );
+ invalid_target = TransportPublishTarget::nostr("wss://relay.example");
+ invalid_target.target_scope = Some(" ".to_owned());
+ assert_eq!(
+ invalid_target.fingerprint(11),
+ Err(TransportPublishProtocolError::InvalidTargetScope { index: 11 })
+ );
+ invalid_target = TransportPublishTarget::nostr("wss://relay.example");
+ invalid_target.target_label = Some(" ".to_owned());
+ assert_eq!(
+ invalid_target.fingerprint(12),
+ Err(TransportPublishProtocolError::EmptyTargetLabel { index: 12 })
+ );
+
+ let target = TransportPublishTarget::nostr("wss://relay.example");
+ let mut different_kind = valid.clone();
+ different_kind.transport_kind = "local".to_owned();
+ different_kind.endpoint_uri = "local:publish".to_owned();
+ assert!(
+ !target
+ .identity_eq(0, &different_kind, 0)
+ .expect("different transport identity")
+ );
+
+ let mut invalid = valid.clone();
+ invalid.transport_kind = " ".to_owned();
+ assert_eq!(
+ validate_target_outcome(&invalid, 1),
+ Err(TransportPublishProtocolError::EmptyTransportKind { index: 1 })
+ );
+ invalid.transport_kind = "Nostr".to_owned();
+ assert_eq!(
+ validate_target_outcome(&invalid, 2),
+ Err(TransportPublishProtocolError::InvalidTransportKind { index: 2 })
+ );
+ assert_eq!(
+ target_outcome_fingerprint(&invalid, 2),
+ Err(TransportPublishProtocolError::InvalidTransportKind { index: 2 })
+ );
+ invalid = valid.clone();
+ invalid.endpoint_uri = " ".to_owned();
+ assert_eq!(
+ validate_target_outcome(&invalid, 3),
+ Err(TransportPublishProtocolError::EmptyEndpointUri { index: 3 })
+ );
+ invalid = valid.clone();
+ invalid.target_scope = Some(" ".to_owned());
+ assert_eq!(
+ validate_target_outcome(&invalid, 4),
+ Err(TransportPublishProtocolError::InvalidTargetScope { index: 4 })
+ );
+ assert_eq!(
+ target_outcome_fingerprint(&invalid, 4),
+ Err(TransportPublishProtocolError::InvalidTargetScope { index: 4 })
+ );
+ invalid = valid.clone();
+ invalid.target_label = Some(" ".to_owned());
+ assert_eq!(
+ validate_target_outcome(&invalid, 5),
+ Err(TransportPublishProtocolError::EmptyTargetLabel { index: 5 })
+ );
+ assert_eq!(
+ target_outcome_fingerprint(&invalid, 5),
+ Err(TransportPublishProtocolError::EmptyTargetLabel { index: 5 })
+ );
+ invalid = valid.clone();
+ invalid.endpoint_uri = "not a URI".to_owned();
+ assert_eq!(
+ target_outcome_fingerprint(&invalid, 6),
+ Err(TransportPublishProtocolError::InvalidEndpointUri { index: 6 })
+ );
+
+ let mut reticulum =
+ reticulum_outcome(TransportPublishOutcomeKind::DeferredUntilImplemented);
+ reticulum.endpoint_uri = "reticulum:other".to_owned();
+ assert_eq!(
+ validate_target_outcome(&reticulum, 7),
+ Err(TransportPublishProtocolError::InvalidReticulumEndpoint { index: 7 })
+ );
+ reticulum.endpoint_uri = RADROOTS_RETICULUM_ENDPOINT_URI.to_owned();
+ reticulum.attempted = true;
+ assert_eq!(
+ validate_target_outcome(&reticulum, 8),
+ Err(TransportPublishProtocolError::InvalidReticulumOutcome { index: 8 })
+ );
+ let mut wrong_source = valid.clone();
+ wrong_source.source = TransportPublishTargetSource::Reticulum;
+ assert_eq!(
+ validate_target_outcome(&wrong_source, 9),
+ Err(TransportPublishProtocolError::InvalidTargetSource { index: 9 })
+ );
+
+ let required = TransportPublishTarget::nostr("wss://required.example")
+ .fingerprint(0)
+ .expect("required fingerprint");
+ let mut unparseable = nostr_outcome(TransportPublishOutcomeKind::Accepted);
+ unparseable.endpoint_uri = "not a URI".to_owned();
+ assert_eq!(
+ required_policy_outcomes(&[required], &[unparseable]),
+ Err(TransportPublishProtocolError::RequiredTargetNotInTargetSet { index: 0 })
+ );
+
+ assert!(
+ validate_job_target_policy_outcomes(
+ &TransportPublishTargetPolicy::nostr(
+ NostrPublishTargetSourcePolicy::ExplicitOnly,
+ Vec::new(),
+ ),
+ &[],
+ )
+ .is_ok()
+ );
+ assert!(
+ validate_job_target_policy_outcomes(
+ &TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::nostr("wss://relay.example"),
+ ]),
+ &[],
+ )
+ .is_ok()
+ );
+
+ let nostr_request = TransportPublishEventRequest {
+ raw_event_json: raw_event_json(),
+ target_policy: TransportPublishTargetPolicy::nostr(
+ NostrPublishTargetSourcePolicy::DaemonDefaultOnly,
+ Vec::new(),
+ ),
+ delivery_policy: TransportPublishDeliveryPolicy::Any,
+ idempotency_key: None,
+ timeout_ms: None,
+ };
+ nostr_request
+ .validate(10)
+ .expect("valid Nostr policy request");
+
+ let accepted_without_targets = TransportPublishJobView {
+ job_id: "accepted-empty".to_owned(),
+ status: TransportPublishJobStatus::Accepted,
+ terminal: false,
+ delivery_satisfied: false,
+ event_id: "0".repeat(64),
+ pubkey: "1".repeat(64),
+ event_kind: 30_402,
+ target_policy: TransportPublishTargetPolicy::nostr(
+ NostrPublishTargetSourcePolicy::DaemonDefaultOnly,
+ Vec::new(),
+ ),
+ delivery_policy: TransportPublishDeliveryPolicy::Any,
+ target_count: 0,
+ acknowledged_count: 0,
+ retryable_count: 0,
+ terminal_count: 0,
+ requested_at_ms: 1,
+ completed_at_ms: None,
+ last_error: None,
+ targets: Vec::new(),
+ };
+ accepted_without_targets
+ .validate()
+ .expect("accepted job may await target resolution");
+ }
+
+ #[test]
+ fn job_count_and_status_guards_reject_every_inconsistent_shape() {
+ let mut retryable_count_mismatch = job_from_targets(
+ TransportPublishJobStatus::DeliveryUnsatisfiedRetryable,
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr(
+ "wss://relay.example.com",
+ )]),
+ vec![nostr_outcome(TransportPublishOutcomeKind::Timeout)],
+ );
+ retryable_count_mismatch.retryable_count = 0;
+ assert_eq!(
+ retryable_count_mismatch.validate(),
+ Err(TransportPublishProtocolError::InvalidJobRetryableCount {
+ expected: 1,
+ actual: 0,
+ })
+ );
+
+ let mut terminal_count_mismatch = job_from_targets(
+ TransportPublishJobStatus::DeliveryUnsatisfiedTerminal,
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr(
+ "wss://relay.example.com",
+ )]),
+ vec![nostr_outcome(TransportPublishOutcomeKind::TargetRejected)],
+ );
+ terminal_count_mismatch.terminal_count = 0;
+ assert_eq!(
+ terminal_count_mismatch.validate(),
+ Err(TransportPublishProtocolError::InvalidJobTerminalCount {
+ expected: 1,
+ actual: 0,
+ })
+ );
+
+ let rejected = rejected_job();
+ assert!(validate_job_status_state(&rejected, 0, 0, 0).is_ok());
+ for (target_count, targets, acknowledged, retryable, terminal) in [
+ (1, Vec::new(), 0, 0, 0),
+ (
+ 0,
+ vec![nostr_outcome(TransportPublishOutcomeKind::Accepted)],
+ 0,
+ 0,
+ 0,
+ ),
+ (0, Vec::new(), 1, 0, 0),
+ (0, Vec::new(), 0, 1, 0),
+ (0, Vec::new(), 0, 0, 1),
+ ] {
+ let mut invalid = rejected.clone();
+ invalid.target_count = target_count;
+ invalid.targets = targets;
+ assert_eq!(
+ validate_job_status_state(&invalid, acknowledged, retryable, terminal),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ }
+
+ let mut empty_terminal = rejected.clone();
+ empty_terminal.status = TransportPublishJobStatus::DeliveryUnsatisfiedTerminal;
+ assert_eq!(
+ validate_job_status_state(&empty_terminal, 0, 0, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ let mut publishing = rejected.clone();
+ publishing.status = TransportPublishJobStatus::Publishing;
+ assert!(validate_job_status_state(&publishing, 0, 0, 0).is_ok());
+
+ let accepted = accepted_job();
+ assert!(validate_job_status_state(&accepted, 1, 0, 0).is_ok());
+ assert_eq!(
+ validate_job_status_state(&accepted, 0, 1, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ let retryable = job_from_targets(
+ TransportPublishJobStatus::DeliveryUnsatisfiedRetryable,
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr(
+ "wss://relay.example.com",
+ )]),
+ vec![nostr_outcome(TransportPublishOutcomeKind::Timeout)],
+ );
+ assert!(validate_job_status_state(&retryable, 0, 1, 0).is_ok());
+ assert_eq!(
+ validate_job_status_state(&retryable, 1, 1, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ assert_eq!(
+ validate_job_status_state(&retryable, 0, 0, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ let terminal = job_from_targets(
+ TransportPublishJobStatus::DeliveryUnsatisfiedTerminal,
+ TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr(
+ "wss://relay.example.com",
+ )]),
+ vec![nostr_outcome(TransportPublishOutcomeKind::TargetRejected)],
+ );
+ assert!(validate_job_status_state(&terminal, 0, 0, 1).is_ok());
+ assert_eq!(
+ validate_job_status_state(&terminal, 0, 1, 1),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ assert_eq!(
+ validate_job_status_state(&terminal, 0, 0, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+
+ for status in [
+ TransportPublishJobStatus::DeliveryDeferred,
+ TransportPublishJobStatus::DeliveryDeferredUntilImplemented,
+ ] {
+ let deferred = job_from_targets(
+ status,
+ TransportPublishTargetPolicy::explicit_targets(vec![
+ TransportPublishTarget::reticulum(
+ TransportPublishReticulumBehavior::DeferDeliveryPlans,
+ ),
+ ]),
+ vec![reticulum_outcome(
+ TransportPublishOutcomeKind::DeferredUntilImplemented,
+ )],
+ );
+ assert!(validate_job_status_state(&deferred, 0, 0, 0).is_ok());
+ assert_eq!(
+ validate_job_status_state(&deferred, 1, 0, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ assert_eq!(
+ validate_job_status_state(&deferred, 0, 1, 0),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ assert_eq!(
+ validate_job_status_state(&deferred, 0, 0, 1),
+ Err(TransportPublishProtocolError::InvalidJobStatusState)
+ );
+ }
+ }
}