holochain_types/
chain.rs

1//! Types related to an agents for chain activity
2use std::iter::Peekable;
3use std::ops::RangeInclusive;
4
5use crate::activity::AgentActivityResponse;
6use crate::activity::ChainItems;
7use crate::warrant::WarrantOp;
8use holo_hash::ActionHash;
9use holo_hash::AgentPubKey;
10use holo_hash::HasHash;
11use holochain_serialized_bytes::prelude::*;
12use holochain_zome_types::prelude::*;
13
14#[cfg(all(test, feature = "test_utils"))]
15mod test;
16
17mod chain_item;
18pub use chain_item::*;
19
20/// Helpers for constructing AgentActivity
21pub trait AgentActivityExt {
22    /// Create an empty chain status
23    fn empty<T>(agent: &AgentPubKey) -> AgentActivityResponse {
24        AgentActivityResponse {
25            agent: agent.clone(),
26            valid_activity: ChainItems::NotRequested,
27            rejected_activity: ChainItems::NotRequested,
28            status: ChainStatus::Empty,
29            // TODO: Add the actual highest observed in a follow up PR
30            highest_observed: None,
31            warrants: vec![],
32        }
33    }
34}
35
36impl AgentActivityExt for AgentActivityResponse {}
37
38#[must_use = "Iterator doesn't do anything unless consumed."]
39#[derive(Debug)]
40/// Iterate over a source chain and apply the [`ChainFilter`] to each element.
41/// This iterator will:
42/// - Ignore any ops that are not a direct ancestor to the starting position.
43/// - Stop at the first gap in the chain.
44/// - Take no **more** then the [`take`]. It may return less.
45/// - Stop at (including) the [`ActionHash`] in [`until`]. But not if this hash is not in the chain.
46///
47/// [`take`]: ChainFilter::take
48/// [`until`]: ChainFilter::until
49pub struct ChainFilterIter<I: AsRef<A>, A: ChainItem = SignedActionHashed> {
50    filter: ChainFilter<A::Hash>,
51    iter: Peekable<std::vec::IntoIter<I>>,
52    end: bool,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56/// A [`ChainFilter`] with the action sequences for the
57/// starting position and any `until` hashes.
58pub struct ChainFilterRange {
59    /// The filter for for this chain.
60    filter: ChainFilter,
61    /// The start of this range is the end of
62    /// the filter iterator.
63    /// The end of this range is the sequence of
64    /// the starting position hash.
65    range: RangeInclusive<u32>,
66    /// The start of the range's type.
67    chain_bottom_type: ChainBottomType,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71/// The type of chain item that forms the bottom of the chain.
72enum ChainBottomType {
73    /// The bottom of the chain is genesis.
74    Genesis,
75    /// The bottom of the chain is the action where `take`
76    /// has reached zero.
77    Take,
78    /// The bottom of the chain is the action where an
79    /// `until` hash was found.
80    Until,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84/// Outcome of trying to find the action sequences in a filter.
85pub enum Sequences {
86    /// Found all action sequences
87    Found(ChainFilterRange),
88    /// The chain top action was not found.
89    ChainTopNotFound(ActionHash),
90    /// The filter produces an empty range.
91    EmptyRange,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, SerializedBytes, Serialize, Deserialize)]
95/// Intermediate data structure used during a `must_get_agent_activity` call.
96/// Note that this is not the final return value of `must_get_agent_activity`.
97pub enum MustGetAgentActivityResponse {
98    /// The activity was found.
99    Activity {
100        /// The actions performed by the agent.
101        activity: Vec<RegisterAgentActivity>,
102        /// Any warrants issued to the agent for this activity.
103        warrants: Vec<WarrantOp>,
104    },
105    /// The requested chain range was incomplete.
106    IncompleteChain,
107    /// The requested chain top was not found in the chain.
108    ChainTopNotFound(ActionHash),
109    /// The filter produces an empty range.
110    EmptyRange,
111}
112
113impl MustGetAgentActivityResponse {
114    /// Constructor
115    #[cfg(feature = "test_utils")]
116    pub fn activity(activity: Vec<RegisterAgentActivity>) -> Self {
117        Self::Activity {
118            activity,
119            warrants: vec![],
120        }
121    }
122}
123
124/// Identical structure to [`MustGetAgentActivityResponse`] except it includes
125/// the [`ChainFilterRange`] that was used to produce the response. Doesn't need
126/// to be serialized because it is only used internally.
127/// Note that this is not the final return value of `must_get_agent_activity`.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum BoundedMustGetAgentActivityResponse {
130    /// The activity was found.
131    Activity {
132        /// The actions performed by the agent.
133        activity: Vec<RegisterAgentActivity>,
134        /// Any warrants issued to the agent for this activity.
135        warrants: Vec<WarrantOp>,
136        /// The filter used to produce this response.
137        filter: ChainFilterRange,
138    },
139    /// The requested chain range was incomplete.
140    IncompleteChain,
141    /// The requested chain top was not found in the chain.
142    ChainTopNotFound(ActionHash),
143    /// The filter produces an empty range.
144    EmptyRange,
145}
146
147impl BoundedMustGetAgentActivityResponse {
148    /// Sort by the chain seq.
149    /// Dedupe by action hash.
150    pub fn normalize(&mut self) {
151        if let Self::Activity { activity, .. } = self {
152            activity.sort_unstable_by_key(|a| a.action.action().action_seq());
153            activity.dedup_by_key(|a| a.action.as_hash().clone());
154        }
155    }
156
157    /// Constructor
158    #[cfg(feature = "test_utils")]
159    pub fn activity(actions: Vec<RegisterAgentActivity>, filter: ChainFilterRange) -> Self {
160        Self::Activity {
161            activity: actions,
162            filter,
163            warrants: vec![],
164        }
165    }
166}
167
168/// Merges two agent activity responses, along with their chain filters if
169/// present. Chain filter range mismatches are treated as an incomplete
170/// chain for the purpose of merging. Merging should only be done on
171/// responses that originate from the same authority, so the chain filters
172/// should always match, or at least their mismatch is the responsibility of
173/// a single authority.
174pub fn merge_bounded_agent_activity_responses(
175    acc: BoundedMustGetAgentActivityResponse,
176    next: &BoundedMustGetAgentActivityResponse,
177) -> BoundedMustGetAgentActivityResponse {
178    match (&acc, next) {
179        // If both sides of the merge have activity then merge them or bail
180        // if the chain filters don't match.
181        (
182            BoundedMustGetAgentActivityResponse::Activity {
183                activity: responses,
184                filter: chain_filter,
185                warrants,
186            },
187            BoundedMustGetAgentActivityResponse::Activity {
188                activity: more_responses,
189                filter: other_chain_filter,
190                warrants: more_warrants,
191            },
192        ) => {
193            if chain_filter == other_chain_filter {
194                let mut merged_responses = responses.clone();
195                merged_responses.extend(more_responses.to_owned());
196                let mut merged_warrants = warrants.clone();
197                merged_warrants.extend(more_warrants.to_owned());
198                let mut merged_activity = BoundedMustGetAgentActivityResponse::Activity {
199                    activity: merged_responses,
200                    filter: chain_filter.clone(),
201                    warrants: merged_warrants,
202                };
203                merged_activity.normalize();
204                merged_activity
205            }
206            // If the chain filters disagree on what the filter is we
207            // have a problem.
208            else {
209                BoundedMustGetAgentActivityResponse::IncompleteChain
210            }
211        }
212        // The acc has activity but the next doesn't so we can just return
213        // the acc.
214        (BoundedMustGetAgentActivityResponse::Activity { .. }, _) => acc,
215        // The next has activity but the acc doesn't so we can just return
216        // the next.
217        (_, BoundedMustGetAgentActivityResponse::Activity { .. }) => next.clone(),
218        // Neither have activity so we can just return the acc.
219        _ => acc,
220    }
221}
222
223impl<I: AsRef<A>, A: ChainItem> ChainFilterIter<I, A> {
224    /// Create an iterator that filters an iterator of actions
225    /// with a [`ChainFilter`].
226    ///
227    /// # Constraints
228    /// - If the iterator does not contain the filter's chain_top
229    ///   then this will be an empty iterator.
230    pub fn new(filter: ChainFilter<A::Hash>, mut chain: Vec<I>) -> Self {
231        // Sort by descending.
232        chain.sort_unstable_by_key(|a| u32::MAX - a.as_ref().seq());
233        // Create a peekable iterator.
234        let mut iter = chain.into_iter().peekable();
235
236        // Discard any ops that are not the chain_top.
237        let i = iter.by_ref();
238        while let Some(op) = i.peek() {
239            if *op.as_ref().get_hash() == filter.chain_top {
240                break;
241            }
242            i.next();
243        }
244
245        Self {
246            filter,
247            iter,
248            end: false,
249        }
250    }
251}
252
253impl<I: AsRef<A>, A: ChainItem> Iterator for ChainFilterIter<I, A> {
254    type Item = I;
255
256    fn next(&mut self) -> Option<Self::Item> {
257        if self.end {
258            return None;
259        }
260
261        let op = self.iter.next()?;
262        let op = loop {
263            let parent = self.iter.peek();
264
265            // Check the next sequence number
266            match parent {
267                Some(parent) => {
268                    let child_seq = op.as_ref().seq();
269                    let parent_seq = parent.as_ref().seq();
270                    match (child_seq.cmp(&parent_seq), op.as_ref().prev_hash()) {
271                        (std::cmp::Ordering::Less, _) => {
272                            // The chain is out of order so we must end here.
273                            self.end = true;
274                            break op;
275                        }
276                        (std::cmp::Ordering::Equal, _) => {
277                            // There is a fork in the chain.
278                            // Discard this parent.
279                            self.iter.next();
280                            // Try the next parent.
281                            continue;
282                        }
283                        (std::cmp::Ordering::Greater, None) => {
284                            // The chain is correct however there is no previous action for this child.
285                            // The child can't be the first chain item and doesn't have a parent like:
286                            // `child != 0 && child -> ()`.
287                            // All we can do is end the iterator.
288                            // I don't think this state is actually reachable
289                            // because the only header that can have no previous action is the `Dna` and
290                            // it is always zero.
291                            return None;
292                        }
293                        (std::cmp::Ordering::Greater, _)
294                            if parent_seq.checked_add(1)? != child_seq =>
295                        {
296                            // There is a gap in the chain so we must end here.
297                            self.end = true;
298                            break op;
299                        }
300                        (std::cmp::Ordering::Greater, Some(prev_hash))
301                            if prev_hash != parent.as_ref().get_hash() =>
302                        {
303                            // Not the parent of this child.
304                            // Discard this parent.
305                            self.iter.next();
306                            // Try the next parent.
307                            continue;
308                        }
309                        (std::cmp::Ordering::Greater, Some(_)) => {
310                            // Correct parent found.
311                            break op;
312                        }
313                    }
314                }
315                None => break op,
316            }
317        };
318
319        match &mut self.filter.filters {
320            // Check if there is any left to take.
321            ChainFilters::Take(n) => *n = n.checked_sub(1)?,
322            // Check if the `until` hash has been found.
323            ChainFilters::Until(until_hashes) => {
324                if until_hashes.contains(op.as_ref().get_hash()) {
325                    // If it has, include it and return on the next call to `next`.
326                    self.end = true;
327                }
328            }
329            // Just keep going till genesis.
330            ChainFilters::ToGenesis => (),
331            // Both filters are active. Return on the first to be hit.
332            ChainFilters::Both(n, until_hashes) => {
333                *n = n.checked_sub(1)?;
334
335                if until_hashes.contains(op.as_ref().get_hash()) {
336                    self.end = true;
337                }
338            }
339        }
340        Some(op)
341    }
342}
343
344impl Sequences {
345    /// Find the action sequences for all hashes in the filter.
346    pub fn find_sequences<F, E>(filter: ChainFilter, mut get_seq: F) -> Result<Self, E>
347    where
348        F: FnMut(&ActionHash) -> Result<Option<u32>, E>,
349    {
350        // Get the top of the chain action sequence.
351        // This is the highest sequence number and also the
352        // start of the iterator.
353        let chain_top = match get_seq(&filter.chain_top)? {
354            Some(seq) => seq,
355            None => return Ok(Self::ChainTopNotFound(filter.chain_top)),
356        };
357
358        // Track why the sequence start of the range was chosen.
359        let mut chain_bottom_type = ChainBottomType::Genesis;
360
361        // If there are any until hashes in the filter,
362        // then find the highest sequence of the set
363        // and find the distance from the position.
364        let distance = match filter.get_until() {
365            Some(until_hashes) => {
366                // Find the highest sequence of the until hashes.
367                let max = until_hashes
368                    .iter()
369                    .filter_map(|hash| {
370                        match get_seq(hash) {
371                            Ok(seq) => {
372                                // Ignore any until hashes that could not be found.
373                                let seq = seq?;
374                                // Ignore any until hashes that are higher then a chain top.
375                                (seq <= chain_top).then(|| Ok(seq))
376                            }
377                            Err(e) => Some(Err(e)),
378                        }
379                    })
380                    .try_fold(0, |max, result| {
381                        let seq = result?;
382                        Ok(max.max(seq))
383                    })?;
384
385                if max != 0 {
386                    // If the max is not genesis then there is an
387                    // until hash that was found.
388                    chain_bottom_type = ChainBottomType::Until;
389                }
390
391                // The distance from the chain top till highest until hash.
392                // Note this cannot be an overflow due to the check above.
393                chain_top - max
394            }
395            // If there is no until hashes then the distance is the chain top
396            // till genesis (or just the chain top).
397            None => chain_top,
398        };
399
400        // Check if there is a take filter and if that
401        // will be reached before any until hashes or genesis.
402        let start = match filter.get_take() {
403            Some(take) => {
404                // A take of zero will produce an empty range.
405                if take == 0 {
406                    return Ok(Self::EmptyRange);
407                } else if take <= distance {
408                    // The take will be reached before genesis or until hashes.
409                    chain_bottom_type = ChainBottomType::Take;
410                    // Add one to include the "position" in the number of
411                    // "take". This matches the rust iterator "take".
412                    chain_top.saturating_sub(take).saturating_add(1)
413                } else {
414                    // The range spans from the position for the distance
415                    // that was determined earlier.
416                    chain_top - distance
417                }
418            }
419            // The range spans from the position for the distance
420            // that was determined earlier.
421            None => chain_top - distance,
422        };
423        Ok(Self::Found(ChainFilterRange {
424            filter,
425            range: start..=chain_top,
426            chain_bottom_type,
427        }))
428    }
429}
430
431impl ChainFilterRange {
432    /// Get the range of action sequences for this filter.
433    pub fn range(&self) -> &RangeInclusive<u32> {
434        &self.range
435    }
436    /// Filter the chain items then check the invariants hold.
437    pub fn filter_then_check(
438        self,
439        chain: Vec<RegisterAgentActivity>,
440        warrants: Vec<WarrantOp>,
441    ) -> MustGetAgentActivityResponse {
442        let until_hashes = self.filter.get_until().cloned();
443
444        // Create the filter iterator and collect the filtered actions.
445        let actions: Vec<_> = ChainFilterIter::new(self.filter, chain).collect();
446
447        // Check the invariants hold.
448        match actions.last().zip(actions.first()) {
449            // The actual results after the filter must match the range.
450            Some((lowest, highest))
451                if (lowest.action.action().action_seq()..=highest.action.action().action_seq())
452                    == self.range =>
453            {
454                // If the range start was an until hash then the first action must
455                // actually be an action from the until set.
456                if let Some(hashes) = until_hashes {
457                    if matches!(self.chain_bottom_type, ChainBottomType::Until)
458                        && !hashes.contains(lowest.action.action_address())
459                    {
460                        return MustGetAgentActivityResponse::IncompleteChain;
461                    }
462                }
463
464                // The constraints are met the activity can be returned.
465                MustGetAgentActivityResponse::Activity {
466                    activity: actions,
467                    warrants,
468                }
469            }
470            // The constraints are not met so the chain is not complete.
471            _ => MustGetAgentActivityResponse::IncompleteChain,
472        }
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::BoundedMustGetAgentActivityResponse;
479    use super::ChainBottomType;
480    use super::ChainFilter;
481    use super::ChainFilterRange;
482    use holochain_types::prelude::*;
483    use test_case::test_case;
484
485    /// If both sides are not activity then the acc should be returned.
486    #[test_case(
487        BoundedMustGetAgentActivityResponse::EmptyRange,
488        BoundedMustGetAgentActivityResponse::EmptyRange
489        => BoundedMustGetAgentActivityResponse::EmptyRange
490    )]
491    #[test_case(
492        BoundedMustGetAgentActivityResponse::EmptyRange,
493        BoundedMustGetAgentActivityResponse::IncompleteChain
494        => BoundedMustGetAgentActivityResponse::EmptyRange
495    )]
496    #[test_case(
497        BoundedMustGetAgentActivityResponse::EmptyRange,
498        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
499        => BoundedMustGetAgentActivityResponse::EmptyRange
500    )]
501    #[test_case(
502        BoundedMustGetAgentActivityResponse::IncompleteChain,
503        BoundedMustGetAgentActivityResponse::IncompleteChain
504        => BoundedMustGetAgentActivityResponse::IncompleteChain
505    )]
506    #[test_case(
507        BoundedMustGetAgentActivityResponse::IncompleteChain,
508        BoundedMustGetAgentActivityResponse::EmptyRange
509        => BoundedMustGetAgentActivityResponse::IncompleteChain
510    )]
511    #[test_case(
512        BoundedMustGetAgentActivityResponse::IncompleteChain,
513        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
514        => BoundedMustGetAgentActivityResponse::IncompleteChain
515    )]
516    #[test_case(
517        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36])),
518        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![1; 36]))
519        => BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
520    )]
521    #[test_case(
522        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36])),
523        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
524        => BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
525    )]
526    #[test_case(
527        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36])),
528        BoundedMustGetAgentActivityResponse::EmptyRange
529        => BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
530    )]
531    #[test_case(
532        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36])),
533        BoundedMustGetAgentActivityResponse::IncompleteChain
534        => BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
535    )]
536    /// If one side is activity and the other is not then the activity should be returned.
537    #[test_case(
538        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
539            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
540            range: 0..=0,
541            chain_bottom_type: ChainBottomType::Genesis,
542        }),
543        BoundedMustGetAgentActivityResponse::EmptyRange
544        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
545            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
546            range: 0..=0,
547            chain_bottom_type: ChainBottomType::Genesis,
548        })
549    )]
550    #[test_case(
551        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
552            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
553            range: 0..=0,
554            chain_bottom_type: ChainBottomType::Genesis,
555        }),
556        BoundedMustGetAgentActivityResponse::IncompleteChain
557        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
558            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
559            range: 0..=0,
560            chain_bottom_type: ChainBottomType::Genesis,
561        })
562    )]
563    #[test_case(
564        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
565            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
566            range: 0..=0,
567            chain_bottom_type: ChainBottomType::Genesis,
568        }),
569        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36]))
570        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
571            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
572            range: 0..=0,
573            chain_bottom_type: ChainBottomType::Genesis,
574        })
575    )]
576    #[test_case(
577        BoundedMustGetAgentActivityResponse::EmptyRange,
578        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
579            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
580            range: 0..=0,
581            chain_bottom_type: ChainBottomType::Genesis,
582        })
583        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
584            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
585            range: 0..=0,
586            chain_bottom_type: ChainBottomType::Genesis,
587        })
588    )]
589    #[test_case(
590        BoundedMustGetAgentActivityResponse::IncompleteChain,
591        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
592            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
593            range: 0..=0,
594            chain_bottom_type: ChainBottomType::Genesis,
595        })
596        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
597            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
598            range: 0..=0,
599            chain_bottom_type: ChainBottomType::Genesis,
600        })
601    )]
602    #[test_case(
603        BoundedMustGetAgentActivityResponse::ChainTopNotFound(ActionHash::from_raw_36(vec![0; 36])),
604        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
605            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
606            range: 0..=0,
607            chain_bottom_type: ChainBottomType::Genesis,
608        })
609        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
610            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
611            range: 0..=0,
612            chain_bottom_type: ChainBottomType::Genesis,
613        })
614    )]
615    /// If both sides are activity then the activity should be merged.
616    #[test_case(
617        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
618            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
619            range: 0..=0,
620            chain_bottom_type: ChainBottomType::Genesis,
621        }),
622        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
623            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
624            range: 0..=0,
625            chain_bottom_type: ChainBottomType::Genesis,
626        })
627        => BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
628            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
629            range: 0..=0,
630            chain_bottom_type: ChainBottomType::Genesis,
631        })
632    )]
633    #[test_case(
634        BoundedMustGetAgentActivityResponse::activity(vec![], ChainFilterRange {
635            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
636            range: 0..=0,
637            chain_bottom_type: ChainBottomType::Genesis,
638        }),
639        BoundedMustGetAgentActivityResponse::activity(vec![RegisterAgentActivity{
640            action: SignedActionHashed::with_presigned(
641                ActionHashed::from_content_sync(Action::Dna(Dna {
642                    author: AgentPubKey::from_raw_36(vec![0; 36]),
643                    timestamp: Timestamp(0),
644                    hash: DnaHash::from_raw_36(vec![0; 36]),
645                })),
646                Signature([0; SIGNATURE_BYTES]),
647            ),
648            cached_entry: None,
649        }], ChainFilterRange {
650            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
651            range: 0..=0,
652            chain_bottom_type: ChainBottomType::Genesis,
653        })
654        => BoundedMustGetAgentActivityResponse::activity(vec![RegisterAgentActivity{
655            action: SignedActionHashed::with_presigned(
656                ActionHashed::from_content_sync(Action::Dna(Dna {
657                    author: AgentPubKey::from_raw_36(vec![0; 36]),
658                    timestamp: Timestamp(0),
659                    hash: DnaHash::from_raw_36(vec![0; 36]),
660                })),
661                Signature([0; SIGNATURE_BYTES]),
662            ),
663            cached_entry: None
664        }], ChainFilterRange {
665            filter: ChainFilter::new(ActionHash::from_raw_36(vec![0; 36])),
666            range: 0..=0,
667            chain_bottom_type: ChainBottomType::Genesis,
668        })
669    )]
670
671    fn test_merge_bounded_agent_activity_responses(
672        acc: BoundedMustGetAgentActivityResponse,
673        next: BoundedMustGetAgentActivityResponse,
674    ) -> BoundedMustGetAgentActivityResponse {
675        super::merge_bounded_agent_activity_responses(acc, &next)
676    }
677}