Skip to main content

commonware_resolver/
delivery.rs

1//! Track pending consumer deliveries for resolver implementations.
2//!
3//! Resolvers often need the same delivery lifecycle: keep a fetch alive while
4//! `Consumer::deliver` validates a response, abort that validation if the fetch
5//! is pruned, and reuse a cached response for subscribers that were added
6//! while validation was in progress. This module owns that lifecycle without
7//! making assumptions about how data is fetched.
8
9use crate::{Consumer, Delivery, Outcome};
10use ahash::AHashMap;
11use commonware_macros::select;
12use commonware_utils::{channel::oneshot, futures::Pool};
13use futures::future::Aborted;
14use std::collections::hash_map::Entry as HashMapEntry;
15use tracing::debug;
16
17/// Completed consumer validation for a delivery.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct Completion<K, S, Context = ()> {
20    /// Resolver-specific context associated with the delivery attempt.
21    pub context: Context,
22
23    /// Key and subscribers that were passed to the consumer.
24    pub delivery: Delivery<K, S>,
25
26    /// Consumer disposition for the delivered response, or `None` if the
27    /// consumer dropped the sender without reporting one.
28    pub outcome: Option<Outcome>,
29
30    /// Whether the tracked fetch was removed before this completion was polled.
31    /// Its verdict applies only to the source, not to any current fetch for the key.
32    pub canceled: bool,
33
34    /// Whether the cached response was already accepted when this delivery began.
35    pub previously_accepted: bool,
36}
37
38// Delivery futures handle cancellation so verdicts sent before receiver closure can still
39// reach source accounting. Canceled deliveries without a verdict return `Aborted`.
40type DeliveryPool<K, S, Context> = Pool<'static, Result<Completion<K, S, Context>, Aborted>>;
41
42// Cached response that can be redelivered while it is accepted or still unjudged.
43struct Response<Context, V> {
44    context: Context,
45    value: V,
46    accepted: bool,
47}
48
49// Per-key delivery state retained while a resolver fetch is active.
50struct Entry<Context, V, State> {
51    delivery: Option<oneshot::Sender<()>>,
52    response: Option<Response<Context, V>>,
53    state: Option<State>,
54}
55
56impl<Context, V, State> Entry<Context, V, State> {
57    const fn new(state: State) -> Self {
58        Self {
59            delivery: None,
60            response: None,
61            state: Some(state),
62        }
63    }
64}
65
66/// Tracks in-flight consumer deliveries keyed by resolver key.
67///
68/// `Context` carries resolver-specific metadata back to the caller when
69/// validation completes.
70///
71/// `State` holds per-key resolver state that is dropped when the key is pruned.
72/// While the key stays tracked, the resolver can read, update, or take it.
73pub struct Tracker<Con, Context = (), State = ()>
74where
75    Con: Consumer,
76    Con::Value: Clone + Send + 'static,
77    Context: Clone + Send + 'static,
78{
79    entries: AHashMap<Con::Key, Entry<Context, Con::Value, State>>,
80    deliveries: DeliveryPool<Con::Key, Con::Subscriber, Context>,
81    consumer: Con,
82}
83
84impl<Con, Context, State> Tracker<Con, Context, State>
85where
86    Con: Consumer,
87    Con::Value: Clone + Send + 'static,
88    Context: Clone + Send + 'static,
89{
90    /// Create an empty tracker backed by the provided consumer.
91    pub fn new(consumer: Con) -> Self {
92        Self {
93            entries: AHashMap::new(),
94            deliveries: Pool::default(),
95            consumer,
96        }
97    }
98
99    /// Returns true when the key has an active tracked fetch.
100    pub fn contains(&self, key: &Con::Key) -> bool {
101        self.entries.contains_key(key)
102    }
103
104    /// Start tracking a key.
105    ///
106    /// Returns `true` when the key was inserted. If the key is already tracked,
107    /// this leaves the existing entry untouched and returns `false`.
108    pub(crate) fn insert_with_state(&mut self, key: Con::Key, state: State) -> bool {
109        match self.entries.entry(key) {
110            HashMapEntry::Vacant(entry) => {
111                entry.insert(Entry::new(state));
112                true
113            }
114            HashMapEntry::Occupied(_) => false,
115        }
116    }
117
118    /// Remove a key and abort any in-progress delivery for it.
119    ///
120    /// Returns true if the key was present. A verdict accepted before cancellation
121    /// closes its receiver is returned as a canceled [`Completion`].
122    pub fn remove(&mut self, key: &Con::Key) -> bool {
123        self.entries.remove(key).is_some()
124    }
125
126    /// Remove a key, aborting any in-progress delivery, and return its state.
127    ///
128    /// Returns `None` if the key was absent. The inner `Option` is `None` if the
129    /// state was already taken while the key stayed active.
130    pub(crate) fn remove_with_state(&mut self, key: &Con::Key) -> Option<Option<State>> {
131        self.entries.remove(key).map(|entry| entry.state)
132    }
133
134    /// Take the key's state without removing the tracked key.
135    ///
136    /// Returns `None` when the key is absent or the state was already taken.
137    pub(crate) fn take_state(&mut self, key: &Con::Key) -> Option<State> {
138        self.entries
139            .get_mut(key)
140            .and_then(|entry| entry.state.take())
141    }
142
143    /// Mutably borrow the key's state without taking it.
144    ///
145    /// Returns `None` when the key is absent or the state was already taken.
146    pub(crate) fn state_mut(&mut self, key: &Con::Key) -> Option<&mut State> {
147        self.entries
148            .get_mut(key)
149            .and_then(|entry| entry.state.as_mut())
150    }
151
152    /// Retain only entries for which the predicate returns true.
153    ///
154    /// Dropped entries abort in-progress deliveries. Returns the number of
155    /// removed entries.
156    pub fn retain<F: FnMut(&Con::Key) -> bool>(&mut self, mut predicate: F) -> usize {
157        self.entries.extract_if(|key, _| !predicate(key)).count()
158    }
159
160    /// Remove all entries and abort all in-progress deliveries.
161    ///
162    /// Returns the number of entries removed.
163    pub fn drain(&mut self) -> usize {
164        let count = self.entries.len();
165        self.entries.clear();
166        count
167    }
168
169    /// Deliver a newly received response to the consumer.
170    ///
171    /// The response is cached so that later retained subscribers can be
172    /// redelivered the same bytes with [`redeliver`](Self::redeliver) once the
173    /// consumer accepts it or drops its verdict. Panics if the key is not tracked.
174    pub fn deliver(
175        &mut self,
176        delivery: Delivery<Con::Key, Con::Subscriber>,
177        context: Context,
178        value: Con::Value,
179    ) {
180        let entry = self.entries.get_mut(&delivery.key).expect("delivery entry");
181        entry.response = Some(Response {
182            context: context.clone(),
183            value: value.clone(),
184            accepted: false,
185        });
186        self.push_delivery(delivery, context, value);
187    }
188
189    /// Deliver the cached response to another set of subscribers.
190    ///
191    /// This is intended for subscribers added while an earlier validation was
192    /// still pending. The cached response is either accepted or still unjudged
193    /// because the consumer dropped the earlier verdict. Panics if the key is not
194    /// tracked or no response is cached.
195    pub fn redeliver(&mut self, delivery: Delivery<Con::Key, Con::Subscriber>) {
196        let (context, value) = {
197            let entry = self.entries.get(&delivery.key).expect("delivery entry");
198            let response = entry.response.as_ref().expect("response");
199            (response.context.clone(), response.value.clone())
200        };
201        self.push_delivery(delivery, context, value);
202    }
203
204    /// Returns true if the cached response for this key has been accepted.
205    pub fn response_accepted(&self, key: &Con::Key) -> bool {
206        self.entries
207            .get(key)
208            .and_then(|entry| entry.response.as_ref())
209            .is_some_and(|response| response.accepted)
210    }
211
212    /// Mark the cached response accepted by the consumer.
213    ///
214    /// Panics if the key is not tracked or no response is cached.
215    pub fn accept_response(&mut self, key: &Con::Key) {
216        let entry = self.entries.get_mut(key).expect("delivery entry");
217        let response = entry.response.as_mut().expect("response");
218        response.accepted = true;
219    }
220
221    /// Drop the cached response without removing the tracked key.
222    ///
223    /// Use this when a response is invalid or does not satisfy every delivered
224    /// subscriber and the resolver wants to retry the key.
225    pub fn discard_response(&mut self, key: &Con::Key) {
226        if let Some(entry) = self.entries.get_mut(key) {
227            entry.response = None;
228        }
229    }
230
231    /// Wait for the next consumer validation result.
232    ///
233    /// Returns [`Aborted`] when cancellation closes the receiver without an accepted
234    /// verdict. Otherwise, canceled deliveries return their verdict for source
235    /// accounting without affecting any current fetch for the key. Active deliveries
236    /// clear their slot so the key can be retried or redelivered. A verdict sender
237    /// dropped by the consumer completes with no outcome.
238    pub async fn next_completion(
239        &mut self,
240    ) -> Result<Completion<Con::Key, Con::Subscriber, Context>, Aborted> {
241        let completed = self.deliveries.next_completed().await?;
242
243        // Cancellation belongs to the removed entry. Only an active completion can
244        // clear a delivery slot; a canceled key may already have a replacement entry.
245        if !completed.canceled {
246            let entry = self
247                .entries
248                .get_mut(&completed.delivery.key)
249                .expect("delivery entry");
250            entry.delivery = None;
251        }
252        Ok(completed)
253    }
254
255    // Start a consumer validation attempt whose cancellation is owned by its entry.
256    fn push_delivery(
257        &mut self,
258        delivery: Delivery<Con::Key, Con::Subscriber>,
259        context: Context,
260        value: Con::Value,
261    ) {
262        let entry = self.entries.get_mut(&delivery.key).expect("delivery entry");
263        let previously_accepted = entry.response.as_ref().expect("response").accepted;
264        let completed = delivery.clone();
265        let mut consumer = self.consumer.clone();
266        let mut receiver = consumer.deliver(delivery, value);
267        let (cancel, canceled) = oneshot::channel::<()>();
268        self.deliveries.push(async move {
269            // Cancellation takes precedence over a ready verdict so feedback from a
270            // removed entry can never complete a replacement. Closing before reading
271            // preserves successful sends without waiting for an unfinished verdict.
272            let (outcome, canceled) = select! {
273                _ = canceled => {
274                    receiver.close();
275                    (receiver.try_recv().ok().map(Into::into), true)
276                },
277                outcome = &mut receiver => {
278                    let outcome = match outcome {
279                        Ok(outcome) => Some(outcome.into()),
280                        Err(_) => {
281                            debug!(key = ?completed.key, "consumer dropped delivery without a verdict");
282                            None
283                        }
284                    };
285                    (outcome, false)
286                },
287            };
288            if canceled && outcome.is_none() {
289                return Err(Aborted);
290            }
291            Ok(Completion {
292                context,
293                delivery: completed,
294                outcome,
295                canceled,
296                previously_accepted,
297            })
298        });
299        assert!(entry.delivery.replace(cancel).is_none());
300    }
301}
302
303impl<Con, Context> Tracker<Con, Context>
304where
305    Con: Consumer,
306    Con::Value: Clone + Send + 'static,
307    Context: Clone + Send + 'static,
308{
309    /// Start tracking a key without any resolver-specific state.
310    ///
311    /// Returns `true` when the key was inserted. If the key is already tracked,
312    /// this leaves the existing entry untouched and returns `false`.
313    pub fn insert(&mut self, key: Con::Key) -> bool {
314        self.insert_with_state(key, ())
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::{
322        Resolution,
323        p2p::mocks::{Consumer as MockConsumer, Key as MockKey},
324    };
325    use bytes::Bytes;
326    use commonware_runtime::{Runner as _, deterministic::Runner};
327    use commonware_utils::{
328        channel::{fallible::FallibleExt, mpsc, oneshot},
329        non_empty_vec,
330    };
331    use futures::FutureExt as _;
332
333    type TestTracker = Tracker<MockConsumer<MockKey, Bytes>, u8>;
334    type StateTracker = Tracker<MockConsumer<MockKey, Bytes>, u8, u8>;
335
336    fn delivery(key: MockKey) -> Delivery<MockKey, ()> {
337        Delivery {
338            key,
339            subscribers: non_empty_vec![((), tracing::Span::none())],
340        }
341    }
342
343    #[derive(Clone)]
344    struct PendingConsumer {
345        sender: mpsc::UnboundedSender<oneshot::Sender<bool>>,
346    }
347
348    impl PendingConsumer {
349        fn new() -> (Self, mpsc::UnboundedReceiver<oneshot::Sender<bool>>) {
350            let (sender, receiver) = mpsc::unbounded_channel();
351            (Self { sender }, receiver)
352        }
353    }
354
355    impl Consumer for PendingConsumer {
356        type Key = MockKey;
357        type Value = Bytes;
358        type Subscriber = ();
359        type Outcome = bool;
360
361        fn deliver(
362            &mut self,
363            _delivery: Delivery<Self::Key, Self::Subscriber>,
364            _value: Self::Value,
365        ) -> oneshot::Receiver<bool> {
366            let (sender, receiver) = oneshot::channel();
367            self.sender.send_lossy(sender);
368            receiver
369        }
370    }
371
372    #[test]
373    fn test_insert_contains_remove_round_trip() {
374        let runner = Runner::default();
375        runner.start(|_| async move {
376            let mut tracker = TestTracker::new(MockConsumer::dummy());
377
378            assert!(!tracker.contains(&MockKey(1)));
379            assert!(tracker.insert(MockKey(1)));
380            assert!(tracker.contains(&MockKey(1)));
381
382            assert!(!tracker.insert(MockKey(1)));
383            assert!(tracker.remove(&MockKey(1)));
384            assert!(!tracker.contains(&MockKey(1)));
385            assert!(!tracker.remove(&MockKey(1)));
386        });
387    }
388
389    #[test]
390    fn test_state_round_trip() {
391        let runner = Runner::default();
392        runner.start(|_| async move {
393            // An untracked key has no state.
394            let mut tracker = StateTracker::new(MockConsumer::dummy());
395            assert_eq!(tracker.state_mut(&MockKey(1)).map(|state| *state), None);
396
397            // Inserted state can be read and updated in place.
398            assert!(tracker.insert_with_state(MockKey(1), 5));
399            assert_eq!(tracker.state_mut(&MockKey(1)).map(|state| *state), Some(5));
400            *tracker
401                .state_mut(&MockKey(1))
402                .expect("inserted key must have state") = 7;
403            assert_eq!(tracker.state_mut(&MockKey(1)).map(|state| *state), Some(7));
404
405            // Taking the state leaves the key tracked without state, so removing
406            // the key returns no state.
407            assert_eq!(tracker.take_state(&MockKey(1)), Some(7));
408            assert!(tracker.contains(&MockKey(1)));
409            assert_eq!(tracker.state_mut(&MockKey(1)), None);
410            assert_eq!(tracker.remove_with_state(&MockKey(1)), Some(None));
411            assert_eq!(tracker.state_mut(&MockKey(1)).map(|state| *state), None);
412        });
413    }
414
415    #[test]
416    fn test_deliver_completes_with_context_and_consumer_result() {
417        let runner = Runner::default();
418        runner.start(|_| async move {
419            let (consumer, mut events) = MockConsumer::<MockKey, Bytes>::new();
420            let mut tracker = TestTracker::new(consumer);
421            let key = MockKey(7);
422            let value = Bytes::from("data");
423
424            tracker.insert(key.clone());
425            tracker.deliver(delivery(key.clone()), 9, value.clone());
426
427            let completed = tracker
428                .next_completion()
429                .await
430                .expect("delivery should complete");
431            assert_eq!(completed.context, 9);
432            assert_eq!(completed.delivery.key, key);
433            assert!(!completed.canceled);
434            assert!(!completed.previously_accepted);
435            assert_eq!(
436                completed.outcome,
437                Some(Outcome::Resolved(Resolution::Complete))
438            );
439
440            let (delivered_key, delivered_value) = events.recv().await.unwrap();
441            assert_eq!(delivered_key, key);
442            assert_eq!(delivered_value, value);
443        });
444    }
445
446    #[test]
447    fn test_remove_aborts_in_flight_delivery() {
448        let runner = Runner::default();
449        runner.start(|_| async move {
450            let (consumer, mut senders) = PendingConsumer::new();
451            let mut tracker = Tracker::<_, u8>::new(consumer);
452            let key = MockKey(1);
453
454            tracker.insert(key.clone());
455            tracker.deliver(delivery(key.clone()), 2, Bytes::from("v"));
456            let sender = senders.recv().await.unwrap();
457            assert!(tracker.remove(&key));
458
459            assert!(matches!(tracker.next_completion().await, Err(Aborted)));
460            assert!(sender.is_closed());
461        });
462    }
463
464    #[test]
465    fn test_canceled_delivery_cannot_complete_reinserted_key() {
466        #[derive(Clone, Copy)]
467        enum VerdictTiming {
468            BeforeRemoval,
469            AfterRemoval,
470            AfterClose,
471        }
472
473        // Every removal path must preserve accepted feedback and isolate the replacement.
474        for removal in 0..4 {
475            for timing in [
476                VerdictTiming::BeforeRemoval,
477                VerdictTiming::AfterRemoval,
478                VerdictTiming::AfterClose,
479            ] {
480                for polled in [false, true] {
481                    Runner::default().start(move |_| async move {
482                        // Keep the old verdict pending or make it ready without consuming it.
483                        let (consumer, mut senders) = PendingConsumer::new();
484                        let mut tracker = Tracker::<PendingConsumer, u8, u8>::new(consumer);
485                        let key = MockKey(1);
486                        assert!(tracker.insert_with_state(key.clone(), 10));
487                        tracker.deliver(delivery(key.clone()), 1, Bytes::from("old"));
488                        let mut old_sender = Some(senders.recv().await.unwrap());
489                        if polled {
490                            assert!(tracker.next_completion().now_or_never().is_none());
491                        }
492                        if matches!(timing, VerdictTiming::BeforeRemoval) {
493                            old_sender.take().unwrap().send(true).unwrap();
494                        }
495
496                        // Reuse the key before polling the canceled delivery.
497                        match removal {
498                            0 => assert!(tracker.remove(&key)),
499                            1 => assert_eq!(tracker.remove_with_state(&key), Some(Some(10))),
500                            2 => assert_eq!(tracker.retain(|_| false), 1),
501                            _ => assert_eq!(tracker.drain(), 1),
502                        }
503                        if matches!(timing, VerdictTiming::AfterRemoval) {
504                            old_sender.take().unwrap().send(true).unwrap();
505                        }
506                        assert!(tracker.insert_with_state(key.clone(), 20));
507                        tracker.deliver(delivery(key.clone()), 2, Bytes::from("new"));
508                        let new_sender = senders.recv().await.unwrap();
509
510                        // Cancellation closes pending verdicts and preserves accepted
511                        // ones without touching the replacement's state or delivery.
512                        let result = tracker.next_completion().await;
513                        if matches!(timing, VerdictTiming::AfterClose) {
514                            assert!(matches!(result, Err(Aborted)));
515                            assert!(old_sender.take().unwrap().send(true).is_err());
516                        } else {
517                            let completed = result.unwrap();
518                            assert!(completed.canceled);
519                            assert!(!completed.previously_accepted);
520                            assert_eq!(completed.context, 1);
521                            assert_eq!(
522                                completed.outcome,
523                                Some(Outcome::Resolved(Resolution::Complete))
524                            );
525                        }
526                        assert_eq!(tracker.state_mut(&key).map(|state| *state), Some(20));
527                        assert!(tracker.entries[&key].delivery.is_some());
528                        assert!(tracker.next_completion().now_or_never().is_none());
529
530                        // Only the replacement's verdict may complete the new entry.
531                        new_sender.send(true).unwrap();
532                        let completed = tracker.next_completion().await.unwrap();
533                        assert!(!completed.canceled);
534                        assert_eq!(completed.context, 2);
535                        assert_eq!(completed.delivery.key, key);
536                        assert_eq!(
537                            completed.outcome,
538                            Some(Outcome::Resolved(Resolution::Complete))
539                        );
540                        assert!(tracker.deliveries.is_empty());
541                    });
542                }
543            }
544        }
545    }
546
547    #[test]
548    fn test_dropped_verdict_completes_without_outcome_and_redelivers() {
549        let runner = Runner::default();
550        runner.start(|_| async move {
551            let (consumer, mut senders) = PendingConsumer::new();
552            let mut tracker = Tracker::<PendingConsumer, u8>::new(consumer);
553            let key = MockKey(3);
554
555            tracker.insert(key.clone());
556            tracker.deliver(delivery(key.clone()), 4, Bytes::from("unjudged"));
557            drop(senders.recv().await.unwrap());
558
559            let completed = tracker
560                .next_completion()
561                .await
562                .expect("dropped verdict should complete");
563            assert_eq!(completed.context, 4);
564            assert_eq!(completed.delivery.key, key);
565            assert_eq!(completed.outcome, None);
566            assert!(!tracker.response_accepted(&key));
567
568            // The unjudged response can still be handed to other subscribers.
569            tracker.redeliver(delivery(key.clone()));
570            senders.recv().await.unwrap().send(true).unwrap();
571            let judged = tracker
572                .next_completion()
573                .await
574                .expect("redelivery should complete");
575            assert_eq!(judged.context, 4);
576            assert_eq!(
577                judged.outcome,
578                Some(Outcome::Resolved(Resolution::Complete))
579            );
580        });
581    }
582
583    #[test]
584    fn test_redeliver_reuses_accepted_response_for_new_subscribers() {
585        let runner = Runner::default();
586        runner.start(|_| async move {
587            let (consumer, mut events) = MockConsumer::<MockKey, Bytes>::new();
588            let mut tracker = TestTracker::new(consumer);
589            let key = MockKey(5);
590            let value = Bytes::from("first");
591
592            tracker.insert(key.clone());
593            tracker.deliver(delivery(key.clone()), 3, value.clone());
594
595            let completed = tracker
596                .next_completion()
597                .await
598                .expect("first delivery should complete");
599            assert!(!completed.previously_accepted);
600            assert_eq!(
601                completed.outcome,
602                Some(Outcome::Resolved(Resolution::Complete))
603            );
604            tracker.accept_response(&key);
605            assert!(tracker.response_accepted(&key));
606
607            tracker.redeliver(delivery(key.clone()));
608            let redelivered = tracker
609                .next_completion()
610                .await
611                .expect("redelivery should complete");
612            assert_eq!(redelivered.context, 3);
613            assert_eq!(redelivered.delivery.key, key);
614            assert!(redelivered.previously_accepted);
615            assert_eq!(
616                redelivered.outcome,
617                Some(Outcome::Resolved(Resolution::Complete))
618            );
619
620            let first = events.recv().await.unwrap();
621            let second = events.recv().await.unwrap();
622            assert_eq!(first, (key.clone(), value.clone()));
623            assert_eq!(second, (key, value));
624        });
625    }
626
627    #[test]
628    #[should_panic(expected = "response")]
629    fn test_redeliver_requires_cached_response() {
630        let runner = Runner::default();
631        runner.start(|_| async move {
632            let (consumer, _events) = MockConsumer::<MockKey, Bytes>::new();
633            let mut tracker = TestTracker::new(consumer);
634            let key = MockKey(7);
635
636            tracker.insert(key.clone());
637            tracker.deliver(delivery(key.clone()), 3, Bytes::from("first"));
638            let completed = tracker
639                .next_completion()
640                .await
641                .expect("first delivery should complete");
642            assert_eq!(
643                completed.outcome,
644                Some(Outcome::Resolved(Resolution::Complete))
645            );
646
647            tracker.discard_response(&key);
648            tracker.redeliver(delivery(key));
649        });
650    }
651
652    #[test]
653    fn test_rejected_response_can_be_discarded_and_replaced() {
654        let runner = Runner::default();
655        runner.start(|_| async move {
656            let (mut consumer, _events) = MockConsumer::<MockKey, Bytes>::new();
657            let key = MockKey(8);
658            consumer.add_expected(key.clone(), Bytes::from("good"));
659            let mut tracker = TestTracker::new(consumer);
660
661            tracker.insert(key.clone());
662            tracker.deliver(delivery(key.clone()), 1, Bytes::from("bad"));
663            let rejected = tracker
664                .next_completion()
665                .await
666                .expect("rejected delivery should complete");
667            assert_eq!(rejected.outcome, Some(Outcome::Invalid));
668
669            tracker.discard_response(&key);
670            assert!(!tracker.response_accepted(&key));
671            tracker.deliver(delivery(key.clone()), 2, Bytes::from("good"));
672
673            let accepted = tracker
674                .next_completion()
675                .await
676                .expect("accepted delivery should complete");
677            assert_eq!(accepted.context, 2);
678            assert_eq!(
679                accepted.outcome,
680                Some(Outcome::Resolved(Resolution::Complete))
681            );
682        });
683    }
684}