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