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 80f71e1979458f8eb1ee24e8270b24dfa360aa06
parent 648e862b6eb15828afec324e8f8cad40b5ef4969
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 14:53:43 +0000

recovery: reconcile native execution before retry

- Adopt bounded OS inventory from the exact AppleKit producer
- Reconcile lost admission replies without a second upload
- Bound observation while retaining original receipt identity
- Verify relaunch and unknown execution across native adapters

Diffstat:
MPackage.resolved | 4++--
MPackage.swift | 3++-
MTera.xcodeproj/project.pbxproj | 10+++++++++-
MTera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4++--
MTera/Runtime/TeraAddMediaCoordinator.swift | 17++++++++---------
MTera/Runtime/TeraBackgroundUploadWaiter.swift | 56+++++++++++++++++++++++++++++++++++---------------------
ATera/Runtime/TeraNativeUploadExecution.swift | 35+++++++++++++++++++++++++++++++++++
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/TeraAddStoreTests.swift | 2+-
MTeraTests/TeraBackgroundUploadFixtures.swift | 4++++
ATeraTests/TeraNativeExecutionFixture.swift | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraNativeExecutionTests.swift | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 4+++-
Mrelease/provenance.json | 12++++++------
Mrelease/sbom.cdx.json | 10+++++-----
Mtest-fixtures/legacy-identifiers.v1.json | 88++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
20 files changed, 436 insertions(+), 73 deletions(-)

