field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }