field_ios

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

commit c605b4e43846cc9fee5f76e3b8b856cb9a545c95
parent 47617a25ad1c38c3e05c3fe7d2680f6c0713559f
Author: triesap <tyson@radroots.org>
Date:   Wed,  9 Sep 2026 07:27:49 +0000

runtime: settle abandoned resource creation

- Separate creation from adoption with one bounded resource cleanup owner.
- Preserve shared startup and remove stale session-wide stop authority.
- Regenerate exact producer receipts while retaining native APIs and bytes.
- Verify fourteen new native tests and reproduce the prior ownership failures.

Diffstat:
MPackage.swift | 2++
MTera.xcodeproj/project.pbxproj | 20++++++++++++++++++++
MTera/Runtime/TeraRuntimeClient.swift | 112+++++++++++++++++++++++++++++++------------------------------------------------
ATera/Runtime/TeraRuntimeResourceCreation.swift | 44++++++++++++++++++++++++++++++++++++++++++++
ATera/Runtime/TeraRuntimeResourceTask.swift | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTera/State/TeraSessionStore.swift | 3+--
MTeraFFI/provenance.json | 14+++++++-------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 8++++----
MTeraFFI/source/aarch64-apple-ios-sim.json | 8++++----
MTeraFFI/source/aarch64-apple-ios.json | 8++++----
ATeraTests/TeraRuntimeResourceFixtures.swift | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraRuntimeResourceLifecycleTests.swift | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraRuntimeResourceTaskTests.swift | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATeraTests/TeraSessionResourceTests.swift | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTeraTests/TeraStateMigrationTests.swift | 96-------------------------------------------------------------------------------
ATeraTests/TeraStateTestFixtures.swift | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 5+++++
Mrelease/provenance.json | 6+++---
Mtest-fixtures/legacy-identifiers.v1.json | 48++++++++++++++++++++++++++++++++++++------------
Mtest-fixtures/swiftlint-maintainability-baseline.v1.json | 4++--
21 files changed, 872 insertions(+), 204 deletions(-)

