submission_contract.rs (6218B)
1 use tera_core::runtime::product_surface::{AddCommandType, ComposerFormInput, ComposerPartialForm}; 2 use tera_ffi::*; 3 mod support; 4 5 fn save_request() -> FfiComposerSaveRequest { 6 let mut input = ComposerFormInput::empty(AddCommandType::CreateEvent); 7 input.content = "PRIVATE incomplete\n\0é".into(); 8 input.event_start_date = Some("2026-09-".into()); 9 FfiComposerSaveRequest { 10 schema_version: 1, 11 scope: FfiComposerScopeRecord { 12 schema_version: 1, 13 author_public_key: support::PUBLIC_KEY.into(), 14 local_network_id: "nearby".into(), 15 }, 16 id: composer_reserve_id().unwrap().id, 17 expected_revision: None, 18 edit_sequence: u64::MAX - 1, 19 form: (&ComposerPartialForm::new(input).unwrap()).into(), 20 } 21 } 22 23 fn submission(saved: &FfiComposerSaveRequest) -> FfiSubmissionReservationRequest { 24 FfiSubmissionReservationRequest { 25 schema_version: 1, 26 command_id: submission_reserve_id().unwrap().id, 27 scope: saved.scope.clone(), 28 composer_id: saved.id.clone(), 29 expected_revision: 1, 30 } 31 } 32 33 #[tokio::test] 34 async fn typed_reservation_replays_exact_historical_input_and_keeps_intentional_posts_distinct() { 35 let (root, runtime) = support::runtime().await; 36 let mut saved = save_request(); 37 let original = runtime.composer_save(saved.clone()).await.unwrap(); 38 let request = submission(&saved); 39 let (a, b) = tokio::join!( 40 runtime.submission_reserve(request.clone()), 41 runtime.submission_reserve(request.clone()) 42 ); 43 let a = a.unwrap(); 44 let b = b.unwrap(); 45 assert_eq!(a.captured, original.draft); 46 assert_eq!(a.reservation_id, b.reservation_id); 47 assert_ne!(a.replayed, b.replayed); 48 assert!(!format!("{a:?}").contains("PRIVATE")); 49 saved.expected_revision = Some(1); 50 saved.edit_sequence = u64::MAX; 51 saved.form.content = "later".into(); 52 let latest = runtime.composer_save(saved.clone()).await.unwrap(); 53 runtime.shutdown().await.unwrap(); 54 drop(runtime); 55 let runtime = TeraRuntime::new( 56 root.path().to_string_lossy().into_owned(), 57 support::PUBLIC_KEY.into(), 58 support::GENERATION.into(), 59 1_800_000_000_000, 60 ProtectedDataAvailability::Available, 61 ) 62 .await 63 .unwrap(); 64 let recovered = runtime.submission_reserve(request.clone()).await.unwrap(); 65 assert!(recovered.replayed); 66 assert_eq!(recovered.captured, original.draft); 67 assert_eq!(recovered.reserved_at_unix_ms, a.reserved_at_unix_ms); 68 assert_eq!( 69 runtime 70 .composer_load(saved.scope.clone(), saved.id.clone()) 71 .await 72 .unwrap(), 73 latest.draft 74 ); 75 let mut changed = request.clone(); 76 changed.expected_revision = 2; 77 let failure = runtime 78 .submission_reserve(changed.clone()) 79 .await 80 .unwrap_err(); 81 assert_eq!(failure.report().code, "idempotency_conflict"); 82 assert_eq!( 83 classify_error_recovery(1, failure.report().code.clone()).disposition, 84 FfiRecoveryDisposition::IdempotencyConflict 85 ); 86 changed.command_id = submission_reserve_id().unwrap().id; 87 let second = runtime.submission_reserve(changed.clone()).await.unwrap(); 88 changed.command_id = submission_reserve_id().unwrap().id; 89 let third = runtime.submission_reserve(changed).await.unwrap(); 90 assert_eq!(second.captured, third.captured); 91 assert_ne!(second.reservation_id, third.reservation_id); 92 assert!(runtime.phase1_draft_heads(100).await.unwrap().is_empty()); 93 runtime.shutdown().await.unwrap(); 94 } 95 96 #[tokio::test] 97 async fn malformed_submission_identity_versions_and_widths_fail_before_reservation() { 98 let (_root, runtime) = support::runtime().await; 99 let saved = save_request(); 100 runtime.composer_save(saved.clone()).await.unwrap(); 101 let request = submission(&saved); 102 let mut invalid = Vec::new(); 103 for command_id in [ 104 "00".repeat(16), 105 "AB".repeat(16), 106 "a".repeat(31), 107 "a".repeat(33), 108 "z".repeat(32), 109 ] { 110 let mut changed = request.clone(); 111 changed.command_id = command_id; 112 invalid.push((changed, "submission_command_id_invalid")); 113 } 114 for expected_revision in [0, u64::MAX] { 115 let mut changed = request.clone(); 116 changed.expected_revision = expected_revision; 117 invalid.push((changed, "composer_revision_invalid")); 118 } 119 let mut changed = request.clone(); 120 changed.schema_version = 2; 121 invalid.push((changed, "submission_schema_unsupported")); 122 let mut changed = request.clone(); 123 changed.scope.schema_version = 2; 124 invalid.push((changed, "composer_schema_unsupported")); 125 let mut changed = request.clone(); 126 changed.composer_id = "00".repeat(16); 127 invalid.push((changed, "composer_id_invalid")); 128 for (request, code) in invalid { 129 let error = runtime.submission_reserve(request).await.unwrap_err(); 130 assert_eq!(error.report().code, code); 131 assert!(!error.report().retryable); 132 } 133 assert!(!runtime.submission_reserve(request).await.unwrap().replayed); 134 runtime.shutdown().await.unwrap(); 135 } 136 137 #[test] 138 fn typed_submission_failures_preserve_recovery_without_leaking_storage_details() { 139 use tera_core::runtime::product_surface::SubmissionReservationError as E; 140 for (error, code, disposition) in [ 141 ( 142 E::CorruptRecord, 143 "submission_record_corrupt", 144 FfiRecoveryDisposition::StorageFailure, 145 ), 146 ( 147 E::UnsupportedSchema, 148 "submission_schema_unsupported", 149 FfiRecoveryDisposition::UnsupportedVersion, 150 ), 151 ( 152 E::InvalidReceipt, 153 "submission_receipt_mismatch", 154 FfiRecoveryDisposition::OutcomeUnknown, 155 ), 156 ( 157 E::ClockUnavailable, 158 "operation_clock_unavailable", 159 FfiRecoveryDisposition::RuntimeUnavailable, 160 ), 161 ] { 162 let error = TeraAppError::from(error); 163 assert_eq!(error.report().code, code); 164 assert_eq!( 165 classify_error_recovery(1, code.into()).disposition, 166 disposition 167 ); 168 } 169 }