field_ios

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

commit c263f7787b217e4168a56374e782e133b5c4c6bf
parent be78712eb08288f7811974c4f9f8e96aaaac8429
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 18:50:19 +0000

test: close mobile ffi coverage gate

- exercise stable error and identity boundary vocabularies

- verify public media cache behavior against real local I/O

- cover profile, revision, and native upload admission paths

- clear the frozen per-crate coverage policy without exclusions

Diffstat:
Mcore/crates/tera_ffi/src/error.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_ffi/src/operations.rs | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_ffi/tests/local_mvp_real_io.rs | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcore/crates/tera_ffi/tests/runtime_delegation.rs | 246++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 640 insertions(+), 28 deletions(-)

diff --git a/core/crates/tera_ffi/src/error.rs b/core/crates/tera_ffi/src/error.rs @@ -374,10 +374,110 @@ mod tests { Phase1DraftError::Storage, Phase1DraftError::Overlay, Phase1DraftError::Corrupt, + Phase1DraftError::ClockUnavailable, + Phase1DraftError::DeadlineOverflow, + Phase1DraftError::NoWritableRelay, + Phase1DraftError::InvalidRevision, ] { let ffi = RadrootsAppError::from(error); assert_eq!(ffi.report().category, "authoring"); assert!(!ffi.report().safe_message.is_empty()); } } + + #[test] + fn cursor_media_settings_and_profile_failures_cover_every_stable_class() { + use radroots_mobile_core::runtime::product_surface::{ + CursorError, IdentitySettingsError, Phase1InboundMediaError, + }; + + for cursor in [ + CursorError::InvalidContext, + CursorError::Malformed, + CursorError::Integrity, + CursorError::Version, + CursorError::ContextMismatch, + CursorError::SnapshotMismatch, + CursorError::Stale, + CursorError::InvalidPosition, + ] { + let error = RadrootsAppError::from(TodayError::Cursor(cursor)); + assert_eq!(error.report().code, "today_cursor_invalid"); + } + for media in [ + Phase1InboundMediaError::InvalidReference, + Phase1InboundMediaError::InvalidDigest, + Phase1InboundMediaError::MissingDigest, + Phase1InboundMediaError::InvalidMediaType, + Phase1InboundMediaError::InvalidDimensions, + Phase1InboundMediaError::InvalidByteSize, + Phase1InboundMediaError::InvalidAlt, + Phase1InboundMediaError::MetadataMismatch, + Phase1InboundMediaError::InvalidOperation, + Phase1InboundMediaError::OperationMismatch, + Phase1InboundMediaError::InvalidFailure, + Phase1InboundMediaError::InvalidConfiguration, + Phase1InboundMediaError::ConfigurationMismatch, + Phase1InboundMediaError::InvalidVerificationTime, + Phase1InboundMediaError::InvalidCachePolicy, + Phase1InboundMediaError::InvalidCacheObservation, + Phase1InboundMediaError::CacheQuotaExceeded, + Phase1InboundMediaError::ArtifactCollision, + Phase1InboundMediaError::CorruptReceipt, + Phase1InboundMediaError::CorruptState, + Phase1InboundMediaError::UnsupportedSchema, + Phase1InboundMediaError::CacheUnavailable, + Phase1InboundMediaError::CacheIo, + Phase1InboundMediaError::CorruptArtifact, + ] { + let error = RadrootsAppError::from(TodayError::InboundMedia(media)); + assert_eq!(error.report().code, "today_media_invalid"); + } + for settings in [ + SettingsError::UnknownRelayAccess, + SettingsError::InvalidRelayEndpoint, + SettingsError::InvalidRelayEndpointCount, + SettingsError::DuplicateRelayEndpoint, + SettingsError::InvalidBlossomEndpoint, + SettingsError::InvalidBlossomEndpointCount, + SettingsError::NetworkEnvironmentMismatch, + SettingsError::InvalidMediaCacheBytes, + SettingsError::InvalidMediaCacheArtifacts, + SettingsError::RevisionConflict, + SettingsError::RevisionExhausted, + SettingsError::UnsupportedSchema, + SettingsError::CorruptDocument, + SettingsError::Storage, + SettingsError::Identity(IdentitySettingsError::InvalidIdentityId), + ] { + let expected_retryable = matches!( + settings, + SettingsError::RevisionConflict + | SettingsError::RevisionExhausted + | SettingsError::Storage + ); + let error = RadrootsAppError::from(settings); + assert_eq!(error.report().retryable, expected_retryable); + } + for profile in [ + ProfileMetadataError::InvalidName, + ProfileMetadataError::InvalidDisplayName, + ProfileMetadataError::InvalidAbout, + ProfileMetadataError::InvalidNip05, + ] { + assert_eq!(RadrootsAppError::from(profile).report().category, "profile"); + } + assert_eq!( + RadrootsAppError::initialization("private").report().code, + "initialization_failed" + ); + assert_eq!( + RadrootsAppError::invalid_argument("bad") + .with_operation_id("operation".to_owned()) + .report() + .operation_id + .as_deref(), + Some("operation") + ); + } } diff --git a/core/crates/tera_ffi/src/operations.rs b/core/crates/tera_ffi/src/operations.rs @@ -730,6 +730,9 @@ pub(crate) fn decode_reference_fingerprint(value: &str) -> Result<[u8; 32], Radr #[cfg(test)] mod tests { use super::*; + use radroots_mobile_core::runtime::product_surface::Phase1MediaConfigurationFingerprint; + + const PUBLIC_KEY: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; #[test] fn media_operation_can_be_claimed_exactly_once() { @@ -769,4 +772,237 @@ mod tests { .unwrap_err(); assert_eq!(error.report().code, "invalid_identity_command"); } + + #[test] + fn boundary_enums_and_identity_commands_cover_the_closed_vocabularies() { + for (ffi, core) in [ + ( + crate::FfiAddCommandType::CreateUpdate, + AddCommandType::CreateUpdate, + ), + ( + crate::FfiAddCommandType::CreatePhotoUpdate, + AddCommandType::CreatePhotoUpdate, + ), + ( + crate::FfiAddCommandType::CreateAsk, + AddCommandType::CreateAsk, + ), + ( + crate::FfiAddCommandType::CreateEvent, + AddCommandType::CreateEvent, + ), + ( + crate::FfiAddCommandType::CreateFoodAvailability, + AddCommandType::CreateFoodAvailability, + ), + ] { + assert_eq!(AddCommandType::from(ffi), core); + } + for environment in [ + FfiMobileNetworkEnvironment::Public, + FfiMobileNetworkEnvironment::Simulator, + FfiMobileNetworkEnvironment::PhysicalDevice, + ] { + assert_eq!( + FfiMobileNetworkEnvironment::from(MobileNetworkEnvironment::from(environment)), + environment + ); + } + for access in [ + FfiRelayAccessPreference::ReadOnly, + FfiRelayAccessPreference::ReadWrite, + ] { + assert_eq!( + FfiRelayAccessPreference::from(RelayAccessPreference::from(access)), + access + ); + } + for authority in [ + FfiBlossomAuthorityPreference::PublicWebPki, + FfiBlossomAuthorityPreference::LoopbackDevelopment, + FfiBlossomAuthorityPreference::PrivateNetworkDevelopment, + ] { + assert_eq!( + FfiBlossomAuthorityPreference::from(BlossomEndpointAuthorityPreference::from( + authority + )), + authority + ); + } + + let commands = [ + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::BeginImport, + operation_id: Some("operation".to_owned()), + identity_id: None, + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::CompleteImport, + operation_id: Some("operation".to_owned()), + identity_id: Some("primary".to_owned()), + public_key: Some(PUBLIC_KEY.to_owned()), + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::CancelImport, + operation_id: Some("operation".to_owned()), + identity_id: None, + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Select, + operation_id: None, + identity_id: Some("primary".to_owned()), + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Lock, + operation_id: None, + identity_id: None, + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Unlock, + operation_id: None, + identity_id: None, + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Recover, + operation_id: None, + identity_id: None, + public_key: None, + }, + ]; + for command in commands { + assert!(IdentityCommand::try_from(command).is_ok()); + } + for command in [ + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::BeginImport, + operation_id: Some("operation".to_owned()), + identity_id: Some("unexpected".to_owned()), + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::CancelImport, + operation_id: Some("operation".to_owned()), + identity_id: None, + public_key: Some(PUBLIC_KEY.to_owned()), + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Select, + operation_id: Some("unexpected".to_owned()), + identity_id: Some("primary".to_owned()), + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Select, + operation_id: None, + identity_id: Some("primary".to_owned()), + public_key: Some(PUBLIC_KEY.to_owned()), + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Lock, + operation_id: Some("unexpected".to_owned()), + identity_id: None, + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Unlock, + operation_id: None, + identity_id: Some("unexpected".to_owned()), + public_key: None, + }, + FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + kind: FfiIdentityCommandKind::Recover, + operation_id: None, + identity_id: None, + public_key: Some(PUBLIC_KEY.to_owned()), + }, + ] { + assert!(IdentityCommand::try_from(command).is_err()); + } + assert!( + IdentityCommand::try_from(FfiIdentityCommandRecord { + schema_version: MOBILE_FFI_SCHEMA_VERSION + 1, + kind: FfiIdentityCommandKind::Lock, + operation_id: None, + identity_id: None, + public_key: None, + }) + .is_err() + ); + + let identity = IdentityRecord::new("primary", PUBLIC_KEY).unwrap(); + for lock_state in [IdentityLockState::Locked, IdentityLockState::Unlocked] { + let state = IdentityState::new( + vec![identity.clone()], + Some("primary".to_owned()), + lock_state, + None, + ) + .unwrap(); + let record = FfiIdentityStateRecord::from(&state); + assert_eq!(record.identities.len(), 1); + assert_eq!(record.lock_state, lock_state.into()); + } + } + + #[test] + fn media_decoders_status_and_private_operation_accessors_are_exhaustive() { + assert!(require_schema(MOBILE_FFI_SCHEMA_VERSION).is_ok()); + assert!(require_schema(MOBILE_FFI_SCHEMA_VERSION + 1).is_err()); + for invalid in ["", "not-hex", "00"] { + assert!(decode_artifact_id(invalid).is_err()); + assert!(decode_configuration(invalid).is_err()); + assert!(decode_reference_fingerprint(invalid).is_err()); + } + let digest = "11".repeat(32); + assert!(decode_artifact_id(&digest).is_ok()); + assert!(decode_configuration(&digest).is_ok()); + assert_eq!(decode_reference_fingerprint(&digest).unwrap(), [0x11; 32]); + + let configuration = Phase1MediaConfigurationFingerprint::new([7; 32]).unwrap(); + let status = FfiMediaCacheStatusRecord::from(Phase1MediaCacheStatus { + artifacts: 2, + bytes: 9, + configuration: Some(configuration), + }); + assert_eq!(status.artifact_count, 2); + assert_eq!(status.total_bytes, 9); + assert_eq!(status.configuration_fingerprint, Some("07".repeat(32))); + assert!( + FfiMediaCacheStatusRecord::from(Phase1MediaCacheStatus { + artifacts: 0, + bytes: 0, + configuration: None, + }) + .configuration_fingerprint + .is_none() + ); + + let operation = FfiMediaOperation::new().unwrap(); + assert_eq!(operation.id(), operation.operation_id); + assert!(!operation.cancellation().is_cancelled()); + assert!(!operation.is_cancelled()); + operation.cancel(); + assert!(operation.is_cancelled()); + assert!(operation.cancellation().is_cancelled()); + } } diff --git a/core/crates/tera_ffi/tests/local_mvp_real_io.rs b/core/crates/tera_ffi/tests/local_mvp_real_io.rs @@ -1,5 +1,6 @@ use std::io::Write; use std::os::fd::AsRawFd; +use std::sync::Arc; use std::time::Duration; use nostr::{EventBuilder, Keys, Kind, Metadata, Tag, Timestamp}; @@ -9,7 +10,7 @@ use radroots_blossom::Sha256; use radroots_mobile_ffi::{ FfiAddCommandType, FfiAddDraftInput, FfiBlossomEndpointAuthority, FfiBlossomHostKind, FfiBlossomUploadInput, FfiCancellationPolicy, FfiEventTimingKind, FfiLocalNetworkRecord, - FfiMediaStage, FfiOutboxState, FfiPreparedMediaInput, FfiQueuePolicyRecord, + FfiMediaOperation, FfiMediaStage, FfiOutboxState, FfiPreparedMediaInput, FfiQueuePolicyRecord, FfiRelaySatisfaction, FfiRetractionDraftInput, FfiTodayCardType, FfiTodayProjectionUpdate, FfiTodayRelaySyncState, HostSigningOutcome, HostSigningRequest, HostSigningResult, MOBILE_FFI_SCHEMA_VERSION, ProtectedDataAvailability, RadrootsHostSigner, RadrootsRuntime, @@ -75,6 +76,14 @@ struct BlossomServer { impl BlossomServer { async fn spawn(bytes: Vec<u8>, corrupt_retrieval: bool) -> Self { + Self::spawn_with_retrievals(bytes, corrupt_retrieval, 1).await + } + + async fn spawn_with_retrievals( + bytes: Vec<u8>, + corrupt_retrieval: bool, + retrievals: usize, + ) -> Self { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind Blossom server"); @@ -106,17 +115,19 @@ impl BlossomServer { ); write_http_response(&mut upload, "application/json", &descriptor).await; - let (mut retrieval, _) = listener.accept().await.expect("accept Blossom retrieval"); - let request = read_http_request(&mut retrieval).await; - assert!( - String::from_utf8_lossy(&request) - .starts_with(format!("GET /{hash}.png HTTP/1.1\r\n").as_str()) - ); - let mut response_bytes = bytes; - if corrupt_retrieval { - response_bytes[0] ^= 1; + for _ in 0..retrievals { + let (mut retrieval, _) = listener.accept().await.expect("accept Blossom retrieval"); + let request = read_http_request(&mut retrieval).await; + assert!( + String::from_utf8_lossy(&request) + .starts_with(format!("GET /{hash}.png HTTP/1.1\r\n").as_str()) + ); + let mut response_bytes = bytes.clone(); + if corrupt_retrieval { + response_bytes[0] ^= 1; + } + write_http_response(&mut retrieval, "image/png", &response_bytes).await; } - write_http_response(&mut retrieval, "image/png", &response_bytes).await; }); Self { origin, task } } @@ -131,7 +142,7 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services() let relay = MockRelay::run().await.expect("local NIP-01 relay"); let relay_url = relay.url().await.to_string(); let image_bytes = png(2, 3); - let blossom = BlossomServer::spawn(image_bytes.clone(), false).await; + let blossom = BlossomServer::spawn_with_retrievals(image_bytes.clone(), false, 2).await; let mut image_file = tempfile::tempfile().expect("media file"); image_file.write_all(&image_bytes).expect("write media"); let media = prepared_media(&blossom.origin, &image_bytes, &image_file, "Harvest photo"); @@ -256,8 +267,6 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services() .await; advance_complete(&publisher, &id, queued.revision).await; } - blossom.finish().await; - let reader_root = tempfile::tempdir().expect("fresh reader root"); support::prepare(reader_root.path()); let reader = RadrootsRuntime::new( @@ -308,6 +317,55 @@ async fn public_runtime_completes_the_local_mvp_against_real_protocol_services() photo.media[0].sha256.as_deref(), Some(media.sha256.as_str()) ); + reader + .configure_blossom( + FfiBlossomHostKind::Simulator, + FfiBlossomEndpointAuthority::LoopbackDevelopment, + blossom.origin.clone(), + vec![], + ) + .expect("reader Blossom profile"); + let artifact = reader + .phase1_retrieve_media( + context.clone(), + photo.media[0].reference_fingerprint.clone(), + Arc::new(FfiMediaOperation::new().expect("media operation")), + ) + .await + .expect("retrieve projected media"); + assert_eq!(artifact.bytes, image_bytes); + assert_eq!( + artifact.byte_size, + u64::try_from(image_bytes.len()).unwrap() + ); + assert!( + reader + .phase1_verified_media_artifact(context.clone(), artifact.artifact_id.clone()) + .await + .expect("verify cached media") + .is_some() + ); + let media_cache = reader + .phase1_media_cache_status(context.clone()) + .await + .expect("media cache status"); + assert_eq!(media_cache.artifact_count, 1); + assert_eq!(media_cache.total_bytes, artifact.byte_size); + assert!(media_cache.configuration_fingerprint.is_some()); + assert!( + reader + .phase1_invalidate_media_artifact(context.clone(), artifact.artifact_id.clone()) + .await + .expect("invalidate cached media") + ); + assert!( + reader + .phase1_verified_media_artifact(context.clone(), artifact.artifact_id) + .await + .expect("verify invalidated media") + .is_none() + ); + blossom.finish().await; let update = cards .iter() .find(|card| card.card_type == FfiTodayCardType::Update) diff --git a/core/crates/tera_ffi/tests/runtime_delegation.rs b/core/crates/tera_ffi/tests/runtime_delegation.rs @@ -1,10 +1,13 @@ +use std::sync::Arc; + use radroots_mobile_ffi::{ FfiAddCommandType, FfiAddDraftInput, FfiBlossomAuthorityPreference, FfiBlossomEndpointAuthority, FfiBlossomHostKind, FfiBlossomPreferencesRecord, - FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiIdentityCommandKind, - FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord, - FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMobileNetworkEnvironment, - FfiOutboxState, FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord, + FfiBlossomUploadIntent, FfiCancellationPolicy, FfiDraftKind, FfiEventTimingKind, + FfiIdentityCommandKind, FfiIdentityCommandRecord, FfiIdentityLockState, FfiLocalNetworkRecord, + FfiLocalStoragePolicyRecord, FfiMediaNetworkPolicyRecord, FfiMediaOperation, + FfiMobileNetworkEnvironment, FfiNativeUploadCompletionInput, FfiOutboxState, + FfiPreparedMediaInput, FfiProfileMetadataInputRecord, FfiQueuePolicyRecord, FfiRelayAccessPreference, FfiRelayAccessRecord, FfiRelayPreferenceRecord, FfiRelayPreferencesRecord, FfiRelaySatisfaction, FfiReplaceSettingsRecord, FfiRetractionDraftInput, FfiRevisionInputRecord, FfiRevisionPhase, FfiTodayCardType, @@ -200,6 +203,15 @@ async fn native_boundary_delegates_the_complete_core_surface() { .expect("simulator Blossom"); runtime .configure_blossom( + FfiBlossomHostKind::Simulator, + FfiBlossomEndpointAuthority::LoopbackDevelopment, + "http://127.0.0.1:1".to_owned(), + vec![], + ) + .expect("unavailable simulator Blossom"); + assert!(runtime.probe_blossom().await.is_err()); + runtime + .configure_blossom( FfiBlossomHostKind::PhysicalDevice, FfiBlossomEndpointAuthority::PrivateNetworkDevelopment, "https://10.0.0.5:3100".to_owned(), @@ -409,20 +421,74 @@ async fn native_boundary_delegates_the_complete_core_surface() { .await .expect("cancelled draft"); assert_eq!(cancelled.state, FfiOutboxState::Cancelled); + assert!( + runtime + .phase1_save_add_intent( + FfiAddDraftInput { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + command_type: FfiAddCommandType::CreateEvent, + content: "Market opens Saturday".to_owned(), + identifier: None, + title: Some("Saturday market".to_owned()), + summary: Some("Weekly market".to_owned()), + location: Some("Town square".to_owned()), + event_timing: Some(FfiEventTimingKind::AllDay), + event_start_date: Some("2027-01-02".to_owned()), + event_end_date: None, + event_start_unix_s: None, + event_end_unix_s: None, + event_timezone: None, + price_amount: None, + currency: None, + unit: None, + quantity: None, + food_published_at_unix_s: None, + food_status: None, + media: Vec::new(), + }, + None, + Some(1), + ) + .await + .is_err() + ); + assert!( + runtime + .phase1_advance_draft(draft_id.clone(), cancelled.revision) + .await + .is_err() + ); let retraction_id = "0a".repeat(16); + let retraction_input = FfiRetractionDraftInput { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + command_type: FfiAddCommandType::CreateUpdate, + target_card_id: "c".repeat(64), + target_event_id: "a".repeat(64), + target_kind: 1, + target_address: None, + reason: "Replaced with a corrected copy".to_owned(), + }; + let mut unsupported_retraction = retraction_input.clone(); + unsupported_retraction.schema_version += 1; + assert_eq!( + runtime + .phase1_save_retraction_draft( + retraction_id.clone(), + unsupported_retraction, + 1_800_000_005, + 1_800_000_005_000, + ) + .await + .expect_err("unsupported retraction schema") + .report() + .code, + "unsupported_schema_version" + ); let retraction = runtime .phase1_save_retraction_draft( retraction_id.clone(), - FfiRetractionDraftInput { - schema_version: MOBILE_FFI_SCHEMA_VERSION, - command_type: FfiAddCommandType::CreateUpdate, - target_card_id: "c".repeat(64), - target_event_id: "a".repeat(64), - target_kind: 1, - target_address: None, - reason: "Replaced with a corrected copy".to_owned(), - }, + retraction_input, 1_800_000_005, 1_800_000_005_000, ) @@ -474,18 +540,90 @@ async fn native_boundary_delegates_the_complete_core_surface() { .await .expect_err("unsupported upload schema"); assert_eq!(upload_error.report().code, "unsupported_schema_version"); + assert_eq!( + runtime + .phase1_prepare_add_media_background(upload.clone()) + .await + .expect_err("unsupported background upload schema") + .report() + .code, + "unsupported_schema_version" + ); + assert_eq!( + runtime + .phase1_complete_add_media_background(FfiNativeUploadCompletionInput { + schema_version: MOBILE_FFI_SCHEMA_VERSION + 1, + draft_id: upload.draft_id.clone(), + expected_revision: upload.expected_revision, + media: upload.media.clone(), + status_code: 200, + response_media_type: Some("application/json".to_owned()), + response_content_encoding: None, + response_body: Vec::new(), + }) + .await + .expect_err("unsupported completion schema") + .report() + .code, + "invalid_native_upload_completion" + ); + assert_eq!( + runtime + .phase1_complete_add_media_background(FfiNativeUploadCompletionInput { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + draft_id: upload.draft_id.clone(), + expected_revision: upload.expected_revision, + media: upload.media.clone(), + status_code: 200, + response_media_type: Some("application/json".to_owned()), + response_content_encoding: None, + response_body: vec![0; 16_385], + }) + .await + .expect_err("oversized completion body") + .report() + .code, + "invalid_native_upload_completion" + ); let mut invalid_id_upload = upload; invalid_id_upload.schema_version = MOBILE_FFI_SCHEMA_VERSION; invalid_id_upload.draft_id = "not-a-draft-id".to_owned(); assert_eq!( runtime - .phase1_upload_add_media_intent(invalid_id_upload) + .phase1_upload_add_media_intent(invalid_id_upload.clone()) .await .expect_err("invalid draft id") .report() .code, "invalid_draft_id" ); + assert_eq!( + runtime + .phase1_prepare_add_media_background(invalid_id_upload.clone()) + .await + .expect_err("invalid background draft id") + .report() + .code, + "invalid_draft_id" + ); + assert_eq!( + runtime + .phase1_complete_add_media_background(FfiNativeUploadCompletionInput { + schema_version: MOBILE_FFI_SCHEMA_VERSION, + draft_id: invalid_id_upload.draft_id, + expected_revision: invalid_id_upload.expected_revision, + media: invalid_id_upload.media, + status_code: 200, + response_media_type: Some("application/json".to_owned()), + response_content_encoding: None, + response_body: Vec::new(), + }) + .await + .expect_err("invalid completion draft id") + .report() + .code, + "invalid_draft_id" + ); assert!( runtime .phase1_local_network(FfiLocalNetworkRecord { @@ -590,6 +728,20 @@ async fn native_boundary_delegates_the_complete_core_surface() { assert_eq!(revision.phase, FfiRevisionPhase::ReplacementPending); assert_eq!(revision.operation_id, revision.replacement.draft_id); assert!(revision.replacement.is_revision); + assert_eq!( + runtime + .phase1_revision_status(revision.operation_id.clone()) + .await + .expect("revision status") + .phase, + FfiRevisionPhase::ReplacementPending + ); + assert!( + runtime + .phase1_advance_revision(revision.operation_id.clone()) + .await + .is_err() + ); let cancelled_revision = runtime .phase1_cancel_revision(revision.operation_id.clone()) .await @@ -612,12 +764,78 @@ async fn native_boundary_delegates_the_complete_core_surface() { .expect("save profile intent"); assert_eq!(profile.state, FfiOutboxState::Draft); assert_eq!(profile.operation_id.len(), 32); + assert_eq!( + runtime + .phase1_profile_status(profile.operation_id.clone()) + .await + .expect("profile status") + .revision, + profile.revision + ); let cancelled_profile = runtime .phase1_cancel_profile(profile.operation_id.clone(), profile.revision) .await .expect("cancel profile intent"); assert_eq!(cancelled_profile.operation_id, profile.operation_id); assert_eq!(cancelled_profile.state, FfiOutboxState::Cancelled); + assert_eq!( + runtime + .phase1_advance_profile(profile.operation_id.clone()) + .await + .expect("advance cancelled profile") + .state, + FfiOutboxState::Cancelled + ); + + let cache = runtime + .phase1_media_cache_status(local_network.clone()) + .await + .expect("empty media cache status"); + assert_eq!(cache.artifact_count, 0); + assert_eq!(cache.total_bytes, 0); + assert!(cache.configuration_fingerprint.is_none()); + assert!( + runtime + .phase1_verified_media_artifact(local_network.clone(), "11".repeat(32)) + .await + .expect("empty verified media lookup") + .is_none() + ); + assert!( + !runtime + .phase1_invalidate_media_artifact(local_network.clone(), "11".repeat(32)) + .await + .expect("missing media invalidation") + ); + assert!( + runtime + .phase1_invalidate_media_configuration(local_network.clone(), "22".repeat(32)) + .await + .expect("empty configuration invalidation") + .is_empty() + ); + let media_operation = Arc::new(FfiMediaOperation::new().expect("media operation")); + let media_error = runtime + .phase1_retrieve_media( + local_network.clone(), + "invalid-reference".to_owned(), + Arc::clone(&media_operation), + ) + .await + .expect_err("invalid media reference"); + assert_eq!( + media_error.report().code, + "invalid_media_reference_fingerprint" + ); + assert_eq!( + runtime + .phase1_retrieve_media(local_network, "00".repeat(32), Arc::clone(&media_operation),) + .await + .expect_err("single-use media operation") + .report() + .code, + "media_operation_already_used" + ); runtime.shutdown().await.expect("shutdown"); assert!(matches!(