Skip to main content

routing/bedrock/
with_porcelain.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::component_instance::{ComponentInstanceInterface, WeakExtendedInstanceInterface};
6use crate::error::{ErrorReporter, RouteRequestErrorInfo, RoutingError};
7use crate::rights::Rights;
8use crate::subdir::SubDir;
9use async_trait::async_trait;
10use capability_source::CapabilitySource;
11use cm_rust::{CapabilityTypeName, EventScope, FidlIntoNative, NativeIntoFidl};
12use cm_types::Availability;
13use fidl_fuchsia_component_runtime::RouteRequest;
14use fidl_fuchsia_component_sandbox as fsandbox;
15use fidl_fuchsia_io as fio;
16use moniker::{ExtendedMoniker, Moniker};
17use router_error::RouterError;
18use runtime_capabilities::{
19    Capability, CapabilityBound, Dictionary, Routable, Router, WeakInstanceToken,
20};
21use std::collections::HashMap;
22use std::sync::{Arc, LazyLock};
23use strum::IntoEnumIterator;
24
25struct PorcelainRouter<T: CapabilityBound, R, C: ComponentInstanceInterface, const D: bool> {
26    router: Router<T>,
27    porcelain_type: CapabilityTypeName,
28    availability: Availability,
29    rights: Option<Rights>,
30    subdir: Option<SubDir>,
31    inherit_rights: Option<bool>,
32    event_stream_scope: Option<(Moniker, Box<[EventScope]>)>,
33    target: WeakExtendedInstanceInterface<C>,
34    route_request: RouteRequestErrorInfo,
35    error_reporter: R,
36}
37
38#[async_trait]
39impl<T: CapabilityBound, R: ErrorReporter, C: ComponentInstanceInterface + 'static, const D: bool>
40    Routable<T> for PorcelainRouter<T, R, C, D>
41{
42    async fn route(
43        &self,
44        request: RouteRequest,
45        target: WeakInstanceToken,
46    ) -> Result<Option<T>, RouterError> {
47        match self.route_inner(request, D, target).await {
48            Ok(res) => Ok(res),
49            Err(err) => {
50                self.error_reporter
51                    .report(&self.route_request, &err, self.target.clone().into())
52                    .await;
53                Err(err)
54            }
55        }
56    }
57
58    async fn route_debug(
59        &self,
60        request: RouteRequest,
61        target: WeakInstanceToken,
62    ) -> Result<CapabilitySource, RouterError> {
63        match self.route_debug_inner(request, D, target).await {
64            Ok(res) => Ok(res),
65            Err(err) => {
66                self.error_reporter
67                    .report(&self.route_request, &err, self.target.clone().into())
68                    .await;
69                Err(err)
70            }
71        }
72    }
73}
74
75impl<T: CapabilityBound, R: ErrorReporter, C: ComponentInstanceInterface + 'static, const D: bool>
76    PorcelainRouter<T, R, C, D>
77{
78    async fn route_inner(
79        &self,
80        request: RouteRequest,
81        supply_default: bool,
82        target: WeakInstanceToken,
83    ) -> Result<Option<T>, RouterError> {
84        let request = self.check_and_compute_request(request, supply_default)?;
85        self.router.route(request, target).await
86    }
87
88    async fn route_debug_inner(
89        &self,
90        request: RouteRequest,
91        supply_default: bool,
92        target: WeakInstanceToken,
93    ) -> Result<CapabilitySource, RouterError> {
94        let request = self.check_and_compute_request(request, supply_default)?;
95        self.router.route_debug(request, target).await
96    }
97
98    fn check_and_compute_request(
99        &self,
100        request: RouteRequest,
101        supply_default: bool,
102    ) -> Result<RouteRequest, RouterError> {
103        let PorcelainRouter {
104            router: _,
105            porcelain_type,
106            availability,
107            rights,
108            subdir,
109            inherit_rights,
110            event_stream_scope,
111            target,
112            route_request: _,
113            error_reporter: _,
114        } = self;
115        let mut request = if request != RouteRequest::default() {
116            request
117        } else {
118            if !supply_default {
119                Err(RouterError::InvalidArgs)?;
120            }
121            let mut request = RouteRequest::default();
122            request.build_type_name = Some(porcelain_type.to_string());
123            request.availability = Some(availability.native_into_fidl());
124            if let Some(rights) = rights {
125                request.directory_rights = Some(fio::Flags::from(*rights));
126            }
127            if let Some(inherit_rights) = inherit_rights {
128                request.inherit_rights = Some(*inherit_rights);
129            }
130            if let Some((scope_moniker, scope)) = event_stream_scope.as_ref() {
131                request.event_stream_scope_moniker = Some(scope_moniker.to_string());
132                request.event_stream_scope = Some(scope.clone().native_into_fidl());
133            }
134            request
135        };
136
137        let moniker: ExtendedMoniker = match target {
138            WeakExtendedInstanceInterface::Component(t) => t.moniker.clone().into(),
139            WeakExtendedInstanceInterface::AboveRoot(_) => ExtendedMoniker::ComponentManager,
140        };
141        check_porcelain_type(&moniker, &request, *porcelain_type)?;
142        let updated_availability = check_availability(&moniker, &request, *availability)?;
143
144        check_and_compute_rights(&moniker, &mut request, &rights)?;
145        if let Some(new_subdir) = check_and_compute_subdir(&moniker, &request, &subdir)? {
146            request.sub_directory_path = Some(new_subdir.as_ref().clone().native_into_fidl());
147        }
148        if let Some((new_scope_moniker, new_scope)) = event_stream_scope.as_ref() {
149            // If the scope is already set then it's a smaller scope (because we can't expose
150            // these), so only set our scope if the request doesn't have one yet.
151            if request.event_stream_scope_moniker.is_none() {
152                request.event_stream_scope_moniker =
153                    Some(new_scope_moniker.clone().native_into_fidl());
154                request.event_stream_scope = Some(new_scope.clone().native_into_fidl());
155            }
156        }
157
158        // Everything checks out, forward the request.
159        request.availability = Some(updated_availability.native_into_fidl());
160        Ok(request)
161    }
162}
163
164fn check_porcelain_type(
165    moniker: &ExtendedMoniker,
166    request: &RouteRequest,
167    expected_type: CapabilityTypeName,
168) -> Result<(), RouterError> {
169    let capability_type: CapabilityTypeName = request
170        .build_type_name
171        .as_ref()
172        .ok_or_else(|| RoutingError::BedrockMissingCapabilityType {
173            type_name: expected_type.to_string(),
174            moniker: moniker.clone(),
175        })?
176        .parse()
177        .map_err(|_| RouterError::InvalidArgs)?;
178    if capability_type != expected_type {
179        Err(RoutingError::BedrockWrongCapabilityType {
180            moniker: moniker.clone(),
181            actual: capability_type.to_string(),
182            expected: expected_type.to_string(),
183        })?;
184    }
185    Ok(())
186}
187
188fn check_availability(
189    moniker: &ExtendedMoniker,
190    request: &RouteRequest,
191    availability: Availability,
192) -> Result<Availability, RouterError> {
193    // The availability of the request must be compatible with the
194    // availability of this step of the route.
195    let request_availability =
196        request.availability.ok_or(fsandbox::RouterError::InvalidArgs).inspect_err(|e| {
197            log::error!("request {:?} did not have availability metadata: {e:?}", request)
198        })?;
199    crate::availability::advance(&moniker, request_availability.fidl_into_native(), availability)
200        .map_err(|e| RoutingError::from(e).into())
201}
202
203fn check_and_compute_rights(
204    moniker: &ExtendedMoniker,
205    request: &mut RouteRequest,
206    rights: &Option<Rights>,
207) -> Result<(), RouterError> {
208    let Some(rights) = rights else {
209        return Ok(());
210    };
211    let inherit = request.inherit_rights.ok_or(RouterError::InvalidArgs)?;
212    let request_rights: Rights = match request.directory_rights {
213        Some(request_rights) => request_rights.into(),
214        None => {
215            if inherit {
216                request.directory_rights = Some(fio::Flags::from(*rights));
217                *rights
218            } else {
219                Err(RouterError::InvalidArgs)?
220            }
221        }
222    };
223    // The rights of the previous step (if any) of the route must be
224    // compatible with this step of the route.
225    if let Some(intermediate_rights) = request.directory_intermediate_rights {
226        Rights::from(intermediate_rights)
227            .validate_next(&rights, moniker.clone().into())
228            .map_err(|e| router_error::RouterError::from(RoutingError::from(e)))?;
229    };
230    request.directory_intermediate_rights = Some(fio::Flags::from(*rights));
231    // The rights of the request must be compatible with the
232    // rights of this step of the route.
233    request_rights.validate_next(&rights, moniker.clone().into()).map_err(RoutingError::from)?;
234    Ok(())
235}
236
237fn check_and_compute_subdir(
238    moniker: &ExtendedMoniker,
239    request: &RouteRequest,
240    subdir: &Option<SubDir>,
241) -> Result<Option<SubDir>, RouterError> {
242    let Some(mut subdir_from_decl) = subdir.clone() else {
243        return Ok(None);
244    };
245
246    let request_subdir: Option<SubDir> =
247        request.sub_directory_path.as_ref().map(|s| SubDir::new(s).expect("invalid sub directory"));
248
249    if let Some(request_subdir) = request_subdir {
250        let success = subdir_from_decl.as_mut().extend(request_subdir.clone().into());
251        if !success {
252            return Err(RoutingError::PathTooLong {
253                moniker: moniker.clone(),
254                path: subdir_from_decl.to_string(),
255                keyword: request_subdir.to_string(),
256            }
257            .into());
258        }
259    }
260    Ok(Some(subdir_from_decl))
261}
262
263pub type DefaultMetadataFn = Arc<dyn Fn(Availability) -> Dictionary + Send + Sync + 'static>;
264
265/// Builds a router that ensures the capability request has an availability strength that is at
266/// least the provided `availability`. A default `RouteRequest` is populated with `metadata_fn` if
267/// the client passes an empty `RouteRequest`.
268pub struct PorcelainBuilder<
269    T: CapabilityBound,
270    R: ErrorReporter,
271    C: ComponentInstanceInterface + 'static,
272    const D: bool,
273> {
274    router: Router<T>,
275    porcelain_type: CapabilityTypeName,
276    availability: Option<Availability>,
277    rights: Option<Rights>,
278    subdir: Option<SubDir>,
279    inherit_rights: Option<bool>,
280    event_stream_scope: Option<(Moniker, Box<[EventScope]>)>,
281    target: Option<WeakExtendedInstanceInterface<C>>,
282    error_info: Option<RouteRequestErrorInfo>,
283    error_reporter: Option<R>,
284}
285
286impl<T: CapabilityBound, R: ErrorReporter, C: ComponentInstanceInterface + 'static, const D: bool>
287    PorcelainBuilder<T, R, C, D>
288{
289    fn new(router: Router<T>, porcelain_type: CapabilityTypeName) -> Self {
290        Self {
291            router,
292            porcelain_type,
293            availability: None,
294            rights: None,
295            subdir: None,
296            inherit_rights: None,
297            event_stream_scope: None,
298            target: None,
299            error_info: None,
300            error_reporter: None,
301        }
302    }
303
304    /// The [Availability] attribute for this route.
305    /// REQUIRED.
306    pub fn availability(mut self, a: Availability) -> Self {
307        self.availability = Some(a);
308        self
309    }
310
311    pub fn rights(mut self, rights: Option<Rights>) -> Self {
312        self.rights = rights;
313        self
314    }
315
316    pub fn subdir(mut self, subdir: SubDir) -> Self {
317        self.subdir = Some(subdir);
318        self
319    }
320
321    pub fn inherit_rights(mut self, inherit_rights: bool) -> Self {
322        self.inherit_rights = Some(inherit_rights);
323        self
324    }
325
326    pub fn event_stream_scope(mut self, scope: (Moniker, Box<[EventScope]>)) -> Self {
327        self.event_stream_scope = Some(scope);
328        self
329    }
330
331    /// The identity of the component on behalf of whom this routing request is performed, if the
332    /// caller passes a `None` request.
333    /// Either this or `target_above_root` is REQUIRED.
334    pub fn target(mut self, t: &Arc<C>) -> Self {
335        self.target = Some(WeakExtendedInstanceInterface::Component(t.as_weak()));
336        self
337    }
338
339    /// The identity of the "above root" instance that is component manager itself.
340    /// Either this or `target` is REQUIRED.
341    pub fn target_above_root(mut self, t: &Arc<C::TopInstance>) -> Self {
342        self.target = Some(WeakExtendedInstanceInterface::AboveRoot(Arc::downgrade(t)));
343        self
344    }
345
346    /// Object used to generate diagnostic information about the route that is logged if the route
347    /// fails. This is usually a [cm_rust] type that is castable to [RouteRequestErrorInfo]
348    /// REQUIRED.
349    pub fn error_info<S>(mut self, r: S) -> Self
350    where
351        RouteRequestErrorInfo: From<S>,
352    {
353        self.error_info = Some(RouteRequestErrorInfo::from(r));
354        self
355    }
356
357    /// The [ErrorReporter] used to log errors if routing fails.
358    /// REQUIRED.
359    pub fn error_reporter(mut self, r: R) -> Self {
360        self.error_reporter = Some(r);
361        self
362    }
363
364    /// Build the [PorcelainRouter] with attributes configured by this builder.
365    pub fn build(self) -> Router<T> {
366        Router::new(PorcelainRouter::<T, R, C, D> {
367            router: self.router,
368            porcelain_type: self.porcelain_type,
369            availability: self.availability.expect("must set availability"),
370            rights: self.rights,
371            subdir: self.subdir,
372            inherit_rights: self.inherit_rights,
373            event_stream_scope: self.event_stream_scope,
374            target: self.target.expect("must set target"),
375            route_request: self.error_info.expect("must set route_request"),
376            error_reporter: self.error_reporter.expect("must set error_reporter"),
377        })
378    }
379}
380
381impl<R: ErrorReporter, T: CapabilityBound, C: ComponentInstanceInterface + 'static, const D: bool>
382    From<PorcelainBuilder<T, R, C, D>> for Capability
383where
384    Router<T>: Into<Capability>,
385{
386    fn from(b: PorcelainBuilder<T, R, C, D>) -> Self {
387        b.build().into()
388    }
389}
390
391/// See [WithPorcelain::with_porcelain] for documentation.
392pub trait WithPorcelain<
393    T: CapabilityBound,
394    R: ErrorReporter,
395    C: ComponentInstanceInterface + 'static,
396>
397{
398    /// Returns a [PorcelainBuilder] you use to construct a new router with porcelain properties
399    /// that augments the `self`. See [PorcelainBuilder] for documentation of the supported
400    /// properties.
401    ///
402    /// If a `None` request is passed into the built router, the router will supply a default
403    /// request based on the values passed to the builder.
404    fn with_porcelain_with_default(
405        self,
406        type_: CapabilityTypeName,
407    ) -> PorcelainBuilder<T, R, C, true>;
408
409    /// Returns a [PorcelainBuilder] you use to construct a new router with porcelain properties
410    /// that augments the `self`. See [PorcelainBuilder] for documentation of the supported
411    /// properties.
412    ///
413    /// If a `None` request is passed into the built router, the router will throw an `InvalidArgs`
414    /// error.
415    fn with_porcelain_no_default(
416        self,
417        type_: CapabilityTypeName,
418    ) -> PorcelainBuilder<T, R, C, false>;
419}
420
421impl<T: CapabilityBound, R: ErrorReporter, C: ComponentInstanceInterface + 'static>
422    WithPorcelain<T, R, C> for Router<T>
423{
424    fn with_porcelain_with_default(
425        self,
426        type_: CapabilityTypeName,
427    ) -> PorcelainBuilder<T, R, C, true> {
428        PorcelainBuilder::<T, R, C, true>::new(self, type_)
429    }
430
431    fn with_porcelain_no_default(
432        self,
433        type_: CapabilityTypeName,
434    ) -> PorcelainBuilder<T, R, C, false> {
435        PorcelainBuilder::<T, R, C, false>::new(self, type_)
436    }
437}
438
439pub fn metadata_for_porcelain_type(
440    typename: CapabilityTypeName,
441) -> Arc<dyn Fn(Availability) -> RouteRequest + Send + Sync + 'static> {
442    type MetadataMap = HashMap<
443        CapabilityTypeName,
444        Arc<dyn Fn(Availability) -> RouteRequest + Send + Sync + 'static>,
445    >;
446    static CLOSURES: LazyLock<MetadataMap> = LazyLock::new(|| {
447        fn entry_for_typename(
448            typename: CapabilityTypeName,
449        ) -> (CapabilityTypeName, Arc<dyn Fn(Availability) -> RouteRequest + Send + Sync + 'static>)
450        {
451            let v = Arc::new(move |availability: Availability| RouteRequest {
452                build_type_name: Some(typename.to_string()),
453                availability: Some(availability.native_into_fidl()),
454                ..Default::default()
455            });
456            (typename, v)
457        }
458        CapabilityTypeName::iter().map(entry_for_typename).collect()
459    });
460    CLOSURES.get(&typename).unwrap().clone()
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use crate::ResolvedInstanceInterface;
467    use crate::bedrock::sandbox_construction::ComponentSandbox;
468    use crate::component_instance::{ExtendedInstanceInterface, TopInstanceInterface};
469    use crate::error::ComponentInstanceError;
470    use crate::policy::GlobalPolicyChecker;
471    use assert_matches::assert_matches;
472    use capability_source::{BuiltinCapabilities, NamespaceCapabilities};
473    use cm_rust_testing::UseBuilder;
474    use cm_types::Url;
475    use fuchsia_sync::Mutex;
476    use moniker::Moniker;
477    use router_error::RouterError;
478    use runtime_capabilities::Data;
479    use std::sync::Arc;
480
481    #[derive(Debug)]
482    struct FakeComponent {
483        moniker: Moniker,
484    }
485
486    #[derive(Debug)]
487    struct FakeTopInstance {
488        ns: NamespaceCapabilities,
489        builtin: BuiltinCapabilities,
490    }
491
492    impl TopInstanceInterface for FakeTopInstance {
493        fn namespace_capabilities(&self) -> &NamespaceCapabilities {
494            &self.ns
495        }
496        fn builtin_capabilities(&self) -> &BuiltinCapabilities {
497            &self.builtin
498        }
499    }
500
501    #[async_trait]
502    impl ComponentInstanceInterface for FakeComponent {
503        type TopInstance = FakeTopInstance;
504
505        fn moniker(&self) -> &Moniker {
506            &self.moniker
507        }
508
509        fn url(&self) -> &Url {
510            panic!()
511        }
512
513        fn config_parent_overrides(&self) -> Option<&[cm_rust::ConfigOverride]> {
514            panic!()
515        }
516
517        fn policy_checker(&self) -> &GlobalPolicyChecker {
518            panic!()
519        }
520
521        fn component_id_index(&self) -> &component_id_index::Index {
522            panic!()
523        }
524
525        fn try_get_parent(
526            &self,
527        ) -> Result<ExtendedInstanceInterface<Self>, ComponentInstanceError> {
528            panic!()
529        }
530
531        async fn lock_resolved_state<'a>(
532            self: &'a Arc<Self>,
533        ) -> Result<Box<dyn ResolvedInstanceInterface<Component = Self> + 'a>, ComponentInstanceError>
534        {
535            panic!()
536        }
537
538        async fn component_sandbox(
539            self: &Arc<Self>,
540        ) -> Result<ComponentSandbox, ComponentInstanceError> {
541            panic!()
542        }
543    }
544
545    #[derive(Clone)]
546    struct TestErrorReporter {
547        reported: Arc<Mutex<bool>>,
548    }
549
550    impl TestErrorReporter {
551        fn new() -> Self {
552            Self { reported: Arc::new(Mutex::new(false)) }
553        }
554    }
555
556    #[async_trait]
557    impl ErrorReporter for TestErrorReporter {
558        async fn report(
559            &self,
560            _request: &RouteRequestErrorInfo,
561            _err: &RouterError,
562            _route_target: WeakInstanceToken,
563        ) {
564            let mut reported = self.reported.lock();
565            if *reported {
566                panic!("report() was called twice");
567            }
568            *reported = true;
569        }
570    }
571
572    fn fake_component() -> Arc<FakeComponent> {
573        Arc::new(FakeComponent { moniker: Moniker::root() })
574    }
575
576    fn error_info() -> cm_rust::UseDecl {
577        UseBuilder::protocol().name("name").build()
578    }
579
580    #[fuchsia::test]
581    async fn success() {
582        let source = Data::String("hello".into());
583        let base = Router::<Data>::new_ok(source);
584        let component = fake_component();
585        let proxy = base
586            .with_porcelain_with_default(CapabilityTypeName::Protocol)
587            .availability(Availability::Optional)
588            .target(&component)
589            .error_info(&error_info())
590            .error_reporter(TestErrorReporter::new())
591            .build();
592        let request = RouteRequest {
593            build_type_name: Some(CapabilityTypeName::Protocol.to_string()),
594            availability: Some(Availability::Optional.native_into_fidl()),
595            ..Default::default()
596        };
597
598        let capability = proxy.route(request, component.as_weak().into()).await.unwrap();
599        let capability = match capability {
600            Some(d) => d,
601            _ => panic!(),
602        };
603        assert_eq!(capability, Data::String("hello".into()));
604    }
605
606    #[fuchsia::test]
607    async fn type_missing() {
608        let reporter = TestErrorReporter::new();
609        let reported = reporter.reported.clone();
610        let source = Data::String("hello".into());
611        let base = Router::<Data>::new_ok(source);
612        let component = fake_component();
613        let proxy = base
614            .with_porcelain_with_default(CapabilityTypeName::Protocol)
615            .availability(Availability::Optional)
616            .target(&component)
617            .error_info(&error_info())
618            .error_reporter(reporter)
619            .build();
620        let request = RouteRequest {
621            availability: Some(Availability::Optional.native_into_fidl()),
622            ..Default::default()
623        };
624
625        let error = proxy.route(request, component.as_weak().into()).await.unwrap_err();
626        assert_matches!(
627            error,
628            RouterError::NotFound(err)
629            if matches!(
630                err.as_any().downcast_ref::<RoutingError>(),
631                Some(RoutingError::BedrockMissingCapabilityType {
632                    moniker,
633                    type_name,
634                }) if moniker == &Moniker::root().into() && type_name == "protocol"
635            )
636        );
637        assert!(*reported.lock());
638    }
639
640    #[fuchsia::test]
641    async fn type_mismatch() {
642        let reporter = TestErrorReporter::new();
643        let reported = reporter.reported.clone();
644        let source = Data::String("hello".into());
645        let base = Router::<Data>::new_ok(source);
646        let component = fake_component();
647        let proxy = base
648            .with_porcelain_with_default(CapabilityTypeName::Protocol)
649            .availability(Availability::Optional)
650            .target(&component)
651            .error_info(&error_info())
652            .error_reporter(reporter)
653            .build();
654        let request = RouteRequest {
655            build_type_name: Some(CapabilityTypeName::Service.to_string()),
656            availability: Some(Availability::Optional.native_into_fidl()),
657            ..Default::default()
658        };
659
660        let error = proxy.route(request, component.as_weak().into()).await.unwrap_err();
661        assert_matches!(
662            error,
663            RouterError::NotFound(err)
664            if matches!(
665                err.as_any().downcast_ref::<RoutingError>(),
666                Some(RoutingError::BedrockWrongCapabilityType {
667                    moniker,
668                    expected,
669                    actual
670                }) if moniker == &Moniker::root().into()
671                    && expected == "protocol" && actual == "service"
672            )
673        );
674        assert!(*reported.lock());
675    }
676
677    #[fuchsia::test]
678    async fn availability_mismatch() {
679        let reporter = TestErrorReporter::new();
680        let reported = reporter.reported.clone();
681        let source = Data::String("hello".into());
682        let base = Router::<Data>::new_ok(source);
683        let component = fake_component();
684        let proxy = base
685            .with_porcelain_with_default(CapabilityTypeName::Protocol)
686            .availability(Availability::Optional)
687            .target(&component)
688            .error_info(&error_info())
689            .error_reporter(reporter)
690            .build();
691        let request = RouteRequest {
692            build_type_name: Some(CapabilityTypeName::Protocol.to_string()),
693            availability: Some(Availability::Required.native_into_fidl()),
694            ..Default::default()
695        };
696
697        let error = proxy.route(request, component.as_weak().into()).await.unwrap_err();
698        assert_matches!(
699            error,
700            RouterError::NotFound(err)
701            if matches!(
702                err.as_any().downcast_ref::<RoutingError>(),
703                Some(RoutingError::AvailabilityRoutingError(
704                        crate::error::AvailabilityRoutingError::TargetHasStrongerAvailability {
705                        moniker
706                    }
707                )) if moniker == &Moniker::root().into()
708            )
709        );
710        assert!(*reported.lock());
711    }
712}