TeraEditingReplacementTests.swift (9125B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraEditingReplacementTests: XCTestCase { 7 func testCleanTypeChangeIsImmediateAndDirtyTypeChangePreservesExactAcknowledgment() async throws { 8 let fixture = try await started() 9 let backend = fixture.backend 10 let client = fixture.client 11 let store = fixture.store 12 store.selectType(.createEvent) 13 XCTAssertEqual(store.form.commandType, .createEvent) 14 store.updateForm(\.eventStartDate, "2026-09-") 15 store.updateForm(\.content, " incomplete event ") 16 await store.save() 17 let original = try XCTUnwrap(store.savedComposer) 18 let pause = await backend.pause(.composer) 19 store.updateForm(\.content, " newer event ") 20 let editing = TeraComposerForm(editing: store.form) 21 store.selectType(.createAsk) 22 await pause.entered.wait() 23 XCTAssertEqual(TeraComposerForm(editing: store.form), editing) 24 store.selectType(.createFoodAvailability) 25 await pause.resume.open() 26 await TeraScopeFixtures.eventually { !store.protection.isWorking } 27 XCTAssertEqual(store.form.commandType, .createAsk) 28 XCTAssertEqual(store.form.content, "") 29 let saved = try await client.loadComposer(scope: original.scope, id: original.id) 30 XCTAssertEqual(saved.form, editing) 31 XCTAssertEqual(saved.revision, original.revision + 1) 32 XCTAssertGreaterThan(saved.editSequence, original.editSequence) 33 await store.save() 34 XCTAssertNotEqual(store.savedComposer?.id, original.id) 35 store.stop() 36 _ = try await client.stop() 37 } 38 39 func testNewerEditingWhileNewSaveCompletesCancelsReplacementAndKeepsNewestForm() async throws { 40 let fixture = try await started() 41 let backend = fixture.backend 42 let client = fixture.client 43 let store = fixture.store 44 let first = await backend.pause(.composer) 45 store.updateForm(\.content, "first") 46 store.newDraft(type: .createEvent) 47 await first.entered.wait() 48 let second = await backend.pause(.composer) 49 store.updateForm(\.content, "newer while saving") 50 await first.resume.open() 51 await second.entered.wait() 52 XCTAssertEqual(store.form.content, "newer while saving") 53 XCTAssertEqual(store.form.commandType, .createUpdate) 54 await second.resume.open() 55 await TeraScopeFixtures.eventually { !store.isWorking } 56 XCTAssertEqual(store.form.content, "newer while saving") 57 XCTAssertEqual(store.savedComposer?.form.content, "newer while saving") 58 XCTAssertFalse(store.protection.failed) 59 store.stop() 60 _ = try await client.stop() 61 } 62 63 func testSaveFailureKeepEditingAndDiscardPreserveLastDurableDraft() async throws { 64 let fixture = try await started() 65 let backend = fixture.backend 66 let client = fixture.client 67 let store = fixture.store 68 store.updateForm(\.content, "durable") 69 await store.save() 70 let original = try XCTUnwrap(store.savedComposer) 71 let failure = await backend.pause(.composer, fails: true) 72 store.updateForm(\.content, "unsaved") 73 store.newDraft() 74 await failure.entered.wait() 75 await failure.resume.open() 76 await TeraScopeFixtures.eventually { store.protection.failed } 77 XCTAssertEqual(store.form.content, "unsaved") 78 store.protection.cancel() 79 store.protection.discard() 80 XCTAssertEqual(store.form.content, "unsaved") 81 let again = await backend.pause(.composer, fails: true) 82 store.newDraft(type: .createEvent) 83 await again.entered.wait() 84 await again.resume.open() 85 await TeraScopeFixtures.eventually { store.protection.failed } 86 store.protection.discard() 87 await TeraScopeFixtures.eventually { !store.protection.isWorking } 88 XCTAssertEqual(store.form.commandType, .createEvent) 89 XCTAssertEqual(store.form.content, "") 90 let saved = try await client.loadComposer(scope: original.scope, id: original.id) 91 XCTAssertEqual(saved, original) 92 store.stop() 93 _ = try await client.stop() 94 } 95 96 func testSavedSelectionRetryPreservesBufferBeforeLoadingAndSignalsSheetDismissal() async throws { 97 let fixture = try await started() 98 let backend = fixture.backend 99 let client = fixture.client 100 let store = fixture.store 101 store.updateForm(\.content, "buffer") 102 await store.save() 103 let original = try XCTUnwrap(store.savedComposer) 104 let failure = await backend.pause(.composer, fails: true) 105 store.updateForm(\.content, "latest buffer") 106 let selection = Task { await store.reopenSaved(.legacy("draft")) } 107 await failure.entered.wait() 108 await failure.resume.open() 109 let applied = await selection.value 110 XCTAssertFalse(applied) 111 XCTAssertEqual(store.form.content, "latest buffer") 112 XCTAssertNil(store.protection.reopened) 113 let counts = await backend.counts 114 XCTAssertNil(counts[.draftStatus]) 115 store.protection.retry() 116 await TeraScopeFixtures.eventually { !store.protection.isWorking } 117 XCTAssertEqual(store.form.content, "old") 118 XCTAssertNotNil(store.protection.reopened) 119 let kept = try await client.loadComposer(scope: original.scope, id: original.id) 120 XCTAssertEqual(kept.form.content, "latest buffer") 121 await store.save() 122 XCTAssertNotEqual(store.savedComposer?.id, original.id) 123 store.stop() 124 _ = try await client.stop() 125 } 126 127 func testScopeChangeCancelsPendingReplacementAndLateSaveCannotReplaceNewAccount() async throws { 128 let fixture = try await started() 129 let backend = fixture.backend 130 let client = fixture.client 131 let store = fixture.store 132 let pause = await backend.pause(.composer) 133 store.updateForm(\.content, "old account") 134 store.newDraft(type: .createEvent) 135 await pause.entered.wait() 136 let snapshot = TeraScopeFixtures.snapshot(account: "b") 137 await backend.configure(snapshot) 138 store.configure(snapshot: snapshot) 139 await store.start() 140 store.updateForm(\.content, "current account") 141 await pause.resume.open() 142 await store.save() 143 XCTAssertEqual(store.form.content, "current account") 144 XCTAssertEqual(store.form.commandType, .createUpdate) 145 XCTAssertEqual(store.savedComposer?.scope.authorPublicKey, String(repeating: "b", count: 64)) 146 XCTAssertFalse(store.protection.failed) 147 store.stop() 148 _ = try await client.stop() 149 } 150 151 func testLegacyRetryPreservesPriorComposerIdentityAndFailedSelectionHasNoEffects() async throws { 152 let fixture = try await started() 153 let backend = fixture.backend 154 let client = fixture.client 155 let store = fixture.store 156 store.updateForm(\.content, "first buffer") 157 await store.save() 158 let original = try XCTUnwrap(store.savedComposer) 159 store.updateForm(\.content, "last buffer") 160 await store.retry(id: "draft") 161 XCTAssertEqual(store.form.content, "old") 162 await store.save() 163 XCTAssertNotEqual(store.savedComposer?.id, original.id) 164 let kept = try await client.loadComposer(scope: original.scope, id: original.id) 165 XCTAssertEqual(kept.form.content, "last buffer") 166 let failure = await backend.pause(.composer, fails: true) 167 store.updateForm(\.content, "keep for cancellation") 168 let cancellation = Task { await store.cancel(id: "draft") } 169 await failure.entered.wait() 170 await failure.resume.open() 171 await cancellation.value 172 XCTAssertEqual(store.form.content, "keep for cancellation") 173 let counts = await backend.counts 174 XCTAssertEqual(counts[.draftStatus], 1) 175 XCTAssertTrue(store.protection.failed) 176 store.stop() 177 _ = try await client.stop() 178 } 179 180 private struct Fixture { 181 let backend: TeraScopeBackend 182 let client: TeraRuntimeClient 183 let store: TeraAddStore 184 } 185 186 func testCancelledSavedSelectionCannotApplyItsLateLoadedForm() async throws { 187 let fixture = try await started() 188 let store = fixture.store 189 store.updateForm(\.content, "keep current") 190 await store.save() 191 let original = try XCTUnwrap(store.savedComposer) 192 var form = TeraComposerForm(commandType: .createAsk) 193 form.content = "other saved editing" 194 let other = try await fixture.client.saveComposer(request: TeraComposerSaveRequest( 195 scope: original.scope, id: String(repeating: "7", count: 32), expectedRevision: nil, editSequence: 1, form: form 196 )) 197 let pause = await fixture.backend.pause(.composerLoad) 198 let selection = Task { await store.reopenSaved(.composer(other.draft.id)) } 199 await pause.entered.wait() 200 selection.cancel() 201 await TeraScopeFixtures.eventually { !store.protection.isWorking } 202 await pause.resume.open() 203 let applied = await selection.value 204 XCTAssertFalse(applied) 205 XCTAssertEqual(store.form.content, "keep current") 206 XCTAssertEqual(store.savedComposer, original) 207 XCTAssertNil(store.protection.reopened) 208 XCTAssertFalse(store.protection.failed) 209 store.stop() 210 _ = try await fixture.client.stop() 211 } 212 213 private func started() async throws -> Fixture { 214 let backend = try TeraScopeBackend() 215 let client = try await TeraScopeFixtures.client(backend) 216 let store = TeraAddStore(runtimeClient: client) 217 store.configure(snapshot: TeraScopeFixtures.snapshot()) 218 await store.start() 219 return Fixture(backend: backend, client: client, store: store) 220 } 221 }