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 }