TeraComposerDurabilityBoundaryTests.swift (5169B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraComposerDurabilityBoundaryTests: XCTestCase { 7 func testTypedStorageAndProtectedDataFailuresKeepEditingAndNeverClaimSaved() async throws { 8 // Actual SQLite COMMIT/capacity/busy faults are qualified by the owning 9 // storage crate. This fixture checks their typed host presentation boundary. 10 let cases: [(String, TeraUserMessageKey)] = [ 11 ("storage_space_insufficient", .storageFull), 12 ("database_busy", .secureStateUnavailable), 13 ("composer_storage_failed", .secureStateUnavailable), 14 ("protected_data_unavailable", .protectedDataUnavailable), 15 ] 16 for (code, message) in cases { 17 let backend = try TeraScopeBackend() 18 let client = try await TeraScopeFixtures.client(backend) 19 let store = TeraAddStore(runtimeClient: client) 20 store.configure(snapshot: TeraScopeFixtures.snapshot()) 21 await store.start() 22 store.updateForm(\.content, "acknowledged baseline") 23 await store.save() 24 let baseline = try XCTUnwrap(store.savedComposer) 25 let failure = TeraRuntimeFailure.local(operation: "test.composer", code: code, 26 safeMessage: "Controlled unavailable storage.") 27 let pause = await backend.pause(.composer, failure: failure) 28 store.updateForm(\.content, "preserve this incomplete edit") 29 let save = Task { await store.save() } 30 await pause.entered.wait() 31 XCTAssertEqual(store.savedComposer, baseline) 32 XCTAssertNil(store.message) 33 await pause.resume.open() 34 await save.value 35 XCTAssertEqual(store.composerState, .failed, code) 36 XCTAssertEqual(store.lastFailureCode, code) 37 XCTAssertEqual(store.message, TeraUserMessages.text(message)) 38 XCTAssertEqual(store.form.content, "preserve this incomplete edit") 39 XCTAssertEqual(store.savedComposer, baseline) 40 store.updateForm(\.content, "newest retained edit") 41 await store.save() 42 let retried = try XCTUnwrap(store.savedComposer) 43 XCTAssertEqual(retried.id, baseline.id) 44 XCTAssertEqual(retried.revision, baseline.revision + 1) 45 XCTAssertEqual(retried.form.content, "newest retained edit") 46 XCTAssertEqual(store.composerState, .saved) 47 XCTAssertEqual(store.message, "Draft saved on this device.") 48 store.stop() 49 _ = try await client.stop() 50 } 51 } 52 53 func testUnavailableProtectedDataRejectsProductionStartupAndReopensBaselineAfterUnlock() async throws { 54 let fixture = try MediaOwnershipFixture() 55 defer { fixture.remove() } 56 let signer = ComposerForbiddenSigner() 57 let client = TeraRuntimeClient.production() 58 let available = configuration(fixture, signer: signer, protectedData: .available) 59 let snapshot = try await client.start(configuration: available) 60 let store = TeraAddStore(runtimeClient: client) 61 store.configure(snapshot: snapshot) 62 await store.start() 63 store.updateForm(\.content, "committed before protected data became unavailable") 64 await store.save() 65 let baseline = try XCTUnwrap(store.savedComposer) 66 store.stop() 67 _ = try await client.stop() 68 do { 69 _ = try await client.start(configuration: configuration(fixture, signer: signer, protectedData: .unavailable)) 70 XCTFail("Unavailable protected data must deny the actual runtime startup.") 71 } catch let TeraRuntimeClientError.startup(failure) { 72 XCTAssertEqual(failure.code, "protected_data_unavailable") 73 XCTAssertEqual(TeraUserMessages.key(for: failure, fallback: .startupFailed), .protectedDataUnavailable) 74 } 75 do { 76 _ = try await client.saveComposer(request: TeraComposerSaveRequest( 77 scope: baseline.scope, id: baseline.id, expectedRevision: baseline.revision, 78 editSequence: baseline.editSequence + 1, form: baseline.form 79 )) 80 XCTFail("A stopped runtime must never acknowledge a write.") 81 } catch { XCTAssertEqual(error as? TeraRuntimeClientError, .notRunning) } 82 _ = try await client.start(configuration: available) 83 let reopened = try await client.loadComposer(scope: baseline.scope, id: baseline.id) 84 XCTAssertEqual(reopened, baseline) 85 _ = try await client.stop() 86 let signingRequests = await signer.requests 87 XCTAssertEqual(signingRequests, 0) 88 } 89 90 private func configuration( 91 _ fixture: MediaOwnershipFixture, signer: ComposerForbiddenSigner, protectedData: TeraProtectedDataState 92 ) -> TeraRuntimeLaunchConfiguration { 93 TeraRuntimeLaunchConfiguration( 94 applicationSupportDirectory: fixture.root.path, 95 publicKeyHex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 96 sourceGenerationHex: String(repeating: "04", count: 32), 97 sourceGenerationCreatedAtUnixMilliseconds: 1_800_000_000_000, 98 protectedData: protectedData, networkProfile: .publicNetwork, 99 writableRelays: ["wss://relay.example"], blossom: nil, 100 app: TeraRuntimeAppMetadata(bundleIdentifier: "test.composer-durability", version: "1", buildNumber: "1", buildSHA: nil), 101 signerGeneration: "composer-durability", signer: signer, adoptBootstrapSettings: false 102 ) 103 } 104 }