Skip to main content

canic_host/subnet_catalog/
evidence.rs

1//! Module: subnet_catalog::evidence
2//!
3//! Responsibility: project upstream typed catalog failures into Canic planning evidence.
4//! Does not own: catalog collection, retry classification, report rendering, or effects.
5//! Boundary: every field is copied from typed `ic-query` data without parsing error prose.
6
7use ic_query::subnet_catalog::{
8    CatalogSourceSelection, SubnetCatalogFailureCacheDisposition, SubnetCatalogField,
9    SubnetCatalogLoadFailure, SubnetCatalogLoadStage, SubnetCatalogRefreshTrigger,
10    SubnetCatalogRegistryRecordEvidence, SubnetCatalogRegistryRecordKind,
11    SubnetCatalogRegistryValueEncoding, SubnetCatalogRetryability, SubnetCatalogSubject,
12    SubnetCatalogUnknownRetryReason,
13};
14use serde::Serialize;
15
16/// Complete Canic-owned projection of one typed upstream catalog-load failure.
17#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
18pub struct SubnetCatalogLoadFailureEvidenceV1 {
19    pub schema_version: u16,
20    pub network: String,
21    pub source_kind: Option<SubnetCatalogSourceKindV1>,
22    pub source_endpoints: Vec<String>,
23    pub source_assurance: Option<String>,
24    pub minimum_assurance: String,
25    pub stage: SubnetCatalogLoadStageV1,
26    pub registry_version: Option<u64>,
27    pub returned_registry_value_version: Option<u64>,
28    pub source_endpoint: Option<String>,
29    pub assurance: Option<String>,
30    pub registry_records: Vec<SubnetCatalogRegistryRecordEvidenceV1>,
31    pub cache_disposition: SubnetCatalogFailureCacheDispositionV1,
32    pub subject: Option<SubnetCatalogSubjectV1>,
33    pub code: String,
34    pub category: String,
35    pub retryability: SubnetCatalogRetryabilityV1,
36    pub source_message: String,
37    pub effects: SubnetCatalogFailureEffectsV1,
38}
39
40impl SubnetCatalogLoadFailureEvidenceV1 {
41    /// Project one detailed failure observed at Canic's read-only preflight boundary.
42    #[must_use]
43    pub fn from_preflight_failure(failure: &SubnetCatalogLoadFailure) -> Self {
44        let (source_kind, source_endpoints, source_assurance) = failure
45            .request
46            .source
47            .as_ref()
48            .map_or_else(|| (None, Vec::new(), None), source_evidence);
49        Self {
50            schema_version: 1,
51            network: failure.request.network.clone(),
52            source_kind,
53            source_endpoints,
54            source_assurance,
55            minimum_assurance: failure.request.minimum_assurance.as_str().to_string(),
56            stage: failure.stage.into(),
57            registry_version: failure.registry_version,
58            returned_registry_value_version: failure.returned_registry_value_version,
59            source_endpoint: failure.source_endpoint.clone(),
60            assurance: failure
61                .assurance
62                .map(|assurance| assurance.as_str().to_string()),
63            registry_records: failure.registry_records.iter().map(Into::into).collect(),
64            cache_disposition: failure.cache_disposition.into(),
65            subject: failure.subject.as_ref().map(Into::into),
66            code: failure.code.as_str().to_string(),
67            category: failure.category.as_str().to_string(),
68            retryability: failure.retryability.into(),
69            source_message: failure.source.to_string(),
70            effects: SubnetCatalogFailureEffectsV1::none_started(),
71        }
72    }
73}
74
75fn source_evidence(
76    source: &CatalogSourceSelection,
77) -> (
78    Option<SubnetCatalogSourceKindV1>,
79    Vec<String>,
80    Option<String>,
81) {
82    let (kind, endpoints) = match source {
83        CatalogSourceSelection::UncertifiedQuery { endpoint } => (
84            SubnetCatalogSourceKindV1::UncertifiedQuery,
85            vec![endpoint.clone()],
86        ),
87        CatalogSourceSelection::MultiEndpointAgreement { endpoints } => (
88            SubnetCatalogSourceKindV1::MultiEndpointAgreement,
89            endpoints.clone(),
90        ),
91    };
92    (
93        Some(kind),
94        endpoints,
95        Some(source.assurance().as_str().to_string()),
96    )
97}
98
99/// Typed source-selection shape attempted by the failed request.
100#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "snake_case")]
102pub enum SubnetCatalogSourceKindV1 {
103    UncertifiedQuery,
104    MultiEndpointAgreement,
105}
106
107/// Exact typed upstream load stage.
108#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
109#[serde(rename_all = "snake_case")]
110pub enum SubnetCatalogLoadStageV1 {
111    RequestValidation,
112    CacheOnlyLoad,
113    CacheLookup,
114    CacheAbsence,
115    CacheRejection,
116    CacheBypass,
117    RefreshAttempted,
118    RefreshFailed,
119    PostRefreshCacheLoadFailed,
120    RuntimeAdapter,
121}
122
123impl From<SubnetCatalogLoadStage> for SubnetCatalogLoadStageV1 {
124    fn from(value: SubnetCatalogLoadStage) -> Self {
125        match value {
126            SubnetCatalogLoadStage::RequestValidation => Self::RequestValidation,
127            SubnetCatalogLoadStage::CacheOnlyLoad => Self::CacheOnlyLoad,
128            SubnetCatalogLoadStage::CacheLookup => Self::CacheLookup,
129            SubnetCatalogLoadStage::CacheAbsence => Self::CacheAbsence,
130            SubnetCatalogLoadStage::CacheRejection => Self::CacheRejection,
131            SubnetCatalogLoadStage::CacheBypass => Self::CacheBypass,
132            SubnetCatalogLoadStage::RefreshAttempted => Self::RefreshAttempted,
133            SubnetCatalogLoadStage::RefreshFailed => Self::RefreshFailed,
134            SubnetCatalogLoadStage::PostRefreshCacheLoadFailed => Self::PostRefreshCacheLoadFailed,
135            SubnetCatalogLoadStage::RuntimeAdapter => Self::RuntimeAdapter,
136        }
137    }
138}
139
140/// Exact typed cache state or attempted action at failure.
141#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
142#[serde(tag = "kind", rename_all = "snake_case")]
143pub enum SubnetCatalogFailureCacheDispositionV1 {
144    NotExamined,
145    CacheOnly,
146    CacheBypassed,
147    CacheMissing,
148    CacheRejected,
149    CacheReadFailed,
150    RefreshAttempted {
151        trigger: SubnetCatalogRefreshTriggerV1,
152    },
153    RefreshFailed {
154        trigger: SubnetCatalogRefreshTriggerV1,
155    },
156    PostRefreshLoadFailed {
157        trigger: SubnetCatalogRefreshTriggerV1,
158    },
159}
160
161impl From<SubnetCatalogFailureCacheDisposition> for SubnetCatalogFailureCacheDispositionV1 {
162    fn from(value: SubnetCatalogFailureCacheDisposition) -> Self {
163        match value {
164            SubnetCatalogFailureCacheDisposition::NotExamined => Self::NotExamined,
165            SubnetCatalogFailureCacheDisposition::CacheOnly => Self::CacheOnly,
166            SubnetCatalogFailureCacheDisposition::CacheBypassed => Self::CacheBypassed,
167            SubnetCatalogFailureCacheDisposition::CacheMissing => Self::CacheMissing,
168            SubnetCatalogFailureCacheDisposition::CacheRejected => Self::CacheRejected,
169            SubnetCatalogFailureCacheDisposition::CacheReadFailed => Self::CacheReadFailed,
170            SubnetCatalogFailureCacheDisposition::RefreshAttempted(trigger) => {
171                Self::RefreshAttempted {
172                    trigger: trigger.into(),
173                }
174            }
175            SubnetCatalogFailureCacheDisposition::RefreshFailed(trigger) => Self::RefreshFailed {
176                trigger: trigger.into(),
177            },
178            SubnetCatalogFailureCacheDisposition::PostRefreshLoadFailed(trigger) => {
179                Self::PostRefreshLoadFailed {
180                    trigger: trigger.into(),
181                }
182            }
183        }
184    }
185}
186
187/// Typed reason the refresh path was selected.
188#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum SubnetCatalogRefreshTriggerV1 {
191    Missing,
192    Rejected,
193    Stale,
194    Forced,
195}
196
197impl From<SubnetCatalogRefreshTrigger> for SubnetCatalogRefreshTriggerV1 {
198    fn from(value: SubnetCatalogRefreshTrigger) -> Self {
199        match value {
200            SubnetCatalogRefreshTrigger::Missing => Self::Missing,
201            SubnetCatalogRefreshTrigger::Rejected => Self::Rejected,
202            SubnetCatalogRefreshTrigger::Stale => Self::Stale,
203            SubnetCatalogRefreshTrigger::Forced => Self::Forced,
204        }
205    }
206}
207
208/// Typed offending catalog identity retained by the upstream collector.
209#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
210#[serde(tag = "kind", rename_all = "snake_case")]
211pub enum SubnetCatalogSubjectV1 {
212    Network {
213        network: String,
214    },
215    Endpoint {
216        endpoint: String,
217    },
218    CachePath {
219        path: String,
220    },
221    RegistryLatestVersion,
222    RegistryRecord {
223        record_kind: SubnetCatalogRegistryRecordKindV1,
224        key: String,
225        subnet: Option<String>,
226        canister_range_start: Option<String>,
227    },
228    Subnet {
229        subnet: String,
230        field: Option<SubnetCatalogFieldV1>,
231    },
232    RegistryRoutingTableEntry {
233        index: usize,
234        field: Option<SubnetCatalogFieldV1>,
235    },
236    RoutingRange {
237        start_canister_id: String,
238        end_canister_id: String,
239        subnet_principal: String,
240        field: Option<SubnetCatalogFieldV1>,
241    },
242    Field {
243        field: SubnetCatalogFieldV1,
244    },
245}
246
247impl From<&SubnetCatalogSubject> for SubnetCatalogSubjectV1 {
248    fn from(value: &SubnetCatalogSubject) -> Self {
249        match value {
250            SubnetCatalogSubject::Network(network) => Self::Network {
251                network: network.clone(),
252            },
253            SubnetCatalogSubject::Endpoint(endpoint) => Self::Endpoint {
254                endpoint: endpoint.clone(),
255            },
256            SubnetCatalogSubject::CachePath(path) => Self::CachePath {
257                path: path.display().to_string(),
258            },
259            SubnetCatalogSubject::RegistryLatestVersion => Self::RegistryLatestVersion,
260            SubnetCatalogSubject::RegistryRecord(record) => Self::RegistryRecord {
261                record_kind: record.kind.into(),
262                key: record.key.clone(),
263                subnet: record.subnet.map(|subnet| subnet.to_text()),
264                canister_range_start: record
265                    .canister_range_start
266                    .map(|canister| canister.to_text()),
267            },
268            SubnetCatalogSubject::Subnet { subnet, field } => Self::Subnet {
269                subnet: subnet.to_text(),
270                field: field.map(Into::into),
271            },
272            SubnetCatalogSubject::RegistryRoutingTableEntry { index, field } => {
273                Self::RegistryRoutingTableEntry {
274                    index: *index,
275                    field: field.map(Into::into),
276                }
277            }
278            SubnetCatalogSubject::RoutingRange { range, field } => Self::RoutingRange {
279                start_canister_id: range.start_canister_id.clone(),
280                end_canister_id: range.end_canister_id.clone(),
281                subnet_principal: range.subnet_principal.clone(),
282                field: field.map(Into::into),
283            },
284            SubnetCatalogSubject::Field(field) => Self::Field {
285                field: (*field).into(),
286            },
287        }
288    }
289}
290
291/// Typed Registry record family retained on failure.
292#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
293#[serde(rename_all = "snake_case")]
294pub enum SubnetCatalogRegistryRecordKindV1 {
295    SubnetList,
296    RoutingTable,
297    SubnetRecord,
298}
299
300impl From<SubnetCatalogRegistryRecordKind> for SubnetCatalogRegistryRecordKindV1 {
301    fn from(value: SubnetCatalogRegistryRecordKind) -> Self {
302        match value {
303            SubnetCatalogRegistryRecordKind::SubnetList => Self::SubnetList,
304            SubnetCatalogRegistryRecordKind::RoutingTable => Self::RoutingTable,
305            SubnetCatalogRegistryRecordKind::SubnetRecord => Self::SubnetRecord,
306        }
307    }
308}
309
310/// One successful Registry value read completed before the catalog failure.
311#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
312pub struct SubnetCatalogRegistryRecordEvidenceV1 {
313    pub record_kind: SubnetCatalogRegistryRecordKindV1,
314    pub key: String,
315    pub subnet: Option<String>,
316    pub canister_range_start: Option<String>,
317    pub requested_registry_version: u64,
318    pub returned_registry_version: u64,
319    pub timestamp_nanoseconds: u64,
320    pub source_endpoint: String,
321    pub assurance: String,
322    pub value_encoding: SubnetCatalogRegistryValueEncodingV1,
323}
324
325impl From<&SubnetCatalogRegistryRecordEvidence> for SubnetCatalogRegistryRecordEvidenceV1 {
326    fn from(value: &SubnetCatalogRegistryRecordEvidence) -> Self {
327        Self {
328            record_kind: value.record.kind.into(),
329            key: value.record.key.clone(),
330            subnet: value.record.subnet.map(|subnet| subnet.to_text()),
331            canister_range_start: value
332                .record
333                .canister_range_start
334                .map(|canister| canister.to_text()),
335            requested_registry_version: value.requested_registry_version,
336            returned_registry_version: value.returned_registry_version,
337            timestamp_nanoseconds: value.timestamp_nanoseconds,
338            source_endpoint: value.source_endpoint.clone(),
339            assurance: value.assurance.as_str().to_string(),
340            value_encoding: value.value_encoding.into(),
341        }
342    }
343}
344
345/// Exact transport representation used for a completed Registry value read.
346#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
347#[serde(rename_all = "snake_case")]
348pub enum SubnetCatalogRegistryValueEncodingV1 {
349    Inline,
350    Chunked,
351}
352
353impl From<SubnetCatalogRegistryValueEncoding> for SubnetCatalogRegistryValueEncodingV1 {
354    fn from(value: SubnetCatalogRegistryValueEncoding) -> Self {
355        match value {
356            SubnetCatalogRegistryValueEncoding::Inline => Self::Inline,
357            SubnetCatalogRegistryValueEncoding::Chunked => Self::Chunked,
358        }
359    }
360}
361
362/// Typed catalog field retained on failure.
363#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
364#[serde(rename_all = "snake_case")]
365pub enum SubnetCatalogFieldV1 {
366    SubnetListEntry,
367    RoutingTableRange,
368    RoutingTableSubnetId,
369    RoutingRangeStart,
370    RoutingRangeEnd,
371    Network,
372    RegistryCanister,
373    RegistryVersion,
374    SourceEndpoint,
375    SubnetPrincipal,
376    CollectionTimestamp,
377    Classification,
378    AgreementDigest,
379    CatalogDigest,
380    Provenance,
381}
382
383impl From<SubnetCatalogField> for SubnetCatalogFieldV1 {
384    fn from(value: SubnetCatalogField) -> Self {
385        match value {
386            SubnetCatalogField::SubnetListEntry => Self::SubnetListEntry,
387            SubnetCatalogField::RoutingTableRange => Self::RoutingTableRange,
388            SubnetCatalogField::RoutingTableSubnetId => Self::RoutingTableSubnetId,
389            SubnetCatalogField::RoutingRangeStart => Self::RoutingRangeStart,
390            SubnetCatalogField::RoutingRangeEnd => Self::RoutingRangeEnd,
391            SubnetCatalogField::Network => Self::Network,
392            SubnetCatalogField::RegistryCanister => Self::RegistryCanister,
393            SubnetCatalogField::RegistryVersion => Self::RegistryVersion,
394            SubnetCatalogField::SourceEndpoint => Self::SourceEndpoint,
395            SubnetCatalogField::SubnetPrincipal => Self::SubnetPrincipal,
396            SubnetCatalogField::CollectionTimestamp => Self::CollectionTimestamp,
397            SubnetCatalogField::Classification => Self::Classification,
398            SubnetCatalogField::AgreementDigest => Self::AgreementDigest,
399            SubnetCatalogField::CatalogDigest => Self::CatalogDigest,
400            SubnetCatalogField::Provenance => Self::Provenance,
401        }
402    }
403}
404
405/// Truthful retry classification, including the typed unknown reason.
406#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
407#[serde(tag = "kind", rename_all = "snake_case")]
408pub enum SubnetCatalogRetryabilityV1 {
409    Retryable,
410    NotRetryable,
411    Unknown {
412        reason: SubnetCatalogUnknownRetryReasonV1,
413    },
414}
415
416impl From<SubnetCatalogRetryability> for SubnetCatalogRetryabilityV1 {
417    fn from(value: SubnetCatalogRetryability) -> Self {
418        match value {
419            SubnetCatalogRetryability::Retryable => Self::Retryable,
420            SubnetCatalogRetryability::NotRetryable => Self::NotRetryable,
421            SubnetCatalogRetryability::Unknown(reason) => Self::Unknown {
422                reason: reason.into(),
423            },
424        }
425    }
426}
427
428/// Typed reason binary retryability is not justified.
429#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
430#[serde(rename_all = "snake_case")]
431pub enum SubnetCatalogUnknownRetryReasonV1 {
432    CacheOperation,
433    RegistryResponse,
434    RegistryTransport,
435    RuntimeAdapter,
436}
437
438impl From<SubnetCatalogUnknownRetryReason> for SubnetCatalogUnknownRetryReasonV1 {
439    fn from(value: SubnetCatalogUnknownRetryReason) -> Self {
440        match value {
441            SubnetCatalogUnknownRetryReason::CacheOperation => Self::CacheOperation,
442            SubnetCatalogUnknownRetryReason::RegistryResponse => Self::RegistryResponse,
443            SubnetCatalogUnknownRetryReason::RegistryTransport => Self::RegistryTransport,
444            SubnetCatalogUnknownRetryReason::RuntimeAdapter => Self::RuntimeAdapter,
445        }
446    }
447}
448
449/// Canic-local fact that catalog failure propagation remains before every effect boundary.
450#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
451pub struct SubnetCatalogFailureEffectsV1 {
452    pub build_started: bool,
453    pub workspace_mutation_started: bool,
454    pub ic_mutation_started: bool,
455}
456
457impl SubnetCatalogFailureEffectsV1 {
458    const fn none_started() -> Self {
459        Self {
460            build_started: false,
461            workspace_mutation_started: false,
462            ic_mutation_started: false,
463        }
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470    use candid::Principal;
471    use ic_query::subnet_catalog::{
472        CatalogAssurance, SubnetCatalogErrorCategory, SubnetCatalogErrorCode,
473        SubnetCatalogHostError, SubnetCatalogLoadFailureRequest,
474        SubnetCatalogRegistryRecordSubject, SubnetCatalogRegistryValueEncoding,
475    };
476
477    #[test]
478    fn projection_preserves_every_typed_failure_field_without_reclassification() {
479        let subnet = Principal::from_slice(&[9; 29]);
480        let canister_range_start = Principal::from_slice(&[7; 29]);
481        let failure = detailed_failure_fixture();
482
483        let evidence = SubnetCatalogLoadFailureEvidenceV1::from_preflight_failure(&failure);
484
485        assert_eq!(evidence.network, "ic");
486        assert_eq!(
487            evidence.source_kind,
488            Some(SubnetCatalogSourceKindV1::UncertifiedQuery)
489        );
490        assert_eq!(evidence.source_endpoints, ["https://ic0.app"]);
491        assert_eq!(
492            evidence.source_assurance.as_deref(),
493            Some("uncertified_query")
494        );
495        assert_eq!(evidence.minimum_assurance, "uncertified_query");
496        assert_eq!(evidence.stage, SubnetCatalogLoadStageV1::RefreshFailed);
497        assert_eq!(evidence.registry_version, Some(881_337));
498        assert_eq!(evidence.returned_registry_value_version, Some(881_336));
499        assert_eq!(evidence.source_endpoint.as_deref(), Some("https://ic0.app"));
500        assert_eq!(evidence.assurance.as_deref(), Some("uncertified_query"));
501        assert_eq!(
502            evidence.registry_records,
503            [SubnetCatalogRegistryRecordEvidenceV1 {
504                record_kind: SubnetCatalogRegistryRecordKindV1::RoutingTable,
505                key: "canister_ranges_test".to_string(),
506                subnet: None,
507                canister_range_start: Some(canister_range_start.to_text()),
508                requested_registry_version: 881_337,
509                returned_registry_version: 881_330,
510                timestamp_nanoseconds: 42,
511                source_endpoint: "https://ic0.app".to_string(),
512                assurance: "uncertified_query".to_string(),
513                value_encoding: SubnetCatalogRegistryValueEncodingV1::Chunked,
514            }]
515        );
516        assert_eq!(
517            evidence.cache_disposition,
518            SubnetCatalogFailureCacheDispositionV1::RefreshFailed {
519                trigger: SubnetCatalogRefreshTriggerV1::Missing,
520            }
521        );
522        assert_eq!(
523            evidence.subject,
524            Some(SubnetCatalogSubjectV1::RegistryRecord {
525                record_kind: SubnetCatalogRegistryRecordKindV1::SubnetRecord,
526                key: "subnet_record_test".to_string(),
527                subnet: Some(subnet.to_text()),
528                canister_range_start: None,
529            })
530        );
531        assert_eq!(evidence.code, "invalid_read_policy");
532        assert_eq!(evidence.category, "input");
533        assert_eq!(
534            evidence.retryability,
535            SubnetCatalogRetryabilityV1::Unknown {
536                reason: SubnetCatalogUnknownRetryReasonV1::RegistryResponse,
537            }
538        );
539        assert!(!evidence.effects.build_started);
540        assert!(!evidence.effects.workspace_mutation_started);
541        assert!(!evidence.effects.ic_mutation_started);
542
543        let json = serde_json::to_value(evidence).expect("serialize typed projection");
544        assert_eq!(json["retryability"]["kind"], "unknown");
545        assert_eq!(json["retryability"]["reason"], "registry_response");
546        assert_eq!(json["registry_records"][0]["value_encoding"], "chunked");
547    }
548
549    fn detailed_failure_fixture() -> SubnetCatalogLoadFailure {
550        let subnet = Principal::from_slice(&[9; 29]);
551        let canister_range_start = Principal::from_slice(&[7; 29]);
552        SubnetCatalogLoadFailure {
553            request: SubnetCatalogLoadFailureRequest {
554                network: "ic".to_string(),
555                source: Some(CatalogSourceSelection::uncertified_query("https://ic0.app")),
556                minimum_assurance: CatalogAssurance::UncertifiedQuery,
557            },
558            stage: SubnetCatalogLoadStage::RefreshFailed,
559            registry_version: Some(881_337),
560            returned_registry_value_version: Some(881_336),
561            source_endpoint: Some("https://ic0.app".to_string()),
562            assurance: Some(CatalogAssurance::UncertifiedQuery),
563            registry_records: vec![SubnetCatalogRegistryRecordEvidence {
564                record: SubnetCatalogRegistryRecordSubject {
565                    kind: SubnetCatalogRegistryRecordKind::RoutingTable,
566                    key: "canister_ranges_test".to_string(),
567                    subnet: None,
568                    canister_range_start: Some(canister_range_start),
569                },
570                requested_registry_version: 881_337,
571                returned_registry_version: 881_330,
572                timestamp_nanoseconds: 42,
573                source_endpoint: "https://ic0.app".to_string(),
574                assurance: CatalogAssurance::UncertifiedQuery,
575                value_encoding: SubnetCatalogRegistryValueEncoding::Chunked,
576            }],
577            cache_disposition: SubnetCatalogFailureCacheDisposition::RefreshFailed(
578                SubnetCatalogRefreshTrigger::Missing,
579            ),
580            subject: Some(SubnetCatalogSubject::RegistryRecord(
581                SubnetCatalogRegistryRecordSubject {
582                    kind: SubnetCatalogRegistryRecordKind::SubnetRecord,
583                    key: "subnet_record_test".to_string(),
584                    subnet: Some(subnet),
585                    canister_range_start: None,
586                },
587            )),
588            code: SubnetCatalogErrorCode::InvalidReadPolicy,
589            category: SubnetCatalogErrorCategory::Input,
590            retryability: SubnetCatalogRetryability::Unknown(
591                SubnetCatalogUnknownRetryReason::RegistryResponse,
592            ),
593            source: SubnetCatalogHostError::InvalidReadPolicy {
594                reason: "typed fixture".to_string(),
595            },
596        }
597    }
598}