TeraComposerSubmissionCaptureTests.swift (6688B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraComposerSubmissionCaptureTests: XCTestCase { 7 private let scope = TeraComposerScope(authorPublicKey: String(repeating: "a", count: 64), localNetworkID: "default") 8 9 func testTapCapturesOneRevisionWhileTenThousandLaterEditsStayUnsaved() async throws { 10 let storage = ComposerTestStorage() 11 let first = await storage.pauseNext() 12 let composer = make(storage) 13 composer.change(form("before the tap")) 14 await first.entered.wait() 15 let second = await storage.pauseNext() 16 let capture = try composer.beginSubmissionCapture(form("captured at the tap")) 17 let save = Task { try await composer.saveSubmissionCapture(capture) } 18 for index in 1 ... 10000 { 19 composer.change(form("later \(index)")) 20 } 21 await first.resume.open() 22 await second.entered.wait() 23 XCTAssertEqual(composer.acknowledged?.form.content, "before the tap") 24 await second.resume.open() 25 let captured = try await save.value 26 XCTAssertEqual(captured.form.content, "captured at the tap") 27 XCTAssertEqual(captured.editSequence, 2) 28 XCTAssertEqual(captured.revision, 2) 29 XCTAssertTrue(composer.isDirty) 30 XCTAssertEqual(composer.state, .unsaved) 31 let writes = await storage.requests 32 XCTAssertEqual(writes.map(\.form.content), ["before the tap", "captured at the tap"]) 33 XCTAssertThrowsError(try composer.beginSubmissionCapture(form("another tap"))) 34 let replay = try await composer.saveSubmissionCapture(capture) 35 XCTAssertEqual(replay, captured) 36 let repeated = await storage.requests 37 XCTAssertEqual(repeated, writes) 38 composer.releaseSubmissionCapture(capture) 39 let latest = try await composer.save(form("later 10000")) 40 XCTAssertEqual(latest.form.content, "later 10000") 41 XCTAssertEqual(latest.revision, 3) 42 XCTAssertEqual(latest.id, captured.id) 43 XCTAssertEqual(latest.editSequence, 10002) 44 } 45 46 func testCaptureCancelsBatchingWithoutLettingTheNewestEditReplaceItsSource() async throws { 47 let storage = ComposerTestStorage() 48 let delay = ComposerCoalescingGate() 49 let composer = TeraComposerAutosave(persistence: storage.port, delay: { try await delay.wait() }) 50 composer.reset(scope: scope) 51 composer.change(form("batched")) 52 await delay.entered.wait() 53 let capture = try composer.beginSubmissionCapture(form("tap")) 54 composer.change(form("typed after tap")) 55 let saved = try await composer.saveSubmissionCapture(capture) 56 XCTAssertEqual(saved.form.content, "tap") 57 XCTAssertEqual(saved.editSequence, 2) 58 let writes = await storage.requests 59 let cancelled = await delay.cancelled 60 XCTAssertEqual(writes.count, 1) 61 XCTAssertTrue(cancelled) 62 XCTAssertTrue(composer.isDirty) 63 composer.releaseSubmissionCapture(capture) 64 let latest = try await composer.save(form("typed after tap")) 65 XCTAssertEqual(latest.revision, 2) 66 } 67 68 func testUnknownCaptureWriteReconcilesTheOriginalBeforeSavingNewerEditing() async throws { 69 let storage = ComposerTestStorage() 70 await storage.failNext(.afterCommit) 71 let composer = make(storage) 72 let capture = try composer.beginSubmissionCapture(form("original")) 73 do { 74 _ = try await composer.saveSubmissionCapture(capture) 75 XCTFail("A lost receipt cannot claim the capture was saved.") 76 } catch { XCTAssertNil(composer.acknowledged) } 77 composer.change(form("newest")) 78 do { 79 _ = try await composer.save(form("newest")) 80 XCTFail("Explicit Save cannot overwrite the source while submission is unresolved.") 81 } catch { 82 XCTAssertEqual(TeraAddPresentation.failure(for: error)?.code, "submission_capture_pending") 83 } 84 let recovered = try await composer.saveSubmissionCapture(capture) 85 XCTAssertEqual(recovered.form.content, "original") 86 XCTAssertEqual(recovered.revision, 1) 87 let writes = await storage.requests 88 let reads = await storage.readCount 89 XCTAssertEqual(writes.count, 1) 90 XCTAssertEqual(reads, 1) 91 composer.releaseSubmissionCapture(capture) 92 let saved = try await composer.save(form("newest")) 93 XCTAssertEqual(saved.revision, 2) 94 XCTAssertEqual(saved.form.content, "newest") 95 } 96 97 func testStopRetainsTheBarrierAndResumeReconcilesTheLateWrite() async throws { 98 let storage = ComposerTestStorage() 99 let pause = await storage.pauseNext() 100 let composer = make(storage) 101 let capture = try composer.beginSubmissionCapture(form("captured")) 102 let save = Task { try await composer.saveSubmissionCapture(capture) } 103 await pause.entered.wait() 104 composer.change(form("newer")) 105 composer.stop() 106 await pause.resume.open() 107 do { _ = try await save.value; XCTFail("A stale waiter cannot acknowledge the write.") } catch {} 108 XCTAssertTrue(composer.hasSubmissionCapture) 109 XCTAssertNil(composer.acknowledged) 110 composer.resume() 111 let recovered = try await composer.saveSubmissionCapture(capture) 112 XCTAssertEqual(recovered.form.content, "captured") 113 XCTAssertEqual(recovered.revision, 1) 114 let writes = await storage.requests 115 XCTAssertEqual(writes.count, 1) 116 composer.releaseSubmissionCapture(capture) 117 _ = try await composer.save(form("newer")) 118 } 119 120 func testOldCaptureCannotReleaseAReplacementScopeBarrier() async throws { 121 let storage = ComposerTestStorage() 122 let pause = await storage.pauseNext() 123 let composer = make(storage) 124 let original = try composer.beginSubmissionCapture(form("old")) 125 let save = Task { try await composer.saveSubmissionCapture(original) } 126 await pause.entered.wait() 127 let nextScope = TeraComposerScope(authorPublicKey: scope.authorPublicKey, localNetworkID: "next") 128 composer.reset(scope: nextScope) 129 let replacement = try composer.beginSubmissionCapture(form("new scope")) 130 composer.releaseSubmissionCapture(original) 131 XCTAssertTrue(composer.hasSubmissionCapture) 132 await pause.resume.open() 133 do { _ = try await save.value; XCTFail("Replaced scope cannot accept an old capture.") } catch {} 134 let current = try await composer.saveSubmissionCapture(replacement) 135 XCTAssertEqual(current.scope, nextScope) 136 XCTAssertEqual(current.form.content, "new scope") 137 composer.releaseSubmissionCapture(replacement) 138 XCTAssertFalse(composer.hasSubmissionCapture) 139 } 140 141 private func make(_ storage: ComposerTestStorage) -> TeraComposerAutosave { 142 let composer = TeraComposerAutosave(persistence: storage.port, delay: {}) 143 composer.reset(scope: scope) 144 return composer 145 } 146 147 private func form(_ text: String) -> TeraComposerForm { 148 var value = TeraComposerForm(commandType: .createUpdate) 149 value.content = text 150 return value 151 } 152 }