field_ios

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

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 }