lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 6c56aed578ec0ba7cd005a3909f3047a439a7e44
parent 1b54111a98cbd402243420846f77bc29b50bbbb1
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Jul 2026 06:27:38 +0000

transport: implement strict satisfaction classes

- expand transport satisfaction classes beyond accepted and delivered
- make delivered satisfaction require delivered evidence only
- add canonical outbox storage labels for new classes
- cover every status and outcome against every satisfaction class

Diffstat:
Mcrates/outbox/src/model.rs | 21++++++++++++++-------
Mcrates/outbox/src/store.rs | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/transport/src/delivery.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/transport/src/status.rs | 15+++++++++++----
Mcrates/transport/tests/transport.rs | 253++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
5 files changed, 374 insertions(+), 126 deletions(-)

diff --git a/crates/outbox/src/model.rs b/crates/outbox/src/model.rs @@ -214,10 +214,17 @@ impl RadrootsOutboxDeliveryTargetStatus { | Self::StoredByGateway | Self::Seen ), - RadrootsTransportSatisfactionClass::Delivered => matches!( - self, - Self::Delivered | Self::Forwarded | Self::StoredByGateway | Self::Seen - ), + RadrootsTransportSatisfactionClass::Forwarded => { + matches!(self, Self::Forwarded | Self::Delivered) + } + RadrootsTransportSatisfactionClass::Stored => matches!(self, Self::StoredByGateway), + RadrootsTransportSatisfactionClass::Seen => { + matches!(self, Self::Seen | Self::Delivered) + } + RadrootsTransportSatisfactionClass::Delivered => matches!(self, Self::Delivered), + RadrootsTransportSatisfactionClass::DurableOrObserved => { + matches!(self, Self::StoredByGateway | Self::Seen | Self::Delivered) + } } } @@ -648,7 +655,7 @@ mod tests { "forwarded", false, true, - true, + false, false, false, ), @@ -657,7 +664,7 @@ mod tests { "stored_by_gateway", false, true, - true, + false, false, false, ), @@ -666,7 +673,7 @@ mod tests { "seen", false, true, - true, + false, false, false, ), diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -2464,7 +2464,25 @@ fn satisfaction_policy_storage_value(policy: &RadrootsTransportSatisfactionPolic fn satisfaction_class_storage_value(class: RadrootsTransportSatisfactionClass) -> &'static str { match class { RadrootsTransportSatisfactionClass::Accepted => "accepted", + RadrootsTransportSatisfactionClass::Forwarded => "forwarded", + RadrootsTransportSatisfactionClass::Stored => "stored", + RadrootsTransportSatisfactionClass::Seen => "seen", RadrootsTransportSatisfactionClass::Delivered => "delivered", + RadrootsTransportSatisfactionClass::DurableOrObserved => "durable_or_observed", + } +} + +fn parse_satisfaction_class_storage_value( + value: &str, +) -> Option<RadrootsTransportSatisfactionClass> { + match value { + "accepted" => Some(RadrootsTransportSatisfactionClass::Accepted), + "forwarded" => Some(RadrootsTransportSatisfactionClass::Forwarded), + "stored" => Some(RadrootsTransportSatisfactionClass::Stored), + "seen" => Some(RadrootsTransportSatisfactionClass::Seen), + "delivered" => Some(RadrootsTransportSatisfactionClass::Delivered), + "durable_or_observed" => Some(RadrootsTransportSatisfactionClass::DurableOrObserved), + _ => None, } } @@ -2472,52 +2490,54 @@ fn parse_satisfaction_policy( value: &str, required_success_count: i64, ) -> Result<RadrootsTransportSatisfactionPolicy, RadrootsOutboxError> { - match value { - "no_wait" if required_success_count == 0 => { - Ok(RadrootsTransportSatisfactionPolicy::no_wait()) - } - "all_accepted" => Ok(RadrootsTransportSatisfactionPolicy::all_accepted()), - "any_accepted" => Ok(RadrootsTransportSatisfactionPolicy::any_accepted()), - "all_delivered" => Ok(RadrootsTransportSatisfactionPolicy::all_delivered()), - "any_delivered" => Ok(RadrootsTransportSatisfactionPolicy::any_delivered()), - stored if stored == format!("quorum_accepted:{required_success_count}") => { - Ok(RadrootsTransportSatisfactionPolicy::quorum_accepted( - required_count_u16(required_success_count)?, - )) - } - stored if stored == format!("quorum_delivered:{required_success_count}") => { - Ok(RadrootsTransportSatisfactionPolicy::quorum_delivered( - required_count_u16(required_success_count)?, - )) + if value == "no_wait" && required_success_count == 0 { + return Ok(RadrootsTransportSatisfactionPolicy::no_wait()); + } + if let Some(class) = value + .strip_prefix("all_") + .and_then(parse_satisfaction_class_storage_value) + { + return Ok(RadrootsTransportSatisfactionPolicy::All { class }); + } + if let Some(class) = value + .strip_prefix("any_") + .and_then(parse_satisfaction_class_storage_value) + { + return Ok(RadrootsTransportSatisfactionPolicy::Any { class }); + } + if let Some((class_label, threshold)) = value + .strip_prefix("quorum_") + .and_then(|stored| stored.split_once(':')) + { + if threshold == required_success_count.to_string() + && let Some(class) = parse_satisfaction_class_storage_value(class_label) + { + return Ok(RadrootsTransportSatisfactionPolicy::Quorum { + class, + threshold: required_count_u16(required_success_count)?, + }); } - stored if stored.starts_with("required_accepted:") => parse_required_target_policy( - stored, - "required_accepted:", - RadrootsTransportSatisfactionClass::Accepted, - required_success_count, - ), - stored if stored.starts_with("required_delivered:") => parse_required_target_policy( - stored, - "required_delivered:", - RadrootsTransportSatisfactionClass::Delivered, - required_success_count, - ), - _ => Err(RadrootsOutboxError::InvalidStoredEnum { - field: "outbox_delivery_plan.satisfaction_policy", - value: value.to_owned(), - }), } + if let Some((class_label, fingerprints)) = value + .strip_prefix("required_") + .and_then(|stored| stored.split_once(':')) + && let Some(class) = parse_satisfaction_class_storage_value(class_label) + { + return parse_required_target_policy(value, fingerprints, class, required_success_count); + } + Err(RadrootsOutboxError::InvalidStoredEnum { + field: "outbox_delivery_plan.satisfaction_policy", + value: value.to_owned(), + }) } fn parse_required_target_policy( stored: &str, - prefix: &str, + fingerprints: &str, class: RadrootsTransportSatisfactionClass, required_success_count: i64, ) -> Result<RadrootsTransportSatisfactionPolicy, RadrootsOutboxError> { - let targets = stored - .strip_prefix(prefix) - .expect("stored required target policy prefix") + let targets = fingerprints .split(',') .map(RadrootsTransportTargetFingerprint::parse) .collect::<Result<Vec<_>, _>>()?; @@ -2644,6 +2664,43 @@ mod tests { } #[test] + fn satisfaction_policy_storage_round_trips_all_transport_classes() { + for (policy, stored, required_count) in [ + ( + RadrootsTransportSatisfactionPolicy::any_forwarded(), + "any_forwarded", + 1, + ), + ( + RadrootsTransportSatisfactionPolicy::all_stored(), + "all_stored", + 2, + ), + ( + RadrootsTransportSatisfactionPolicy::quorum_seen(2), + "quorum_seen:2", + 2, + ), + ( + RadrootsTransportSatisfactionPolicy::any_durable_or_observed(), + "any_durable_or_observed", + 1, + ), + ( + RadrootsTransportSatisfactionPolicy::quorum_delivered(2), + "quorum_delivered:2", + 2, + ), + ] { + assert_eq!(satisfaction_policy_storage_value(&policy), stored); + assert_eq!( + parse_satisfaction_policy(stored, required_count).expect("parse policy"), + policy + ); + } + } + + #[test] fn required_target_policy_idempotency_is_order_independent() { let first = nostr_target("wss://required-one.example"); let second = nostr_target("wss://required-two.example"); diff --git a/crates/transport/src/delivery.rs b/crates/transport/src/delivery.rs @@ -11,7 +11,11 @@ use alloc::vec::Vec; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsTransportSatisfactionClass { Accepted, + Forwarded, + Stored, + Seen, Delivered, + DurableOrObserved, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -58,6 +62,63 @@ impl RadrootsTransportSatisfactionPolicy { } } + pub fn any_forwarded() -> Self { + Self::Any { + class: RadrootsTransportSatisfactionClass::Forwarded, + } + } + + pub fn all_forwarded() -> Self { + Self::All { + class: RadrootsTransportSatisfactionClass::Forwarded, + } + } + + pub fn quorum_forwarded(threshold: u16) -> Self { + Self::Quorum { + class: RadrootsTransportSatisfactionClass::Forwarded, + threshold, + } + } + + pub fn any_stored() -> Self { + Self::Any { + class: RadrootsTransportSatisfactionClass::Stored, + } + } + + pub fn all_stored() -> Self { + Self::All { + class: RadrootsTransportSatisfactionClass::Stored, + } + } + + pub fn quorum_stored(threshold: u16) -> Self { + Self::Quorum { + class: RadrootsTransportSatisfactionClass::Stored, + threshold, + } + } + + pub fn any_seen() -> Self { + Self::Any { + class: RadrootsTransportSatisfactionClass::Seen, + } + } + + pub fn all_seen() -> Self { + Self::All { + class: RadrootsTransportSatisfactionClass::Seen, + } + } + + pub fn quorum_seen(threshold: u16) -> Self { + Self::Quorum { + class: RadrootsTransportSatisfactionClass::Seen, + threshold, + } + } + pub fn any_delivered() -> Self { Self::Any { class: RadrootsTransportSatisfactionClass::Delivered, @@ -77,6 +138,25 @@ impl RadrootsTransportSatisfactionPolicy { } } + pub fn any_durable_or_observed() -> Self { + Self::Any { + class: RadrootsTransportSatisfactionClass::DurableOrObserved, + } + } + + pub fn all_durable_or_observed() -> Self { + Self::All { + class: RadrootsTransportSatisfactionClass::DurableOrObserved, + } + } + + pub fn quorum_durable_or_observed(threshold: u16) -> Self { + Self::Quorum { + class: RadrootsTransportSatisfactionClass::DurableOrObserved, + threshold, + } + } + pub fn required_targets( class: RadrootsTransportSatisfactionClass, mut targets: Vec<RadrootsTransportTargetFingerprint>, diff --git a/crates/transport/src/status.rs b/crates/transport/src/status.rs @@ -33,10 +33,7 @@ impl RadrootsTransportDeliveryTargetStatus { } pub fn counts_as_delivered_satisfaction(self) -> bool { - matches!( - self, - Self::Delivered | Self::Forwarded | Self::StoredByGateway | Self::Seen - ) + matches!(self, Self::Delivered) } pub fn counts_as_satisfied( @@ -45,9 +42,19 @@ impl RadrootsTransportDeliveryTargetStatus { ) -> bool { match satisfaction_class { RadrootsTransportSatisfactionClass::Accepted => self.counts_as_accepted_satisfaction(), + RadrootsTransportSatisfactionClass::Forwarded => { + matches!(self, Self::Forwarded | Self::Delivered) + } + RadrootsTransportSatisfactionClass::Stored => matches!(self, Self::StoredByGateway), + RadrootsTransportSatisfactionClass::Seen => { + matches!(self, Self::Seen | Self::Delivered) + } RadrootsTransportSatisfactionClass::Delivered => { self.counts_as_delivered_satisfaction() } + RadrootsTransportSatisfactionClass::DurableOrObserved => { + matches!(self, Self::StoredByGateway | Self::Seen | Self::Delivered) + } } } diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs @@ -150,6 +150,10 @@ fn satisfaction_policy_counts_target_statuses() { let any = RadrootsTransportSatisfactionPolicy::any_accepted(); let two = RadrootsTransportSatisfactionPolicy::quorum_accepted(2); let delivered = RadrootsTransportSatisfactionPolicy::quorum_delivered(2); + let forwarded = RadrootsTransportSatisfactionPolicy::any_forwarded(); + let stored = RadrootsTransportSatisfactionPolicy::all_stored(); + let seen = RadrootsTransportSatisfactionPolicy::quorum_seen(2); + let durable_or_observed = RadrootsTransportSatisfactionPolicy::any_durable_or_observed(); assert_eq!(no_wait.required_target_count(0).expect("no wait"), 0); assert_eq!(no_wait.required_target_count(3).expect("no wait"), 0); @@ -169,6 +173,58 @@ fn satisfaction_policy_counts_target_statuses() { Some(RadrootsTransportSatisfactionClass::Delivered) ); assert_eq!( + forwarded.target_satisfaction_class(), + Some(RadrootsTransportSatisfactionClass::Forwarded) + ); + assert_eq!( + stored.target_satisfaction_class(), + Some(RadrootsTransportSatisfactionClass::Stored) + ); + assert_eq!( + seen.target_satisfaction_class(), + Some(RadrootsTransportSatisfactionClass::Seen) + ); + assert_eq!( + durable_or_observed.target_satisfaction_class(), + Some(RadrootsTransportSatisfactionClass::DurableOrObserved) + ); + for (policy, class) in [ + ( + RadrootsTransportSatisfactionPolicy::all_forwarded(), + RadrootsTransportSatisfactionClass::Forwarded, + ), + ( + RadrootsTransportSatisfactionPolicy::quorum_forwarded(2), + RadrootsTransportSatisfactionClass::Forwarded, + ), + ( + RadrootsTransportSatisfactionPolicy::any_stored(), + RadrootsTransportSatisfactionClass::Stored, + ), + ( + RadrootsTransportSatisfactionPolicy::quorum_stored(2), + RadrootsTransportSatisfactionClass::Stored, + ), + ( + RadrootsTransportSatisfactionPolicy::any_seen(), + RadrootsTransportSatisfactionClass::Seen, + ), + ( + RadrootsTransportSatisfactionPolicy::all_seen(), + RadrootsTransportSatisfactionClass::Seen, + ), + ( + RadrootsTransportSatisfactionPolicy::all_durable_or_observed(), + RadrootsTransportSatisfactionClass::DurableOrObserved, + ), + ( + RadrootsTransportSatisfactionPolicy::quorum_durable_or_observed(2), + RadrootsTransportSatisfactionClass::DurableOrObserved, + ), + ] { + assert_eq!(policy.target_satisfaction_class(), Some(class)); + } + assert_eq!( any.is_satisfied_by(0, 0).expect_err("zero target set"), RadrootsTransportError::InvalidSatisfactionPolicy ); @@ -636,51 +692,82 @@ fn satisfaction_and_target_status_cover_all_contract_states() { RadrootsTransportError::InvalidSatisfactionPolicy ); - let statuses = [ - RadrootsTransportDeliveryTargetStatus::Pending, - RadrootsTransportDeliveryTargetStatus::Accepted, - RadrootsTransportDeliveryTargetStatus::Delivered, - RadrootsTransportDeliveryTargetStatus::Forwarded, - RadrootsTransportDeliveryTargetStatus::StoredByGateway, - RadrootsTransportDeliveryTargetStatus::Seen, - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, - RadrootsTransportDeliveryTargetStatus::PreviewUnavailable, - RadrootsTransportDeliveryTargetStatus::SkippedPolicyDenied, - RadrootsTransportDeliveryTargetStatus::FailedRetryable, - RadrootsTransportDeliveryTargetStatus::FailedTerminal, + let classes = [ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + RadrootsTransportSatisfactionClass::Stored, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::Delivered, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ]; + let cases: &[( + RadrootsTransportDeliveryTargetStatus, + &[RadrootsTransportSatisfactionClass], + )] = &[ + (RadrootsTransportDeliveryTargetStatus::Pending, &[]), + ( + RadrootsTransportDeliveryTargetStatus::Accepted, + &[RadrootsTransportSatisfactionClass::Accepted], + ), + ( + RadrootsTransportDeliveryTargetStatus::Delivered, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::Delivered, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], + ), + ( + RadrootsTransportDeliveryTargetStatus::Forwarded, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + ], + ), + ( + RadrootsTransportDeliveryTargetStatus::StoredByGateway, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Stored, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], + ), + ( + RadrootsTransportDeliveryTargetStatus::Seen, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], + ), + ( + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, + &[], + ), + ( + RadrootsTransportDeliveryTargetStatus::PreviewUnavailable, + &[], + ), + ( + RadrootsTransportDeliveryTargetStatus::SkippedPolicyDenied, + &[], + ), + (RadrootsTransportDeliveryTargetStatus::FailedRetryable, &[]), + (RadrootsTransportDeliveryTargetStatus::FailedTerminal, &[]), ]; assert!(RadrootsTransportDeliveryTargetStatus::Pending.is_ready_for_attempt()); assert!(RadrootsTransportDeliveryTargetStatus::FailedRetryable.is_ready_for_attempt()); - assert!( - RadrootsTransportDeliveryTargetStatus::Accepted - .counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted) - ); - assert!( - !RadrootsTransportDeliveryTargetStatus::Accepted - .counts_as_satisfied(RadrootsTransportSatisfactionClass::Delivered) - ); - for status in [ - RadrootsTransportDeliveryTargetStatus::Delivered, - RadrootsTransportDeliveryTargetStatus::Forwarded, - RadrootsTransportDeliveryTargetStatus::StoredByGateway, - RadrootsTransportDeliveryTargetStatus::Seen, - ] { - assert!(status.counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted)); - assert!(status.counts_as_satisfied(RadrootsTransportSatisfactionClass::Delivered)); + for (status, satisfied_classes) in cases { + for class in classes { + assert_eq!( + status.counts_as_satisfied(class), + satisfied_classes.contains(&class), + "{status:?} / {class:?}" + ); + } } - assert!( - statuses - .iter() - .filter(|status| !matches!( - status, - RadrootsTransportDeliveryTargetStatus::Accepted - | RadrootsTransportDeliveryTargetStatus::Delivered - | RadrootsTransportDeliveryTargetStatus::Forwarded - | RadrootsTransportDeliveryTargetStatus::StoredByGateway - | RadrootsTransportDeliveryTargetStatus::Seen - )) - .all(|status| !status.counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted)) - ); assert!(RadrootsTransportDeliveryTargetStatus::PreviewUnavailable.is_deferred_preview()); assert!(RadrootsTransportDeliveryTargetStatus::FailedRetryable.is_retryable_failure()); assert!(RadrootsTransportDeliveryTargetStatus::FailedTerminal.is_terminal_failure()); @@ -688,120 +775,130 @@ fn satisfaction_and_target_status_cover_all_contract_states() { #[test] fn typed_outcome_kinds_drive_status_and_satisfaction_semantics() { + let classes = [ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + RadrootsTransportSatisfactionClass::Stored, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::Delivered, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ]; let cases = [ ( RadrootsTransportOutcomeKind::Accepted, "accepted", RadrootsTransportDeliveryTargetStatus::Accepted, - true, - false, + &[RadrootsTransportSatisfactionClass::Accepted] as &[_], ), ( RadrootsTransportOutcomeKind::DuplicateAccepted, "duplicate_accepted", RadrootsTransportDeliveryTargetStatus::Accepted, - true, - false, + &[RadrootsTransportSatisfactionClass::Accepted], ), ( RadrootsTransportOutcomeKind::Delivered, "delivered", RadrootsTransportDeliveryTargetStatus::Delivered, - true, - true, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::Delivered, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], ), ( RadrootsTransportOutcomeKind::Forwarded, "forwarded", RadrootsTransportDeliveryTargetStatus::Forwarded, - true, - true, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Forwarded, + ], ), ( RadrootsTransportOutcomeKind::StoredByGateway, "stored_by_gateway", RadrootsTransportDeliveryTargetStatus::StoredByGateway, - true, - true, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Stored, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], ), ( RadrootsTransportOutcomeKind::Seen, "seen", RadrootsTransportDeliveryTargetStatus::Seen, - true, - true, + &[ + RadrootsTransportSatisfactionClass::Accepted, + RadrootsTransportSatisfactionClass::Seen, + RadrootsTransportSatisfactionClass::DurableOrObserved, + ], ), ( RadrootsTransportOutcomeKind::DeferredUntilImplemented, "deferred_until_implemented", RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::Rejected, "rejected", RadrootsTransportDeliveryTargetStatus::FailedTerminal, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::RouteUnavailable, "route_unavailable", RadrootsTransportDeliveryTargetStatus::FailedTerminal, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::PayloadTooLarge, "payload_too_large", RadrootsTransportDeliveryTargetStatus::FailedTerminal, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::PolicyDenied, "policy_denied", RadrootsTransportDeliveryTargetStatus::SkippedPolicyDenied, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::Timeout, "timeout", RadrootsTransportDeliveryTargetStatus::FailedRetryable, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::ConnectionFailed, "connection_failed", RadrootsTransportDeliveryTargetStatus::FailedRetryable, - false, - false, + &[], ), ( RadrootsTransportOutcomeKind::TransportUnavailable, "transport_unavailable", RadrootsTransportDeliveryTargetStatus::FailedRetryable, - false, - false, + &[], ), ]; - for (kind, label, status, accepted, delivered) in cases { + for (kind, label, status, satisfied_classes) in cases { let outcome = RadrootsTransportOutcome::new(kind).with_message("transport detail"); assert_eq!(kind.as_str(), label); assert_eq!(outcome.kind, kind); assert_eq!(outcome.status, status); - assert_eq!( - kind.counts_as_satisfied(RadrootsTransportSatisfactionClass::Accepted), - accepted - ); - assert_eq!( - kind.counts_as_satisfied(RadrootsTransportSatisfactionClass::Delivered), - delivered - ); + for class in classes { + assert_eq!( + kind.counts_as_satisfied(class), + satisfied_classes.contains(&class), + "{kind:?} / {class:?}" + ); + } assert_eq!(outcome.message.as_deref(), Some("transport detail")); }