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 55be33f20161caeb012f2d17661735ec13e58eff
parent d88fe5d594e11fe43bdbe8437fbab7835837c1d8
Author: triesap <tyson@radroots.org>
Date:   Mon, 21 Sep 2026 10:55:32 +0000

delivery: retain late acceptance after cancellation

- Qualify stop after TCP connect and before relay acknowledgement
- Retain exact late target acceptance across SQLite restart
- Reject stale publication callbacks in replacement scopes
- Qualify native artifacts and standalone consumer checks

Diffstat:
MTeraFFI/provenance.json | 14+++++++-------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 8++++----
MTeraFFI/source/aarch64-apple-ios-sim.json | 8++++----
MTeraFFI/source/aarch64-apple-ios.json | 8++++----
MTeraTests/TeraSubmissionStoreTests.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs | 14+++++++++++---
Mrelease/provenance.json | 4++--
8 files changed, 80 insertions(+), 26 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 114576, "path": "source/aarch64-apple-darwin.json", - "sha256": "ade6467725e6becfcd10a7b2435dc2c606d51c59eacae98dbecfe51ceada326f" + "sha256": "775ae3b8acbc3bf8065299cde99aafd2694dc78f647c66223573eca939c9b768" }, { "bytes": 114420, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "16d8391ea1655be6d26e55ca0f68d92b7ca4390d31d753948d74d61940c9c543" + "sha256": "fa1d306014b9c75af14cd630325119903fb5242d127a94e03267614860dc9060" }, { "bytes": 114416, "path": "source/aarch64-apple-ios.json", - "sha256": "4d431474b9f2c082210a1af2a9e36548bd63ad253d544513b6f85a50247a9175" + "sha256": "5471a6cd1f04ac4b36c4f6ad1a9b5b69486e2bf53c1dd244027f26712b9c7da4" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "e7a3b88cba7e4ab4676694822d7d222f68d36a65" + "tree": "6fb1acade84d5425e22521d77f5642658fb8991b" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 114576, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "ade6467725e6becfcd10a7b2435dc2c606d51c59eacae98dbecfe51ceada326f" + "sha256": "775ae3b8acbc3bf8065299cde99aafd2694dc78f647c66223573eca939c9b768" }, { "bytes": 114420, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "16d8391ea1655be6d26e55ca0f68d92b7ca4390d31d753948d74d61940c9c543" + "sha256": "fa1d306014b9c75af14cd630325119903fb5242d127a94e03267614860dc9060" }, { "bytes": 114416, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "4d431474b9f2c082210a1af2a9e36548bd63ad253d544513b6f85a50247a9175" + "sha256": "5471a6cd1f04ac4b36c4f6ad1a9b5b69486e2bf53c1dd244027f26712b9c7da4" } ], "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 = "e7a3b88cba7e4ab4676694822d7d222f68d36a65" -manifest_sha256 = "3e7d842546a817d9e9af7c7f690cb7c9dff60b8eb84ff929a1305b68b4657528" +source_tree = "6fb1acade84d5425e22521d77f5642658fb8991b" +manifest_sha256 = "f36cab89ff95e94f1eb58134d70f684dc9859660049e6d543857b67b67a03a75" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1329,10 +1329,10 @@ "sha256": "27037487bf6b305efc2ba43e75c8ec20c597ebd0a1bac19fcf2a0f80ceb7c7da" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs": { - "bytes": 8874, - "git_blob": "e67764c7a458985d3202874e44cae1de0d90ac7e", + "bytes": 9351, + "git_blob": "b573241041d60551ec8a74e8076c35ad4537f197", "mode": "100644", - "sha256": "2b605aaafeed08af3b310f5ceea454bc273905d6343ed23c9e13c83cc9c2f59a" + "sha256": "a4d7273d94f474ecbc5803f895a83489ea1d5bd60e94b7092b2699ee910a8cae" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_tests.rs": { "bytes": 7315, @@ -2440,6 +2440,6 @@ } }, "policy": "staged_inputs", - "tree": "e7a3b88cba7e4ab4676694822d7d222f68d36a65" + "tree": "6fb1acade84d5425e22521d77f5642658fb8991b" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1325,10 +1325,10 @@ "sha256": "27037487bf6b305efc2ba43e75c8ec20c597ebd0a1bac19fcf2a0f80ceb7c7da" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs": { - "bytes": 8874, - "git_blob": "e67764c7a458985d3202874e44cae1de0d90ac7e", + "bytes": 9351, + "git_blob": "b573241041d60551ec8a74e8076c35ad4537f197", "mode": "100644", - "sha256": "2b605aaafeed08af3b310f5ceea454bc273905d6343ed23c9e13c83cc9c2f59a" + "sha256": "a4d7273d94f474ecbc5803f895a83489ea1d5bd60e94b7092b2699ee910a8cae" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_tests.rs": { "bytes": 7315, @@ -2436,6 +2436,6 @@ } }, "policy": "staged_inputs", - "tree": "e7a3b88cba7e4ab4676694822d7d222f68d36a65" + "tree": "6fb1acade84d5425e22521d77f5642658fb8991b" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1325,10 +1325,10 @@ "sha256": "27037487bf6b305efc2ba43e75c8ec20c597ebd0a1bac19fcf2a0f80ceb7c7da" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs": { - "bytes": 8874, - "git_blob": "e67764c7a458985d3202874e44cae1de0d90ac7e", + "bytes": 9351, + "git_blob": "b573241041d60551ec8a74e8076c35ad4537f197", "mode": "100644", - "sha256": "2b605aaafeed08af3b310f5ceea454bc273905d6343ed23c9e13c83cc9c2f59a" + "sha256": "a4d7273d94f474ecbc5803f895a83489ea1d5bd60e94b7092b2699ee910a8cae" }, "core/crates/tera_core/src/runtime/product_surface/submission/stop_tests.rs": { "bytes": 7315, @@ -2436,6 +2436,6 @@ } }, "policy": "staged_inputs", - "tree": "e7a3b88cba7e4ab4676694822d7d222f68d36a65" + "tree": "6fb1acade84d5425e22521d77f5642658fb8991b" } } diff --git a/TeraTests/TeraSubmissionStoreTests.swift b/TeraTests/TeraSubmissionStoreTests.swift @@ -243,6 +243,52 @@ final class TeraSubmissionStoreTests: XCTestCase { } extension TeraSubmissionStoreTests { + func testLateAcceptedPublicationCannotPopulateAReplacementAccountOrNetworkScope() async throws { + for replacingAccount in [false, true] { + let backend = AddBackend() + let client = try await TeraAddStoreTests.startedClient(backend) + let store = await make(client, backend: backend) + let pause = ResourceTestPause() + await backend.submissionBackend.pauseAdvance(pause) + store.updateForm(\.content, "Original publication") + let task = Task { await store.submit() } + await entered(pause) + let original = try XCTUnwrap(store.submissions.status) + let replacement = TeraComposerScope( + authorPublicKey: replacingAccount ? String(repeating: "cd", count: 32) : original.request.scope.authorPublicKey, + localNetworkID: replacingAccount ? original.request.scope.localNetworkID : "replacement-network" + ) + let context = TeraLocalNetwork(schemaVersion: 1, id: replacement.localNetworkID, label: "Replacement", + relayURLs: [], locality: nil, followedAuthors: [], generation: 2) + store.submissions.configure(scope: replacement, context: context) + store.submissions.start() + await pause.resume.open() + await task.value + var recovered: TeraSubmissionStatus? + let deadline = ContinuousClock.now.advanced(by: .seconds(3)) + repeat { + recovered = try? await client.submissionStatus(request: original.request) + if recovered?.delivery.state == .accepted { + break + } + try await Task.sleep(for: .milliseconds(1)) + } while ContinuousClock.now < deadline + let accepted = try XCTUnwrap(recovered) + XCTAssertEqual(accepted.delivery.state, .accepted) + XCTAssertEqual(accepted.request, original.request) + XCTAssertEqual(accepted.operationID, original.operationID) + XCTAssertEqual(accepted.captured, original.captured) + XCTAssertNil(store.submissions.request) + XCTAssertNil(store.submissions.status) + XCTAssertNil(store.submissions.message) + XCTAssertNil(store.submissions.failureCode) + let advances = await backend.submissionBackend.advanceCount + XCTAssertEqual(advances, 1) + store.stop() + _ = try await client.stop() + } + } + func testStopBeforeIntentCommitStaysUnconfirmedUntilOriginalPreparationReturns() async throws { let backend = AddBackend() let client = try await TeraAddStoreTests.startedClient(backend) diff --git a/core/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs b/core/crates/tera_core/src/runtime/product_surface/submission/stop_effect_tests.rs @@ -13,7 +13,7 @@ use crate::runtime::product_surface::{ #[tokio::test] async fn stopped_socket_retains_unknown_then_late_ok_across_sqlite_reopen() { - for sqlite in [false, true] { + for (sqlite, before_handshake) in [(false, false), (true, false), (false, true), (true, true)] { let root = tempfile::tempdir().unwrap(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let url = format!("ws://{}", listener.local_addr().unwrap()); @@ -30,13 +30,21 @@ async fn stopped_socket_retains_unknown_then_late_ok_across_sqlite_reopen() { tokio::spawn(async move { tokio::time::timeout(Duration::from_secs(15), async move { let (stream, _) = listener.accept().await.unwrap(); + if before_handshake { + // TCP is connected, but the relay has not admitted a + // websocket or received any publication frame yet. + entered.notify_one(); + resume.notified().await; + } let mut socket = accept_async(stream).await.unwrap(); while let Some(message) = socket.next().await { if let Message::Text(text) = message.unwrap() { let value: serde_json::Value = serde_json::from_str(&text).unwrap(); if value[0] == "EVENT" { - entered.notify_one(); - resume.notified().await; + if !before_handshake { + entered.notify_one(); + resume.notified().await; + } socket .send(Message::Text( serde_json::json!(["OK", value[1]["id"], true, ""]) diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "bcedcb4883745ef79dacb26d1b9f0fc852216d21c23e315a7adb39347a2abb23", - "ffi_provenance_sha256": "3e7d842546a817d9e9af7c7f690cb7c9dff60b8eb84ff929a1305b68b4657528", + "ffi_provenance_sha256": "f36cab89ff95e94f1eb58134d70f684dc9859660049e6d543857b67b67a03a75", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "d65b792e2be7b95ae70c95aa8f9710074004610f8f252f0fc7340533114f4cbb", @@ -22,7 +22,7 @@ "lib_revision": "858b9e0d4fc25c3bc53ca9550c898fe8148c1bd8", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "a7e31d77d31ecbc1e0e5a78f2681e3fb0c6f367af0edf8b4ceef0af973a78681", - "tera_ffi_source_tree": "e7a3b88cba7e4ab4676694822d7d222f68d36a65", + "tera_ffi_source_tree": "6fb1acade84d5425e22521d77f5642658fb8991b", "xcode_package_lock_sha256": "d98a614a38dce7b55c2925dee51c2d4bf8dcfc4e31779d0c5fe8561c45d6e3ba" }, "version": "0.1.0-alpha"