field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

recovery_contract.rs (7971B)


      1 use tera_core::runtime::product_surface::{Phase1DraftError, Phase1InboundMediaError, TodayError};
      2 use tera_ffi::{
      3     FfiRecoveryDisposition as Recovery, FfiRetryDisposition as Retry, TeraAppError,
      4     classify_error_recovery,
      5 };
      6 
      7 #[test]
      8 fn stable_codes_distinguish_recovery_without_granting_a_new_publication() {
      9     for (code, disposition, retry) in [
     10         (
     11             "unknown_future_retryable_network",
     12             Recovery::Unknown,
     13             Retry::NotAllowed,
     14         ),
     15         (
     16             "invalid_argument",
     17             Recovery::InvalidInput,
     18             Retry::NotAllowed,
     19         ),
     20         (
     21             "draft_revision_conflict",
     22             Recovery::StaleRevision,
     23             Retry::AfterRecovery,
     24         ),
     25         (
     26             "idempotency_conflict",
     27             Recovery::IdempotencyConflict,
     28             Retry::NotAllowed,
     29         ),
     30         (
     31             "today_cursor_invalid",
     32             Recovery::StaleCursor,
     33             Retry::AfterRecovery,
     34         ),
     35         (
     36             "protected_data_unavailable",
     37             Recovery::ProtectedDataUnavailable,
     38             Retry::AfterRecovery,
     39         ),
     40         (
     41             "identity_unavailable",
     42             Recovery::IdentityUnavailable,
     43             Retry::AfterRecovery,
     44         ),
     45         (
     46             "database_busy",
     47             Recovery::StorageFailure,
     48             Retry::AfterRecovery,
     49         ),
     50         (
     51             "storage_space_insufficient",
     52             Recovery::QuotaExhausted,
     53             Retry::AfterRecovery,
     54         ),
     55         (
     56             "cancelled_no_commit",
     57             Recovery::CancelledBeforeEffect,
     58             Retry::NotAllowed,
     59         ),
     60         (
     61             "ios.runtime.cancelled",
     62             Recovery::OutcomeUnknown,
     63             Retry::ReconcileExistingOperation,
     64         ),
     65         (
     66             "deadline_exceeded",
     67             Recovery::OutcomeUnknown,
     68             Retry::ReconcileExistingOperation,
     69         ),
     70         (
     71             "local_committed_delivery_pending",
     72             Recovery::OutcomeUnknown,
     73             Retry::ReconcileExistingOperation,
     74         ),
     75         (
     76             "schema_too_new",
     77             Recovery::UnsupportedVersion,
     78             Retry::NotAllowed,
     79         ),
     80         (
     81             "blossom_resolution_failed",
     82             Recovery::NetworkUnavailable,
     83             Retry::AfterRecovery,
     84         ),
     85         (
     86             "relay_rate_limited",
     87             Recovery::NetworkPolicy,
     88             Retry::AfterRecovery,
     89         ),
     90         (
     91             "today_relay_partial",
     92             Recovery::PartialResult,
     93             Retry::AfterRecovery,
     94         ),
     95         (
     96             "today_media_corrupt",
     97             Recovery::MediaCorrupt,
     98             Retry::NotAllowed,
     99         ),
    100         (
    101             "client_closed",
    102             Recovery::RuntimeUnavailable,
    103             Retry::NotAllowed,
    104         ),
    105     ] {
    106         let decision = classify_error_recovery(1, code.into());
    107         assert_eq!(decision.disposition, disposition, "{code}");
    108         assert_eq!(decision.retry, retry, "{code}");
    109     }
    110     for version in [0, 2, u16::MAX] {
    111         let decision = classify_error_recovery(version, "today_relay_offline".into());
    112         assert_eq!(decision.disposition, Recovery::UnsupportedVersion);
    113         assert_eq!(decision.retry, Retry::NotAllowed);
    114     }
    115 }
    116 
    117 #[test]
    118 fn real_application_error_adapters_preserve_storage_quota_and_corruption() {
    119     for (error, expected) in [
    120         (
    121             TodayError::InboundMedia(Phase1InboundMediaError::CacheQuotaExceeded),
    122             Recovery::QuotaExhausted,
    123         ),
    124         (
    125             TodayError::InboundMedia(Phase1InboundMediaError::CacheIo),
    126             Recovery::StorageFailure,
    127         ),
    128         (
    129             TodayError::InboundMedia(Phase1InboundMediaError::UnsupportedSchema),
    130             Recovery::UnsupportedVersion,
    131         ),
    132         (
    133             TodayError::InboundMedia(Phase1InboundMediaError::CorruptArtifact),
    134             Recovery::MediaCorrupt,
    135         ),
    136         (TodayError::CorruptProjection, Recovery::StaleRevision),
    137     ] {
    138         let error = TeraAppError::from(error);
    139         let report = error.report();
    140         assert_eq!(
    141             classify_error_recovery(report.schema_version, report.code.clone()).disposition,
    142             expected
    143         );
    144     }
    145     for (error, expected) in [
    146         (Phase1DraftError::Storage, Recovery::StorageFailure),
    147         (
    148             Phase1DraftError::IdentityUnavailable,
    149             Recovery::IdentityUnavailable,
    150         ),
    151         (Phase1DraftError::Operation, Recovery::OutcomeUnknown),
    152         (Phase1DraftError::Overlay, Recovery::StaleRevision),
    153     ] {
    154         let error = TeraAppError::from(error);
    155         assert_eq!(
    156             classify_error_recovery(1, error.report().code.clone()).disposition,
    157             expected
    158         );
    159     }
    160 }
    161 
    162 #[test]
    163 fn complete_safe_envelopes_survive_mapping_without_relabeling_future_versions() {
    164     let sdk = tera_core::SdkErrorRecord {
    165         schema_version: 2,
    166         code: "future_network_code".into(),
    167         class: "network".into(),
    168         retryable: true,
    169         recovery_actions: vec!["publish_again".into()],
    170         operation_id: Some("existing-operation".into()),
    171         capability_id: Some("transport".into()),
    172         message: "Texte localisé: retry corruption offline".into(),
    173     };
    174     let error = TeraAppError::from(tera_core::TeraAppError::Sdk {
    175         report: sdk.clone(),
    176     });
    177     let report = error.report();
    178     assert_eq!(report.schema_version, sdk.schema_version);
    179     assert_eq!(report.code, sdk.code);
    180     assert_eq!(report.category, sdk.class);
    181     assert_eq!(report.retryable, sdk.retryable);
    182     assert_eq!(report.recovery_actions, sdk.recovery_actions);
    183     assert_eq!(report.operation_id, sdk.operation_id);
    184     assert_eq!(report.capability_id, sdk.capability_id);
    185     assert_eq!(report.safe_message, sdk.message);
    186     let decision = classify_error_recovery(report.schema_version, report.code.clone());
    187     assert_eq!(decision.disposition, Recovery::UnsupportedVersion);
    188     assert_eq!(decision.retry, Retry::NotAllowed);
    189 
    190     let store = tera_core::StoreErrorRecord {
    191         schema_version: 2,
    192         code: sdk.code,
    193         class: sdk.class,
    194         retryable: sdk.retryable,
    195         recovery_actions: sdk.recovery_actions,
    196         message: sdk.message,
    197     };
    198     let error = TeraAppError::from(tera_core::TeraAppError::Store {
    199         report: store.clone(),
    200     });
    201     let report = error.report();
    202     assert_eq!(report.schema_version, store.schema_version);
    203     assert_eq!(report.code, store.code);
    204     assert_eq!(report.category, store.class);
    205     assert_eq!(report.retryable, store.retryable);
    206     assert_eq!(report.recovery_actions, store.recovery_actions);
    207     assert_eq!(report.safe_message, store.message);
    208     assert_eq!(report.operation_id, None);
    209     assert_eq!(report.capability_id, None);
    210 }
    211 
    212 #[test]
    213 fn closing_admission_preserves_the_same_typed_catalog_across_product_families() {
    214     use tera_core::runtime::{lifecycle::RuntimeLifecycleError, product_surface::SettingsError};
    215     for state in [
    216         RuntimeLifecycleError::Closing,
    217         RuntimeLifecycleError::Closed,
    218         RuntimeLifecycleError::CloseInProgress,
    219     ] {
    220         let expected = TeraAppError::from(tera_core::TeraAppError::from(state));
    221         for observed in [
    222             TeraAppError::from(TodayError::Lifecycle(state)),
    223             TeraAppError::from(Phase1DraftError::Lifecycle(state)),
    224             TeraAppError::from(SettingsError::Lifecycle(state)),
    225         ] {
    226             assert_eq!(observed.report(), expected.report());
    227             let recovery = classify_error_recovery(1, observed.report().code.clone());
    228             assert_eq!(
    229                 recovery.disposition,
    230                 if state == RuntimeLifecycleError::Closed {
    231                     Recovery::RuntimeUnavailable
    232                 } else {
    233                     Recovery::OutcomeUnknown
    234                 }
    235             );
    236         }
    237     }
    238 }