diff --git a/Package.resolved b/Package.resolved @@ -1,12 +1,12 @@ { - "originHash" : "af1e4801441368b16f3a5881b4211a2a61e1dc8260fc679374e19a250f41ecbb", + "originHash" : "9a390a0ccee3bc2f0f7cd3e10c768e9c4f1e53025db434ed3cd70c9b0108019b", "pins" : [ { "identity" : "apple_kit", "kind" : "remoteSourceControl", "location" : "https://github.com/radrootslabs/apple_kit.git", "state" : { - "revision" : "bf6eff1ebb4c854983a651b333d00d622714199b" + "revision" : "75a4a63149adb55b6a07f94686ef74b5446f1136" } }, { diff --git a/Package.swift b/Package.swift @@ -14,7 +14,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/radrootslabs/apple_kit.git", - revision: "bf6eff1ebb4c854983a651b333d00d622714199b" + revision: "75a4a63149adb55b6a07f94686ef74b5446f1136" ), ], targets: [ @@ -59,6 +59,7 @@ let package = Package( "Runtime/TeraStoppedUploadRecovery.swift", "Runtime/TeraPublicationEvidence.swift", "Runtime/TeraBackgroundUploadWaiter.swift", + "Runtime/TeraNativeUploadExecution.swift", "Runtime/TeraGeneratedRuntimeBackendSubmission.swift", "Runtime/TeraGeneratedSubmissionOperation.swift", "Runtime/TeraNativeTransferJob.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -97,10 +97,12 @@ C73B145B6858220FFFA587B2 /* TeraComposerSubmissionCaptureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A65AF934F7D75487094C79 /* TeraComposerSubmissionCaptureTests.swift */; }; CDBE38E6B521611436F28858 /* TeraMediaStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */; }; CFE386A2E4F9D50F0DF57036 /* TeraNativeRepairTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A506C45B5EE1AAEF0D7F671 /* TeraNativeRepairTests.swift */; }; + D1C6D5CF5F60ED2646465AF6 /* TeraNativeExecutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9849DFB0F2822630588D59F /* TeraNativeExecutionTests.swift */; }; D64AC2FDC6FE05A39114D2DB /* TeraMediaFileFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3C914B349DAE2EF66658229 /* TeraMediaFileFixture.swift */; }; D86A417A36A2186AB7777753 /* TeraTodayReconciliationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 714B4DD70B881D29A89812C9 /* TeraTodayReconciliationTests.swift */; }; D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */; }; DBA4B403D65289242AFDDB07 /* TeraTodaySyncReceiptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC55E41FBE589010038FDA5 /* TeraTodaySyncReceiptTests.swift */; }; + DC5718D38B57C17D832904FD /* TeraNativeExecutionFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9E33BFF3A26F00634BFFA /* TeraNativeExecutionFixture.swift */; }; DF97F408D7D32F72C5AF5939 /* TeraRootShellUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */; }; E4E85524E39F13D6C76650F1 /* TeraTodaySyncFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5854504D0CAD5ABC29302292 /* TeraTodaySyncFixtures.swift */; }; E508A7061082B091B733D62B /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67ACCE25979BA86C1EC5BFD6 /* App.swift */; }; @@ -223,12 +225,14 @@ C4721B7A69E2FB989732A64D /* TeraSubmissionReservationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSubmissionReservationTests.swift; sourceTree = "<group>"; }; C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceTaskTests.swift; sourceTree = "<group>"; }; C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraLifecycleTests.swift; sourceTree = "<group>"; }; + C9849DFB0F2822630588D59F /* TeraNativeExecutionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraNativeExecutionTests.swift; sourceTree = "<group>"; }; CF28A9D711806A07A4A65EA8 /* TeraScopedStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedStoreTests.swift; sourceTree = "<group>"; }; D2C0B898B9BFDB82CF3B5EFB /* TeraAddMediaHarness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraAddMediaHarness.swift; sourceTree = "<group>"; }; D3C914B349DAE2EF66658229 /* TeraMediaFileFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaFileFixture.swift; sourceTree = "<group>"; }; D8ED2718D5E9E8980ADF3423 /* TeraAddSchemaFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraAddSchemaFixtures.swift; sourceTree = "<group>"; }; DE21EEF3B1A13F1B5A10A3BF /* TeraComposerStartupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraComposerStartupTests.swift; sourceTree = "<group>"; }; DE9E724A23014C6EA9042123 /* Tera.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Tera.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E0B9E33BFF3A26F00634BFFA /* TeraNativeExecutionFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraNativeExecutionFixture.swift; sourceTree = "<group>"; }; E351F52E77C74A4E99095E45 /* TeraRemoteQualificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRemoteQualificationTests.swift; sourceTree = "<group>"; }; E55E47B15C42F13ABF2DFF27 /* TeraComposerAutosaveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraComposerAutosaveTests.swift; sourceTree = "<group>"; }; E62DD95FB04526E6E69AB3D8 /* TeraSubmissionFormTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSubmissionFormTests.swift; sourceTree = "<group>"; }; @@ -347,6 +351,8 @@ 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */, 27E1E87F9A11E85700F38635 /* TeraMediaVisibilityTests.swift */, 02F322CF98F8BEB908B93215 /* TeraMutationAdmissionTests.swift */, + E0B9E33BFF3A26F00634BFFA /* TeraNativeExecutionFixture.swift */, + C9849DFB0F2822630588D59F /* TeraNativeExecutionTests.swift */, FA0B5B9169842E64FF792D88 /* TeraNativeRepairPreviewTests.swift */, 6A506C45B5EE1AAEF0D7F671 /* TeraNativeRepairTests.swift */, 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */, @@ -648,6 +654,8 @@ CDBE38E6B521611436F28858 /* TeraMediaStoreTests.swift in Sources */, 5F252ABA8CA48D5E3A7A0157 /* TeraMediaVisibilityTests.swift in Sources */, BB71614D2A6017C071A39BED /* TeraMutationAdmissionTests.swift in Sources */, + DC5718D38B57C17D832904FD /* TeraNativeExecutionFixture.swift in Sources */, + D1C6D5CF5F60ED2646465AF6 /* TeraNativeExecutionTests.swift in Sources */, 7B67600F1C1DA7DD780FA515 /* TeraNativeRepairPreviewTests.swift in Sources */, CFE386A2E4F9D50F0DF57036 /* TeraNativeRepairTests.swift in Sources */, AC33144E09CB0384C4FB090C /* TeraObservationCoalescingTests.swift in Sources */, @@ -1036,7 +1044,7 @@ repositoryURL = "https://github.com/radrootslabs/apple_kit.git"; requirement = { kind = revision; - revision = bf6eff1ebb4c854983a651b333d00d622714199b; + revision = 75a4a63149adb55b6a07f94686ef74b5446f1136; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,12 @@ { - "originHash" : "c884fda8f632e8793480d4b025692ad1ecff40c0eb6afddf727b760185478508", + "originHash" : "0175a3717e07ba9cab3302d8194ef31bc66ed3ce9ef39274587ae709219a1523", "pins" : [ { "identity" : "apple_kit", "kind" : "remoteSourceControl", "location" : "https://github.com/radrootslabs/apple_kit.git", "state" : { - "revision" : "bf6eff1ebb4c854983a651b333d00d622714199b" + "revision" : "75a4a63149adb55b6a07f94686ef74b5446f1136" } }, { diff --git a/Tera/Runtime/TeraAddMediaCoordinator.swift b/Tera/Runtime/TeraAddMediaCoordinator.swift @@ -241,29 +241,28 @@ actor TeraAddMediaCoordinator: TeraAddMediaHandling { let request = try await TeraBackgroundUploadRequest.prepare( job: job, media: media, preparer: preparer ) - let identifier = request.identifier let persisted = try await TeraBackgroundUploadWaiter.matchingPersistedUpload(transfer: transfer, draftID: job.ownerID, expectedRevision: job.expectedRevision, request: request) - let activeIdentifier: RadrootsBackgroundTransferIdentifier + let active: RadrootsBackgroundTransferSnapshot if let persisted { - activeIdentifier = persisted.identifier if [.failed, .interrupted, .cancelled, .expired].contains(persisted.state) { - let retry = try TeraBackgroundUploadRequest.replacingIdentifier(in: request, with: activeIdentifier) + let retry = try TeraBackgroundUploadRequest.replacingIdentifier(in: request, with: persisted.identifier) try Task.checkCancellation() - _ = try await transfer.retry(retry) + active = try await TeraNativeUploadExecution.start(transfer: transfer, request: retry, retrying: true) + } else { + active = persisted } } else { try Task.checkCancellation() - _ = try await transfer.enqueue(request) - activeIdentifier = identifier + active = try await TeraNativeUploadExecution.start(transfer: transfer, request: request, retrying: false) } return try await TeraBackgroundUploadWaiter.receipt(transfer: transfer, - for: activeIdentifier, + for: active.identifier, draftID: job.ownerID, expectedRevision: job.expectedRevision, - request: request) + request: request, baseline: active) } func settleBackgroundUpload(identifier: String, accepted: Bool) async throws { diff --git a/Tera/Runtime/TeraBackgroundUploadWaiter.swift b/Tera/Runtime/TeraBackgroundUploadWaiter.swift @@ -63,54 +63,68 @@ enum TeraBackgroundUploadWaiter { for identifier: RadrootsBackgroundTransferIdentifier, draftID: String, expectedRevision: UInt64, - request: RadrootsBackgroundTransferRequest + request: RadrootsBackgroundTransferRequest, + baseline: RadrootsBackgroundTransferSnapshot? = nil, + waitNanoseconds: UInt64 = TeraRuntimeDeadlinePolicy.production.operationNanoseconds ) async throws -> TeraAddBackgroundUploadReceipt { + guard waitNanoseconds > 0, waitNanoseconds <= TeraRuntimeDeadlinePolicy.production.operationNanoseconds, + let identity = TeraBackgroundUploadRequest.transferIdentity(identifier), + identity.draftID == draftID, identity.revision <= expectedRevision + else { throw TeraNativeUploadExecution.unknown } + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .nanoseconds(Int64(waitNanoseconds))) + var observed = baseline while true { try Task.checkCancellation() + guard clock.now < deadline else { throw TeraNativeUploadExecution.unknown } guard let snapshot = try await transfer.snapshot(for: identifier) else { throw failure( code: "ios.add.background_upload_missing", message: "The background photo upload could not be recovered." ) } - guard TeraBackgroundUploadRequest.persistedRequestMatches(snapshot.request, request: request) else { + try Task.checkCancellation() + guard clock.now < deadline else { throw TeraNativeUploadExecution.unknown } + guard snapshot.identifier == identifier, + TeraBackgroundUploadRequest.persistedRequestMatches(snapshot.request, request: request), + observed == nil || observed?.executionID == snapshot.executionID + else { throw Self.failure( code: "ios.add.background_upload_mismatch", message: "The persisted photo upload no longer matches its request." ) } + observed = snapshot switch snapshot.state { case .awaitingVerification, .completed: try Task.checkCancellation() - guard let response = snapshot.response, - let statusCode = UInt16(exactly: response.statusCode), - let body = response.body - else { - throw Self.failure( - code: "ios.add.background_response_invalid", - message: "The photo service returned an invalid response." - ) - } - return TeraAddBackgroundUploadReceipt( - identifier: identifier.rawValue, - draftID: draftID, - expectedRevision: expectedRevision, - statusCode: statusCode, - mediaType: response.mediaType, - contentEncoding: response.contentEncoding, - body: body - ) + return try completedReceipt(snapshot, identity: identity) case .failed, .interrupted, .cancelled, .expired: throw Self.failure( code: snapshot.failure?.rawValue ?? "ios.add.background_upload_failed", message: TeraUserMessages.text(.backgroundTransferFailed) ) case .queued, .running: - try await Task.sleep(for: .milliseconds(100)) + try await clock.sleep(until: min(deadline, clock.now.advanced(by: .milliseconds(100)))) } } } + private static func completedReceipt(_ snapshot: RadrootsBackgroundTransferSnapshot, + identity: TeraNativeTransferIdentity) throws -> TeraAddBackgroundUploadReceipt + { + guard let response = snapshot.response, + let statusCode = UInt16(exactly: response.statusCode), let body = response.body + else { + throw failure(code: "ios.add.background_response_invalid", + message: "The photo service returned an invalid response.") + } + return TeraAddBackgroundUploadReceipt(identifier: snapshot.identifier.rawValue, + draftID: identity.draftID, expectedRevision: identity.revision, + statusCode: statusCode, mediaType: response.mediaType, + contentEncoding: response.contentEncoding, body: body) + } + private static func failure(code: String, message: String) -> TeraRuntimeFailure { .local(operation: "add.media.background", code: code, safeMessage: message) } diff --git a/Tera/Runtime/TeraNativeUploadExecution.swift b/Tera/Runtime/TeraNativeUploadExecution.swift @@ -0,0 +1,35 @@ +import Foundation +import RadrootsKit + +/// A lost admission reply is not permission to start a second native task. +enum TeraNativeUploadExecution { + static var unknown: TeraRuntimeFailure { + .local(operation: "add.media.background", code: "ios.add.background_upload_unknown", + safeMessage: "The photo upload outcome is not yet confirmed. Check recovery before trying again.") + } + + static func start(transfer: any RadrootsBackgroundTransfer, + request: RadrootsBackgroundTransferRequest, retrying: Bool) async throws -> RadrootsBackgroundTransferSnapshot + { + try Task.checkCancellation() + do { + if retrying { + _ = try await transfer.retry(request) + } else { + _ = try await transfer.enqueue(request) + } + } catch { + try Task.checkCancellation() + // A lost reply still requires the same exact query below; never retry. + } + // The producer queries OS tasks before returning reconciled snapshots. + // Carry the observed execution into receipt waiting across this await. + let snapshot = try await transfer.snapshot(for: request.identifier) + try Task.checkCancellation() + guard let snapshot, snapshot.identifier == request.identifier, + TeraBackgroundUploadRequest.persistedRequestMatches(snapshot.request, request: request), + [.queued, .running, .awaitingVerification, .completed].contains(snapshot.state) + else { throw unknown } + return snapshot + } +} diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 110571, "path": "source/aarch64-apple-darwin.json", - "sha256": "1abad8e6e2476030a8b26ce3b5c9d768b88db9bdd4bd2ada9bebce9688cf1d57" + "sha256": "92eb6f98257a130f3f7cc2e27780c83a04d8cbb8cbf202140b1377aa3a626ec1" }, { "bytes": 110415, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "050b58f6d15fbe2028dc5195f3fb8c1b93d47a6cead3ed84f7599a1afa2348f9" + "sha256": "3b7b1ab5731c7ea4cb3e0258b395fd6598c8511dc151fe3b4c86243644d61374" }, { "bytes": 110411, "path": "source/aarch64-apple-ios.json", - "sha256": "0aeaaf3f429f77ccb1e03a1ef6cf1fa2f7a5a23b59244ef579651e0a414c5d60" + "sha256": "986465f0e4bdf6b603368adf02ccdc78672459f0ca35504208d59b0773c7c6bb" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89" + "tree": "e3bf7db2c860042ad23669e90f684303d1763c4f" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 110571, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "1abad8e6e2476030a8b26ce3b5c9d768b88db9bdd4bd2ada9bebce9688cf1d57" + "sha256": "92eb6f98257a130f3f7cc2e27780c83a04d8cbb8cbf202140b1377aa3a626ec1" }, { "bytes": 110415, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "050b58f6d15fbe2028dc5195f3fb8c1b93d47a6cead3ed84f7599a1afa2348f9" + "sha256": "3b7b1ab5731c7ea4cb3e0258b395fd6598c8511dc151fe3b4c86243644d61374" }, { "bytes": 110411, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "0aeaaf3f429f77ccb1e03a1ef6cf1fa2f7a5a23b59244ef579651e0a414c5d60" + "sha256": "986465f0e4bdf6b603368adf02ccdc78672459f0ca35504208d59b0773c7c6bb" } ], "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 = "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89" -manifest_sha256 = "a17a258aa37a9344cda351e74734679b1ee590483eeadf8904887004f37601ed" +source_tree = "e3bf7db2c860042ad23669e90f684303d1763c4f" +manifest_sha256 = "eb8227eea16e0a9713c9418f93fbcfd6cd34e9964c1eff57f61bfbe776174b0c" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -2349,13 +2349,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 155816, - "git_blob": "e771ba846d3c220aa3dd2df3d508025d3c2cf6df", + "bytes": 157974, + "git_blob": "78e19633a10d89f6166671f15d1d6b45bc2739f7", "mode": "100644", - "sha256": "88ab33bd5eea18f3add396780f07f65edbcb493980e4c32c3dfc347bbf122dd7" + "sha256": "71d706a5efc39edad3c72c0e2a92346fddc15ee71a48ad0b87f1722eec73a577" } }, "policy": "staged_inputs", - "tree": "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89" + "tree": "e3bf7db2c860042ad23669e90f684303d1763c4f" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -2345,13 +2345,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 155816, - "git_blob": "e771ba846d3c220aa3dd2df3d508025d3c2cf6df", + "bytes": 157974, + "git_blob": "78e19633a10d89f6166671f15d1d6b45bc2739f7", "mode": "100644", - "sha256": "88ab33bd5eea18f3add396780f07f65edbcb493980e4c32c3dfc347bbf122dd7" + "sha256": "71d706a5efc39edad3c72c0e2a92346fddc15ee71a48ad0b87f1722eec73a577" } }, "policy": "staged_inputs", - "tree": "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89" + "tree": "e3bf7db2c860042ad23669e90f684303d1763c4f" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -2345,13 +2345,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 155816, - "git_blob": "e771ba846d3c220aa3dd2df3d508025d3c2cf6df", + "bytes": 157974, + "git_blob": "78e19633a10d89f6166671f15d1d6b45bc2739f7", "mode": "100644", - "sha256": "88ab33bd5eea18f3add396780f07f65edbcb493980e4c32c3dfc347bbf122dd7" + "sha256": "71d706a5efc39edad3c72c0e2a92346fddc15ee71a48ad0b87f1722eec73a577" } }, "policy": "staged_inputs", - "tree": "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89" + "tree": "e3bf7db2c860042ad23669e90f684303d1763c4f" } } diff --git a/TeraTests/TeraAddStoreTests.swift b/TeraTests/TeraAddStoreTests.swift @@ -28,7 +28,7 @@ final class TeraAddStoreTests: XCTestCase { media: fixture.media ) XCTAssertEqual(replayed.identifier, first.identifier) - XCTAssertEqual(replayed.expectedRevision, 3) + XCTAssertEqual(replayed.expectedRevision, 2) counts = await transfer.counts XCTAssertEqual(counts.enqueue, 1) diff --git a/TeraTests/TeraBackgroundUploadFixtures.swift b/TeraTests/TeraBackgroundUploadFixtures.swift @@ -47,6 +47,10 @@ final class BackgroundUploadFixture: @unchecked Sendable { roots.stagedBlobsRoot.appendingPathComponent(media.sha256) } + var nativeStore: RadrootsAppleBackgroundTransferStore { + RadrootsAppleBackgroundTransferStore(roots: roots) + } + func coordinator(transfer: any RadrootsBackgroundTransfer) -> TeraAddMediaCoordinator { TeraAddMediaCoordinator( roots: roots, diff --git a/TeraTests/TeraNativeExecutionFixture.swift b/TeraTests/TeraNativeExecutionFixture.swift @@ -0,0 +1,83 @@ +import Foundation +import RadrootsKit +@testable import TeraApp + +actor NativeExecutionTransfer: RadrootsBackgroundTransfer { + let base: BackgroundTransferHarness + let unknownInventory: Bool + let replaceExecution: Bool + private var reads = 0 + + init(base: BackgroundTransferHarness = BackgroundTransferHarness(), unknownInventory: Bool = false, + replaceExecution: Bool = false) + { + self.base = base + self.unknownInventory = unknownInventory + self.replaceExecution = replaceExecution + } + + func enqueue(_ request: RadrootsBackgroundTransferRequest) async throws -> RadrootsBackgroundTransferHandle { + _ = try await base.enqueue(request) + throw RadrootsBackgroundTransferError.transferFailure + } + + func retry(_ request: RadrootsBackgroundTransferRequest) async throws -> RadrootsBackgroundTransferHandle { + _ = try await base.retry(request) + throw RadrootsBackgroundTransferError.transferFailure + } + + func snapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> RadrootsBackgroundTransferSnapshot? { + if unknownInventory { + throw RadrootsBackgroundTransferError.transferFailure + } + reads += 1 + let value = try await base.snapshot(for: identifier) + guard replaceExecution, reads > 1, let value else { return value } + return try RadrootsBackgroundTransferSnapshot(request: value.request, state: value.state, executionID: UUID()) + } + + func snapshots() async throws -> [RadrootsBackgroundTransferSnapshot] { + if unknownInventory { + throw RadrootsBackgroundTransferError.transferFailure + } + return try await base.snapshots() + } + + func cancel(_ identifier: RadrootsBackgroundTransferIdentifier) async throws { + try await base.cancel(identifier) + } + + func expire(_ identifier: RadrootsBackgroundTransferIdentifier) async throws { + try await base.expire(identifier) + } + + func settle(_ identifier: RadrootsBackgroundTransferIdentifier, verification: RadrootsBackgroundTransferVerification) async throws { + try await base.settle(identifier, verification: verification) + } + + func handleEventsForBackgroundURLSession(identifier _: String, completionHandler: @escaping @Sendable () -> Void) async { + completionHandler() + } +} + +actor LostNativeAdmission { + private(set) var starts = 0 + private var active: Set<RadrootsBackgroundTransferIdentifier> = [] + + nonisolated var adapters: RadrootsAppleBackgroundTransferAdapters { + .init(enqueue: { request, _ in try await self.enqueue(request) }, + cancel: { _ in throw RadrootsBackgroundTransferError.transferFailure }, + activeTransferIdentifiers: { await self.identifiers() }, + handleBackgroundEvents: { _, completion in completion() }) + } + + func enqueue(_ request: RadrootsBackgroundTransferRequest) throws { + starts += 1 + active.insert(request.identifier) + throw RadrootsBackgroundTransferError.transferFailure + } + + func identifiers() -> Set<RadrootsBackgroundTransferIdentifier> { + active + } +} diff --git a/TeraTests/TeraNativeExecutionTests.swift b/TeraTests/TeraNativeExecutionTests.swift @@ -0,0 +1,135 @@ +import Foundation +import RadrootsKit +@testable import TeraApp +import XCTest + +final class TeraNativeExecutionTests: XCTestCase { + func testLostEnqueueReplyRejoinsExactReceiptWithoutAnotherEffect() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = NativeExecutionTransfer() + let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) + let receipt = try await fixture.coordinator(transfer: transfer).uploadInBackground(job: job, media: fixture.media) + XCTAssertEqual(receipt.identifier, job.transferIdentifier) + XCTAssertEqual(receipt.expectedRevision, 2) + let later = fixture.job(revision: 3, operation: String(repeating: "b", count: 32)) + let replay = try await fixture.coordinator(transfer: transfer).uploadInBackground(job: later, media: fixture.media) + XCTAssertEqual(replay, receipt) + let counts = await transfer.base.counts + XCTAssertEqual(counts.enqueue, 1) + XCTAssertEqual(counts.retry, 0) + XCTAssertEqual(counts.acceptedSettlement, 0) + } + + func testLostRetryReplyDoesNotScheduleAnotherRetry() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = NativeExecutionTransfer() + let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) + try await transfer.base.seed(request: fixture.request(job: job), state: .interrupted) + let receipt = try await fixture.coordinator(transfer: transfer).uploadInBackground(job: job, media: fixture.media) + XCTAssertEqual(receipt.identifier, job.transferIdentifier) + let counts = await transfer.base.counts + XCTAssertEqual(counts.retry, 1) + XCTAssertEqual(counts.enqueue, 0) + } + + func testUnknownInventoryDoesNotEnqueueRetryOrSelectForegroundFallback() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = NativeExecutionTransfer(unknownInventory: true) + let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) + try await transfer.base.seed(request: fixture.request(job: job), state: .running) + let before = try await transfer.base.snapshots() + let coordinator = fixture.coordinator(transfer: transfer) + do { + _ = try await coordinator.uploadInBackground(job: job, media: fixture.media) + XCTFail("Unknown inventory cannot admit an upload") + } catch let error as RadrootsBackgroundTransferError { XCTAssertEqual(error, .transferFailure) } + do { + _ = try await coordinator.prefersSharedForegroundUpload(ownerID: fixture.draftID) + XCTFail("Unknown native inventory cannot authorize foreground fallback") + } catch let error as RadrootsBackgroundTransferError { XCTAssertEqual(error, .transferFailure) } + let after = try await transfer.base.snapshots() + XCTAssertEqual(after, before) + let counts = await transfer.base.counts + XCTAssertEqual(counts.enqueue, 0) + XCTAssertEqual(counts.retry, 0) + XCTAssertEqual(counts.cancel, 0) + } + + func testBoundedObservationPreservesRunningTransferAndLaterOriginalReceipt() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = BackgroundTransferHarness() + let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))) + try await transfer.seed(request: request, state: .running) + let before = try await transfer.snapshots() + do { + _ = try await TeraBackgroundUploadWaiter.receipt(transfer: transfer, for: request.identifier, + draftID: fixture.draftID, expectedRevision: 3, request: request, waitNanoseconds: 1_000_000) + XCTFail("A still-running transfer must remain unknown after the observation budget") + } catch let failure as TeraRuntimeFailure { XCTAssertEqual(failure.code, "ios.add.background_upload_unknown") } + let after = try await transfer.snapshots() + XCTAssertEqual(after, before) + try await transfer.setState(.awaitingVerification) + let receipt = try await TeraBackgroundUploadWaiter.receipt(transfer: transfer, for: request.identifier, + draftID: fixture.draftID, expectedRevision: 3, request: request) + XCTAssertEqual(receipt.expectedRevision, 2) + let counts = await transfer.counts + XCTAssertEqual(counts.cancel, 0) + XCTAssertEqual(counts.retry, 0) + XCTAssertEqual(counts.enqueue, 0) + XCTAssertEqual(counts.acceptedSettlement, 0) + } + + func testObservationRejectsExecutionReplacementWithoutMutatingNativeState() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let request = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))) + for carryAdmission in [false, true] { + let transfer = NativeExecutionTransfer(replaceExecution: true) + try await transfer.base.seed(request: request, state: .running) + let baseline = carryAdmission ? try await transfer.snapshot(for: request.identifier) : nil + do { + _ = try await TeraBackgroundUploadWaiter.receipt(transfer: transfer, for: request.identifier, + draftID: fixture.draftID, expectedRevision: 2, request: request, baseline: baseline) + XCTFail("A replacement execution cannot supply this observation's receipt") + } catch let failure as TeraRuntimeFailure { XCTAssertEqual(failure.code, "ios.add.background_upload_mismatch") } + let counts = await transfer.base.counts + XCTAssertEqual(counts.cancel, 0) + XCTAssertEqual(counts.retry, 0) + XCTAssertEqual(counts.acceptedSettlement, 0) + } + } + + func testRealNativeAdmissionRefusesDuplicateAfterLostReplyAndRelaunch() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let platform = LostNativeAdmission() + let store = fixture.nativeStore + let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) + for _ in 0 ..< 2 { + let native = RadrootsAppleBackgroundTransfer(store: store, adapters: platform.adapters) + do { + _ = try await fixture.coordinator(transfer: native).uploadInBackground(job: job, media: fixture.media) + XCTFail("An active task with a lost response is unknown") + } catch let failure as TeraRuntimeFailure { XCTAssertEqual(failure.code, "ios.add.background_upload_unknown") } + } + let starts = await platform.starts + XCTAssertEqual(starts, 1) + let snapshots = try await store.loadSnapshots() + let retained = try XCTUnwrap(snapshots.first) + XCTAssertNotNil(retained.executionID) + XCTAssertTrue(retained.possibleRemoteOrphan) + let late = try RadrootsBackgroundTransferSnapshot(request: retained.request, state: .awaitingVerification, + response: RadrootsBackgroundTransferResponse(statusCode: 200, mediaType: "application/json", body: Data("{}".utf8)), + executionID: retained.executionID) + try await store.saveSnapshot(late) + let native = RadrootsAppleBackgroundTransfer(store: fixture.nativeStore, adapters: platform.adapters) + let receipt = try await fixture.coordinator(transfer: native).uploadInBackground(job: job, media: fixture.media) + XCTAssertEqual(receipt.identifier, job.transferIdentifier) + let after = await platform.starts + XCTAssertEqual(after, 1) + } +} diff --git a/project.yml b/project.yml @@ -13,7 +13,7 @@ packages: path: . RadrootsKit: url: https://github.com/radrootslabs/apple_kit.git - revision: bf6eff1ebb4c854983a651b333d00d622714199b + revision: 75a4a63149adb55b6a07f94686ef74b5446f1136 targets: Tera: @@ -57,6 +57,8 @@ targets: - path: TeraTests/TeraRecoveryCompletionTests.swift - path: TeraTests/TeraRecoverySettlementTests.swift - path: TeraTests/TeraNativeRepairTests.swift + - path: TeraTests/TeraNativeExecutionTests.swift + - path: TeraTests/TeraNativeExecutionFixture.swift - path: TeraTests/TeraNativeRepairPreviewTests.swift - path: TeraTests/TeraComposerMediaOwnershipTests.swift - path: TeraTests/TeraPublicationEvidenceTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "5e7329d4bfa6834b9256d792dc8ce7327a842a8806c7d0a7b6b048384dd3c020", - "ffi_provenance_sha256": "a17a258aa37a9344cda351e74734679b1ee590483eeadf8904887004f37601ed", + "ffi_provenance_sha256": "eb8227eea16e0a9713c9418f93fbcfd6cd34e9964c1eff57f61bfbe776174b0c", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", - "sbom_sha256": "3670c97ee6ffc2c2abd3249885c89bd7c20ec43fedcb66442501a2f42d74fc2a", - "xcode_project_sha256": "8f632137537b672d53b42eb8868933a188e2e0eb4b423032ea12237b26e58e15" + "sbom_sha256": "7670a647b443eca6cac6dd0b69e7ca335afdb3772204c4b91fe24920f651f027", + "xcode_project_sha256": "634a750ca283d93e3f82dcba798fd793bc0b7ffe6708cc5bf86916932826f09e" }, "disposition": "unsigned", "platforms": [ @@ -21,9 +21,9 @@ "consumer_source_lock_sha256": "f70d511d93a7a4395e632373222b84c2d62a6395a7586888abaa2e371bda0176", "lib_revision": "91006304a47ff335c6f36fbbcfcb68ecfc76f6c7", "source_date_epoch": 1787871027, - "swift_package_lock_sha256": "29d68f2242a7725bc38381520ec196803893bf689c8e6b848647e664d6262316", - "tera_ffi_source_tree": "3eb3cacbbf9c6bb51db451ef31d27071b2f79d89", - "xcode_package_lock_sha256": "c468f2b0469438387c519e1b2de424a118dfa9145ea07dcfd2bc5cc714ec8559" + "swift_package_lock_sha256": "03ae32145d887565c7feca38b99aa0f8ee4aee55bdfbe6f367bab0602ccca824", + "tera_ffi_source_tree": "e3bf7db2c860042ad23669e90f684303d1763c4f", + "xcode_package_lock_sha256": "a95880b5360e93d3702e6a8ad5668204a5466b29578079bf241de892e849f1a7" }, "version": "0.1.0-alpha" } diff --git a/release/sbom.cdx.json b/release/sbom.cdx.json @@ -7856,7 +7856,7 @@ "version": "1.0.23" }, { - "bom-ref": "swift:apple_kit@bf6eff1ebb4c854983a651b333d00d622714199b?source=https://github.com/radrootslabs/apple_kit.git", + "bom-ref": "swift:apple_kit@75a4a63149adb55b6a07f94686ef74b5446f1136?source=https://github.com/radrootslabs/apple_kit.git", "name": "apple_kit", "properties": [ { @@ -7869,11 +7869,11 @@ }, { "name": "radroots.package.revision", - "value": "bf6eff1ebb4c854983a651b333d00d622714199b" + "value": "75a4a63149adb55b6a07f94686ef74b5446f1136" } ], "type": "library", - "version": "bf6eff1ebb4c854983a651b333d00d622714199b" + "version": "75a4a63149adb55b6a07f94686ef74b5446f1136" }, { "bom-ref": "swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git", @@ -10767,14 +10767,14 @@ "cargo:tera_core@0.1.0-alpha?source=workspace", "cargo:tera_ffi@0.1.0-alpha?source=workspace", "cargo:tera_wasm@0.1.0-alpha?source=workspace", - "swift:apple_kit@bf6eff1ebb4c854983a651b333d00d622714199b?source=https://github.com/radrootslabs/apple_kit.git", + "swift:apple_kit@75a4a63149adb55b6a07f94686ef74b5446f1136?source=https://github.com/radrootslabs/apple_kit.git", "swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git" ], "ref": "pkg:generic/tera@0.1.0-alpha" }, { "dependsOn": [], - "ref": "swift:apple_kit@bf6eff1ebb4c854983a651b333d00d622714199b?source=https://github.com/radrootslabs/apple_kit.git" + "ref": "swift:apple_kit@75a4a63149adb55b6a07f94686ef74b5446f1136?source=https://github.com/radrootslabs/apple_kit.git" }, { "dependsOn": [], diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -179,6 +179,20 @@ { "path": "Tera/Runtime/TeraLifecycleCoordinator.swift", "count": 1 + }, + { + "path": "TeraTests/TeraNativeExecutionTests.swift", + "count": 2 + } + ] + }, + { + "identifier": "RadrootsAppleBackgroundTransferAdapters", + "category": "shared_apple_api", + "occurrences": [ + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 1 } ] }, @@ -201,6 +215,10 @@ "count": 1 }, { + "path": "TeraTests/TeraBackgroundUploadFixtures.swift", + "count": 2 + }, + { "path": "TeraTests/TeraMediaCleanupTests.swift", "count": 1 } @@ -647,6 +665,10 @@ "count": 1 }, { + "path": "Tera/Runtime/TeraNativeUploadExecution.swift", + "count": 1 + }, + { "path": "Tera/Runtime/TeraNativeUploadReconciliation.swift", "count": 2 }, @@ -657,6 +679,10 @@ { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", "count": 2 + }, + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 1 } ] }, @@ -671,6 +697,10 @@ { "path": "Tera/Runtime/TeraUserMessageClassifier.swift", "count": 2 + }, + { + "path": "TeraTests/TeraNativeExecutionTests.swift", + "count": 2 } ] }, @@ -707,6 +737,10 @@ "count": 3 }, { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 6 + }, + { "path": "TeraTests/TeraUserMessagesTests.swift", "count": 1 } @@ -729,6 +763,10 @@ { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", "count": 4 + }, + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 2 } ] }, @@ -738,7 +776,7 @@ "occurrences": [ { "path": "Tera/Runtime/TeraAddMediaCoordinator.swift", - "count": 2 + "count": 1 }, { "path": "Tera/Runtime/TeraBackgroundUploadRequest.swift", @@ -753,6 +791,10 @@ "count": 6 }, { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 6 + }, + { "path": "TeraTests/TeraNativeRepairTests.swift", "count": 1 }, @@ -785,8 +827,16 @@ "count": 2 }, { + "path": "Tera/Runtime/TeraNativeUploadExecution.swift", + "count": 1 + }, + { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", "count": 9 + }, + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 3 } ] }, @@ -807,6 +857,10 @@ { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", "count": 1 + }, + { + "path": "TeraTests/TeraNativeExecutionTests.swift", + "count": 1 } ] }, @@ -826,11 +880,11 @@ "occurrences": [ { "path": "Tera/Runtime/TeraAddMediaCoordinator.swift", - "count": 1 + "count": 2 }, { "path": "Tera/Runtime/TeraBackgroundUploadWaiter.swift", - "count": 1 + "count": 3 }, { "path": "Tera/Runtime/TeraMediaCleanup.swift", @@ -841,6 +895,10 @@ "count": 4 }, { + "path": "Tera/Runtime/TeraNativeUploadExecution.swift", + "count": 1 + }, + { "path": "Tera/Runtime/TeraNativeUploadReconciliation.swift", "count": 3 }, @@ -855,6 +913,14 @@ { "path": "TeraTests/TeraMediaCleanupTests.swift", "count": 1 + }, + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 3 + }, + { + "path": "TeraTests/TeraNativeExecutionTests.swift", + "count": 1 } ] }, @@ -899,6 +965,10 @@ { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", "count": 1 + }, + { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 1 } ] }, @@ -1379,6 +1449,10 @@ "count": 1 }, { + "path": "Tera/Runtime/TeraNativeUploadExecution.swift", + "count": 1 + }, + { "path": "Tera/Runtime/TeraNativeUploadReconciliation.swift", "count": 1 }, @@ -1455,6 +1529,14 @@ "count": 1 }, { + "path": "TeraTests/TeraNativeExecutionFixture.swift", + "count": 1 + }, + { + "path": "TeraTests/TeraNativeExecutionTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraNativeRepairTests.swift", "count": 1 },