TeraSubmissionOperationFFITests.swift (12413B)
1 import Foundation 2 @testable import TeraApp 3 import TeraKitBindings 4 import XCTest 5 6 final class TeraSubmissionOperationFFITests: XCTestCase { 7 private let scope = TeraComposerScope( 8 authorPublicKey: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", localNetworkID: "nearby" 9 ) 10 11 @MainActor 12 func testNewerEditingSavesAcrossBlockedAndFailedMediaPreparationWithoutChangingReservedSource() async throws { 13 let fixture = try MediaOwnershipFixture() 14 defer { fixture.remove() } 15 let client = TeraRuntimeClient.production() 16 let configuration = configuration(fixture, signer: ComposerForbiddenSigner()) 17 let snapshot = try await client.start(configuration: configuration) 18 let pause = ResourceTestPause() 19 let media = AddMediaHarness(openPause: pause, failOpen: true) 20 let store = TeraAddStore(runtimeClient: client, media: media) 21 store.configure(snapshot: snapshot) 22 await store.start() 23 store.selectType(.createPhotoUpdate) 24 store.updateForm(\.content, "original reserved photo") 25 store.updateForm(\.media, [fixture.media]) 26 let submit = Task { await store.submit() } 27 await pause.entered.wait() 28 let request = try XCTUnwrap(store.submissions.request) 29 let reserved = try await client.reserveSubmission(request: request) 30 XCTAssertEqual(reserved.captured.form.content, "original reserved photo") 31 store.updateForm(\.content, "editing during blocked preparation") 32 XCTAssertTrue(store.canSave) 33 await store.save() 34 XCTAssertEqual(store.savedComposer?.form.content, "editing during blocked preparation") 35 await pause.resume.open() 36 await submit.value 37 XCTAssertEqual(store.submissions.request, request) 38 XCTAssertNil(store.submissions.status) 39 store.updateForm(\.content, "editing after preparation failed") 40 XCTAssertTrue(store.canSave) 41 await store.save() 42 let editing = try XCTUnwrap(store.savedComposer) 43 XCTAssertNotEqual(editing.id, request.composerID) 44 XCTAssertEqual(editing.form.content, "editing after preparation failed") 45 let original = try await client.loadComposer(scope: request.scope, id: request.composerID) 46 XCTAssertEqual(original, reserved.captured) 47 store.stop() 48 _ = try await client.stop() 49 _ = try await client.start(configuration: configuration) 50 let restored = try await client.loadComposer(scope: editing.scope, id: editing.id) 51 let replay = try await client.reserveSubmission(request: request) 52 XCTAssertEqual(restored, editing) 53 XCTAssertEqual(restored.form.content, "editing after preparation failed") 54 XCTAssertEqual(replay.captured, reserved.captured) 55 XCTAssertTrue(replay.replayed) 56 // The original source head still satisfies the production prepare CAS. 57 let file = try fixture.original() 58 defer { try? file.close() } 59 let handle = try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: UInt64(file.fileDescriptor)) 60 let committed = try await client.prepareSubmission(request: request, media: [handle]) 61 XCTAssertEqual(committed.captured, reserved.captured) 62 XCTAssertEqual(committed.request, request) 63 _ = try await client.stop() 64 } 65 66 func testAllFiveProductionCapturesReplayAcrossEditingAndSQLiteReconstruction() async throws { 67 let fixture = try MediaOwnershipFixture() 68 defer { fixture.remove() } 69 let runtime = try await fixture.runtime() 70 try await runtime.configureSimulatorRelays(loopbackRelays: ["ws://127.0.0.1:19999"]) 71 let backend = TeraGeneratedRuntimeBackend(runtime: runtime) 72 let file = try fixture.original() 73 defer { try? file.close() } 74 let media = try TeraPreparedMediaHandle(media: fixture.media, fileDescriptor: UInt64(file.fileDescriptor)) 75 var committed: [TeraSubmissionStatus] = [] 76 for type in TeraAddCommandType.allCases { 77 let form = form(type, media: fixture.media) 78 let id = try await backend.reserveComposerID() 79 let saved = try await backend.saveComposer(request: TeraComposerSaveRequest( 80 scope: scope, id: id, expectedRevision: nil, editSequence: 1, form: form 81 )) 82 let request = try await TeraSubmissionRequest(commandID: backend.reserveSubmissionID(), 83 scope: scope, composerID: id, expectedRevision: saved.draft.revision) 84 let handles = type == .createPhotoUpdate ? [media] : [] 85 async let left = backend.prepareSubmission(request: request, media: handles) 86 async let right = backend.prepareSubmission(request: request, media: handles) 87 let (first, duplicate) = try await (left, right) 88 XCTAssertEqual(first, duplicate) 89 XCTAssertEqual(first.captured, saved.draft) 90 XCTAssertEqual(first.state, type == .createPhotoUpdate ? .mediaPreparing : .readyToSign) 91 XCTAssertEqual(first.settlement.signed, 0) 92 var editing = form 93 editing.content = "newer incomplete editing" 94 _ = try await backend.saveComposer(request: TeraComposerSaveRequest( 95 scope: scope, id: id, expectedRevision: 1, editSequence: 2, form: editing 96 )) 97 let replay = try await backend.prepareSubmission(request: request, media: []) 98 XCTAssertEqual(replay, first, "Committed replay must not reopen local media") 99 committed.append(first) 100 } 101 let legacy = try await backend.draftHeads(limit: 100) 102 XCTAssertTrue(legacy.isEmpty) 103 let page = try await backend.listSubmissions(scope: scope, limit: 100, cursor: nil) 104 XCTAssertEqual(page.entries.count, 5) 105 XCTAssertEqual(Set(committed.map(\.operationID)).count, 5) 106 _ = try await runtime.shutdown() 107 try await assertReconstruction(fixture, committed: committed) 108 } 109 110 func testProductionClientRetainsOriginalQueuedStatusAfterUnavailableSignerAndRestart() async throws { 111 let fixture = try MediaOwnershipFixture() 112 defer { fixture.remove() } 113 let signer = ComposerForbiddenSigner() 114 let client = TeraRuntimeClient.production() 115 let configuration = configuration(fixture, signer: signer) 116 _ = try await client.start(configuration: configuration) 117 let source = try await client.saveComposer(request: TeraComposerSaveRequest( 118 scope: scope, id: client.reserveComposerID(), expectedRevision: nil, editSequence: 1, 119 form: form(.createUpdate, media: fixture.media) 120 )) 121 let request = try await TeraSubmissionRequest(commandID: client.reserveSubmissionID(), 122 scope: scope, composerID: source.draft.id, expectedRevision: 1) 123 let prepared = try await client.prepareSubmission(request: request, media: []) 124 do { 125 _ = try await client.advanceSubmission(request: request, expectedRevision: prepared.revision) 126 XCTFail("The fixture refuses every signing request") 127 } catch {} 128 let queued = try await client.submissionStatus(request: request) 129 XCTAssertEqual(queued.operationID, prepared.operationID) 130 XCTAssertEqual(queued.captured, prepared.captured) 131 XCTAssertEqual(queued.settlement.signed, 0) 132 _ = try await client.stop() 133 _ = try await client.start(configuration: configuration) 134 let replay = try await client.recoverSubmission(request: request) 135 XCTAssertEqual(replay, queued) 136 _ = try await client.stop() 137 } 138 139 func testGeneratedOperationRejectsWrongCaptureRequestSchemaAndSettlementVersion() async throws { 140 let fixture = try MediaOwnershipFixture() 141 defer { fixture.remove() } 142 let runtime = try await fixture.runtime() 143 try await runtime.configureSimulatorRelays(loopbackRelays: ["ws://127.0.0.1:19999"]) 144 let source = try TeraComposerSaveRequest(scope: scope, id: composerReserveId().id, 145 expectedRevision: nil, editSequence: 1, form: form(.createUpdate, media: fixture.media)) 146 _ = try await runtime.composerSave(request: source.generatedValue) 147 let request = try TeraSubmissionRequest(commandID: submissionReserveId().id, 148 scope: scope, composerID: source.id, expectedRevision: 1) 149 let wire = try await runtime.submissionPrepare(request: request.generatedValue, media: []) 150 _ = try TeraGeneratedSubmission.operation(wire, expected: request) 151 var wrong = wire 152 wrong.captured.revision = 2 153 XCTAssertThrowsError(try TeraGeneratedSubmission.operation(wrong, expected: request)) 154 wrong = wire 155 wrong.request.commandId = String(repeating: "b", count: 32) 156 XCTAssertThrowsError(try TeraGeneratedSubmission.operation(wrong, expected: request)) 157 wrong = wire 158 wrong.schemaVersion = 2 159 XCTAssertThrowsError(try TeraGeneratedSubmission.operation(wrong, expected: request)) 160 wrong = wire 161 wrong.settlement.schemaVersion = 2 162 XCTAssertThrowsError(try TeraGeneratedSubmission.operation(wrong, expected: request)) 163 _ = try await runtime.shutdown() 164 } 165 166 private func assertReconstruction(_ fixture: MediaOwnershipFixture, committed: [TeraSubmissionStatus]) async throws { 167 let reopened = try await fixture.runtime() 168 try await reopened.configureSimulatorRelays(loopbackRelays: ["ws://127.0.0.1:19998"]) 169 let recovered = TeraGeneratedRuntimeBackend(runtime: reopened) 170 for original in committed { 171 let recoveredStatus = try await recovered.recoverSubmission(request: original.request) 172 let status = try XCTUnwrap(recoveredStatus) 173 XCTAssertEqual(status.request, original.request) 174 XCTAssertEqual(status.operationID, original.operationID) 175 XCTAssertEqual(status.intentID, original.intentID) 176 XCTAssertEqual(status.captured, original.captured) 177 XCTAssertEqual(status.revision, original.revision) 178 XCTAssertEqual(status.media, original.media) 179 XCTAssertEqual(status.committedAtUnixMilliseconds, original.committedAtUnixMilliseconds) 180 XCTAssertTrue(status.delivery.isStopped) 181 XCTAssertEqual(status.state, .cancelled) 182 XCTAssertEqual(status.settlement.signed, 0) 183 try await reopened.configureSimulatorRelays(loopbackRelays: ["ws://127.0.0.1:19999", "ws://127.0.0.1:19998"]) 184 let afterReaddition = try await recovered.recoverSubmission(request: original.request) 185 XCTAssertEqual(afterReaddition, status) 186 do { 187 _ = try await recovered.advanceSubmission(request: original.request, expectedRevision: status.revision) 188 XCTFail("Re-adding a removed relay must not revive stopped work") 189 } catch {} 190 let editing = try await recovered.loadComposer(scope: scope, id: original.captured.id) 191 XCTAssertEqual(editing.form.content, "newer incomplete editing") 192 XCTAssertEqual(editing.revision, 2) 193 } 194 _ = try await reopened.shutdown() 195 } 196 197 private func form(_ type: TeraAddCommandType, media: TeraPreparedMedia) -> TeraComposerForm { 198 var value = TeraAddForm.empty(type) 199 value.content = "PRIVATE captured publication" 200 switch type { 201 case .createUpdate, .createAsk: break 202 case .createPhotoUpdate: value.media = [media] 203 case .createEvent: 204 value.identifier = "fixture-market" 205 value.title = "Market" 206 value.location = "Town square" 207 value.eventTiming = .allDay 208 value.eventStartDate = "2026-10-01" 209 value.eventEndDate = "2026-10-02" 210 value.eventStartUnixSeconds = nil 211 value.eventEndUnixSeconds = nil 212 value.eventTimezone = nil 213 case .createFoodAvailability: 214 value.identifier = "fixture-carrots" 215 value.title = "Carrots" 216 value.summary = "Fresh carrots" 217 value.location = "Town square" 218 value.priceAmount = "3.25" 219 value.currency = "CAD" 220 value.unit = "lb" 221 } 222 return TeraComposerForm(editing: value) 223 } 224 225 private func configuration(_ fixture: MediaOwnershipFixture, signer: ComposerForbiddenSigner) -> TeraRuntimeLaunchConfiguration { 226 TeraRuntimeLaunchConfiguration(applicationSupportDirectory: fixture.root.path, publicKeyHex: scope.authorPublicKey, 227 sourceGenerationHex: String(repeating: "04", count: 32), sourceGenerationCreatedAtUnixMilliseconds: 1_800_000_000_000, 228 protectedData: .available, networkProfile: .publicNetwork, writableRelays: ["wss://relay.example"], blossom: nil, 229 app: TeraRuntimeAppMetadata(bundleIdentifier: "test.submission", version: "1", buildNumber: "1", buildSHA: nil), 230 signerGeneration: "submission-test", signer: signer, adoptBootstrapSettings: false) 231 } 232 }