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 0a4a580945f3fe4615090d165784a16d19ac0bd1
parent 1d3942a9f07da09aacfdd293ec05aac2d6e66b10
Author: triesap <tyson@radroots.org>
Date:   Thu, 10 Sep 2026 01:44:55 +0000

today: Verify direct ingestion before canonical storage

- Recheck actual signatures and shared profile admission before durable writes
- Reject permissive external evidence without poisoning cached projections
- Qualify all card families and malformed inputs through shared boundaries
- Regenerate and verify unchanged public APIs across supported native lanes

Diffstat:
MTeraFFI/provenance.json | 52++++++++++++++++++++++++++--------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 20+++++++++++++-------
MTeraFFI/source/aarch64-apple-ios-sim.json | 20+++++++++++++-------
MTeraFFI/source/aarch64-apple-ios.json | 20+++++++++++++-------
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 14++++++++++++--
Acore/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs | 452+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 20++++++++++++++++++++
9 files changed, 553 insertions(+), 53 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71312488, + "bytes": 71312552, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "9230ac85b7c104fb70fde726ab61287563741bef4bf282acbd652d874883f9b6" + "sha256": "dd161ee145cc17a320243465379abd6dd77a2099ea5cbd5e0a05fb668d2ce2c8" }, { "bytes": 70759, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71384240, + "bytes": 71384352, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "059bf68e4635c45b96e6432bbfd505e9686e38bab5c9c749d2f930d564a0531c" + "sha256": "060a7eae135c135ee9455dca0ab7242a00f18c1450f1ead05c0087e9ac834cb2" }, { "bytes": 42118, @@ -79,32 +79,32 @@ { "bytes": 19919632, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "9086aefe05af76debd6f0f0829ef05777cbf1eed6abbdcea2884e52370500890" + "sha256": "3f8d2f7a8981edd1585f90ba2950312a66d19acc0e9ecc7aeb032ed181d94d5a" }, { - "bytes": 71312488, + "bytes": 71312552, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "9230ac85b7c104fb70fde726ab61287563741bef4bf282acbd652d874883f9b6" + "sha256": "dd161ee145cc17a320243465379abd6dd77a2099ea5cbd5e0a05fb668d2ce2c8" }, { - "bytes": 71384240, + "bytes": 71384352, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "059bf68e4635c45b96e6432bbfd505e9686e38bab5c9c749d2f930d564a0531c" + "sha256": "060a7eae135c135ee9455dca0ab7242a00f18c1450f1ead05c0087e9ac834cb2" }, { - "bytes": 65163, + "bytes": 65457, "path": "source/aarch64-apple-darwin.json", - "sha256": "156356a9a59b74966564dc9ba7aab5af93b411850ec9b5ef11aae90f66316019" + "sha256": "fb63c03c91703553af22970c25ceffb638ba796b8c89b2b27718f60d81568759" }, { - "bytes": 65007, + "bytes": 65301, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "0c5d8319fa8e3accad2f5c5030333f4d47aa4411cbe4934c1417e1aac0e3a419" + "sha256": "ba6a8976e57a3b3d59765fc77f6eab8ec35f2b23e0f4fcf1c9010f8ee34749be" }, { - "bytes": 65003, + "bytes": 65297, "path": "source/aarch64-apple-ios.json", - "sha256": "e4dd8b9fa281f7b34c9049b4851cc0e757487c32ca68275874009b2153fed42f" + "sha256": "80c64057c5cddef9cac82ab6957b45e39d80f2c9640e00bae463ff57d64c11cc" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "6ea8d0b4311bc5b146778388ed016e16ed0d70de" + "tree": "108f397ef5709858a375cd6c1beb6fa38d8c732d" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71312488, + "bytes": 71312552, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "9230ac85b7c104fb70fde726ab61287563741bef4bf282acbd652d874883f9b6" + "sha256": "dd161ee145cc17a320243465379abd6dd77a2099ea5cbd5e0a05fb668d2ce2c8" }, { "bytes": 70759, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 71384240, + "bytes": 71384352, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "059bf68e4635c45b96e6432bbfd505e9686e38bab5c9c749d2f930d564a0531c" + "sha256": "060a7eae135c135ee9455dca0ab7242a00f18c1450f1ead05c0087e9ac834cb2" }, { "bytes": 510216, @@ -174,19 +174,19 @@ "sha256": "4612ea78e68402fbd05ae3a33455ca74380a93940f457524c642020c7bbff272" }, { - "bytes": 65163, + "bytes": 65457, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "156356a9a59b74966564dc9ba7aab5af93b411850ec9b5ef11aae90f66316019" + "sha256": "fb63c03c91703553af22970c25ceffb638ba796b8c89b2b27718f60d81568759" }, { - "bytes": 65007, + "bytes": 65301, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "0c5d8319fa8e3accad2f5c5030333f4d47aa4411cbe4934c1417e1aac0e3a419" + "sha256": "ba6a8976e57a3b3d59765fc77f6eab8ec35f2b23e0f4fcf1c9010f8ee34749be" }, { - "bytes": 65003, + "bytes": 65297, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "e4dd8b9fa281f7b34c9049b4851cc0e757487c32ca68275874009b2153fed42f" + "sha256": "80c64057c5cddef9cac82ab6957b45e39d80f2c9640e00bae463ff57d64c11cc" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "6ea8d0b4311bc5b146778388ed016e16ed0d70de" -manifest_sha256 = "544dd3a858d45dc8c3a8845d94262872f5b6f7dda08bda6a4e2b47413325d625" +source_tree = "108f397ef5709858a375cd6c1beb6fa38d8c732d" +manifest_sha256 = "e66d74806ce1c4de91bacb8d032d239e427426e516eeabd7a915298f41c7d814" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -721,10 +721,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 137675, - "git_blob": "1932f8e554c1a8f06294510f701a2e66d87378b6", + "bytes": 138243, + "git_blob": "4f0267e4680d1bb86c70743f7f66345d1e0e36c0", "mode": "100644", - "sha256": "a32f65028ac00d42301f61c5429eec6baa6907d3df8d8d067024fb9fd9fdae8c" + "sha256": "b5a4d30bb6ed7a0909f3debe51fbbee06f73b02a708c02b00933a3958ba6c78b" + }, + "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { + "bytes": 14242, + "git_blob": "f9bbc6b4f723b398070dc4f582203fb9ace08444", + "mode": "100644", + "sha256": "50a50a7627a22350c86e3f8d5121eb03405e628f47935a8461ad9272302b9739" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -1405,13 +1411,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 115096, - "git_blob": "14f84049a92a278905e8b08bf1ba2338e70d0639", + "bytes": 115786, + "git_blob": "a1c9508fc6f3834dadcc9c23671d0a18cf9dcccf", "mode": "100644", - "sha256": "a5aa54ddc8d6cff6e342148206d1fb0b899cdd93d11790f1117ea94680710627" + "sha256": "c106a0014ddba52c170d128c42bcd6f2a965dfa108b2d8e06411019c3c75b11c" } }, "policy": "staged_inputs", - "tree": "6ea8d0b4311bc5b146778388ed016e16ed0d70de" + "tree": "108f397ef5709858a375cd6c1beb6fa38d8c732d" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -717,10 +717,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 137675, - "git_blob": "1932f8e554c1a8f06294510f701a2e66d87378b6", + "bytes": 138243, + "git_blob": "4f0267e4680d1bb86c70743f7f66345d1e0e36c0", "mode": "100644", - "sha256": "a32f65028ac00d42301f61c5429eec6baa6907d3df8d8d067024fb9fd9fdae8c" + "sha256": "b5a4d30bb6ed7a0909f3debe51fbbee06f73b02a708c02b00933a3958ba6c78b" + }, + "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { + "bytes": 14242, + "git_blob": "f9bbc6b4f723b398070dc4f582203fb9ace08444", + "mode": "100644", + "sha256": "50a50a7627a22350c86e3f8d5121eb03405e628f47935a8461ad9272302b9739" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -1401,13 +1407,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 115096, - "git_blob": "14f84049a92a278905e8b08bf1ba2338e70d0639", + "bytes": 115786, + "git_blob": "a1c9508fc6f3834dadcc9c23671d0a18cf9dcccf", "mode": "100644", - "sha256": "a5aa54ddc8d6cff6e342148206d1fb0b899cdd93d11790f1117ea94680710627" + "sha256": "c106a0014ddba52c170d128c42bcd6f2a965dfa108b2d8e06411019c3c75b11c" } }, "policy": "staged_inputs", - "tree": "6ea8d0b4311bc5b146778388ed016e16ed0d70de" + "tree": "108f397ef5709858a375cd6c1beb6fa38d8c732d" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -717,10 +717,16 @@ "sha256": "76e3bddfc237dac63293d264d68991b489585136fe377e39dbba72aa3a1181f4" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 137675, - "git_blob": "1932f8e554c1a8f06294510f701a2e66d87378b6", + "bytes": 138243, + "git_blob": "4f0267e4680d1bb86c70743f7f66345d1e0e36c0", "mode": "100644", - "sha256": "a32f65028ac00d42301f61c5429eec6baa6907d3df8d8d067024fb9fd9fdae8c" + "sha256": "b5a4d30bb6ed7a0909f3debe51fbbee06f73b02a708c02b00933a3958ba6c78b" + }, + "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { + "bytes": 14242, + "git_blob": "f9bbc6b4f723b398070dc4f582203fb9ace08444", + "mode": "100644", + "sha256": "50a50a7627a22350c86e3f8d5121eb03405e628f47935a8461ad9272302b9739" }, "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs": { "bytes": 6898, @@ -1401,13 +1407,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 115096, - "git_blob": "14f84049a92a278905e8b08bf1ba2338e70d0639", + "bytes": 115786, + "git_blob": "a1c9508fc6f3834dadcc9c23671d0a18cf9dcccf", "mode": "100644", - "sha256": "a5aa54ddc8d6cff6e342148206d1fb0b899cdd93d11790f1117ea94680710627" + "sha256": "c106a0014ddba52c170d128c42bcd6f2a965dfa108b2d8e06411019c3c75b11c" } }, "policy": "staged_inputs", - "tree": "6ea8d0b4311bc5b146778388ed016e16ed0d70de" + "tree": "108f397ef5709858a375cd6c1beb6fa38d8c732d" } } diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -81,6 +81,10 @@ mod sync_live_tests; #[path = "today_sync_caps_tests.rs"] mod sync_caps_tests; +#[cfg(all(test, feature = "mobile-social"))] +#[path = "today_admission_tests.rs"] +mod admission_tests; + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub enum TodayProjectionUpdate { @@ -223,8 +227,8 @@ struct FrozenTodaySnapshot { } impl TeraRuntime { - /// Durably admits one already verified and visibility-authorized relay observation, - /// then advances the selected LocalNetwork projection. + /// Rechecks the application's cryptographic/profile boundary before durably + /// admitting a visibility-authorized observation and advancing its projection. pub async fn phase1_ingest_visible( &self, admission: EventAdmission, @@ -235,6 +239,12 @@ impl TeraRuntime { if admission.visible_event().is_none() { return Err(TodayError::EventNotVisible); } + // Shared typestates can carry a different host's verifier or policy. + // Apply our real signature and typed profile checks before storage, + // not only while rebuilding a projection after the durable write. + let verified = verify_nip01_event(admission.event().envelope().clone()) + .map_err(|_| TodayError::EventNotVisible)?; + admit_verified_event(verified).map_err(|_| TodayError::EventNotVisible)?; let storage = self .client .storage() diff --git a/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs @@ -0,0 +1,452 @@ +use super::sync_tests::runtime; +use super::tests::{context, signed}; +use super::*; +use radroots_event::{ + SignedEvent, + admission::{RawEvent, SignatureVerifier, VisibilityPolicy}, +}; +use radroots_event_codec::verify::Nip01SignatureVerifier; +use radroots_transport::{ + Error, EventSource, FetchPage, FetchRequest, SourceStatus, Target, TransportId, + outcome::{FetchTargetOutcome, FetchTargetState}, + source::{EventProvenance, NextPage, ObservedEvent}, +}; +use std::sync::Arc; + +struct AdmissionSource(Vec<SignedEvent>); + +impl EventSource for AdmissionSource { + fn status(&self) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, Error>> { + Box::pin(async { Err(Error::UnsupportedOperation) }) + } + + fn fetch( + &self, + request: FetchRequest, + ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, Error>> { + Box::pin(async move { + let target = request.target_set().targets()[0].fingerprint().clone(); + let provenance = + EventProvenance::new(TransportId::NOSTR, target.clone(), 2_000_000_100_000) + .unwrap(); + FetchPage::for_request( + &request, + self.0 + .iter() + .cloned() + .map(|event| ObservedEvent::new(event, provenance.clone())) + .collect(), + vec![FetchTargetOutcome::new(target, FetchTargetState::Complete)], + NextPage::Complete, + ) + }) + } +} + +fn invalid_signature() -> SignedEvent { + let original = signed(1, vec![], "invalid signature", 2_000_000_000); + let mut wire = original.wire().clone(); + wire.sig = "0".repeat(128); + let raw = serde_json::to_string(&wire).unwrap(); + let event = radroots_event_codec::decode::signed_event(&raw).unwrap(); + assert!(verify_nip01_event(event.envelope().clone()).is_err()); + event +} + +fn observed(event: SignedEvent) -> ObservedEvent { + let target = Target::nostr_relay("wss://relay.example").unwrap(); + ObservedEvent::new( + event, + EventProvenance::new( + TransportId::NOSTR, + target.fingerprint().clone(), + 2_000_000_100_000, + ) + .unwrap(), + ) +} + +// Shared typestates accept caller-supplied verifier/policy implementations. +// The app's public direct-ingest boundary must use its actual crypto/profile +// rules before writing even if another host supplied permissive evidence. +struct PermissiveEvidence; +impl SignatureVerifier for PermissiveEvidence { + fn verify_signature( + &self, + _: &radroots_event::envelope::EventEnvelope, + ) -> Result<(), radroots_event::admission::Error> { + Ok(()) + } +} +impl radroots_event::admission::AdmissionPolicy for PermissiveEvidence { + type Error = std::convert::Infallible; + fn policy_id(&self) -> &'static str { + "tera.test.permissive-admission" + } + fn admit( + &self, + _: &radroots_event::admission::ContractValidatedEvent, + ) -> Result<(), Self::Error> { + Ok(()) + } +} +impl VisibilityPolicy for PermissiveEvidence { + type Error = std::convert::Infallible; + fn policy_id(&self) -> &'static str { + "tera.test.permissive-visibility" + } + fn make_visible( + &self, + _: &radroots_event::admission::AdmittedEvent, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[tokio::test] +async fn direct_visible_ingest_cannot_poison_storage_with_permissive_signature_evidence() { + let runtime = TeraRuntime::test_memory().unwrap(); + let selected = context(None, 1); + let valid = signed(1, vec![], "valid", 2_000_000_000); + let contract = admit_verified_event(verify_nip01_event(valid.envelope().clone()).unwrap()) + .unwrap() + .contract_id(); + let event = invalid_signature(); + let visible = RawEvent::new(event.envelope().clone()) + .verify_id() + .unwrap() + .verify_signature(&PermissiveEvidence) + .unwrap() + .validate_contract_for_admission(contract) + .unwrap() + .admit_with(&PermissiveEvidence) + .unwrap() + .make_visible_with(&PermissiveEvidence) + .unwrap(); + let admission = EventAdmission::visible(observed(event), visible).unwrap(); + let result = runtime + .phase1_ingest_visible(admission, &selected, 2_000_000_100) + .await; + assert_eq!( + EventStore::status(runtime.client.storage().unwrap()) + .await + .unwrap() + .raw_events(), + 0, + "an invalid signature must be rejected before durable admission" + ); + assert!(matches!(result, Err(TodayError::EventNotVisible))); + super::tests::ingest(&runtime, &selected, valid, 2_000_000_101).await; + let page = runtime + .phase1_today_page(&selected, TodayPageRequest::first(10, 2_000_000_102)) + .await + .unwrap(); + assert_eq!( + page.items.len(), + 1, + "legitimate visible ingestion still works after refusal" + ); +} + +#[tokio::test] +async fn raw_and_verified_only_observations_cannot_enter_the_direct_visible_path() { + let runtime = TeraRuntime::test_memory().unwrap(); + let event = signed(1, vec![], "verified but not authorized", 2_000_000_000); + let verified = RawEvent::new(event.envelope().clone()) + .verify_id() + .unwrap() + .verify_signature(&Nip01SignatureVerifier) + .unwrap(); + let raw = EventAdmission::raw(observed(event.clone())); + let verified = EventAdmission::verified(observed(event), verified).unwrap(); + for admission in [raw, verified] { + assert!(matches!( + runtime + .phase1_ingest_visible(admission, &context(None, 1), 2_000_000_100) + .await, + Err(TodayError::EventNotVisible) + )); + } + assert_eq!( + EventStore::status(runtime.client.storage().unwrap()) + .await + .unwrap() + .raw_events(), + 0 + ); +} + +#[test] +fn invalid_ids_and_explicit_unregistered_contract_versions_fail_at_shared_boundaries() { + let event = signed(1, vec![], "current wire", 2_000_000_000); + let mut changed: serde_json::Value = serde_json::from_str(event.raw_json()).unwrap(); + changed["content"] = serde_json::json!("tampered after signing"); + assert!(radroots_event_codec::decode::signed_event(&changed.to_string()).is_err()); + changed["id"] = serde_json::json!("0".repeat(64)); + assert!(radroots_event_codec::decode::signed_event(&changed.to_string()).is_err()); + let current = admit_verified_event(verify_nip01_event(event.envelope().clone()).unwrap()) + .unwrap() + .contract_id(); + let unsupported = format!("{}.v999", current.strip_suffix(".v1").unwrap()); + let verified = RawEvent::new(event.envelope().clone()) + .verify_id() + .unwrap() + .verify_signature(&Nip01SignatureVerifier) + .unwrap(); + assert!( + verified + .validate_contract_for_admission(&unsupported) + .is_err() + ); +} + +#[tokio::test] +async fn malformed_known_profiles_and_bad_signatures_do_not_displace_valid_batch_content() { + let malformed = [ + signed(0, vec![], "not profile JSON", 2_000_000_001), + signed( + 31_922, + vec![ + vec!["d", "date"], + vec!["title", "Invalid date"], + vec!["start", "2026-02-30"], + ], + "", + 2_000_000_002, + ), + signed( + 31_923, + vec![ + vec!["d", "time"], + vec!["title", "Invalid time"], + vec!["start", "-1"], + ], + "", + 2_000_000_003, + ), + signed( + 30_402, + vec![ + vec!["d", "food"], + vec!["title", "Food"], + vec!["price", "-1", "CAD"], + vec!["radroots:price_unit", "lb"], + vec!["status", "active"], + ], + "", + 2_000_000_004, + ), + signed( + 1, + vec![vec!["e", "not-an-event-id", "", "root"]], + "hostile required reference", + 2_000_000_005, + ), + ]; + for event in &malformed { + assert!( + admit_verified_event(verify_nip01_event(event.envelope().clone()).unwrap()).is_err(), + "fixture must fail shared admission" + ); + } + let valid = signed( + 1, + vec![], + "ordinary prose about food and events", + 2_000_000_010, + ); + let valid_id = valid.id().to_hex(); + let mut events = malformed.to_vec(); + events.push(invalid_signature()); + events.push(valid); + let runtime = runtime(Arc::new(AdmissionSource(events))); + let selected = context(None, 1); + let receipt = runtime + .phase1_sync_today(&selected, 2_000_000_100, TodayProjectionUpdate::Incremental) + .await + .unwrap(); + assert_eq!( + ( + receipt.events_observed, + receipt.events_admitted, + receipt.events_rejected + ), + (7, 1, 6) + ); + assert_eq!( + ( + receipt.projection.source_events, + receipt.projection.visible_cards + ), + (1, 1) + ); + let page = runtime + .phase1_today_page(&selected, TodayPageRequest::first(10, 2_000_000_100)) + .await + .unwrap(); + assert_eq!(page.items[0].card.source_event_id, valid_id); + assert_eq!( + page.items[0].card.card_type, + TodayCardType::Update, + "prose does not select a product profile" + ); +} + +#[tokio::test] +async fn an_out_of_selector_kind_is_rejected_before_today_ingest() { + let runtime = runtime(Arc::new(AdmissionSource(vec![signed( + 42, + vec![], + "not a Today kind", + 2_000_000_000, + )]))); + let receipt = runtime + .phase1_sync_today( + &context(None, 1), + 2_000_000_100, + TodayProjectionUpdate::Incremental, + ) + .await + .unwrap(); + assert_eq!(receipt.termination, TodaySyncTermination::SourceFailed); + assert_eq!( + ( + receipt.events_observed, + receipt.projection.source_events, + receipt.projection.visible_cards + ), + (0, 0, 0) + ); +} + +#[tokio::test] +async fn all_current_card_families_survive_shared_ingest_while_supporting_records_stay_supporting() +{ + let photo_hash = format!("x {}", "a".repeat(64)); + let roots = vec![ + ( + signed(1, vec![], "root update", 2_000_000_000), + TodayCardType::Update, + ), + ( + signed( + 1, + vec![vec![ + "imeta", + "url https://media.example/photo.jpg", + &photo_hash, + "m image/jpeg", + "dim 10x20", + "size 123", + "alt Field photo", + ]], + "photo https://media.example/photo.jpg", + 2_000_000_001, + ), + TodayCardType::PhotoUpdate, + ), + ( + signed( + 1, + vec![vec!["t", "RADROOTS-ASK"]], + "Anyone have carrots?", + 2_000_000_002, + ), + TodayCardType::Ask, + ), + ( + signed( + 31_922, + vec![ + vec!["d", "all-day"], + vec!["title", "Market day"], + vec!["start", "2033-05-18"], + ], + "", + 2_000_000_003, + ), + TodayCardType::Event, + ), + ( + signed( + 31_923, + vec![ + vec!["d", "timed"], + vec!["title", "Market hour"], + vec!["start", "2000000200"], + vec!["D", "23148"], + ], + "", + 2_000_000_004, + ), + TodayCardType::Event, + ), + ( + signed( + 30_402, + vec![ + vec!["d", "carrots"], + vec!["title", "Carrots"], + vec!["summary", "Fresh"], + vec!["published_at", "2000000005"], + vec!["location", "Victoria"], + vec!["price", "3", "CAD"], + vec!["radroots:price_unit", "lb"], + vec!["status", "active"], + ], + "available", + 2_000_000_005, + ), + TodayCardType::FoodAvailability, + ), + ]; + let root_id = roots[0].0.id().to_hex(); + let mut events = roots + .iter() + .map(|(event, _)| event.clone()) + .collect::<Vec<_>>(); + events.extend([ + signed(0, vec![], r#"{"name":"Supported profile"}"#, 2_000_000_006), + signed( + 1, + vec![vec!["e", &root_id, "", "root"]], + "reply stays in its thread", + 2_000_000_007, + ), + ]); + let runtime = runtime(Arc::new(AdmissionSource(events))); + let selected = context(None, 1); + let receipt = runtime + .phase1_sync_today(&selected, 2_000_000_100, TodayProjectionUpdate::Incremental) + .await + .unwrap(); + assert_eq!( + ( + receipt.events_observed, + receipt.events_admitted, + receipt.events_rejected + ), + (8, 8, 0) + ); + assert_eq!( + ( + receipt.projection.visible_cards, + receipt.projection.profiles, + receipt.projection.thread_entries + ), + (6, 1, 1) + ); + let page = runtime + .phase1_today_page(&selected, TodayPageRequest::first(10, 2_000_000_100)) + .await + .unwrap(); + assert_eq!(page.items.len(), 6); + for (source, kind) in roots { + let item = page + .items + .iter() + .find(|item| item.card.source_event_id == source.id().to_hex()) + .unwrap(); + assert_eq!(item.card.card_type, kind); + } +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "4612ea78e68402fbd05ae3a33455ca74380a93940f457524c642020c7bbff272", - "ffi_provenance_sha256": "544dd3a858d45dc8c3a8845d94262872f5b6f7dda08bda6a4e2b47413325d625", + "ffi_provenance_sha256": "e66d74806ce1c4de91bacb8d032d239e427426e516eeabd7a915298f41c7d814", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "73c919d0547948de01ba49bc8c038f0bb0b80c4278d18e6acc47348f3d67c378", @@ -22,7 +22,7 @@ "lib_revision": "9119bb31c59cd84a4a05c0a75d65d708b49cdd8e", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "6ea8d0b4311bc5b146778388ed016e16ed0d70de", + "tera_ffi_source_tree": "108f397ef5709858a375cd6c1beb6fa38d8c732d", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -75,6 +75,10 @@ { "path": "core/crates/tera_core/src/runtime/product_surface/projection.rs", "count": 1 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", + "count": 1 } ] }, @@ -1775,6 +1779,10 @@ "count": 3 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/store.rs", "count": 2 }, @@ -4009,6 +4017,10 @@ "count": 5 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", + "count": 6 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_backfill_tests.rs", "count": 1 }, @@ -4057,6 +4069,10 @@ { "path": "core/crates/tera_core/src/runtime/product_surface/today.rs", "count": 2 + }, + { + "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", + "count": 4 } ] }, @@ -4501,6 +4517,10 @@ "count": 4 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs", + "count": 3 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_backfill_cursor.rs", "count": 1 },