diff --git a/Package.swift b/Package.swift @@ -61,6 +61,8 @@ let package = Package( "Runtime/TeraRuntimeBoundedTask.swift", "Runtime/TeraRuntimeClient.swift", "Runtime/TeraRuntimeModels.swift", + "Runtime/TeraRuntimeResourceCreation.swift", + "Runtime/TeraRuntimeResourceTask.swift", "Runtime/TeraSessionGeneration.swift", "Runtime/TeraUserMessageClassifier.swift", "Runtime/TeraUserMessages.swift", diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -11,10 +11,12 @@ 229C9AC991E67663DA53D339 /* TeraRuntimeBoundedTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */; }; 2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */; }; 2C6DDB502DFA11C2CBCA49C4 /* TeraApp in Frameworks */ = {isa = PBXBuildFile; productRef = A1979630FE6BC343F0E59F24 /* TeraApp */; }; + 31CBA09815D972CCE187009F /* TeraSessionResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */; }; 38EF7FA8874E82B28110A232 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */; }; 4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */; }; 64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */; }; 671EF50DB4EFBC25AABF26DC /* TeraLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */; }; + 6B0BE8E92892EF60B8E996A5 /* TeraRuntimeResourceFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */; }; 70417FB635F06CC9829FB38C /* TeraClockTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506546B0EC1792240C36F7 /* TeraClockTests.swift */; }; 772A9B1AD8DE1D2E0969F285 /* TeraCalendarFFICompatibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */; }; 79D06C6BF2B55C0C4104FB91 /* TeraSessionGenerationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0836F5D5F71F9A54795C57 /* TeraSessionGenerationTests.swift */; }; @@ -22,7 +24,9 @@ 852FF7514FF7F682A246FB47 /* TeraStateMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */; }; 912C8743116D929BB6A02778 /* TeraRuntimeClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */; }; 9AD968F0B37C64B5AF8D9A68 /* TeraRootShellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B8677670E203FF68EC88983 /* TeraRootShellTests.swift */; }; + 9B37084901EC52EB44969304 /* TeraRuntimeResourceLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */; }; A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */; }; + B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */; }; B2DAAE01D16E1FAC35816DED /* RadrootsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1A9F6E32DE0C3728D4DB8E60 /* RadrootsKit */; }; B4AE86BE0A9F7FBE0DEBD9AC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = ADFD21D28C5F9E58B70E76CA /* Localizable.strings */; }; B64A836C5E84924D28CB5D47 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AB8A30A75EFD65EFD938D248 /* Assets.xcassets */; }; @@ -33,6 +37,7 @@ DF97F408D7D32F72C5AF5939 /* TeraRootShellUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */; }; E508A7061082B091B733D62B /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67ACCE25979BA86C1EC5BFD6 /* App.swift */; }; EF7293C553BF1DB45EEEBD49 /* local-social-personas.v1.json in Resources */ = {isa = PBXBuildFile; fileRef = 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */; }; + F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,11 +59,13 @@ /* Begin PBXFileReference section */ 0CDE3D76E1EDF909C19D0506 /* TeraUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = TeraUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceLifecycleTests.swift; sourceTree = "<group>"; }; 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "local-social-personas.v1.json"; sourceTree = "<group>"; }; 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeBoundedTaskTests.swift; sourceTree = "<group>"; }; 2A506546B0EC1792240C36F7 /* TeraClockTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraClockTests.swift; sourceTree = "<group>"; }; 32CBDAE974FB8D7408108E69 /* TeraRemoteQualificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRemoteQualificationUITests.swift; sourceTree = "<group>"; }; 3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCalendarFFICompatibilityTests.swift; sourceTree = "<group>"; }; + 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSessionResourceTests.swift; sourceTree = "<group>"; }; 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraUserMessagesTests.swift; sourceTree = "<group>"; }; 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellUITests.swift; sourceTree = "<group>"; }; 57CA176D0A7566A4ADD2CEA4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; @@ -76,11 +83,14 @@ 9F54FC4930051FC4611B37D3 /* tera */ = {isa = PBXFileReference; lastKnownFileType = folder; name = tera; path = .; sourceTree = SOURCE_ROOT; }; AB8A30A75EFD65EFD938D248 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; ADFD21D28C5F9E58B70E76CA /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; }; + BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraStateTestFixtures.swift; sourceTree = "<group>"; }; C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraSupportingStoreTests.swift; sourceTree = "<group>"; }; C1DBB5D9AC2FE8EA3AE68458 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; + C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceTaskTests.swift; sourceTree = "<group>"; }; C890BDE885536BC2AB215393 /* TeraLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraLifecycleTests.swift; sourceTree = "<group>"; }; DE9E724A23014C6EA9042123 /* Tera.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Tera.app; sourceTree = BUILT_PRODUCTS_DIR; }; E351F52E77C74A4E99095E45 /* TeraRemoteQualificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRemoteQualificationTests.swift; sourceTree = "<group>"; }; + F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; }; FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeClientTests.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -155,8 +165,13 @@ 2A4FFC0023C620B68D0EC0ED /* TeraRuntimeBoundedTaskTests.swift */, 88ED360C84532DA7088BFC6E /* TeraRuntimeClientFixtures.swift */, FB77C087F15C2F42CD959607 /* TeraRuntimeClientTests.swift */, + F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */, + 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */, + C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */, 7C0836F5D5F71F9A54795C57 /* TeraSessionGenerationTests.swift */, + 4383B870D20178E633610113 /* TeraSessionResourceTests.swift */, 98E969BCFF7CED33A1D4A040 /* TeraStateMigrationTests.swift */, + BAD45A3134A16704CBE4B1A5 /* TeraStateTestFixtures.swift */, C0636AEDEB50DDDD328374A9 /* TeraSupportingStoreTests.swift */, 794D475C70BA8ED994D1CA66 /* TeraTodayStoreTests.swift */, 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */, @@ -378,8 +393,13 @@ 229C9AC991E67663DA53D339 /* TeraRuntimeBoundedTaskTests.swift in Sources */, C219B5F4E24188E1116BF45B /* TeraRuntimeClientFixtures.swift in Sources */, 912C8743116D929BB6A02778 /* TeraRuntimeClientTests.swift in Sources */, + 6B0BE8E92892EF60B8E996A5 /* TeraRuntimeResourceFixtures.swift in Sources */, + 9B37084901EC52EB44969304 /* TeraRuntimeResourceLifecycleTests.swift in Sources */, + F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */, 79D06C6BF2B55C0C4104FB91 /* TeraSessionGenerationTests.swift in Sources */, + 31CBA09815D972CCE187009F /* TeraSessionResourceTests.swift in Sources */, 852FF7514FF7F682A246FB47 /* TeraStateMigrationTests.swift in Sources */, + B1B83B8673660E0EC109F74C /* TeraStateTestFixtures.swift in Sources */, 2479571FA99A7962794999C4 /* TeraSupportingStoreTests.swift in Sources */, 64D49ADFD3FDD54CBA509DC5 /* TeraTodayStoreTests.swift in Sources */, 4734A842C366608EC1BD31C5 /* TeraUserMessagesTests.swift in Sources */, diff --git a/Tera/Runtime/TeraRuntimeClient.swift b/Tera/Runtime/TeraRuntimeClient.swift @@ -292,7 +292,8 @@ actor TeraRuntimeClient { private struct StartupOperation: Sendable { let identity: TeraRuntimeOperationIdentity let configuration: TeraRuntimeLaunchConfiguration - let task: TeraRuntimeBoundedTask<TeraRuntimeBackendStart> + let task: TeraRuntimeResourceTask<TeraRuntimeBackendStart> + var waiters = 0 } private struct ShutdownOperation: Sendable { @@ -385,33 +386,9 @@ actor TeraRuntimeClient { lifecycleState = .starting(generation: operationGeneration) let identity = nextIdentity(kind: kind) - let factory = factory - let cleanupDeadline = deadlines.shutdownNanoseconds - let task = TeraRuntimeBoundedTask<TeraRuntimeBackendStart>( - deadlineNanoseconds: deadlines.startupNanoseconds, - operation: { - do { - return try await .success(factory(requestedConfiguration)) - } catch { - return .failure(Self.failure(from: error, operation: identity.rawValue)) - } - }, - onAbandonedResult: { result in - guard case let .success(started) = result else { return } - let cleanup = TeraRuntimeBoundedTask<TeraRuntimeShutdownReceipt>( - deadlineNanoseconds: cleanupDeadline, - operation: { - do { - return try await .success(started.backend.shutdown()) - } catch { - return .failure( - Self.failure(from: error, operation: "runtime.abandoned_startup") - ) - } - } - ) - _ = await cleanup.value() - } + let task = TeraRuntimeResourceCreation.startup( + configuration: requestedConfiguration, factory: factory, + identity: identity, deadlines: deadlines ) let operation = StartupOperation( identity: identity, @@ -754,36 +731,12 @@ actor TeraRuntimeClient { token: nil ) - let subscriptionDeadline = deadlines.subscriptionNanoseconds - let task = TeraRuntimeBoundedTask<any TeraRuntimeSubscriptionToken>( - deadlineNanoseconds: subscriptionDeadline, - operation: { - do { - return try await .success( - backend.subscribe(bufferCapacity: bufferCapacity) { [weak self] change in - await self?.receive( - change, - subscriptionID: id, - generation: subscriptionGeneration - ) - } - ) - } catch { - return .failure(Self.failure(from: error, operation: identity.rawValue)) - } - }, - onAbandonedResult: { result in - guard case let .success(token) = result else { return } - let cancellation = TeraRuntimeBoundedTask<Void>( - deadlineNanoseconds: subscriptionDeadline, - operation: { - await token.cancel() - return .success(()) - } - ) - _ = await cancellation.value() - } - ) + let task = TeraRuntimeResourceCreation.subscription( + backend: backend, bufferCapacity: bufferCapacity, + identity: identity, deadline: deadlines.subscriptionNanoseconds + ) { [weak self] change in + await self?.receive(change, subscriptionID: id, generation: subscriptionGeneration) + } activeOperations[identity.sequence] = ActiveOperation( identity: identity, cancel: { task.cancel() } @@ -796,14 +749,22 @@ actor TeraRuntimeClient { var subscription = subscriptions[id] else { subscriptions.removeValue(forKey: id)?.continuation.finish() - if case let .completed(.success(token)) = outcome { - cancelTokenDetached(token) - } + task.cancel() throw TeraRuntimeClientError.superseded } + if Task.isCancelled { + subscriptions.removeValue(forKey: id)?.continuation.finish() + task.cancel() + throw TeraRuntimeClientError.subscription(Self.cancellationFailure(identity: identity)) + } + switch outcome { case let .completed(.success(token)): + guard task.adopt() else { + subscriptions.removeValue(forKey: id)?.continuation.finish() + throw TeraRuntimeClientError.superseded + } subscription.token = token subscriptions[id] = subscription return pair.stream @@ -864,17 +825,18 @@ actor TeraRuntimeClient { private func finishStartup( _ operation: StartupOperation ) async throws -> TeraRuntimeSnapshot { + if startupOperation?.identity == operation.identity { + startupOperation?.waiters += 1 + } + defer { releaseStartupWaiter(operation) } let outcome = await operation.task.value(cancelsOperationWhenWaiterCancelled: false) guard generation == operation.identity.generation else { + operation.task.cancel() throw TeraRuntimeClientError.superseded } - if case .cancelled = outcome, - startupOperation?.identity == operation.identity - { - throw TeraRuntimeClientError.startup( - Self.cancellationFailure(identity: operation.identity) - ) + if Task.isCancelled { + throw TeraRuntimeClientError.startup(Self.cancellationFailure(identity: operation.identity)) } if startupOperation?.identity == operation.identity { @@ -890,6 +852,7 @@ actor TeraRuntimeClient { switch outcome { case let .completed(.success(started)): + guard operation.task.adopt() else { throw TeraRuntimeClientError.superseded } backend = started.backend configuration = operation.configuration lifecycleState = .running(generation: operation.identity.generation) @@ -908,6 +871,19 @@ actor TeraRuntimeClient { } } + private func releaseStartupWaiter(_ operation: StartupOperation) { + guard var pending = startupOperation, pending.identity == operation.identity else { return } + pending.waiters -= 1 + guard pending.waiters == 0 else { + startupOperation = pending + return + } + pending.task.cancel() + startupOperation = nil + configuration = nil + lifecycleState = .stopped + } + private func beginShutdown() -> ShutdownOperation { generation = generation.invalidated() let operationGeneration = generation @@ -1180,7 +1156,7 @@ actor TeraRuntimeClient { ) } - private static func failure(from error: Error, operation: String) -> TeraRuntimeFailure { + static func failure(from error: Error, operation: String) -> TeraRuntimeFailure { if let failure = error as? TeraRuntimeFailure { return failure } diff --git a/Tera/Runtime/TeraRuntimeResourceCreation.swift b/Tera/Runtime/TeraRuntimeResourceCreation.swift @@ -0,0 +1,44 @@ +enum TeraRuntimeResourceCreation { + static func startup( + configuration: TeraRuntimeLaunchConfiguration, + factory: @escaping TeraRuntimeBackendFactory, + identity: TeraRuntimeOperationIdentity, + deadlines: TeraRuntimeDeadlinePolicy + ) -> TeraRuntimeResourceTask<TeraRuntimeBackendStart> { + TeraRuntimeResourceTask( + deadlineNanoseconds: deadlines.startupNanoseconds, + cleanupDeadlineNanoseconds: deadlines.shutdownNanoseconds, + operation: { + do { + return try await .success(factory(configuration)) + } catch { + return .failure(TeraRuntimeClient.failure(from: error, operation: identity.rawValue)) + } + }, + cleanup: { started in + _ = try? await started.backend.shutdown() + } + ) + } + + static func subscription( + backend: any TeraRuntimeBackend, + bufferCapacity: Int, + identity: TeraRuntimeOperationIdentity, + deadline: UInt64, + receive: @escaping @Sendable (TeraRuntimeChange) async -> Void + ) -> TeraRuntimeResourceTask<any TeraRuntimeSubscriptionToken> { + TeraRuntimeResourceTask( + deadlineNanoseconds: deadline, + cleanupDeadlineNanoseconds: deadline, + operation: { + do { + return try await .success(backend.subscribe(bufferCapacity: bufferCapacity, receive: receive)) + } catch { + return .failure(TeraRuntimeClient.failure(from: error, operation: identity.rawValue)) + } + }, + cleanup: { token in await token.cancel() } + ) + } +} diff --git a/Tera/Runtime/TeraRuntimeResourceTask.swift b/Tera/Runtime/TeraRuntimeResourceTask.swift @@ -0,0 +1,118 @@ +import Foundation + +/// Creation completion and adoption are separate events. Only the transition +/// to abandoned may start cleanup; adoption permanently transfers that duty. +final class TeraRuntimeResourceTask<Value: Sendable>: Sendable { + private enum State { + case pending + case created(Value) + case adopted + case abandoned + } + + private let ownership: Ownership + private let task: TeraRuntimeBoundedTask<Value> + + init( + deadlineNanoseconds: UInt64, + cleanupDeadlineNanoseconds: UInt64, + operation: @escaping @Sendable () async -> Result<Value, TeraRuntimeFailure>, + cleanup: @escaping @Sendable (Value) async -> Void + ) { + let ownership = Ownership(deadline: cleanupDeadlineNanoseconds, cleanup: cleanup) + self.ownership = ownership + task = TeraRuntimeBoundedTask(deadlineNanoseconds: deadlineNanoseconds) { + let result = await operation() + if case let .success(value) = result { + ownership.created(value) + } + return result + } onAbandonedResult: { _ in + ownership.abandon() + } + } + + func value(cancelsOperationWhenWaiterCancelled: Bool = true) async -> TeraRuntimeBoundedOutcome<Value> { + let outcome = await task.value(cancelsOperationWhenWaiterCancelled: cancelsOperationWhenWaiterCancelled) + switch outcome { + case .timedOut: + ownership.abandon() + case .cancelled where cancelsOperationWhenWaiterCancelled: + ownership.abandon() + default: + break + } + return outcome + } + + func adopt() -> Bool { + ownership.adopt() + } + + func cancel() { + ownership.abandon() + task.cancel() + } + + /// Every mutable field is lock protected. Cleanup starts outside the lock, + /// has the existing host deadline, and retains its resource until it returns. + private final class Ownership: @unchecked Sendable { + private let lock = NSLock() + private var state = State.pending + private var cleanupTask: TeraRuntimeBoundedTask<Void>? + private let deadline: UInt64 + private let cleanup: @Sendable (Value) async -> Void + + init(deadline: UInt64, cleanup: @escaping @Sendable (Value) async -> Void) { + self.deadline = deadline + self.cleanup = cleanup + } + + func created(_ value: Value) { + let abandoned = lock.withLock { + if case .abandoned = state { + return true + } + state = .created(value) + return false + } + if abandoned { + startCleanup(value) + } + } + + func adopt() -> Bool { + lock.withLock { + guard case .created = state else { return false } + state = .adopted + return true + } + } + + func abandon() { + let value: Value? = lock.withLock { + switch state { + case let .created(value): + state = .abandoned + return value + case .pending: + state = .abandoned + return nil + case .adopted, .abandoned: + return nil + } + } + if let value { + startCleanup(value) + } + } + + private func startCleanup(_ value: Value) { + let task = TeraRuntimeBoundedTask<Void>(deadlineNanoseconds: deadline) { [cleanup] in + await cleanup(value) + return .success(()) + } + lock.withLock { cleanupTask = task } + } + } +} diff --git a/Tera/State/TeraSessionStore.swift b/Tera/State/TeraSessionStore.swift @@ -347,6 +347,7 @@ actor TeraSessionStore { ) let sourceGeneration = try await configurationStore.sourceGeneration() let signer = try await identityStore.signer(for: identity) + guard generation == requestedGeneration else { throw TeraRuntimeClientError.superseded } let launchConfiguration = TeraRuntimeLaunchConfiguration( applicationSupportDirectory: mobileStore.applicationSupportDirectory.path, publicKeyHex: publicKeyHex, @@ -368,7 +369,6 @@ actor TeraSessionStore { try await runtimeClient.start(configuration: launchConfiguration) } guard generation == requestedGeneration else { - _ = try? await runtimeClient.stop() throw TeraRuntimeClientError.superseded } try await reconcileIdentity(identity) @@ -380,7 +380,6 @@ actor TeraSessionStore { ) } guard generation == requestedGeneration else { - _ = try? await runtimeClient.stop() throw TeraRuntimeClientError.superseded } return .running(snapshot) diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -94,17 +94,17 @@ { "bytes": 55357, "path": "source/aarch64-apple-darwin.json", - "sha256": "238afaca3ed63bc11a6cb725d959ea07adadbe65702b4513f4d922d737ea7345" + "sha256": "bd0f9c9ef8f74f381bec5b3102c2103d76ab0a013ebeb1cfacfc2e46db8d28b7" }, { "bytes": 55201, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "8d1b2dfcbeeeadb49c8b3711d5c23e1eb042c5f3fc1ce45012e35b0eacb5327d" + "sha256": "bc13a2c56369c028d16c766928f932ff6251b01d213676c3c44fa33a2e42f4f3" }, { "bytes": 55197, "path": "source/aarch64-apple-ios.json", - "sha256": "99acf9fc1abb5514e5bec3a73891dfb829f223d031c618df25fd564ce69214be" + "sha256": "c52645bf421065857e8638b5f6736a53e13f16e47a1c021c14f84ec1898d1f3f" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3" + "tree": "ee3a2c5daa71f0fb972496b44ca3be606a887c16" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -176,17 +176,17 @@ { "bytes": 55357, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "238afaca3ed63bc11a6cb725d959ea07adadbe65702b4513f4d922d737ea7345" + "sha256": "bd0f9c9ef8f74f381bec5b3102c2103d76ab0a013ebeb1cfacfc2e46db8d28b7" }, { "bytes": 55201, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "8d1b2dfcbeeeadb49c8b3711d5c23e1eb042c5f3fc1ce45012e35b0eacb5327d" + "sha256": "bc13a2c56369c028d16c766928f932ff6251b01d213676c3c44fa33a2e42f4f3" }, { "bytes": 55197, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "99acf9fc1abb5514e5bec3a73891dfb829f223d031c618df25fd564ce69214be" + "sha256": "c52645bf421065857e8638b5f6736a53e13f16e47a1c021c14f84ec1898d1f3f" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3" -manifest_sha256 = "2e47dd8016a3980af7df78961e0a105cbc542cd92d372539d4ed1105c6754d55" +source_tree = "ee3a2c5daa71f0fb972496b44ca3be606a887c16" +manifest_sha256 = "d8a97dce55ee2f58e56cc6bebc5870956df106ed7715e144f13c617be94c9d2a" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -1189,13 +1189,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 110025, - "git_blob": "686dcad1b181044b69599fa90f27b35fb8f8c61c", + "bytes": 110621, + "git_blob": "14d5342029968785335a2466d25832fcc8b85e9d", "mode": "100644", - "sha256": "27fd4299dc34e40bf719f059762401d79df44de339d657c29afd5ea290f36eed" + "sha256": "e45baeb16a4b77dd90962651ee9b8398040e3685b38feb298b8b5d5fdc437d67" } }, "policy": "staged_inputs", - "tree": "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3" + "tree": "ee3a2c5daa71f0fb972496b44ca3be606a887c16" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -1185,13 +1185,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 110025, - "git_blob": "686dcad1b181044b69599fa90f27b35fb8f8c61c", + "bytes": 110621, + "git_blob": "14d5342029968785335a2466d25832fcc8b85e9d", "mode": "100644", - "sha256": "27fd4299dc34e40bf719f059762401d79df44de339d657c29afd5ea290f36eed" + "sha256": "e45baeb16a4b77dd90962651ee9b8398040e3685b38feb298b8b5d5fdc437d67" } }, "policy": "staged_inputs", - "tree": "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3" + "tree": "ee3a2c5daa71f0fb972496b44ca3be606a887c16" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -1185,13 +1185,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 110025, - "git_blob": "686dcad1b181044b69599fa90f27b35fb8f8c61c", + "bytes": 110621, + "git_blob": "14d5342029968785335a2466d25832fcc8b85e9d", "mode": "100644", - "sha256": "27fd4299dc34e40bf719f059762401d79df44de339d657c29afd5ea290f36eed" + "sha256": "e45baeb16a4b77dd90962651ee9b8398040e3685b38feb298b8b5d5fdc437d67" } }, "policy": "staged_inputs", - "tree": "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3" + "tree": "ee3a2c5daa71f0fb972496b44ca3be606a887c16" } } diff --git a/TeraTests/TeraRuntimeResourceFixtures.swift b/TeraTests/TeraRuntimeResourceFixtures.swift @@ -0,0 +1,153 @@ +import Foundation +@testable import TeraApp + +actor ResourceTestGate { + private var opened = false + private var waiters: [CheckedContinuation<Void, Never>] = [] + + func wait() async { + guard !opened else { return } + await withCheckedContinuation { waiters.append($0) } + } + + func open() { + guard !opened else { return } + opened = true + let pending = waiters + waiters.removeAll() + for waiter in pending { + waiter.resume() + } + } +} + +struct ResourceTestPause: Sendable { + let entered = ResourceTestGate() + let resume = ResourceTestGate() + + func wait() async { + await entered.open() + await resume.wait() + } +} + +actor ResourceTestToken: TeraRuntimeSubscriptionToken { + let cancelled = ResourceTestGate() + private(set) var cancelCount = 0 + + func cancel() async { + cancelCount += 1 + await cancelled.open() + } +} + +actor ResourceTestBackend: TeraRuntimeBackend { + let token = ResourceTestToken() + let closed = ResourceTestGate() + private(set) var shutdownCount = 0 + private let value: TeraRuntimeSnapshot + private var subscriptionPause: ResourceTestPause? + private var snapshotPause: ResourceTestPause? + private var settingsPause: ResourceTestPause? + private var receive: (@Sendable (TeraRuntimeChange) async -> Void)? + + init(publicKeyHex: String) { + value = TeraRuntimeSnapshot( + identity: TeraRuntimeIdentity(publicKeyHex: publicKeyHex, hostSignerConfigured: true), + relay: nil, blossomConfiguration: nil, blossomEvidence: nil, + crateName: "tera_ffi", crateVersion: "0.1.0-alpha", isClosed: false + ) + } + + func start() -> TeraRuntimeBackendStart { + TeraRuntimeBackendStart(backend: self, snapshot: value) + } + + func pauseSubscription(_ pause: ResourceTestPause) { + subscriptionPause = pause + } + + func pauseSnapshot(_ pause: ResourceTestPause) { + snapshotPause = pause + } + + func pauseSettings(_ pause: ResourceTestPause) { + settingsPause = pause + } + + func snapshot() async throws -> TeraRuntimeSnapshot { + let pause = snapshotPause + snapshotPause = nil + await pause?.wait() + guard shutdownCount == 0 else { throw unsupported() } + return value + } + + func todayPage(request _: TeraTodayPageRequest) throws -> TeraTodayPage { + throw unsupported() + } + + func refreshToday( + context _: TeraLocalNetwork, nowUnixSeconds _: UInt64, update _: TeraTodayProjectionUpdate + ) throws -> TeraTodayRefreshReceipt { + throw unsupported() + } + + func subscribe( + bufferCapacity _: Int, receive: @escaping @Sendable (TeraRuntimeChange) async -> Void + ) async -> any TeraRuntimeSubscriptionToken { + self.receive = receive + let pause = subscriptionPause + subscriptionPause = nil + await pause?.wait() + return token + } + + func emit(_ revision: UInt64) async { + await receive?(TeraRuntimeChange( + schemaVersion: 1, generation: TeraProjectionRevision(rawValue: revision), kind: .today, entityID: nil + )) + } + + func shutdown() async -> TeraRuntimeShutdownReceipt { + shutdownCount += 1 + await closed.open() + return TeraRuntimeShutdownReceipt(state: "closed", alreadyClosed: shutdownCount > 1) + } + + func mobileSettings() async -> TeraMobileSettings { + let pause = settingsPause + settingsPause = nil + await pause?.wait() + return settings() + } + + func applyIdentityCommand( + expectedRevision _: UInt64, command _: TeraIdentityCommand + ) -> TeraSettingsTransition { + TeraSettingsTransition( + settings: settings(), runtimeRestartRequired: false, + outboxRequeueRequired: false, mediaCacheInvalidationRequired: false + ) + } + + private func settings() -> TeraMobileSettings { + TeraMobileSettings( + revision: 1, + identity: TeraSettingsIdentityState( + identities: [TeraSettingsIdentity(id: "identity", publicKeyHex: value.identity.publicKeyHex)], + activeIdentityID: "identity", lockState: .unlocked, pendingImportOperationID: nil + ), + networkEnvironment: .simulator, + relays: [TeraRelayPreference(url: "ws://127.0.0.1:7447", access: .readWrite)], + blossomAuthority: .loopbackDevelopment, blossomPrimaryOrigin: "http://127.0.0.1:3000", + blossomFallbackOrigins: [], allowCellularDownloads: true, + allowCellularUploads: true, allowBackgroundTransfers: true, + mediaCacheBytes: 256 * 1_048_576, mediaCacheArtifacts: 1024 + ) + } + + private func unsupported() -> TeraRuntimeFailure { + .local(operation: "test.resource", code: "test.unavailable", safeMessage: "Test operation unavailable") + } +} diff --git a/TeraTests/TeraRuntimeResourceLifecycleTests.swift b/TeraTests/TeraRuntimeResourceLifecycleTests.swift @@ -0,0 +1,163 @@ +@testable import TeraApp +import XCTest + +final class TeraRuntimeResourceLifecycleTests: XCTestCase { + func testStartupTimeoutClosesLateBackendWithoutResurrection() async throws { + let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "30") + let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) + let pause = ResourceTestPause() + let deadlines = TeraRuntimeDeadlinePolicy(startupNanoseconds: 1_000_000) + let client = TeraRuntimeClient( + factory: { _ in await pause.wait(); return await backend.start() }, deadlines: deadlines + ) + let start = Task { try await client.start(configuration: configuration) } + await pause.entered.wait() + do { + _ = try await start.value + XCTFail("Startup deadline must end the wait") + } catch let TeraRuntimeClientError.startup(failure) { + XCTAssertEqual(failure.code, "ios.runtime.deadline_exceeded") + } + await pause.resume.open() + await assertClosed(backend) + guard case .failed = await client.lifecycle() else { return XCTFail("Late success cannot revive the runtime") } + _ = try await client.stop() + } + + func testLastCancelledStartupWaiterClosesItsLateBackend() async throws { + let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "31") + let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) + let pause = ResourceTestPause() + let client = TeraRuntimeClient(factory: { _ in await pause.wait(); return await backend.start() }) + let start = Task { try await client.start(configuration: configuration) } + await pause.entered.wait() + start.cancel() + do { + _ = try await start.value + XCTFail("A cancelled sole waiter must not adopt the runtime") + } catch let TeraRuntimeClientError.startup(failure) { + XCTAssertEqual(failure.code, "ios.runtime.cancelled") + } + let lifecycle = await client.lifecycle() + XCTAssertEqual(lifecycle, .stopped) + await pause.resume.open() + await assertClosed(backend) + _ = try await client.stop() + let count = await backend.shutdownCount + XCTAssertEqual(count, 1) + } + + func testLateSupersededStartupCannotCloseNewRuntime() async throws { + let firstConfig = TeraRuntimeClientTests().makeConfiguration(generation: "32") + let nextConfig = TeraRuntimeClientTests().makeConfiguration(generation: "33") + let old = ResourceTestBackend(publicKeyHex: firstConfig.publicKeyHex) + let current = ResourceTestBackend(publicKeyHex: nextConfig.publicKeyHex) + let pause = ResourceTestPause() + let client = TeraRuntimeClient(factory: { configuration in + if configuration == firstConfig { + await pause.wait(); return await old.start() + } + return await current.start() + }) + let first = Task { try await client.start(configuration: firstConfig) } + await pause.entered.wait() + _ = try await client.start(configuration: nextConfig) + do { + _ = try await first.value + XCTFail("Old startup must be superseded") + } catch { + XCTAssertEqual(error as? TeraRuntimeClientError, .superseded) + } + await pause.resume.open() + await assertClosed(old) + let snapshot = try await client.snapshot() + let activeCloses = await current.shutdownCount + XCTAssertEqual(snapshot.identity.publicKeyHex, nextConfig.publicKeyHex) + XCTAssertEqual(activeCloses, 0) + _ = try await client.stop() + } + + func testLateSubscriptionAfterCancellationDetachesOnce() async throws { + try await assertLateSubscription(termination: .cancel) + } + + func testLateSubscriptionAfterTimeoutDetachesOnce() async throws { + try await assertLateSubscription(termination: .timeout) + } + + func testLateSubscriptionAfterStopDetachesOnce() async throws { + try await assertLateSubscription(termination: .stop) + } + + func testAdoptedSubscriptionFinishesAndDetachesOnceOnRepeatedStop() async throws { + let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "34") + let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) + let client = TeraRuntimeClient(factory: { _ in await backend.start() }) + _ = try await client.start(configuration: configuration) + let stream = try await client.changes() + await backend.emit(1) + var iterator = stream.makeAsyncIterator() + let first = await iterator.next() + XCTAssertEqual(first?.generation.rawValue, 1) + _ = try await client.stop() + _ = try await client.stop() + await backend.emit(2) + let final = await iterator.next() + let cancels = await backend.token.cancelCount + let closes = await backend.shutdownCount + XCTAssertNil(final) + XCTAssertEqual(cancels, 1) + XCTAssertEqual(closes, 1) + } + + private enum Termination { case cancel, timeout, stop } + + private func assertLateSubscription(termination: Termination) async throws { + let configuration = TeraRuntimeClientTests().makeConfiguration(generation: "35") + let backend = ResourceTestBackend(publicKeyHex: configuration.publicKeyHex) + let pause = ResourceTestPause() + await backend.pauseSubscription(pause) + let deadlines = TeraRuntimeDeadlinePolicy( + subscriptionNanoseconds: termination == .timeout ? 1_000_000 : 10_000_000_000 + ) + let client = TeraRuntimeClient(factory: { _ in await backend.start() }, deadlines: deadlines) + _ = try await client.start(configuration: configuration) + let creation = Task { try await client.changes() } + await pause.entered.wait() + switch termination { + case .cancel: creation.cancel() + case .stop: _ = try await client.stop() + case .timeout: break + } + do { + _ = try await creation.value + XCTFail("Abandoned creation must not return an observer") + } catch { + if termination == .stop { + XCTAssertEqual(error as? TeraRuntimeClientError, .superseded) + } else { + guard case let TeraRuntimeClientError.subscription(failure) = error else { throw error } + XCTAssertEqual(failure.code, termination == .timeout ? "ios.runtime.deadline_exceeded" : "ios.runtime.cancelled") + } + } + await pause.resume.open() + let detached = expectation(description: "Late token detached") + let wait = Task { await backend.token.cancelled.wait(); detached.fulfill() } + await fulfillment(of: [detached], timeout: 2) + await backend.token.cancelled.open() + await wait.value + _ = try await client.stop() + let count = await backend.token.cancelCount + XCTAssertEqual(count, 1) + } + + private func assertClosed(_ backend: ResourceTestBackend) async { + let closed = expectation(description: "Late backend closed") + let wait = Task { await backend.closed.wait(); closed.fulfill() } + await fulfillment(of: [closed], timeout: 2) + await backend.closed.open() + await wait.value + let count = await backend.shutdownCount + XCTAssertEqual(count, 1) + } +} diff --git a/TeraTests/TeraRuntimeResourceTaskTests.swift b/TeraTests/TeraRuntimeResourceTaskTests.swift @@ -0,0 +1,81 @@ +@testable import TeraApp +import XCTest + +final class TeraRuntimeResourceTaskTests: XCTestCase { + func testCompletedResourceIsCleanedOnceWhenAbandonedBeforeAdoption() async { + let cleaned = expectation(description: "Completed but unadopted resource is closed") + cleaned.assertForOverFulfill = true + let task = TeraRuntimeResourceTask<Int>( + deadlineNanoseconds: .max, cleanupDeadlineNanoseconds: .max, + operation: { .success(42) }, cleanup: { value in + XCTAssertEqual(value, 42) + cleaned.fulfill() + } + ) + guard case .completed(.success(42)) = await task.value() else { return XCTFail("Creation must complete") } + task.cancel() + task.cancel() + XCTAssertFalse(task.adopt()) + await fulfillment(of: [cleaned], timeout: 2) + } + + func testCancellationBeforeCreationCleansLateSuccessOnce() async { + let pause = ResourceTestPause() + let cleaned = expectation(description: "Late resource is closed") + cleaned.assertForOverFulfill = true + let task = TeraRuntimeResourceTask<Int>( + deadlineNanoseconds: .max, cleanupDeadlineNanoseconds: .max, + operation: { await pause.wait(); return .success(42) }, + cleanup: { _ in cleaned.fulfill() } + ) + await pause.entered.wait() + task.cancel() + task.cancel() + guard case .cancelled = await task.value() else { return XCTFail("Caller must stop waiting") } + await pause.resume.open() + await fulfillment(of: [cleaned], timeout: 2) + XCTAssertFalse(task.adopt()) + } + + func testTimeoutDuringCreationCleansLateSuccessOnce() async { + let pause = ResourceTestPause() + let cleaned = expectation(description: "Timed out resource is closed") + cleaned.assertForOverFulfill = true + let task = TeraRuntimeResourceTask<Int>( + deadlineNanoseconds: 1_000_000, cleanupDeadlineNanoseconds: .max, + operation: { await pause.wait(); return .success(42) }, + cleanup: { _ in cleaned.fulfill() } + ) + await pause.entered.wait() + guard case .timedOut = await task.value() else { return XCTFail("Deadline must end the wait") } + task.cancel() + await pause.resume.open() + await fulfillment(of: [cleaned], timeout: 2) + XCTAssertFalse(task.adopt()) + } + + func testAdoptedResourceCannotBeCleanedByAnOldWaiter() async { + let task = TeraRuntimeResourceTask<Int>( + deadlineNanoseconds: .max, cleanupDeadlineNanoseconds: .max, + operation: { .success(42) }, cleanup: { _ in XCTFail("The active owner holds this resource") } + ) + guard case .completed(.success(42)) = await task.value() else { return XCTFail("Creation must complete") } + XCTAssertTrue(task.adopt()) + XCTAssertFalse(task.adopt()) + task.cancel() + task.cancel() + guard case .completed(.success(42)) = await task.value() else { return XCTFail("Keep the useful result") } + } + + func testCreationFailureHasNoResourceToClean() async { + let failure = TeraRuntimeFailure.local(operation: "test", code: "test.failure", safeMessage: "Test failure") + let task = TeraRuntimeResourceTask<Int>( + deadlineNanoseconds: .max, cleanupDeadlineNanoseconds: .max, + operation: { .failure(failure) }, cleanup: { _ in XCTFail("No resource was created") } + ) + guard case let .completed(.failure(observed)) = await task.value() else { return XCTFail("Failure must survive") } + XCTAssertEqual(observed, failure) + task.cancel() + XCTAssertFalse(task.adopt()) + } +} diff --git a/TeraTests/TeraSessionResourceTests.swift b/TeraTests/TeraSessionResourceTests.swift @@ -0,0 +1,80 @@ +import Foundation +import RadrootsKit +@testable import TeraApp +import XCTest + +final class TeraSessionResourceTests: XCTestCase { + func testStaleStartupSnapshotDoesNotStopNewSharedSession() async throws { + let fixture = try await Fixture() + defer { fixture.state.remove() } + let initial = await fixture.session.start() + guard case .running = initial else { return XCTFail("Initial session must run") } + let pause = ResourceTestPause() + await fixture.backend.pauseSnapshot(pause) + let old = Task { await fixture.session.start() } + await pause.entered.wait() + let current = await fixture.session.start() + guard case .running = current else { return XCTFail("New session must adopt the shared runtime") } + await pause.resume.open() + _ = await old.value + try await assertRunning(fixture, phase: current) + _ = await fixture.session.stop() + } + + func testStaleIdentityReconciliationDoesNotStopNewSharedSession() async throws { + let fixture = try await Fixture() + defer { fixture.state.remove() } + let pause = ResourceTestPause() + await fixture.backend.pauseSettings(pause) + let old = Task { await fixture.session.start() } + await pause.entered.wait() + let current = await fixture.session.start() + guard case .running = current else { return XCTFail("New session must adopt the shared runtime") } + await pause.resume.open() + _ = await old.value + try await assertRunning(fixture, phase: current) + _ = await fixture.session.stop() + } + + private func assertRunning(_ fixture: Fixture, phase: TeraSessionPhase) async throws { + let observed = await fixture.session.currentPhase() + let closes = await fixture.backend.shutdownCount + let snapshot = try await fixture.client.snapshot() + XCTAssertEqual(observed, phase) + XCTAssertFalse(snapshot.isClosed) + XCTAssertEqual(closes, 0) + } + + private struct Fixture { + let state: StateFixture + let backend: ResourceTestBackend + let client: TeraRuntimeClient + let session: TeraSessionStore + + init() async throws { + state = try StateFixture() + let secure = InMemorySecureStore() + let custody = try RadrootsIdentityCustody( + configuration: RadrootsIdentityCustodyConfiguration( + namespace: "radroots_identity_v1", secretPolicy: .secureLocalSecret + ), + secureStore: secure, metadataStore: InMemoryIdentityMetadataStore(), + userPresence: AllowingUserPresence() + ) + let identity = TeraIdentityStore( + custody: custody, secureStore: secure, + servicePrefix: "org.tera.tests.session.\(UUID().uuidString.lowercased())" + ) + let created = try await identity.create() + let backend = try ResourceTestBackend(publicKeyHex: XCTUnwrap(created.publicKeyHex)) + self.backend = backend + let client = TeraRuntimeClient(factory: { _ in await backend.start() }) + self.client = client + session = TeraSessionStore( + configurationStore: TeraConfigurationStore(bootstrap: state.bootstrap, roots: state.roots), + identityStore: identity, runtimeClient: client, roots: state.roots, + protectedData: TeraProtectedDataMonitor(available: true) + ) + } + } +} diff --git a/TeraTests/TeraStateMigrationTests.swift b/TeraTests/TeraStateMigrationTests.swift @@ -620,99 +620,3 @@ final class TeraStateMigrationTests: XCTestCase { XCTAssertEqual(signatureHex.count, 128) } } - -private struct StateFixture { - let root: URL - let roots: RadrootsAppleFileRoots - let bootstrap: TeraConfigurationBootstrap - - init() throws { - root = FileManager.default.temporaryDirectory - .appendingPathComponent("radroots-state-tests-\(UUID().uuidString.lowercased())") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) - roots = try RadrootsAppleFileRoots( - appIdentifier: "org.radroots.tests", - dataRoot: root.appendingPathComponent("data"), - cacheRoot: root.appendingPathComponent("cache"), - temporaryRoot: root.appendingPathComponent("tmp") - ) - bootstrap = TeraConfigurationBootstrap( - runtimeMode: "localhost-dev", - relayURLs: ["ws://127.0.0.1:8080"], - blossomOrigins: ["http://127.0.0.1:3000"], - keychainServicePrefix: "org.radroots.tests", - bundleIdentifier: "org.radroots.tests", - appMetadata: TeraRuntimeAppMetadata( - bundleIdentifier: "org.radroots.tests", - version: "0.1.0-alpha", - buildNumber: "1", - buildSHA: nil - ) - ) - } - - func remove() { - try? FileManager.default.removeItem(at: root) - } -} - -private final class InMemorySecureStore: RadrootsSecureStore, @unchecked Sendable { - private let lock = NSLock() - private var values: [RadrootsSecureStoreKey: Data] = [:] - - func put( - _ value: Data, - for key: RadrootsSecureStoreKey, - policy _: RadrootsSecretAccessPolicy - ) throws { - lock.withLock { values[key] = value } - } - - func contains(_ key: RadrootsSecureStoreKey) throws -> Bool { - lock.withLock { values[key] != nil } - } - - func get(_ key: RadrootsSecureStoreKey) throws -> Data? { - lock.withLock { values[key] } - } - - func delete(_ key: RadrootsSecureStoreKey) throws { - lock.withLock { _ = values.removeValue(forKey: key) } - } - - func deleteNamespace(_ namespace: String) throws { - lock.withLock { values = values.filter { $0.key.namespace != namespace } } - } -} - -private final class InMemoryIdentityMetadataStore: RadrootsIdentityMetadataStore, @unchecked Sendable { - private let lock = NSLock() - private var values: [RadrootsIdentityMetadataSlot: Data] = [:] - - func data(for slot: RadrootsIdentityMetadataSlot) throws -> Data? { - lock.withLock { values[slot] } - } - - func put(_ data: Data, for slot: RadrootsIdentityMetadataSlot) throws { - lock.withLock { values[slot] = data } - } - - func delete(_ slot: RadrootsIdentityMetadataSlot) throws { - lock.withLock { _ = values.removeValue(forKey: slot) } - } -} - -private struct AllowingUserPresence: RadrootsUserPresence { - func currentStatus() async throws -> RadrootsUserPresenceStatus { - RadrootsUserPresenceStatus( - support: .deviceCredential, - biometryKind: .none, - canEvaluateDeviceCredential: true, - canEvaluateBiometrics: false - ) - } - - func verify(_ request: RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult { - RadrootsUserPresenceResult(policy: request.policy, verified: true) - } -} diff --git a/TeraTests/TeraStateTestFixtures.swift b/TeraTests/TeraStateTestFixtures.swift @@ -0,0 +1,99 @@ +import Foundation +import RadrootsKit +@testable import TeraApp + +struct StateFixture { + let root: URL + let roots: RadrootsAppleFileRoots + let bootstrap: TeraConfigurationBootstrap + + init() throws { + root = FileManager.default.temporaryDirectory + .appendingPathComponent("radroots-state-tests-\(UUID().uuidString.lowercased())") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + roots = try RadrootsAppleFileRoots( + appIdentifier: "org.radroots.tests", + dataRoot: root.appendingPathComponent("data"), + cacheRoot: root.appendingPathComponent("cache"), + temporaryRoot: root.appendingPathComponent("tmp") + ) + bootstrap = TeraConfigurationBootstrap( + runtimeMode: "localhost-dev", + relayURLs: ["ws://127.0.0.1:8080"], + blossomOrigins: ["http://127.0.0.1:3000"], + keychainServicePrefix: "org.radroots.tests", + bundleIdentifier: "org.radroots.tests", + appMetadata: TeraRuntimeAppMetadata( + bundleIdentifier: "org.radroots.tests", + version: "0.1.0-alpha", + buildNumber: "1", + buildSHA: nil + ) + ) + } + + func remove() { + try? FileManager.default.removeItem(at: root) + } +} + +final class InMemorySecureStore: RadrootsSecureStore, @unchecked Sendable { + private let lock = NSLock() + private var values: [RadrootsSecureStoreKey: Data] = [:] + + func put( + _ value: Data, + for key: RadrootsSecureStoreKey, + policy _: RadrootsSecretAccessPolicy + ) throws { + lock.withLock { values[key] = value } + } + + func contains(_ key: RadrootsSecureStoreKey) throws -> Bool { + lock.withLock { values[key] != nil } + } + + func get(_ key: RadrootsSecureStoreKey) throws -> Data? { + lock.withLock { values[key] } + } + + func delete(_ key: RadrootsSecureStoreKey) throws { + lock.withLock { _ = values.removeValue(forKey: key) } + } + + func deleteNamespace(_ namespace: String) throws { + lock.withLock { values = values.filter { $0.key.namespace != namespace } } + } +} + +final class InMemoryIdentityMetadataStore: RadrootsIdentityMetadataStore, @unchecked Sendable { + private let lock = NSLock() + private var values: [RadrootsIdentityMetadataSlot: Data] = [:] + + func data(for slot: RadrootsIdentityMetadataSlot) throws -> Data? { + lock.withLock { values[slot] } + } + + func put(_ data: Data, for slot: RadrootsIdentityMetadataSlot) throws { + lock.withLock { values[slot] = data } + } + + func delete(_ slot: RadrootsIdentityMetadataSlot) throws { + lock.withLock { _ = values.removeValue(forKey: slot) } + } +} + +struct AllowingUserPresence: RadrootsUserPresence { + func currentStatus() async throws -> RadrootsUserPresenceStatus { + RadrootsUserPresenceStatus( + support: .deviceCredential, + biometryKind: .none, + canEvaluateDeviceCredential: true, + canEvaluateBiometrics: false + ) + } + + func verify(_ request: RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult { + RadrootsUserPresenceResult(policy: request.policy, verified: true) + } +} diff --git a/project.yml b/project.yml @@ -50,6 +50,11 @@ targets: - path: TeraTests/TeraSessionGenerationTests.swift - path: TeraTests/TeraErrorRecoveryTests.swift - path: TeraTests/TeraRuntimeBoundedTaskTests.swift + - path: TeraTests/TeraRuntimeResourceFixtures.swift + - path: TeraTests/TeraRuntimeResourceTaskTests.swift + - path: TeraTests/TeraRuntimeResourceLifecycleTests.swift + - path: TeraTests/TeraSessionResourceTests.swift + - path: TeraTests/TeraStateTestFixtures.swift - path: TeraTests/TeraRootShellTests.swift - path: TeraTests/TeraStateMigrationTests.swift - path: TeraTests/TeraTodayStoreTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "37ba223303e6268d12d8581ba9a1a77a327b132122e29284bec715cca3c19369", - "ffi_provenance_sha256": "2e47dd8016a3980af7df78961e0a105cbc542cd92d372539d4ed1105c6754d55", + "ffi_provenance_sha256": "d8a97dce55ee2f58e56cc6bebc5870956df106ed7715e144f13c617be94c9d2a", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "6b5ce897d5273290febc8b831663e12dea759cb0084f182ba7949e1c593fc410", - "xcode_project_sha256": "4d8248344c4728688df5b51fc9127b08a50aab252b177956ea76a8ab31ecfb31" + "xcode_project_sha256": "33057e3e83dacfa3c9a41c270347485339da5101c217a005770022f881c1596c" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "da2fa1a2912ebae4fe1cddf0423f1565e676d9a3", + "tera_ffi_source_tree": "ee3a2c5daa71f0fb972496b44ca3be606a887c16", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -257,7 +257,7 @@ "count": 1 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 2 } ] @@ -861,6 +861,10 @@ "count": 5 }, { + "path": "TeraTests/TeraSessionResourceTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraStateMigrationTests.swift", "count": 3 } @@ -875,6 +879,10 @@ "count": 4 }, { + "path": "TeraTests/TeraSessionResourceTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraStateMigrationTests.swift", "count": 3 } @@ -909,7 +917,7 @@ "category": "shared_apple_api", "occurrences": [ { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 4 } ] @@ -919,7 +927,7 @@ "category": "shared_apple_api", "occurrences": [ { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -1051,10 +1059,18 @@ "count": 1 }, { + "path": "TeraTests/TeraSessionResourceTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraStateMigrationTests.swift", "count": 1 }, { + "path": "TeraTests/TeraStateTestFixtures.swift", + "count": 1 + }, + { "path": "TeraTests/TeraUserMessagesTests.swift", "count": 1 }, @@ -1299,7 +1315,7 @@ "category": "shared_apple_api", "occurrences": [ { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -1313,7 +1329,7 @@ "count": 2 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -1328,7 +1344,11 @@ }, { "path": "TeraTests/TeraStateMigrationTests.swift", - "count": 6 + "count": 1 + }, + { + "path": "TeraTests/TeraStateTestFixtures.swift", + "count": 5 } ] }, @@ -1567,7 +1587,7 @@ "count": 1 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -1615,7 +1635,7 @@ "count": 1 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -1629,7 +1649,7 @@ "count": 2 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 2 } ] @@ -1643,7 +1663,7 @@ "count": 2 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 2 } ] @@ -1901,7 +1921,7 @@ "category": "verification_compatibility", "occurrences": [ { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 1 } ] @@ -3501,7 +3521,7 @@ "count": 1 }, { - "path": "TeraTests/TeraStateMigrationTests.swift", + "path": "TeraTests/TeraStateTestFixtures.swift", "count": 4 } ] @@ -4047,6 +4067,10 @@ "count": 1 }, { + "path": "TeraTests/TeraSessionResourceTests.swift", + "count": 1 + }, + { "path": "TeraTests/TeraStateMigrationTests.swift", "count": 3 } diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/App\/TeraAppModel.swift","line":55,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 255 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAppModel: ObservableObject {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/App\/TeraAppModel.swift","line":71,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" init("},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":73,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 427 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":192,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 55 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func uploadInBackground("},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":580,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 580","ruleDescription":"Files should not span too many lines."},"text":"#endif"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":134,"character":15},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1696,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 1000 lines or less: currently contains 1696","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraLifecycleCoordinator.swift","line":434,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 434","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":291,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 847 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraRuntimeClient {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":353,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func start("},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":731,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 88 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":911,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func beginShutdown() -> ShutdownOperation {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":1221,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 1000 lines or less: currently contains 1221","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Runtime\/TeraRuntimeModels.swift","line":1234,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 1000 lines or less: currently contains 1234","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 741 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","line":280,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func submit() async {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","line":438,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraAddStore.swift","line":524,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func uploadPendingMedia(_ initial: TeraDraftStatus) async throws"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraAddStore.swift","line":810,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 810","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":78,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":148,"character":5},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":40,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 431 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"actor TeraSessionStore {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":70,"character":10},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" static func production("},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":182,"character":11},"ruleName":"Cyclomatic Complexity","severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 14","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":182,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 64 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":389,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func reconcileIdentity(_ identity: TeraAppIdentity) async throws {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":493,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 493","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/State\/TeraSupportingStores.swift","line":451,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 451","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Views\/TeraAddView.swift","line":4,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraAddView: View {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Views\/TeraAddView.swift","line":560,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 560","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":365,"character":1},"ruleName":"Type Body Length","severity":"error","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","line":629,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 629","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"Tera\/Views\/TeraTodayView.swift","line":493,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 493","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":847,"character":9},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 579 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor AddBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":934,"character":22},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 89 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" nonisolated static func schemas() -> [TeraAddSchema] {"},{"violation":{"ruleIdentifier":"function_parameter_count","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1371,"character":11},"ruleName":"Function Parameter Count","severity":"error","reason":"Function should have 5 parameters or less: it currently has 8","ruleDescription":"Number of function parameters should be low."},"text":" private func makeStatus("},{"violation":{"ruleIdentifier":"large_tuple","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1635,"character":15},"ruleName":"Large Tuple","severity":"error","reason":"Tuples should have at most 2 members","ruleDescription":"Tuples shouldn't have too many members. Create a custom type instead."},"text":" var counts: (enqueue: Int, retry: Int, cancel: Int, acceptedSettlement: Int) {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1784,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 1000 lines or less: currently contains 1784","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":45,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":212,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 334 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":6,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":7,"character":5},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","line":718,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 718","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":207,"character":9},"ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 400 lines or less: currently contains 487","ruleDescription":"Files should not span too many lines."},"text":"}"},{"violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":4,"character":7},"ruleName":"Type Body Length","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleDescription":"Type bodies should not span too many lines"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":174,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"ruleName":"Cyclomatic Complexity","severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 18","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"violation":{"ruleIdentifier":"cyclomatic_complexity","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"ruleName":"Cyclomatic Complexity","severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleDescription":"Complexity of function bodies should be limited."},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":767,"character":11},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {"},{"violation":{"ruleIdentifier":"function_body_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":1736,"character":10},"ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","ruleDescription":"Function bodies should not span too many lines"},"text":" static func environment("},{"violation":{"ruleIdentifier":"file_length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":2212,"character":1},"ruleName":"File Length","severity":"error","reason":"File should contain 1000 lines or less: currently contains 2212","ruleDescription":"Files should not span too many lines."},"text":"}"}] -\ No newline at end of file +[{"text":"final class TeraAppModel: ObservableObject {","violation":{"severity":"error","ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 255 lines","ruleName":"Type Body Length","location":{"file":"Tera\/App\/TeraAppModel.swift","character":7,"line":55},"ruleIdentifier":"type_body_length"}},{"text":" init(","violation":{"ruleDescription":"Function bodies should not span too many lines","severity":"error","ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","ruleIdentifier":"function_body_length","location":{"character":3,"file":"Tera\/App\/TeraAppModel.swift","line":71}}},{"text":"actor TeraAddMediaCoordinator: TeraAddMediaHandling {","violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 427 lines","ruleName":"Type Body Length","severity":"error","ruleDescription":"Type bodies should not span too many lines","location":{"line":73,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","character":1},"ruleIdentifier":"type_body_length"}},{"text":" func uploadInBackground(","violation":{"location":{"line":192,"character":3,"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift"},"severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 55 lines","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length"}},{"text":"#endif","violation":{"location":{"file":"Tera\/Runtime\/TeraAddMediaCoordinator.swift","line":580,"character":1},"ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 580","severity":"error","ruleIdentifier":"file_length","ruleName":"File Length"}},{"text":"private final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {","violation":{"ruleDescription":"Type bodies should not span too many lines","location":{"character":15,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":134},"reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 545 lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","severity":"error"}},{"text":"}","violation":{"ruleName":"File Length","severity":"error","ruleDescription":"Files should not span too many lines.","location":{"character":1,"file":"Tera\/Runtime\/TeraGeneratedRuntimeBackend.swift","line":1696},"ruleIdentifier":"file_length","reason":"File should contain 1000 lines or less: currently contains 1696"}},{"text":"}","violation":{"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","severity":"error","ruleIdentifier":"file_length","reason":"File should contain 400 lines or less: currently contains 434","location":{"character":1,"line":434,"file":"Tera\/Runtime\/TeraLifecycleCoordinator.swift"}}},{"violation":{"location":{"character":1,"line":291,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 821 lines","ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","severity":"error","ruleName":"Type Body Length"},"text":"actor TeraRuntimeClient {"},{"violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 71 lines","ruleName":"Function Body Length","location":{"character":3,"file":"Tera\/Runtime\/TeraRuntimeClient.swift","line":708},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"severity":"error","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","ruleIdentifier":"function_body_length","location":{"file":"Tera\/Runtime\/TeraRuntimeClient.swift","character":11,"line":887},"ruleName":"Function Body Length"},"text":" private func beginShutdown() -> ShutdownOperation {"},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 1197","ruleName":"File Length","severity":"error","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","location":{"line":1197,"character":1,"file":"Tera\/Runtime\/TeraRuntimeClient.swift"}},"text":"}"},{"violation":{"severity":"error","ruleName":"File Length","location":{"character":1,"line":1234,"file":"Tera\/Runtime\/TeraRuntimeModels.swift"},"reason":"File should contain 1000 lines or less: currently contains 1234","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length"},"text":"}"},{"violation":{"severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 741 lines","location":{"file":"Tera\/State\/TeraAddStore.swift","line":11,"character":7},"ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","ruleIdentifier":"type_body_length"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","location":{"file":"Tera\/State\/TeraAddStore.swift","line":280,"character":3},"severity":"error","ruleIdentifier":"function_body_length"},"text":" func submit() async {"},{"violation":{"ruleName":"Function Body Length","location":{"character":3,"line":438,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 51 lines"},"text":" func retract(_ card: TeraTodayCard) async {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","location":{"line":524,"character":11,"file":"Tera\/State\/TeraAddStore.swift"},"ruleIdentifier":"function_body_length","ruleName":"Function Body Length","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 60 lines"},"text":" private func uploadPendingMedia(_ initial: TeraDraftStatus) async throws"},{"violation":{"ruleDescription":"Files should not span too many lines.","severity":"error","ruleIdentifier":"file_length","location":{"line":810,"file":"Tera\/State\/TeraAddStore.swift","character":1},"reason":"File should contain 400 lines or less: currently contains 810","ruleName":"File Length"},"text":"}"},{"violation":{"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","ruleName":"Type Body Length","ruleDescription":"Type bodies should not span too many lines","severity":"error","location":{"line":78,"character":1,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"type_body_length"},"text":"actor TeraConfigurationStore {"},{"violation":{"reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","ruleName":"Function Body Length","ruleDescription":"Function bodies should not span too many lines","severity":"error","location":{"line":148,"character":5,"file":"Tera\/State\/TeraConfigurationStore.swift"},"ruleIdentifier":"function_body_length"},"text":" func load() throws -> TeraAppConfiguration {"},{"text":"}","violation":{"location":{"file":"Tera\/State\/TeraConfigurationStore.swift","line":759,"character":1},"reason":"File should contain 400 lines or less: currently contains 759","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","severity":"error","ruleIdentifier":"file_length"}},{"text":"actor TeraSessionStore {","violation":{"severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"character":1,"file":"Tera\/State\/TeraSessionStore.swift","line":40},"reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 430 lines"}},{"text":" static func production(","violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"line":70,"character":10,"file":"Tera\/State\/TeraSessionStore.swift"},"severity":"error"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"ruleName":"Cyclomatic Complexity","ruleIdentifier":"cyclomatic_complexity","reason":"Function should have complexity 10 or less; currently complexity is 14","ruleDescription":"Complexity of function bodies should be limited.","location":{"line":182,"character":11,"file":"Tera\/State\/TeraSessionStore.swift"},"severity":"error"}},{"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {","violation":{"severity":"error","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","location":{"file":"Tera\/State\/TeraSessionStore.swift","line":182,"character":11},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 64 lines","ruleDescription":"Function bodies should not span too many lines"}},{"text":" private func reconcileIdentity(_ identity: TeraAppIdentity) async throws {","violation":{"ruleName":"Function Body Length","ruleIdentifier":"function_body_length","location":{"character":11,"file":"Tera\/State\/TeraSessionStore.swift","line":388},"ruleDescription":"Function bodies should not span too many lines","severity":"error","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 492","severity":"error","ruleName":"File Length","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","location":{"line":492,"character":1,"file":"Tera\/State\/TeraSessionStore.swift"}}},{"text":"}","violation":{"ruleDescription":"Files should not span too many lines.","severity":"error","reason":"File should contain 400 lines or less: currently contains 451","ruleIdentifier":"file_length","ruleName":"File Length","location":{"line":451,"character":1,"file":"Tera\/State\/TeraSupportingStores.swift"}}},{"text":"struct TeraAddView: View {","violation":{"ruleDescription":"Type bodies should not span too many lines","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":4},"reason":"Struct body should span 350 lines or less excluding comments and whitespace: currently spans 454 lines","ruleName":"Type Body Length","severity":"error","ruleIdentifier":"type_body_length"}},{"text":"}","violation":{"ruleIdentifier":"file_length","severity":"error","ruleName":"File Length","ruleDescription":"Files should not span too many lines.","location":{"file":"Tera\/Views\/TeraAddView.swift","character":1,"line":560},"reason":"File should contain 400 lines or less: currently contains 560"}},{"text":"struct TeraSettingsView: View {","violation":{"ruleName":"Type Body Length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":365},"ruleIdentifier":"type_body_length","ruleDescription":"Type bodies should not span too many lines","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","severity":"error"}},{"text":"}","violation":{"ruleName":"File Length","location":{"file":"Tera\/Views\/TeraSupportingViews.swift","character":1,"line":629},"ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","reason":"File should contain 400 lines or less: currently contains 629","severity":"error"}},{"text":"}","violation":{"ruleIdentifier":"file_length","severity":"error","ruleName":"File Length","location":{"file":"Tera\/Views\/TeraTodayView.swift","line":493,"character":1},"reason":"File should contain 400 lines or less: currently contains 493","ruleDescription":"Files should not span too many lines."}},{"text":"final class TeraAddStoreTests: XCTestCase {","violation":{"ruleDescription":"Type bodies should not span too many lines","severity":"error","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 665 lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":6,"character":7}}},{"text":"private actor AddBackend: TeraRuntimeBackend {","violation":{"ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":9,"line":847},"ruleIdentifier":"type_body_length","severity":"error","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 579 lines"}},{"text":" nonisolated static func schemas() -> [TeraAddSchema] {","violation":{"ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 89 lines","ruleName":"Function Body Length","severity":"error","ruleIdentifier":"function_body_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","character":22,"line":934}}},{"text":" private func makeStatus(","violation":{"ruleDescription":"Number of function parameters should be low.","ruleIdentifier":"function_parameter_count","ruleName":"Function Parameter Count","reason":"Function should have 5 parameters or less: it currently has 8","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1371,"character":11},"severity":"error"}},{"text":" var counts: (enqueue: Int, retry: Int, cancel: Int, acceptedSettlement: Int) {","violation":{"ruleDescription":"Tuples shouldn't have too many members. Create a custom type instead.","location":{"line":1635,"character":15,"file":"TeraTests\/TeraAddStoreTests.swift"},"ruleIdentifier":"large_tuple","reason":"Tuples should have at most 2 members","ruleName":"Large Tuple","severity":"error"}},{"text":"}","violation":{"ruleDescription":"Files should not span too many lines.","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1784","severity":"error","ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraAddStoreTests.swift","line":1784,"character":1}}},{"text":"final class TeraRemoteQualificationTests: XCTestCase {","violation":{"severity":"error","ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","location":{"file":"TeraTests\/TeraRemoteQualificationTests.swift","line":7,"character":7},"reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines"}},{"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {","violation":{"location":{"line":45,"character":3,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 94 lines","ruleIdentifier":"function_body_length","severity":"error","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length"}},{"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {","violation":{"ruleName":"Function Body Length","location":{"character":3,"line":212,"file":"TeraTests\/TeraRemoteQualificationTests.swift"},"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 53 lines","severity":"error"}},{"text":"final class TeraRuntimeClientTests: XCTestCase {","violation":{"ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraRuntimeClientTests.swift","line":4},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 334 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":"final class TeraStateMigrationTests: XCTestCase {","violation":{"ruleName":"Type Body Length","location":{"character":7,"file":"TeraTests\/TeraStateMigrationTests.swift","line":6},"severity":"error","ruleIdentifier":"type_body_length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","ruleDescription":"Type bodies should not span too many lines"}},{"text":" func testRelayValidationMatchesRuntimeProfiles() throws {","violation":{"location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":5,"line":7},"ruleDescription":"Function bodies should not span too many lines","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 61 lines","ruleIdentifier":"function_body_length","severity":"error","ruleName":"Function Body Length"}},{"text":"}","violation":{"reason":"File should contain 400 lines or less: currently contains 622","ruleName":"File Length","severity":"error","ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","location":{"file":"TeraTests\/TeraStateMigrationTests.swift","character":1,"line":622}}},{"text":"private actor SupportingBackend: TeraRuntimeBackend {","violation":{"ruleIdentifier":"type_body_length","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","character":9,"line":207},"ruleDescription":"Type bodies should not span too many lines","ruleName":"Type Body Length","severity":"error","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 253 lines"}},{"text":"}","violation":{"severity":"error","location":{"file":"TeraTests\/TeraSupportingStoreTests.swift","line":487,"character":1},"reason":"File should contain 400 lines or less: currently contains 487","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","ruleName":"File Length"}},{"text":"final class TeraRemoteQualificationUITests: XCTestCase {","violation":{"ruleDescription":"Type bodies should not span too many lines","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1493 lines","ruleIdentifier":"type_body_length","location":{"line":4,"character":7,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"ruleName":"Type Body Length","severity":"error"}},{"text":" func testLocalSocialFiveFlowScenario() throws {","violation":{"ruleIdentifier":"function_body_length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 73 lines","ruleName":"Function Body Length","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":62,"character":3},"ruleDescription":"Function bodies should not span too many lines","severity":"error"}},{"text":" func testLocalSocialDeterministicPersonas() throws {","violation":{"ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","severity":"error","location":{"line":174,"character":3,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"},"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines"}},{"text":" func testRemoteBlossomUploadAndRecovery() throws {","violation":{"location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":247,"character":3},"severity":"error","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","ruleName":"Function Body Length","reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 54 lines"}},{"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {","violation":{"ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","ruleName":"Cyclomatic Complexity","location":{"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","line":573,"character":11},"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 18"}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"severity":"error","reason":"Function should have complexity 10 or less; currently complexity is 11","ruleIdentifier":"cyclomatic_complexity","ruleDescription":"Complexity of function bodies should be limited.","ruleName":"Cyclomatic Complexity","location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11}}},{"text":" private func preparePhotoUpdate(_ app: XCUIApplication, marker: String) -> XCUIElement? {","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 66 lines","severity":"error","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","location":{"line":767,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":11},"ruleName":"Function Body Length"}},{"text":" static func environment(","violation":{"reason":"Function body should span 50 lines or less excluding comments and whitespace: currently spans 67 lines","severity":"error","ruleIdentifier":"function_body_length","ruleDescription":"Function bodies should not span too many lines","location":{"line":1736,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift","character":10},"ruleName":"Function Body Length"}},{"violation":{"reason":"File should contain 1000 lines or less: currently contains 2212","severity":"error","ruleIdentifier":"file_length","ruleDescription":"Files should not span too many lines.","ruleName":"File Length","location":{"line":2212,"character":1,"file":"TeraUITests\/TeraRemoteQualificationUITests.swift"}},"text":"}"}] +\ No newline at end of file