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 dc9bfd80bf5fef1299be98e3ab2a4c6f9d9f1f9b
parent ba7ab538ebaabf2cdae1f42b8f27dac621bafb8b
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 13:34:23 +0000

recovery: settle already-verified native transfers

- Confirm exact durable Rust completion before native settlement
- Converge on duplicate settlement and matching lost-reply success
- Retain uncertain evidence without another upload or invented revision
- Verify recovery ordering and native integration across crash windows

Diffstat:
MTera.xcodeproj/project.pbxproj | 4++++
MTera/Runtime/TeraAddMediaCoordinator.swift | 34+++++++++++++++-------------------
MTera/Runtime/TeraNativeRecoveryInventory.swift | 7++-----
MTera/Runtime/TeraNativeUploadReconciliation.swift | 74++++++++++++++++++++++++++++++++------------------------------------------
MTera/Runtime/TeraRecoveryCompletion.swift | 5+++--
MTera/State/TeraSubmissionEffects.swift | 4++--
MTera/State/TeraSubmissionStore.swift | 2+-
MTeraFFI/provenance.json | 42+++++++++++++++++++++---------------------
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++++++++++----------
MTeraTests/TeraAddMediaHarness.swift | 2+-
MTeraTests/TeraAddStoreTests.swift | 35-----------------------------------
MTeraTests/TeraBackgroundUploadFixtures.swift | 13+++++++++++++
MTeraTests/TeraRecoveryInventoryTests.swift | 10+++++-----
ATeraTests/TeraRecoverySettlementTests.swift | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs | 25+++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs | 10++++++++--
Mproject.yml | 1+
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 12++++++++++--
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 2+-
23 files changed, 315 insertions(+), 173 deletions(-)

diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 3D219E759CFB685936EB7068 /* TeraCalendarEditingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C48B4B5442B1A404C1A328 /* TeraCalendarEditingTests.swift */; }; 3DED3C2547AC6F8D00913FE8 /* TeraStoppedUploadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D82A4781624F36870A6316 /* TeraStoppedUploadTests.swift */; }; 41ED082A17594FE682A381DD /* TeraSubmissionQualificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019FAA997BEDE9CC84785DAE /* TeraSubmissionQualificationSupport.swift */; }; + 45328FDB3E5F4715743FE7CD /* TeraRecoverySettlementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47E7B696B1BDDBF3B7FE838F /* TeraRecoverySettlementTests.swift */; }; 46ECA0EA5CE33D30BB5E08AF /* TeraOfflineMediaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649F88AE533C3BEEBC466894 /* TeraOfflineMediaTests.swift */; }; 4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */; }; 4B093458FAF4C6E4860E313A /* TeraComposerAutosaveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55E47B15C42F13ABF2DFF27 /* TeraComposerAutosaveTests.swift */; }; @@ -160,6 +161,7 @@ 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSessionResourceTests.swift; sourceTree = "<group>"; }; 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraAddObservationStartupTests.swift; sourceTree = "<group>"; }; 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraUserMessagesTests.swift; sourceTree = "<group>"; }; + 47E7B696B1BDDBF3B7FE838F /* TeraRecoverySettlementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRecoverySettlementTests.swift; sourceTree = "<group>"; }; 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellUITests.swift; sourceTree = "<group>"; }; 53CB51FEF4CE772320B3264C /* TeraTodayCacheFirstTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayCacheFirstTests.swift; sourceTree = "<group>"; }; 545A0222D8C5B735130C8A12 /* TeraSubmissionAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSubmissionAdmissionTests.swift; sourceTree = "<group>"; }; @@ -349,6 +351,7 @@ 3B954B7006CC5904C98138C6 /* TeraPublicationEvidenceTests.swift */, 156F07F1F2961FDF6539099A /* TeraRecoveryCompletionTests.swift */, 77F0FDC0D59BFCB5D7F928D5 /* TeraRecoveryInventoryTests.swift */, + 47E7B696B1BDDBF3B7FE838F /* TeraRecoverySettlementTests.swift */, E351F52E77C74A4E99095E45 /* TeraRemoteQualificationTests.swift */, 6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */, 8591841AD9AB9B718A37EB47 /* TeraRuntimeBackpressureTests.swift */, @@ -647,6 +650,7 @@ 093A6AC6C9B724E465507004 /* TeraPublicationEvidenceTests.swift in Sources */, 36AB69B9F8DB9AADB8124795 /* TeraRecoveryCompletionTests.swift in Sources */, 5DFD8053E97DED72B42BF528 /* TeraRecoveryInventoryTests.swift in Sources */, + 45328FDB3E5F4715743FE7CD /* TeraRecoverySettlementTests.swift in Sources */, 81AF9DDA0E64DC4EEFCEF25D /* TeraRemoteQualificationTests.swift in Sources */, 9AD968F0B37C64B5AF8D9A68 /* TeraRootShellTests.swift in Sources */, 2B50FA35AD753EFA15CF00CD /* TeraRuntimeBackpressureTests.swift in Sources */, diff --git a/Tera/Runtime/TeraAddMediaCoordinator.swift b/Tera/Runtime/TeraAddMediaCoordinator.swift @@ -31,10 +31,10 @@ protocol TeraAddMediaHandling: Sendable { media: TeraPreparedMedia ) async throws -> TeraAddBackgroundUploadReceipt func uploadInBackground(transfer: TeraNativeTransferJob, media: TeraPreparedMedia) async throws -> TeraAddBackgroundUploadReceipt - func reconcileBackgroundSubmissions(_ submissions: [TeraSubmissionStatus]) async throws + func reconcileBackgroundSubmissions(_ submissions: [TeraSubmissionStatus], client: TeraRuntimeClient) async throws func retainedSubmissionUpload(_ submission: TeraSubmissionStatus, media: TeraPreparedMedia) async throws -> TeraAddBackgroundUploadReceipt? func settleBackgroundUpload(identifier: String, accepted: Bool) async throws - func reconcileBackgroundUploads(drafts: [TeraDraftStatus]) async throws + func reconcileBackgroundUploads(drafts: [TeraDraftStatus], client: TeraRuntimeClient) async throws } extension TeraAddMediaHandling { @@ -63,7 +63,9 @@ extension TeraAddMediaHandling { ) } - func reconcileBackgroundSubmissions(_: [TeraSubmissionStatus]) async throws {} + func reconcileBackgroundSubmissions(_: [TeraSubmissionStatus], client: TeraRuntimeClient) async throws { + _ = try await recoverNativeUploads(client: client) + } func retainedSubmissionUpload(_: TeraSubmissionStatus, media _: TeraPreparedMedia) async throws -> TeraAddBackgroundUploadReceipt? { nil @@ -71,7 +73,9 @@ extension TeraAddMediaHandling { func settleBackgroundUpload(identifier _: String, accepted _: Bool) async throws {} - func reconcileBackgroundUploads(drafts _: [TeraDraftStatus]) async throws {} + func reconcileBackgroundUploads(drafts _: [TeraDraftStatus], client: TeraRuntimeClient) async throws { + _ = try await recoverNativeUploads(client: client) + } } actor TeraAddMediaCoordinator: TeraAddMediaHandling { @@ -289,26 +293,12 @@ actor TeraAddMediaCoordinator: TeraAddMediaHandling { } } - func reconcileBackgroundUploads(drafts: [TeraDraftStatus]) async throws { - try await reconcile(drafts.map(TeraNativeUploadRecoveryOwner.init(draft:))) - } - - func reconcileBackgroundSubmissions(_ submissions: [TeraSubmissionStatus]) async throws { - try await reconcile(submissions.map(TeraNativeUploadRecoveryOwner.init(submission:))) - } - func retainedSubmissionUpload(_ submission: TeraSubmissionStatus, media: TeraPreparedMedia) async throws -> TeraAddBackgroundUploadReceipt? { let mediaUse = try TeraMediaProcessUse.admit(root: roots.dataRoot) defer { withExtendedLifetime(mediaUse) {} } return try await TeraStoppedUploadRecovery.receipt(submission, media: media, transfer: transfer) } - private func reconcile(_ owners: [TeraNativeUploadRecoveryOwner]) async throws { - let mediaUse = try TeraMediaProcessUse.admit(root: roots.dataRoot) - defer { withExtendedLifetime(mediaUse) {} } - try await TeraNativeUploadReconciliation.reconcile(owners, transfer: transfer) - } - private func prepare(_ asset: RadrootsMediaAsset) async throws -> TeraPreparedMedia { let prepared = try await preparer.prepareImage( RadrootsAppleImagePreparationRequest(source: .file(asset.file)) @@ -345,9 +335,15 @@ extension TeraAddMediaCoordinator { let receipt = try await client.recoverNativeUpload(input, media: handle) try Task.checkCancellation() try receipt.confirm(input) - try await self.settleBackgroundUpload(identifier: snapshot.identifier.rawValue, accepted: true) + try await self.settleRecoveredUpload(snapshot, input: input, receipt: receipt) }, lookup: { key in try await client.recoveryUploadOwner(key: key) }) recoveryCursor = result.cursor return result.progress } + + private func settleRecoveredUpload(_ snapshot: RadrootsBackgroundTransferSnapshot, input: TeraRecoveryUploadReceipt, + receipt: TeraRecoveryCompletionReceipt) async throws + { + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: receipt, transfer: transfer) + } } diff --git a/Tera/Runtime/TeraNativeRecoveryInventory.swift b/Tera/Runtime/TeraNativeRecoveryInventory.swift @@ -25,11 +25,8 @@ enum TeraNativeRecoveryInventory { let owner = try await lookup(identity.draftID), owner.id == identity.draftID else { throw TeraComposerAcknowledgment.unconfirmed } try Task.checkCancellation() - if owner.media.contains(where: { $0.sha256 == snapshot.request.expectedSourceSHA256 && $0.remoteURL.map(owner.verifiedURLs.contains) == true }) { - try await TeraNativeUploadReconciliation.reconcile(snapshot, owner: owner, transfer: transfer) - } else { - try await complete(snapshot, owner) - } + // A presentation status never substitutes for exact durable Rust proof. + try await complete(snapshot, owner) } catch { try Task.checkCancellation() // Keep this receipt unchanged. One missing/ambiguous parent never diff --git a/Tera/Runtime/TeraNativeUploadReconciliation.swift b/Tera/Runtime/TeraNativeUploadReconciliation.swift @@ -1,53 +1,43 @@ import Foundation import RadrootsKit +/// Rust has already confirmed the exact attempt. Native settlement cannot +/// create or advance that completion, including after a lost native reply. enum TeraNativeUploadReconciliation { - static func reconcile(_ owners: [TeraNativeUploadRecoveryOwner], transfer: any RadrootsBackgroundTransfer) async throws { - var draftsByID: [String: TeraNativeUploadRecoveryOwner] = [:] - for draft in owners { - guard draftsByID.updateValue(draft, forKey: draft.id) == nil else { - throw failure( - code: "ios.add.background_draft_ambiguous", - message: "The persisted draft inventory is ambiguous." - ) - } + static func settle(_ expected: RadrootsBackgroundTransferSnapshot, input: TeraRecoveryUploadReceipt, + receipt: TeraRecoveryCompletionReceipt, transfer: any RadrootsBackgroundTransfer) async throws + { + try receipt.confirm(input) + guard input.response.identifier == expected.identifier.rawValue, + input.uploadURL == expected.request.remoteURL.absoluteString, + input.media.sha256 == expected.request.expectedSourceSHA256 else { throw TeraComposerAcknowledgment.unconfirmed } + let current = try await matchingSnapshot(expected, transfer: transfer) + try Task.checkCancellation() + if current.state == .completed { + return } - for snapshot in try await transfer.snapshots() - where snapshot.state == .awaitingVerification { + guard current.state == .awaitingVerification else { throw TeraComposerAcknowledgment.unconfirmed } + do { + try await transfer.settle(expected.identifier, verification: .accepted) + } catch { try Task.checkCancellation() - guard let identity = TeraBackgroundUploadRequest.transferIdentity(snapshot.identifier), - let draft = draftsByID[identity.draftID] - else { continue } - try await reconcile(snapshot, owner: draft, transfer: transfer) + // A storage or transport reply can be lost after its state is committed. + // Only matching durable native success makes this success-equivalent. + let recovered = try await matchingSnapshot(expected, transfer: transfer) + guard recovered.state == .completed else { throw error } + return } + let confirmed = try await matchingSnapshot(expected, transfer: transfer) + guard confirmed.state == .completed else { throw TeraComposerAcknowledgment.unconfirmed } } - static func reconcile( - _ snapshot: RadrootsBackgroundTransferSnapshot, owner draft: TeraNativeUploadRecoveryOwner, - transfer: any RadrootsBackgroundTransfer - ) async throws { - guard let identity = TeraBackgroundUploadRequest.transferIdentity(snapshot.identifier), - snapshot.state == .awaitingVerification, draft.id == identity.draftID, - draft.revision > identity.revision, - let media = draft.media.first(where: { - $0.sha256 == snapshot.request.expectedSourceSHA256 - && ($0.remoteURL == snapshot.request.remoteURL.absoluteString - || $0.remoteURL.flatMap { draft.uploadURLs[$0] } == snapshot.request.remoteURL.absoluteString) - }), - let canonicalURL = media.remoteURL, draft.verifiedURLs.contains(canonicalURL), - try TeraBackgroundUploadRequest.persistedRequestMatchesMedia( - snapshot.request, media: media, uploadURL: draft.uploadURLs[canonicalURL] - ) - else { - throw failure( - code: "ios.add.background_upload_mismatch", - message: "The persisted photo upload does not match its verified draft." - ) - } - try await transfer.settle(snapshot.identifier, verification: .accepted) - } - - private static func failure(code: String, message: String) -> TeraRuntimeFailure { - .local(operation: "add.media.background", code: code, safeMessage: message) + private static func matchingSnapshot(_ expected: RadrootsBackgroundTransferSnapshot, + transfer: any RadrootsBackgroundTransfer) async throws -> RadrootsBackgroundTransferSnapshot + { + guard let current = try await transfer.snapshot(for: expected.identifier), + current.identifier == expected.identifier, current.request == expected.request, + current.response == expected.response, current.executionID == expected.executionID, + current.uploadLease == expected.uploadLease else { throw TeraComposerAcknowledgment.unconfirmed } + return current } } diff --git a/Tera/Runtime/TeraRecoveryCompletion.swift b/Tera/Runtime/TeraRecoveryCompletion.swift @@ -17,7 +17,8 @@ struct TeraRecoveryUploadReceipt: Sendable { let media = owner.media.first(where: { $0.sha256 == hash }), owner.media.filter({ $0.sha256 == hash }).count == 1, let url = media.remoteURL, let uploadURL = owner.uploadURLs[url], - snapshot.request.remoteURL.absoluteString == uploadURL, + snapshot.request.remoteURL.absoluteString == uploadURL + || (owner.verifiedURLs.contains(url) && snapshot.request.remoteURL.absoluteString == url), try TeraBackgroundUploadRequest.persistedRequestMatchesMedia(snapshot.request, media: media, uploadURL: uploadURL), let response = snapshot.response, let status = UInt16(exactly: response.statusCode), let body = response.body, @@ -28,7 +29,7 @@ struct TeraRecoveryUploadReceipt: Sendable { parent = identity.draftID revision = identity.revision attempt = identity.attempt - self.uploadURL = uploadURL + self.uploadURL = snapshot.request.remoteURL.absoluteString self.media = media self.response = .init(identifier: snapshot.identifier.rawValue, draftID: parent, expectedRevision: revision, statusCode: status, mediaType: response.mediaType, contentEncoding: response.contentEncoding, body: body) diff --git a/Tera/State/TeraSubmissionEffects.swift b/Tera/State/TeraSubmissionEffects.swift @@ -18,7 +18,7 @@ struct TeraSubmissionEffects { } guard mayStart() else { return } if [.complete, .terminal, .cancelled].contains(initial.state) { - try await media?.reconcileBackgroundSubmissions([initial]) + try await media?.reconcileBackgroundSubmissions([initial], client: client) try ensure() return } @@ -31,7 +31,7 @@ struct TeraSubmissionEffects { current = try await client.advanceSubmission(request: current.request, expectedRevision: current.revision) try accept(current) } - try await media?.reconcileBackgroundSubmissions([current]) + try await media?.reconcileBackgroundSubmissions([current], client: client) try ensure() } diff --git a/Tera/State/TeraSubmissionStore.swift b/Tera/State/TeraSubmissionStore.swift @@ -305,7 +305,7 @@ private extension TeraSubmissionStore { ensure: { try self.ensureCurrent(requested) }, accept: { try self.accept($0, generation: requested) }).reconcileStopped(current) } else { - try await media?.reconcileBackgroundSubmissions([current]) + try await media?.reconcileBackgroundSubmissions([current], client: client) } } } diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76792384, + "bytes": 76794320, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "086b64f200a25ff680a5bc0fc1e23b6bdcff01d741e768b419dc1c1030f35a4e" + "sha256": "32e95513eb12e2cc26fe403e93f6ee0b72979f1cc760166d07a58e0ba0d62115" }, { "bytes": 84483, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76872752, + "bytes": 76873936, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "fca2e8192e18e4237206eb680811689d9004781dbdf79ae88af380f6169edef5" + "sha256": "8515a130c3b01645724b41982e0794b5c1f9181f1ba9ee7b52f8308be9771649" }, { "bytes": 51991, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 22105680, + "bytes": 22105344, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "15ec94f7ab9704b23e2522aa4fc39263424583a15d33466e4a3960a032be474b" + "sha256": "00394002b5a1c3cd9452b16d8390c4141461d1ab0e5da8b99f9c7ce12fe3713e" }, { - "bytes": 76792384, + "bytes": 76794320, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "086b64f200a25ff680a5bc0fc1e23b6bdcff01d741e768b419dc1c1030f35a4e" + "sha256": "32e95513eb12e2cc26fe403e93f6ee0b72979f1cc760166d07a58e0ba0d62115" }, { - "bytes": 76872752, + "bytes": 76873936, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "fca2e8192e18e4237206eb680811689d9004781dbdf79ae88af380f6169edef5" + "sha256": "8515a130c3b01645724b41982e0794b5c1f9181f1ba9ee7b52f8308be9771649" }, { "bytes": 109729, "path": "source/aarch64-apple-darwin.json", - "sha256": "7c5184084317b5e0824f4b1516739b4e12203c7d2a2c30875142ce3d24b7ce6f" + "sha256": "d19b5e29a0a56348983ebf6ffe7c202959801bc54cb5063da2485cd766929a16" }, { "bytes": 109573, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "777d3f14e77cb40459a11786ce763684f428bfc10ab43f9c292c70d6100dcb86" + "sha256": "0e0574f04f2c42b6256e2283607c9dbdd71103949cdc42aa07322c6a124d0c36" }, { "bytes": 109569, "path": "source/aarch64-apple-ios.json", - "sha256": "57ebaf14ca4d03d89d0b59951156a9e53c782dfb6d98245c2c7d6559c146fa9d" + "sha256": "5b6cbcfadb5bf91c73a345246712235d923dbf651ae789f92042e4e384590e2e" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "401ae5006ca81e09c46cbdb5594741f1c7acdcc1" + "tree": "d3d3d35df9b6df80241338fb60dcd58ce581d13d" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76792384, + "bytes": 76794320, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "086b64f200a25ff680a5bc0fc1e23b6bdcff01d741e768b419dc1c1030f35a4e" + "sha256": "32e95513eb12e2cc26fe403e93f6ee0b72979f1cc760166d07a58e0ba0d62115" }, { "bytes": 84483, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76872752, + "bytes": 76873936, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "fca2e8192e18e4237206eb680811689d9004781dbdf79ae88af380f6169edef5" + "sha256": "8515a130c3b01645724b41982e0794b5c1f9181f1ba9ee7b52f8308be9771649" }, { "bytes": 673213, @@ -176,17 +176,17 @@ { "bytes": 109729, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "7c5184084317b5e0824f4b1516739b4e12203c7d2a2c30875142ce3d24b7ce6f" + "sha256": "d19b5e29a0a56348983ebf6ffe7c202959801bc54cb5063da2485cd766929a16" }, { "bytes": 109573, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "777d3f14e77cb40459a11786ce763684f428bfc10ab43f9c292c70d6100dcb86" + "sha256": "0e0574f04f2c42b6256e2283607c9dbdd71103949cdc42aa07322c6a124d0c36" }, { "bytes": 109569, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "57ebaf14ca4d03d89d0b59951156a9e53c782dfb6d98245c2c7d6559c146fa9d" + "sha256": "5b6cbcfadb5bf91c73a345246712235d923dbf651ae789f92042e4e384590e2e" } ], "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 = "401ae5006ca81e09c46cbdb5594741f1c7acdcc1" -manifest_sha256 = "44640fb11d10a7ed2f9694ff3dffb4bdea18ca0f70454ad14299c52b805fe09e" +source_tree = "d3d3d35df9b6df80241338fb60dcd58ce581d13d" +manifest_sha256 = "0b8af7001fff4aee20c21d07d2c5a6ff3e87a75271538067c3df1d64208666a0" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -879,10 +879,10 @@ "sha256": "129522c664188effaacc3112f023cd3a5d044b574ab74e5e15917bae954de84f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs": { - "bytes": 5178, - "git_blob": "0a469075821f358dabca60e987f354ee6ed55554", + "bytes": 6022, + "git_blob": "e578dd3f37f65f1d7efbc0ce9d0dc6078ae9dea6", "mode": "100644", - "sha256": "433fcfe772e701908101667c97a5a24c717c87f629537e3144e62accea1ff45c" + "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, "core/crates/tera_core/src/runtime/product_surface/outbox/upload_attempt.rs": { "bytes": 3981, @@ -921,10 +921,10 @@ "sha256": "d718cb9f72314600735daa03770418ea1b1c708b91d1b9426283c68194626d79" }, "core/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs": { - "bytes": 4800, - "git_blob": "d28b3136555cd545c9cc7dc3e53c422adbf25628", + "bytes": 5020, + "git_blob": "bf6b1fa8b985e58d5cf20f244548a5d69f1d67a3", "mode": "100644", - "sha256": "bae90f2edff54447a8b968773af9a984fa17e41d23677793ae4a3e236563701c" + "sha256": "2faae5e5108a50bbf3ee95d6e35fad2245da992931b15a0d64a24997a766a522" }, "core/crates/tera_core/src/runtime/product_surface/recovery_inventory.rs": { "bytes": 7866, @@ -2331,13 +2331,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 154853, - "git_blob": "ac55310b77ffb1383e4984a8ec639585508f65c8", + "bytes": 155066, + "git_blob": "0ee737f5d39c5c2dd1b29637ce299a56e6aad304", "mode": "100644", - "sha256": "eeb6dd91ba3762dec2bfff4a64a87d57aa1451c94332071b74778fd14ba38700" + "sha256": "86247a22fc319fbd4a1caed60ad8472c94ae9039902c506e5c2ee448cca0b952" } }, "policy": "staged_inputs", - "tree": "401ae5006ca81e09c46cbdb5594741f1c7acdcc1" + "tree": "d3d3d35df9b6df80241338fb60dcd58ce581d13d" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -875,10 +875,10 @@ "sha256": "129522c664188effaacc3112f023cd3a5d044b574ab74e5e15917bae954de84f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs": { - "bytes": 5178, - "git_blob": "0a469075821f358dabca60e987f354ee6ed55554", + "bytes": 6022, + "git_blob": "e578dd3f37f65f1d7efbc0ce9d0dc6078ae9dea6", "mode": "100644", - "sha256": "433fcfe772e701908101667c97a5a24c717c87f629537e3144e62accea1ff45c" + "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, "core/crates/tera_core/src/runtime/product_surface/outbox/upload_attempt.rs": { "bytes": 3981, @@ -917,10 +917,10 @@ "sha256": "d718cb9f72314600735daa03770418ea1b1c708b91d1b9426283c68194626d79" }, "core/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs": { - "bytes": 4800, - "git_blob": "d28b3136555cd545c9cc7dc3e53c422adbf25628", + "bytes": 5020, + "git_blob": "bf6b1fa8b985e58d5cf20f244548a5d69f1d67a3", "mode": "100644", - "sha256": "bae90f2edff54447a8b968773af9a984fa17e41d23677793ae4a3e236563701c" + "sha256": "2faae5e5108a50bbf3ee95d6e35fad2245da992931b15a0d64a24997a766a522" }, "core/crates/tera_core/src/runtime/product_surface/recovery_inventory.rs": { "bytes": 7866, @@ -2327,13 +2327,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 154853, - "git_blob": "ac55310b77ffb1383e4984a8ec639585508f65c8", + "bytes": 155066, + "git_blob": "0ee737f5d39c5c2dd1b29637ce299a56e6aad304", "mode": "100644", - "sha256": "eeb6dd91ba3762dec2bfff4a64a87d57aa1451c94332071b74778fd14ba38700" + "sha256": "86247a22fc319fbd4a1caed60ad8472c94ae9039902c506e5c2ee448cca0b952" } }, "policy": "staged_inputs", - "tree": "401ae5006ca81e09c46cbdb5594741f1c7acdcc1" + "tree": "d3d3d35df9b6df80241338fb60dcd58ce581d13d" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -875,10 +875,10 @@ "sha256": "129522c664188effaacc3112f023cd3a5d044b574ab74e5e15917bae954de84f" }, "core/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs": { - "bytes": 5178, - "git_blob": "0a469075821f358dabca60e987f354ee6ed55554", + "bytes": 6022, + "git_blob": "e578dd3f37f65f1d7efbc0ce9d0dc6078ae9dea6", "mode": "100644", - "sha256": "433fcfe772e701908101667c97a5a24c717c87f629537e3144e62accea1ff45c" + "sha256": "f627f5c0461cdb233f58b24a25b2e9dc79c3f14c6418dec0e1ee05874a5284df" }, "core/crates/tera_core/src/runtime/product_surface/outbox/upload_attempt.rs": { "bytes": 3981, @@ -917,10 +917,10 @@ "sha256": "d718cb9f72314600735daa03770418ea1b1c708b91d1b9426283c68194626d79" }, "core/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs": { - "bytes": 4800, - "git_blob": "d28b3136555cd545c9cc7dc3e53c422adbf25628", + "bytes": 5020, + "git_blob": "bf6b1fa8b985e58d5cf20f244548a5d69f1d67a3", "mode": "100644", - "sha256": "bae90f2edff54447a8b968773af9a984fa17e41d23677793ae4a3e236563701c" + "sha256": "2faae5e5108a50bbf3ee95d6e35fad2245da992931b15a0d64a24997a766a522" }, "core/crates/tera_core/src/runtime/product_surface/recovery_inventory.rs": { "bytes": 7866, @@ -2327,13 +2327,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 154853, - "git_blob": "ac55310b77ffb1383e4984a8ec639585508f65c8", + "bytes": 155066, + "git_blob": "0ee737f5d39c5c2dd1b29637ce299a56e6aad304", "mode": "100644", - "sha256": "eeb6dd91ba3762dec2bfff4a64a87d57aa1451c94332071b74778fd14ba38700" + "sha256": "86247a22fc319fbd4a1caed60ad8472c94ae9039902c506e5c2ee448cca0b952" } }, "policy": "staged_inputs", - "tree": "401ae5006ca81e09c46cbdb5594741f1c7acdcc1" + "tree": "d3d3d35df9b6df80241338fb60dcd58ce581d13d" } } diff --git a/TeraTests/TeraAddMediaHarness.swift b/TeraTests/TeraAddMediaHarness.swift @@ -95,7 +95,7 @@ actor AddMediaHarness: TeraAddMediaHandling { return .init(visited: 0, remaining: 0, needsAttention: false) } - func reconcileBackgroundSubmissions(_: [TeraSubmissionStatus]) { + func reconcileBackgroundSubmissions(_: [TeraSubmissionStatus], client _: TeraRuntimeClient) { submissionReconciliations += 1 } diff --git a/TeraTests/TeraAddStoreTests.swift b/TeraTests/TeraAddStoreTests.swift @@ -191,41 +191,6 @@ final class TeraAddStoreTests: XCTestCase { XCTAssertEqual(counts.enqueue, 1) } - func testVerifiedRustDraftReconcilesAwaitingReceiptAfterRelaunch() async throws { - for legacyPath in [false, true] { - let fixture = try BackgroundUploadFixture() - defer { fixture.remove() } - let transfer = BackgroundTransferHarness() - let coordinator = fixture.coordinator(transfer: transfer) - let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) - try await transfer.seed(request: fixture.request(job: job, remoteURL: legacyPath ? job.remoteURL : nil), - state: .awaitingVerification) - - try await coordinator.reconcileBackgroundUploads( - drafts: [fixture.draft(revision: 3, stage: .verified)] - ) - - let counts = await transfer.counts - let state = await transfer.state - XCTAssertEqual(counts.acceptedSettlement, 1) - XCTAssertEqual(state, .completed) - } - } - - func testBackgroundReconciliationRejectsDuplicateDraftInventory() async throws { - let fixture = try BackgroundUploadFixture() - defer { fixture.remove() } - let coordinator = fixture.coordinator(transfer: BackgroundTransferHarness()) - let draft = fixture.draft(revision: 3, stage: .verified) - - do { - try await coordinator.reconcileBackgroundUploads(drafts: [draft, draft]) - XCTFail("expected duplicate draft rejection") - } catch let failure as TeraRuntimeFailure { - XCTAssertEqual(failure.code, "ios.add.background_draft_ambiguous") - } - } - @MainActor func testProductSurfaceSnapshotAndSchemaInventoryAreExact() async throws { XCTAssertEqual( diff --git a/TeraTests/TeraBackgroundUploadFixtures.swift b/TeraTests/TeraBackgroundUploadFixtures.swift @@ -168,6 +168,11 @@ actor BackgroundTransferHarness: RadrootsBackgroundTransfer { private(set) var cancelCount = 0 private(set) var acceptedSettlementCount = 0 private(set) var snapshotCount = 0 + private var settlementFailure: Bool? + + func failNextSettlement(afterWrite: Bool) { + settlementFailure = afterWrite + } init( enqueueState: RadrootsBackgroundTransferState = .awaitingVerification, @@ -269,8 +274,16 @@ actor BackgroundTransferHarness: RadrootsBackgroundTransfer { } switch verification { case .accepted: + let failure = settlementFailure + settlementFailure = nil + if failure == false { + throw RadrootsBackgroundTransferError.transferFailure + } acceptedSettlementCount += 1 values[identifier] = try snapshot(request: value.request, state: .completed) + if failure == true { + throw RadrootsBackgroundTransferError.transferFailure + } case let .rejected(failure): values[identifier] = try RadrootsBackgroundTransferSnapshot( request: value.request, diff --git a/TeraTests/TeraRecoveryInventoryTests.swift b/TeraTests/TeraRecoveryInventoryTests.swift @@ -59,7 +59,7 @@ final class TeraRecoveryInventoryTests: XCTestCase { var visited = 0 var passes = 0 repeat { - let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: cursor) { key in + let result = try await RecoverySettlementFixture.run(transfer: transfer, cursor: cursor) { key in await seen.append(key) if key == Self.key(1) { return nil @@ -83,7 +83,7 @@ final class TeraRecoveryInventoryTests: XCTestCase { XCTAssertEqual(counts.acceptedSettlement, 999) XCTAssertEqual(counts.enqueue, 0) XCTAssertEqual(counts.retry, 0) - let fresh = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { key in + let fresh = try await RecoverySettlementFixture.run(transfer: transfer, cursor: nil) { key in Self.owner(key: key, template: verified) } XCTAssertEqual(fresh.progress, .init(visited: 1, remaining: 0, needsAttention: false)) @@ -100,7 +100,7 @@ final class TeraRecoveryInventoryTests: XCTestCase { let pause = ResourceTestPause() let owner = TeraNativeUploadRecoveryOwner(draft: fixture.draft(revision: 3, stage: .verified)) let task = Task { - try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { _ in + try await RecoverySettlementFixture.run(transfer: transfer, cursor: nil) { _ in await pause.wait() return owner } @@ -111,7 +111,7 @@ final class TeraRecoveryInventoryTests: XCTestCase { do { _ = try await task.value; XCTFail("Cancelled pass must not settle") } catch is CancellationError {} let counts = await transfer.counts XCTAssertEqual(counts.acceptedSettlement, 0) - let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { _ in owner } + let result = try await RecoverySettlementFixture.run(transfer: transfer, cursor: nil) { _ in owner } XCTAssertEqual(result.progress, .init(visited: 1, remaining: 0, needsAttention: false)) } @@ -121,7 +121,7 @@ final class TeraRecoveryInventoryTests: XCTestCase { let transfer = BackgroundTransferHarness() try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))), state: .awaitingVerification) let owner = TeraNativeUploadRecoveryOwner(draft: fixture.draft(revision: 3, stage: .verified)) - let result = try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: nil) { _ in + let result = try await RecoverySettlementFixture.run(transfer: transfer, cursor: nil) { _ in Self.owner(key: Self.key(7), template: owner) } XCTAssertTrue(result.progress.needsAttention) diff --git a/TeraTests/TeraRecoverySettlementTests.swift b/TeraTests/TeraRecoverySettlementTests.swift @@ -0,0 +1,136 @@ +import Foundation +import RadrootsKit +@testable import TeraApp +import XCTest + +/// Fixture evidence only. Production obtains this receipt from Rust. +enum RecoverySettlementFixture { + static func receipt(_ input: TeraRecoveryUploadReceipt) -> TeraRecoveryCompletionReceipt { + .init(parent: input.parent, attempt: input.attempt, canonicalURL: input.media.remoteURL ?? "", + sha256: input.media.sha256, mediaType: input.media.mediaType, byteSize: input.media.byteSize, verifiedAtUnixMS: 1_800_000_000_000) + } + + static func run(transfer: BackgroundTransferHarness, cursor: String?, + lookup: @Sendable (String) async throws -> TeraNativeUploadRecoveryOwner?) async throws + -> (progress: TeraNativeRecoveryProgress, cursor: String?) + { + try await TeraNativeRecoveryInventory.run(transfer: transfer, cursor: cursor, complete: { snapshot, owner in + let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: owner) + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: receipt(input), transfer: transfer) + }, lookup: lookup) + } +} + +final class TeraRecoverySettlementTests: XCTestCase { + @MainActor + func testVerifiedRustDraftReconcilesAwaitingReceiptAfterRelaunch() async throws { + for legacyPath in [false, true] { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = BackgroundTransferHarness() + let coordinator = fixture.coordinator(transfer: transfer) + let job = fixture.job(revision: 2, operation: String(repeating: "a", count: 32)) + try await transfer.seed(request: fixture.request(job: job, remoteURL: legacyPath ? job.remoteURL : nil), + state: .awaitingVerification) + + let backend = try TeraScopeBackend() + let draft = fixture.draft(revision: 3, stage: .verified) + await backend.setDrafts([draft]) + await backend.setRecoveryCompletion { input, _ in + XCTAssertEqual(input.revision, 2) + XCTAssertEqual(input.uploadURL, legacyPath ? job.remoteURL : job.uploadURL) + return RecoverySettlementFixture.receipt(input) + } + let client = try await TeraScopeFixtures.client(backend) + try await coordinator.reconcileBackgroundUploads(drafts: [draft], client: client) + _ = try await client.stop() + + let counts = await transfer.counts + let state = await transfer.state + XCTAssertEqual(counts.acceptedSettlement, 1) + XCTAssertEqual(state, .completed) + } + } + + @MainActor + func testDuplicateDisplayHintsCannotDuplicateSettlement() async throws { + let fixture = try BackgroundUploadFixture() + defer { fixture.remove() } + let transfer = BackgroundTransferHarness() + let coordinator = fixture.coordinator(transfer: transfer) + let draft = fixture.draft(revision: 3, stage: .verified) + try await transfer.seed(request: fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32))), state: .awaitingVerification) + let backend = try TeraScopeBackend() + await backend.setDrafts([draft]) + await backend.setRecoveryCompletion { input, _ in RecoverySettlementFixture.receipt(input) } + let client = try await TeraScopeFixtures.client(backend) + try await coordinator.reconcileBackgroundUploads(drafts: [draft, draft], client: client) + let counts = await transfer.counts + XCTAssertEqual(counts.acceptedSettlement, 1) + _ = try await client.stop() + } + + func testDuplicateAndLostSettlementReplyConvergeWithoutAnotherWrite() async throws { + for lostReply in [false, true] { + 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: .awaitingVerification) + let value = try await transfer.snapshot(for: request.identifier) + let snapshot = try XCTUnwrap(value) + let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified))) + if lostReply { + await transfer.failNextSettlement(afterWrite: true) + } + for _ in 0 ..< 2 { + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer) + } + let counts = await transfer.counts + XCTAssertEqual(counts.acceptedSettlement, 1) + XCTAssertEqual(counts.enqueue, 0) + XCTAssertEqual(counts.retry, 0) + } + } + + func testUnconfirmedSettlementRetainsReceiptAndCanResume() 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: .awaitingVerification) + let value = try await transfer.snapshot(for: request.identifier) + let snapshot = try XCTUnwrap(value) + let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified))) + await transfer.failNextSettlement(afterWrite: false) + do { + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer) + XCTFail("A lost reply without completed native state cannot succeed") + } catch {} + let state = await transfer.state + XCTAssertEqual(state, .awaitingVerification) + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer) + let counts = await transfer.counts + XCTAssertEqual(counts.acceptedSettlement, 1) + } + + func testChangedNativeAssociationCannotSettle() 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: .awaitingVerification) + let value = try await transfer.snapshot(for: request.identifier) + let snapshot = try XCTUnwrap(value) + let input = try TeraRecoveryUploadReceipt(snapshot: snapshot, owner: .init(draft: fixture.draft(revision: 3, stage: .verified))) + let changed = try fixture.request(job: fixture.job(revision: 2, operation: String(repeating: "a", count: 32)), remoteURL: "http://127.0.0.1:3000/changed") + try await transfer.seed(request: changed, state: .awaitingVerification) + do { + try await TeraNativeUploadReconciliation.settle(snapshot, input: input, receipt: RecoverySettlementFixture.receipt(input), transfer: transfer) + XCTFail("Changed request must retain evidence") + } catch {} + let counts = await transfer.counts + XCTAssertEqual(counts.acceptedSettlement, 0) + XCTAssertEqual(counts.cancel, 0) + } +} diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs b/core/crates/tera_core/src/runtime/product_surface/outbox/tests/recovery_completion_tests.rs @@ -107,6 +107,19 @@ async fn legacy_recovery_completion_binds_saved_attempt_and_replays_without_curr }; let source = || RecoveryMedia::new("media:legacy".into(), bytes.clone(), kind.clone(), 2, 2).unwrap(); + let canonical_native = || { + let mut value = native(); + value.identity.upload_url = descriptor.url().as_str().into(); + value + }; + // Compatibility with a historical canonical destination is evidence-only; + // it cannot authorize the first durable completion. + assert!( + runtime + .recover_native_upload(canonical_native(), source()) + .await + .is_err() + ); let remote = bytes.clone(); let server = tokio::spawn(async move { let (mut stream, _) = listener.accept().await.unwrap(); @@ -131,6 +144,11 @@ async fn legacy_recovery_completion_binds_saved_attempt_and_replays_without_curr .unwrap(); server.await.unwrap(); let committed = runtime.phase1_draft_status([91; 16]).await.unwrap(); + for invalid_time in [0, u64::MAX] { + let mut corrupted = committed.media()[0].clone(); + corrupted.verified_at_unix_ms = Some(invalid_time); + assert!(RecoveryCompletionReceipt::durable(&native(), &corrupted).is_err()); + } runtime .configure_blossom( radroots_sdk::transport::BlossomHostKind::Simulator, @@ -148,6 +166,13 @@ async fn legacy_recovery_completion_binds_saved_attempt_and_replays_without_curr proof ); assert_eq!( + runtime + .recover_native_upload(canonical_native(), source()) + .await + .unwrap(), + proof + ); + assert_eq!( runtime.phase1_draft_status([91; 16]).await.unwrap(), committed ); diff --git a/core/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs b/core/crates/tera_core/src/runtime/product_surface/recovery_completion/validation.rs @@ -50,7 +50,9 @@ pub(in crate::runtime::product_surface) fn admit( return Err(E::InvalidMedia); } let url = BlobUrl::parse(current.url()).map_err(|_| E::InvalidMedia)?; - if url.upload_url() != native.identity.upload_url { + if url.upload_url() != native.identity.upload_url + && !(current.is_remote_verified() && url.as_str() == native.identity.upload_url) + { return Err(E::InvalidMedia); } let (status, media_type, encoding, body) = native.response.parts(); @@ -113,6 +115,10 @@ impl RecoveryCompletionReceipt { if !media.is_remote_verified() || media.recovery_attempt()? != native.identity.attempt { return Err(E::InvalidMedia); } + let verified_at_unix_ms = media + .verified_at_unix_ms() + .filter(|value| (1..=i64::MAX as u64).contains(value)) + .ok_or(E::Corrupt)?; Ok(Self { parent: *native.identity.parent.as_bytes(), attempt: *native.identity.attempt.as_bytes(), @@ -120,7 +126,7 @@ impl RecoveryCompletionReceipt { sha256: media.sha256().into(), media_type: media.media_type().into(), byte_size: media.byte_size(), - verified_at_unix_ms: media.verified_at_unix_ms().ok_or(E::Corrupt)?, + verified_at_unix_ms, }) } } diff --git a/project.yml b/project.yml @@ -55,6 +55,7 @@ targets: - path: TeraTests/TeraMediaCleanupTests.swift - path: TeraTests/TeraRecoveryInventoryTests.swift - path: TeraTests/TeraRecoveryCompletionTests.swift + - path: TeraTests/TeraRecoverySettlementTests.swift - path: TeraTests/TeraComposerMediaOwnershipTests.swift - path: TeraTests/TeraPublicationEvidenceTests.swift - path: TeraTests/TeraStoppedUploadTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "327b453cd291a8da46205a9bdc13871e29cb9dd97327a48d3e8084303f6d5c96", - "ffi_provenance_sha256": "44640fb11d10a7ed2f9694ff3dffb4bdea18ca0f70454ad14299c52b805fe09e", + "ffi_provenance_sha256": "0b8af7001fff4aee20c21d07d2c5a6ff3e87a75271538067c3df1d64208666a0", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "3670c97ee6ffc2c2abd3249885c89bd7c20ec43fedcb66442501a2f42d74fc2a", - "xcode_project_sha256": "0d4052671b0b87fae4798154bab14f64d86bd9e2b9c3282bb6123f81498a1c3e" + "xcode_project_sha256": "e233947465df921a07ec804e1195bcce34e18eb039cd92e9fe062a49fb7625cf" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "91006304a47ff335c6f36fbbcfcb68ecfc76f6c7", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "29d68f2242a7725bc38381520ec196803893bf689c8e6b848647e664d6262316", - "tera_ffi_source_tree": "401ae5006ca81e09c46cbdb5594741f1c7acdcc1", + "tera_ffi_source_tree": "d3d3d35df9b6df80241338fb60dcd58ce581d13d", "xcode_package_lock_sha256": "c468f2b0469438387c519e1b2de424a118dfa9145ea07dcfd2bc5cc714ec8559" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -696,7 +696,7 @@ "occurrences": [ { "path": "TeraTests/TeraBackgroundUploadFixtures.swift", - "count": 1 + "count": 3 }, { "path": "TeraTests/TeraUserMessagesTests.swift", @@ -813,6 +813,10 @@ "category": "shared_apple_api", "occurrences": [ { + "path": "Tera/Runtime/TeraAddMediaCoordinator.swift", + "count": 1 + }, + { "path": "Tera/Runtime/TeraBackgroundUploadWaiter.swift", "count": 1 }, @@ -826,7 +830,7 @@ }, { "path": "Tera/Runtime/TeraNativeUploadReconciliation.swift", - "count": 1 + "count": 3 }, { "path": "Tera/Runtime/TeraRecoveryCompletion.swift", @@ -1447,6 +1451,10 @@ "count": 1 }, { + "path": "TeraTests/TeraRecoverySettlementTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraRemoteQualificationTests.swift", "count": 1 }, diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"text":" init(","violation":{"severity":"warning","location":{"character":3,"line":21,"file":"Tera/App/TeraAppModel.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"severity":"warning","location":{"character":1,"line":55,"file":"Tera/Runtime/TeraAddMediaCoordinator.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 321 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"#endif","violation":{"severity":"warning","location":{"character":1,"line":456,"file":"Tera/Runtime/TeraAddMediaCoordinator.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 456","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"severity":"error","location":{"character":7,"line":31,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 413 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1188,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1188","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraRuntimeClient {","violation":{"severity":"error","location":{"character":1,"line":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 698 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"severity":"warning","location":{"character":3,"line":380,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":776,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 776","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1145,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1145","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"severity":"error","location":{"character":7,"line":4,"file":"Tera/State/TeraAddStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 524 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":711,"file":"Tera/State/TeraAddStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 711","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraConfigurationStore {","violation":{"severity":"error","location":{"character":1,"line":78,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"severity":"error","location":{"character":5,"line":148,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":759,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraSessionStore {","violation":{"severity":"error","location":{"character":1,"line":39,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"severity":"warning","location":{"character":11,"line":125,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","ruleDescription":"Complexity of function bodies should be limited."}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":452,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraAddView: View {","violation":{"severity":"warning","location":{"character":1,"line":4,"file":"Tera/Views/TeraAddView.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"struct TeraSettingsView: View {","violation":{"severity":"warning","location":{"character":1,"line":365,"file":"Tera/Views/TeraSupportingViews.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":629,"file":"Tera/Views/TeraSupportingViews.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":6,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 637 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"actor AddBackend: TeraRuntimeBackend {","violation":{"severity":"error","location":{"character":1,"line":717,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func makeStatus(","violation":{"severity":"warning","location":{"character":11,"line":1118,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Function Parameter Count","ruleIdentifier":"function_parameter_count","reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low."}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1330,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1330","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"severity":"warning","location":{"character":7,"line":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"severity":"warning","location":{"character":3,"line":45,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"severity":"warning","location":{"character":3,"line":212,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"severity":"warning","location":{"character":7,"line":4,"file":"TeraTests/TeraRuntimeClientTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":6,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"severity":"warning","location":{"character":5,"line":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":622,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 622","ruleDescription":"Files should not span too many lines."}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"severity":"warning","location":{"character":9,"line":207,"file":"TeraTests/TeraSupportingStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":486,"file":"TeraTests/TeraSupportingStoreTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 486","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":4,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"severity":"warning","location":{"character":3,"line":62,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 65 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"severity":"warning","location":{"character":3,"line":166,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"severity":"warning","location":{"character":3,"line":239,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"severity":"warning","location":{"character":11,"line":566,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" static func environment(","violation":{"severity":"warning","location":{"character":10,"line":1601,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":2077,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 2077","ruleDescription":"Files should not span too many lines."}}] +[{"text":" init(","violation":{"severity":"warning","location":{"character":3,"line":21,"file":"Tera/App/TeraAppModel.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"severity":"warning","location":{"character":1,"line":55,"file":"Tera/Runtime/TeraAddMediaCoordinator.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 321 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"#endif","violation":{"severity":"warning","location":{"character":1,"line":456,"file":"Tera/Runtime/TeraAddMediaCoordinator.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 456","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"severity":"error","location":{"character":7,"line":31,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 413 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1188,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1188","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraRuntimeClient {","violation":{"severity":"error","location":{"character":1,"line":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 698 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {","violation":{"severity":"warning","location":{"character":3,"line":380,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 70 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":776,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 776","ruleDescription":"Files should not span too many lines."}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1145,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1145","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStore: ObservableObject {","violation":{"severity":"error","location":{"character":7,"line":4,"file":"Tera/State/TeraAddStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 524 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":711,"file":"Tera/State/TeraAddStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 711","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraConfigurationStore {","violation":{"severity":"error","location":{"character":1,"line":78,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func load() throws -> TeraAppConfiguration {","violation":{"severity":"error","location":{"character":5,"line":148,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":759,"file":"Tera/State/TeraConfigurationStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines."}},{"text":"actor TeraSessionStore {","violation":{"severity":"error","location":{"character":1,"line":39,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"severity":"warning","location":{"character":11,"line":125,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","ruleDescription":"Complexity of function bodies should be limited."}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":452,"file":"Tera/State/TeraSessionStore.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 452","ruleDescription":"Files should not span too many lines."}},{"text":"struct TeraAddView: View {","violation":{"severity":"warning","location":{"character":1,"line":4,"file":"Tera/Views/TeraAddView.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"struct TeraSettingsView: View {","violation":{"severity":"warning","location":{"character":1,"line":365,"file":"Tera/Views/TeraSupportingViews.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":629,"file":"Tera/Views/TeraSupportingViews.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":6,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 607 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"actor AddBackend: TeraRuntimeBackend {","violation":{"severity":"error","location":{"character":1,"line":717,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" private func makeStatus(","violation":{"severity":"warning","location":{"character":11,"line":1118,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"Function Parameter Count","ruleIdentifier":"function_parameter_count","reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low."}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":1295,"file":"TeraTests/TeraAddStoreTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1295","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"severity":"warning","location":{"character":7,"line":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"severity":"warning","location":{"character":3,"line":45,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"severity":"warning","location":{"character":3,"line":212,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"severity":"warning","location":{"character":7,"line":4,"file":"TeraTests/TeraRuntimeClientTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":6,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"severity":"warning","location":{"character":5,"line":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":622,"file":"TeraTests/TeraStateMigrationTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 622","ruleDescription":"Files should not span too many lines."}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"severity":"warning","location":{"character":9,"line":207,"file":"TeraTests/TeraSupportingStoreTests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"}","violation":{"severity":"warning","location":{"character":1,"line":486,"file":"TeraTests/TeraSupportingStoreTests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 486","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"severity":"error","location":{"character":7,"line":4,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Type Body Length","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"severity":"warning","location":{"character":3,"line":62,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 65 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"severity":"warning","location":{"character":3,"line":166,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"severity":"warning","location":{"character":3,"line":239,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"severity":"warning","location":{"character":11,"line":566,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited."}},{"text":" static func environment(","violation":{"severity":"warning","location":{"character":10,"line":1601,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":"}","violation":{"severity":"error","location":{"character":1,"line":2077,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"ruleName":"File Length","ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 2077","ruleDescription":"Files should not span too many lines."}}]