lib

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

commit e1f4c4091704acb47930cf3f48de5037e83c55a5
parent fc09360cfd4e161e13e72127e350852dcf47c18a
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 01:19:56 +0000

outbox: exclude preview targets from satisfaction capacity

- compute required successes from delivery-capable targets
- preserve preview Reticulum targets in stored delivery plans
- prove Hybrid AllTargets completion through Nostr success
- reject quorum policies that require preview target success

Diffstat:
Mcrates/outbox/src/store.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 161 insertions(+), 3 deletions(-)

diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -1300,9 +1300,7 @@ fn prepare_delivery_plan( RadrootsTransportError::InvalidTransportKind, )); } - let required_success_count = input - .satisfaction_policy - .required_target_count(targets.len())? as i64; + let total_target_count = targets.len(); let prepared_targets = targets .into_iter() .map(|target| { @@ -1315,6 +1313,13 @@ fn prepare_delivery_plan( }) }) .collect::<Result<Vec<_>, RadrootsOutboxError>>()?; + let required_success_count = + input + .satisfaction_policy + .required_target_count(satisfaction_target_count( + &prepared_targets, + total_target_count, + ))? as i64; let initial_status = initial_delivery_plan_status(&prepared_targets); let target_policy_fingerprint = target_policy_fingerprint( &input.satisfaction_policy, @@ -1339,6 +1344,20 @@ fn prepare_delivery_plan( }) } +fn satisfaction_target_count( + prepared_targets: &[PreparedDeliveryTarget], + total_target_count: usize, +) -> usize { + let delivery_capable_target_count = prepared_targets + .iter() + .filter(|target| !target.initial_status.is_deferred_preview()) + .count(); + if delivery_capable_target_count == 0 { + return total_target_count; + } + delivery_capable_target_count +} + fn validate_delivery_target(target: &RadrootsTransportTarget) -> Result<(), RadrootsOutboxError> { if target.kind == RadrootsTransportKind::Reticulum && target.uri.as_str() != RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI @@ -3543,6 +3562,145 @@ mod tests { } #[tokio::test] + async fn hybrid_all_targets_completes_after_nostr_success_with_reticulum_preview_preserved() { + let outbox = RadrootsOutbox::open_memory().await.expect("open"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid all targets"); + let signed_event = + radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); + let receipt = outbox + .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( + "publish_post", + draft, + signed_event, + RadrootsOutboxDeliveryPlanInput::new( + "transport.hybrid", + 1, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![ + nostr_target(NOSTR_PRIMARY_WSS), + reticulum_target("reticulum:preview-unavailable"), + ], + ), + true, + 1_007, + 1_000, + )) + .await + .expect("enqueue"); + let plans = outbox + .delivery_plans(receipt.outbox_event_id) + .await + .expect("plans"); + assert_eq!(plans.len(), 1); + assert_eq!(plans[0].required_success_count, 1); + assert_eq!(plans[0].status, RadrootsOutboxDeliveryPlanStatus::Queued); + let targets = outbox + .delivery_targets(receipt.outbox_event_id) + .await + .expect("targets"); + assert_eq!(targets.len(), 2); + assert!( + targets + .iter() + .any(|target| target.status == RadrootsOutboxDeliveryTargetStatus::Pending) + ); + assert!(targets.iter().any(|target| { + target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + })); + let claimed = outbox + .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000) + .await + .expect("claim") + .expect("claimed"); + assert_eq!(claimed.delivery_targets.len(), 1); + outbox + .mark_delivery_target_accepted( + receipt.outbox_event_id, + "claim-a", + claimed.delivery_targets[0].delivery_target_id, + 1_100, + ) + .await + .expect("accepted"); + + let state = outbox + .complete_publish_attempt( + receipt.outbox_event_id, + "claim-a", + "retryable", + "terminal", + 2_500, + 1_200, + ) + .await + .expect("complete"); + + assert_eq!(state, RadrootsOutboxEventState::Published); + let operation = outbox + .get_operation(receipt.operation_id) + .await + .expect("operation") + .expect("operation"); + assert_eq!(operation.status, RadrootsOutboxOperationStatus::Complete); + let plans = outbox + .delivery_plans(receipt.outbox_event_id) + .await + .expect("plans"); + assert_eq!(plans[0].status, RadrootsOutboxDeliveryPlanStatus::Complete); + assert_eq!(plans[0].satisfied_at_ms, Some(1_200)); + let targets = outbox + .delivery_targets(receipt.outbox_event_id) + .await + .expect("targets"); + assert!(targets.iter().any(|target| { + target.status == RadrootsOutboxDeliveryTargetStatus::Accepted + && target.attempt_count == 1 + })); + assert!(targets.iter().any(|target| { + target.status == RadrootsOutboxDeliveryTargetStatus::PreviewUnavailable + && target.attempt_count == 0 + && target.completed_at_ms.is_none() + })); + } + + #[tokio::test] + async fn hybrid_quorum_rejects_preview_targets_as_required_success_capacity() { + let outbox = RadrootsOutbox::open_memory().await.expect("open"); + let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid quorum"); + let signed_event = + radroots_nostr_sign_frozen_draft(&fixture_keys(), &draft).expect("signed event"); + let err = outbox + .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( + "publish_post", + draft, + signed_event, + RadrootsOutboxDeliveryPlanInput::new( + "transport.hybrid", + 1, + RadrootsTransportSatisfactionPolicy::quorum_accepted(2), + vec![ + nostr_target(NOSTR_PRIMARY_WSS), + reticulum_target("reticulum:preview-unavailable"), + ], + ), + true, + 1_007, + 1_000, + )) + .await + .expect_err("quorum exceeds delivery-capable targets"); + + assert!(matches!( + err, + RadrootsOutboxError::Transport(RadrootsTransportError::InvalidSatisfactionPolicy) + )); + assert_eq!(table_count(&outbox, "outbox_operations").await, 0); + assert_eq!(table_count(&outbox, "outbox_event").await, 0); + assert_eq!(table_count(&outbox, "outbox_delivery_plan").await, 0); + assert_eq!(table_count(&outbox, "outbox_delivery_target").await, 0); + } + + #[tokio::test] async fn hybrid_nostr_and_reticulum_preview_preserves_ready_nostr_work() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let draft = post_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX, "hybrid preview");