commit fdf07f5c74c8379b8f653fae64e50ef7df1980a8 parent 601aaa13d10c5486716b2a42e1301a1953ff5a65 Author: triesap <tyson@radroots.org> Date: Mon, 21 Sep 2026 23:36:39 +0000 ios: report evidence-backed publication stages - Scope completion to the saved relay policy - Preserve mixed and stopped publication evidence - Align accessible actions with typed retry decisions - Verify all stages without changing persisted authority Diffstat:
19 files changed, 371 insertions(+), 121 deletions(-)
diff --git a/Package.swift b/Package.swift @@ -58,6 +58,7 @@ let package = Package( "Runtime/TeraNativeUploadReconciliation.swift", "Runtime/TeraStoppedUploadRecovery.swift", "Runtime/TeraPublicationEvidence.swift", + "Runtime/TeraPublicationStage.swift", "Runtime/TeraPublicationTargets.swift", "Runtime/TeraPublicationRetry.swift", "Runtime/TeraBackgroundUploadWaiter.swift", @@ -148,6 +149,7 @@ let package = Package( "State/TeraAddStore.swift", "State/TeraAddSelectionActions.swift", "State/TeraAddAffordances.swift", + "State/TeraPublicationActions.swift", "State/TeraRevisionPreparation.swift", "State/TeraAddCardIntents.swift", "State/TeraEditingProtection.swift", diff --git a/README.md b/README.md @@ -334,3 +334,5 @@ does not extend the saved delivery window. Review the device clock and current revision before choosing another intentional request. Civil calendar dates remain unchanged. This local policy cannot establish globally accurate time or prevent an unseen remote replacement. + +Publication stage labels report saved local, signing, media and relay facts. Completion means the operation met its saved relay policy; it does not mean every relay accepted it or will retain it permanently. Mixed legacy settlement counts remain visible alongside the current stage, including pending, unknown, exhausted, failed and stopped work. Stopping local work preserves remote evidence, and retraction is a deletion request rather than proof of erasure. Submit and continuation controls follow the saved typed retry decision; status checks remain available without authorizing new effects. diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ 9B37084901EC52EB44969304 /* TeraRuntimeResourceLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226F5C2370522AAFAD9D1C3D /* TeraRuntimeResourceLifecycleTests.swift */; }; A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */; }; A65623800C28B2FEEFB49780 /* TeraEditingProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F27D9EF66493E92AFDD53213 /* TeraEditingProtectionTests.swift */; }; + A92BADDE348A4D5C56EDE2A1 /* TeraPublicationStageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635EB4A552C563E85557A485 /* TeraPublicationStageTests.swift */; }; AA7C3FC1F46B6723A39B1D09 /* TeraTodayPresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BE248D1450D76476255626 /* TeraTodayPresentationTests.swift */; }; AA8B25877C9A3BD331B1B1AE /* TeraPublicationRetryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F7B42264CD896F76189F33 /* TeraPublicationRetryTests.swift */; }; AC33144E09CB0384C4FB090C /* TeraObservationCoalescingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD785439F415278214D746 /* TeraObservationCoalescingTests.swift */; }; @@ -200,6 +201,7 @@ 5EB21C33D6173E4FED05FDA0 /* TeraTodayReconciliationTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayReconciliationTaskTests.swift; sourceTree = "<group>"; }; 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaStoreTests.swift; sourceTree = "<group>"; }; 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraBackgroundUploadFixtures.swift; sourceTree = "<group>"; }; + 635EB4A552C563E85557A485 /* TeraPublicationStageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraPublicationStageTests.swift; sourceTree = "<group>"; }; 649F88AE533C3BEEBC466894 /* TeraOfflineMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraOfflineMediaTests.swift; sourceTree = "<group>"; }; 67ACCE25979BA86C1EC5BFD6 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 6967B3EAC4CF1A410B26DD1A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; @@ -399,6 +401,7 @@ 7AF60EF9DCFDC4ECE77AED67 /* TeraProductStartupTests.swift */, 3B954B7006CC5904C98138C6 /* TeraPublicationEvidenceTests.swift */, 56F7B42264CD896F76189F33 /* TeraPublicationRetryTests.swift */, + 635EB4A552C563E85557A485 /* TeraPublicationStageTests.swift */, 293D968D4E2ACA3BA414BF8D /* TeraPublicationTargetTests.swift */, 156F07F1F2961FDF6539099A /* TeraRecoveryCompletionTests.swift */, BB4F87BA96C92630FE795A9D /* TeraRecoveryContinuationTests.swift */, @@ -717,6 +720,7 @@ C46F8EB5B196FFDD37E0279A /* TeraProductStartupTests.swift in Sources */, 093A6AC6C9B724E465507004 /* TeraPublicationEvidenceTests.swift in Sources */, AA8B25877C9A3BD331B1B1AE /* TeraPublicationRetryTests.swift in Sources */, + A92BADDE348A4D5C56EDE2A1 /* TeraPublicationStageTests.swift in Sources */, 4D24610694C7F80F7B5DA904 /* TeraPublicationTargetTests.swift in Sources */, 36AB69B9F8DB9AADB8124795 /* TeraRecoveryCompletionTests.swift in Sources */, E8F853EE5ECE3EC16384A56D /* TeraRecoveryContinuationTests.swift in Sources */, diff --git a/Tera/Runtime/TeraDraftInventoryModels.swift b/Tera/Runtime/TeraDraftInventoryModels.swift @@ -51,7 +51,7 @@ struct TeraLegacyDraftSummary: Sendable, Equatable, Identifiable { if revisionParentID != nil { return "Retraction belongs to a saved revision." } - return settlement?.summary ?? state.label + return state.summary(settlement: settlement) } var mediaSummary: String { diff --git a/Tera/Runtime/TeraDraftStatus.swift b/Tera/Runtime/TeraDraftStatus.swift @@ -41,6 +41,6 @@ struct TeraDraftStatus: Sendable, Equatable, Hashable, Identifiable { if revisionParentID != nil { return "Retraction belongs to a saved revision." } - return settlement?.summary ?? state.label + return state.summary(settlement: settlement) } } diff --git a/Tera/Runtime/TeraPublicationStage.swift b/Tera/Runtime/TeraPublicationStage.swift @@ -0,0 +1,84 @@ +import Foundation + +enum TeraOutboxState: String, CaseIterable, Sendable, Equatable, Hashable { + case draft + case mediaPreparing + case mediaUploading + case readyToSign + case signing + case signed + case queued + case delivering + case partiallyDelivered + case retryable + case terminal + case cancelled + case complete + + var isEditable: Bool { + self == .draft || self == .mediaPreparing + } + + var canAdvance: Bool { + self == .queued || self == .retryable || self == .partiallyDelivered + } + + var canCancel: Bool { + ![.cancelled, .complete, .terminal].contains(self) + } + + var label: String { + switch self { + case .draft: "Saved on this device." + case .mediaPreparing: "Preparing photo." + case .mediaUploading: "Photo upload awaiting verification." + case .readyToSign, .signing: "Awaiting signing." + case .signed: "Signed; local admission is pending." + case .queued: "Queued for the saved relays." + case .delivering: "Sending to the saved relays." + case .partiallyDelivered: "Partially delivered; review the saved relay outcomes." + case .retryable: "Saved for retry." + case .terminal: "Publication needs attention." + case .cancelled: "Local work stopped. Recorded remote effects are retained." + case .complete: "Delivery completed for the saved relay policy." + } + } + + func summary(settlement: TeraOperationSettlement?) -> String { + guard let settlement, !settlement.summary.isEmpty else { return label } + return "\(label) \(settlement.summary)" + } +} + +extension TeraOperationSettlement { + /// Counts are retained facts, not a global publication or erasure receipt. + /// A satisfied plan must never hide other pending or uncertain work. + var summary: String { + var facts: [String] = [] + if deliverySatisfied > 0 { + facts.append("Saved relay requirements met for \(deliverySatisfied) of \(deliveryPlans) delivery plans.") + } + if indeterminate > 0 { + facts.append("\(indeterminate) artifact outcomes remain unknown.") + } + if deliveryPending > 0 || pending > 0 { + facts.append("Work remains pending.") + } + if deliveryRetryable > 0 || retryable > 0 { + facts.append("Work is saved for retry; current permissions must be checked.") + } + if deliveryExhausted > 0 { + facts.append("A saved delivery limit was reached; review is needed.") + } + if failedTerminal > 0 || deliveryFailedTerminal > 0 { + facts.append("Some work needs attention after failure.") + } + if cancelled > 0 || deliveryCancelled > 0 { + facts.append("Some local work was stopped; recorded effects are retained.") + } + if signed > 0 { + facts.append("\(signed) signed artifacts retained; \(admitted) admitted locally.") + } + return facts.joined(separator: " ") + } +} diff --git a/Tera/Runtime/TeraRuntimeModels.swift b/Tera/Runtime/TeraRuntimeModels.swift @@ -310,7 +310,7 @@ struct TeraProfileStatus: Sendable, Equatable, Identifiable { let settlement: TeraOperationSettlement? var honestSummary: String { - settlement?.summary ?? state.label + state.summary(settlement: settlement) } } @@ -957,40 +957,6 @@ enum TeraDraftKind: String, Sendable, Equatable, Hashable { case retraction } -enum TeraOutboxState: String, Sendable, Equatable, Hashable { - case draft - case mediaPreparing - case mediaUploading - case readyToSign - case signing - case signed - case queued - case delivering - case partiallyDelivered - case retryable - case terminal - case cancelled - case complete - - var isEditable: Bool { - self == .draft || self == .mediaPreparing - } - - var canAdvance: Bool { - self == .queued || self == .retryable || self == .partiallyDelivered - } - - var canCancel: Bool { - ![.cancelled, .complete, .terminal].contains(self) - } - - var label: String { - rawValue - .replacingOccurrences(of: "([a-z])([A-Z])", with: "$1 $2", options: .regularExpression) - .capitalized - } -} - enum TeraDraftMediaStage: String, Sendable, Equatable, Hashable { case pending case preparing @@ -1027,28 +993,6 @@ struct TeraOperationSettlement: Sendable, Equatable, Hashable { let deliveryExhausted: UInt16 let deliveryFailedTerminal: UInt16 let deliveryCancelled: UInt16 - - var summary: String { - if deliverySatisfied > 0 { - return "Delivered" - } - if deliveryRetryable > 0 || retryable > 0 { - return "Saved; delivery can be retried" - } - if indeterminate > 0 { - return "Delivery outcome is not yet known" - } - if failedTerminal > 0 || deliveryFailedTerminal > 0 { - return "Delivery failed" - } - if admitted > 0 { - return "Published locally; relay delivery is pending" - } - if signed > 0 { - return "Signed; local publication is pending" - } - return "Waiting" - } } struct TeraRetractionDraftInput: Sendable, Equatable, Hashable { diff --git a/Tera/Runtime/TeraSubmissionOperationModels.swift b/Tera/Runtime/TeraSubmissionOperationModels.swift @@ -61,20 +61,11 @@ struct TeraSubmissionStatus: Sendable, Equatable, Identifiable, CustomStringConv if case .needsAction = retry, delivery.state != .accepted { return "Saved publication needs attention." } - switch state { - case .draft: return "Saved on this device." - case .mediaPreparing: return "Preparing photo." - case .mediaUploading: return "Photo upload awaiting verification." - case .readyToSign, .signing: return "Awaiting signing." - case .signed: return "Signed; local admission is pending." - case .queued: return "Queued for the saved relays." - case .delivering: return "Sending to the saved relays." - case .partiallyDelivered: return "Partially delivered." - case .retryable: return "Saved for retry." - case .terminal: return "Delivery needs attention." - case .cancelled: return "Local work stopped. Recorded remote effects are retained." - case .complete: return "Delivery completed for the saved relay policy." - } + return state.label + } + + var canOfferStop: Bool { + !delivery.isStopped && (state.canCancel || delivery.unresolvedClaims) } /// Presentation of an explicit action, never authority to bypass Rust policy. diff --git a/Tera/State/TeraAddAffordances.swift b/Tera/State/TeraAddAffordances.swift @@ -24,7 +24,10 @@ extension TeraAddStore { } var canSubmit: Bool { - isProductReady && !isWorking && !submissions.isWorking + if activeDraft == nil, submissions.hasAction { + return isProductReady && !isWorking && submissions.canContinue + } + return isProductReady && !isWorking && !submissions.isWorking && activeDraft?.coordinateWritable != false && (activeDraft?.isRevision == true || activeDraft?.canAdvance == true || activeDraft?.canQueue == true diff --git a/Tera/State/TeraPublicationActions.swift b/Tera/State/TeraPublicationActions.swift @@ -0,0 +1,69 @@ +import Foundation + +extension TeraSubmissionStore { + var canContinue: Bool { + hasAction && !isWorking && status?.canOfferContinuation != false + } + + var actionLabel: String { + if isWorking { + return "Submitting captured form…" + } + if status?.canOfferContinuation != false { + return "Continue original submission" + } + switch status?.retry { + case .complete: return "Delivery policy complete" + case .stopped: return "Publication stopped" + case .deferredUntil, .inFlightUntil: return "Publication waiting" + case .needsAction: return "Publication needs attention" + default: return "Review saved submission" + } + } + + var actionExplanation: String { + [status?.summary, status?.retry.explanation, message] + .compactMap(\.self).joined(separator: " ") + } +} + +extension TeraAddStore { + var submitAccessibilityValue: String { + if activeDraft == nil, submissions.hasAction { + let message = submissions.actionExplanation + if let code = submissions.failureCode { + return "\(message) Error code \(code)" + } + return message + } + if isWorking { + return "Working" + } + if let message { + if let code = lastFailureCode { + return "\(message) Error code \(code)" + } + return message + } + if let activeDraft { + return activeDraft.honestSummary + } + return canSubmit ? "Ready" : "Unavailable" + } + + var submitLabel: String { + if activeDraft == nil, submissions.hasAction { + return submissions.actionLabel + } + if activeDraft?.kind == .retraction { + return "Retry retraction" + } + if activeDraft?.coordinateWritable == false { + return "Publication held" + } + if activeDraft?.canAdvance == true { + return "Retry delivery" + } + return "Submit" + } +} diff --git a/Tera/Views/TeraAddSubmitButton.swift b/Tera/Views/TeraAddSubmitButton.swift @@ -7,53 +7,14 @@ struct TeraAddSubmitButton: View { Button { Task { await store.submit() } } label: { - Text(submitLabel) + Text(store.submitLabel) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity) } .accessibilityIdentifier("radroots.add.submit") - .accessibilityValue(submitAccessibilityValue) + .accessibilityValue(store.submitAccessibilityValue) .buttonStyle(.borderedProminent) .tint(.primary) .disabled(!store.canSubmit) } - - private var submitAccessibilityValue: String { - if store.activeDraft == nil, store.submissions.hasAction { - let message = store.submissions.message ?? store.submissions.status?.summary ?? "Original submission in progress" - if let code = store.submissions.failureCode { - return "\(message) Error code \(code)" - } - return message - } - if store.isWorking { - return "Working" - } - if let message = store.message { - if let code = store.lastFailureCode { - return "\(message) Error code \(code)" - } - return message - } - if let activeDraft = store.activeDraft { - return activeDraft.honestSummary - } - return store.canSubmit ? "Ready" : "Unavailable" - } - - private var submitLabel: String { - if store.activeDraft == nil, store.submissions.hasAction { - return store.submissions.isWorking ? "Submitting captured form…" : "Retry original submission" - } - if store.activeDraft?.kind == .retraction { - return "Retry retraction" - } - if store.activeDraft?.coordinateWritable == false { - return "Publication held" - } - if store.activeDraft?.canAdvance == true { - return "Retry delivery" - } - return "Submit" - } } diff --git a/Tera/Views/TeraAddView.swift b/Tera/Views/TeraAddView.swift @@ -41,7 +41,7 @@ struct TeraAddView: View { if store.activeDraft?.kind == .retraction { Section("Retraction") { - Text("This saved operation removes one of your published posts.") + Text("This saved operation requests deletion of one of your posts. Remote erasure is not guaranteed.") .foregroundStyle(.secondary) } } else { diff --git a/Tera/Views/TeraRevisionDetailView.swift b/Tera/Views/TeraRevisionDetailView.swift @@ -60,7 +60,7 @@ struct TeraRevisionDetailView: View { private func branch(_ title: String, draft: TeraDraftStatus, progress: TeraRevisionBranchStatus) -> some View { Section(title) { LabeledContent("Saved operation", value: draft.id).textSelection(.enabled) - Text(draft.settlement?.summary ?? draft.state.label) + Text(draft.state.summary(settlement: draft.settlement)) if progress.stopped { Text("Further local work is stopped; retained relay evidence remains visible.") } diff --git a/Tera/Views/TeraSubmissionViews.swift b/Tera/Views/TeraSubmissionViews.swift @@ -6,7 +6,7 @@ struct TeraSubmissionStatusView: View { var body: some View { if store.hasAction { Section("Submission") { - if store.status?.delivery.isStopped != true { + if store.status?.canOfferStop != false { Button("Stop publication") { Task { await store.requestStop() } } .accessibilityIdentifier("tera.add.submission.stop") } @@ -21,7 +21,7 @@ struct TeraSubmissionStatusView: View { Button("Check saved submission status") { Task { await store.refreshSelected() } } .accessibilityIdentifier("tera.add.submission.check") } - if store.status?.canOfferContinuation != false { + if store.canContinue { Button("Continue original submission") { Task { await store.continueSelected() } } .accessibilityIdentifier("tera.add.submission.continue") } diff --git a/TeraTests/TeraPublicationStageTests.swift b/TeraTests/TeraPublicationStageTests.swift @@ -0,0 +1,183 @@ +import Foundation +@testable import TeraApp +import XCTest + +@MainActor +final class TeraPublicationStageTests: XCTestCase { + func testEveryDurablePhaseHasScopedStageAndEligibleActions() async throws { + let backend = AddBackend(advanceOffline: true) + let client = try await TeraAddStoreTests.startedClient(backend) + let store = TeraAddStore(runtimeClient: client) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.updateForm(\.content, "Saved original") + await store.submit() + let original = try XCTUnwrap(store.submissions.status) + let expected: [(TeraOutboxState, String)] = [ + (.draft, "Saved on this device."), (.mediaPreparing, "Preparing photo."), + (.mediaUploading, "Photo upload awaiting verification."), + (.readyToSign, "Awaiting signing."), (.signing, "Awaiting signing."), + (.signed, "Signed; local admission is pending."), (.queued, "Queued for the saved relays."), + (.delivering, "Sending to the saved relays."), + (.partiallyDelivered, "Partially delivered; review the saved relay outcomes."), + (.retryable, "Saved for retry."), (.terminal, "Publication needs attention."), + (.cancelled, "Local work stopped. Recorded remote effects are retained."), + (.complete, "Delivery completed for the saved relay policy."), + ] + XCTAssertEqual(Set(expected.map(\.0)), Set(TeraOutboxState.allCases)) + for (state, label) in expected { + let value = replacing(original, state: state) + XCTAssertEqual(value.summary, label) + XCTAssertEqual(state.summary(settlement: nil), label) + XCTAssertEqual(value.canOfferContinuation, ![.complete, .cancelled, .terminal].contains(state)) + XCTAssertEqual(value.canOfferStop, state.canCancel) + } + store.stop() + _ = try await client.stop() + } + + func testLegacySatisfiedPlanDoesNotHideMixedOrStoppedEffects() { + let settlement = TeraOperationSettlement( + artifacts: 7, signed: 4, admitted: 3, pending: 1, retryable: 1, + indeterminate: 1, failedTerminal: 1, cancelled: 1, + deliveryPlans: 6, deliverySatisfied: 1, deliveryPending: 1, + deliveryRetryable: 1, deliveryExhausted: 1, deliveryFailedTerminal: 1, deliveryCancelled: 1 + ) + let summary = TeraOutboxState.cancelled.summary(settlement: settlement) + for retained in ["Local work stopped", "1 of 6", "unknown", "pending", "saved for retry", + "limit was reached", "failure", "effects are retained", "4 signed", "3 admitted locally"] + { + XCTAssertTrue(summary.contains(retained), retained) + } + XCTAssertFalse(summary.contains("Delivery completed")) + XCTAssertFalse(summary.contains("Published")) + let draft = TeraDraftStatus( + id: "draft", revision: 1, authorPublicKey: "author", kind: .add, commandType: .createUpdate, + form: nil, state: .cancelled, cardID: "card", operationID: "operation", + createdAtUnixMilliseconds: 1, updatedAtUnixMilliseconds: 2, + media: [], settlement: settlement, isRevision: false + ) + XCTAssertEqual(draft.honestSummary, summary) + let legacy = TeraLegacyDraftSummary( + id: draft.id, revision: 1, kind: .add, commandType: .createUpdate, state: .cancelled, + hasForm: false, isRevision: false, createdAtUnixMilliseconds: 1, updatedAtUnixMilliseconds: 2, + mediaCount: 0, verifiedMediaCount: 0, possibleOrphanCount: 0, settlement: settlement + ) + XCTAssertEqual(legacy.honestSummary, summary) + } + + func testMainSubmitAndAccessibleStatusHonorEverySavedHoldWithoutEffects() async throws { + let backend = AddBackend(advanceOffline: true) + let client = try await TeraAddStoreTests.startedClient(backend) + let store = TeraAddStore(runtimeClient: client) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.updateForm(\.content, "Saved original") + await store.submit() + let original = try XCTUnwrap(store.submissions.status) + let before = await backend.submissionBackend.advanceCount + let decisions: [(TeraPublicationRetry, String)] = [ + (.deferredUntil(1), "Publication waiting"), (.inFlightUntil(1), "Publication waiting"), + (.needsAction(.deadlineExceeded), "Publication needs attention"), + (.needsAction(.attemptLimit), "Publication needs attention"), + (.needsAction(.coordinateChanged), "Publication needs attention"), + (.needsAction(.authenticationRequired), "Publication needs attention"), + (.needsAction(.quotaExceeded), "Publication needs attention"), + (.needsAction(.invalidPayload), "Publication needs attention"), + (.needsAction(.deliveryRefused), "Publication needs attention"), + ] + for (retry, label) in decisions { + let value = replacing(original, retry: retry) + await backend.submissionBackend.installPresentationFixture(value) + await store.submissions.refreshSelected() + XCTAssertFalse(store.canSubmit) + XCTAssertFalse(store.submissions.canContinue) + XCTAssertEqual(store.submitLabel, label) + XCTAssertTrue(try store.submitAccessibilityValue.contains(XCTUnwrap(retry.explanation))) + await store.submit() + await store.submissions.continueSelected() + XCTAssertEqual(store.submissions.status?.request, original.request) + XCTAssertEqual(store.submissions.status?.captured, original.captured) + } + let after = await backend.submissionBackend.advanceCount + XCTAssertEqual(after, before) + await backend.submissionBackend.installPresentationFixture(original) + await store.submissions.refreshSelected() + XCTAssertTrue(store.canSubmit) + XCTAssertEqual(store.submitLabel, "Continue original submission") + store.stop() + _ = try await client.stop() + } + + func testAcceptedCompletionAndStoppedEffectsRemainAccessibleWithoutRetry() async throws { + let backend = AddBackend() + let client = try await TeraAddStoreTests.startedClient(backend) + let store = TeraAddStore(runtimeClient: client) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.updateForm(\.content, "Saved original") + await store.submit() + XCTAssertEqual(store.submissions.status?.state, .complete) + XCTAssertFalse(store.canSubmit) + XCTAssertEqual(store.submitLabel, "Delivery policy complete") + XCTAssertTrue(store.submitAccessibilityValue.contains("saved relay policy")) + XCTAssertFalse(try XCTUnwrap(store.submissions.status).canOfferStop) + // A previously requested stop can arrive after acceptance; preserve both. + await store.submissions.requestStop() + XCTAssertFalse(store.canSubmit) + XCTAssertEqual(store.submitLabel, "Publication stopped") + XCTAssertTrue(store.submitAccessibilityValue.contains("Stopped")) + XCTAssertTrue(store.submitAccessibilityValue.contains("accepted")) + XCTAssertTrue(try XCTUnwrap(store.submissions.status).targetDetails.targets[0].accepted) + await store.submissions.refreshSelected() + XCTAssertTrue(store.submitAccessibilityValue.contains("accepted")) + store.stop() + _ = try await client.stop() + } + + func testPartialUnknownAndStoppedFactsTakePrecedenceOverNominalStage() async throws { + let backend = AddBackend(advanceOffline: true) + let client = try await TeraAddStoreTests.startedClient(backend) + let store = TeraAddStore(runtimeClient: client) + await store.configure(snapshot: backend.snapshot()) + await store.start() + store.updateForm(\.content, "Saved original") + await store.submit() + let original = try XCTUnwrap(store.submissions.status) + for delivery in [TeraPublicationDeliveryState.unknown, .partiallyAccepted, .accepted] { + let evidence = TeraPublicationEvidence( + state: delivery, stopRequestedAtUnixMilliseconds: nil, schedulingRevision: 2, + retainedFacts: 1, recordedAttempts: 1, unresolvedClaims: true + ) + let current = replacing(original, state: .complete, delivery: evidence) + XCTAssertTrue(current.canOfferStop) + XCTAssertFalse(current.canOfferContinuation) + if delivery != .accepted { + XCTAssertFalse(current.summary.contains("completed")) + } + let stopped = TeraPublicationEvidence( + state: delivery, stopRequestedAtUnixMilliseconds: 10, schedulingRevision: 3, + retainedFacts: 1, recordedAttempts: 1, unresolvedClaims: true + ) + let value = replacing(original, state: .cancelled, delivery: stopped) + XCTAssertEqual(value.summary, stopped.stoppedSummary) + XCTAssertFalse(value.canOfferContinuation) + XCTAssertFalse(value.canOfferStop) + } + store.stop() + _ = try await client.stop() + } + + private func replacing(_ original: TeraSubmissionStatus, state: TeraOutboxState = .queued, + retry: TeraPublicationRetry = .ready, delivery: TeraPublicationEvidence? = nil) -> TeraSubmissionStatus + { + TeraSubmissionStatus( + request: original.request, intentID: original.intentID, operationID: original.operationID, + revision: original.revision, captured: original.captured, state: state, + committedAtUnixMilliseconds: original.committedAtUnixMilliseconds, + updatedAtUnixMilliseconds: original.updatedAtUnixMilliseconds, + media: original.media, settlement: original.settlement, delivery: delivery ?? original.delivery, + targetDetails: original.targetDetails, retry: retry + ) + } +} diff --git a/TeraTests/TeraSubmissionTestBackend.swift b/TeraTests/TeraSubmissionTestBackend.swift @@ -68,6 +68,12 @@ actor SubmissionTestBackend { failReads = value } + /// Injects saved facts for presentation tests; it does not simulate policy. + func installPresentationFixture(_ value: TeraSubmissionStatus) { + precondition(operations[value.request.commandID]?.request == value.request) + operations[value.request.commandID] = value + } + func reserveID() -> String { idCount += 1 return String(format: "%032x", 1000 + idCount) diff --git a/project.yml b/project.yml @@ -65,6 +65,7 @@ targets: - path: TeraTests/TeraNativeExecutionFixture.swift - path: TeraTests/TeraNativeRepairPreviewTests.swift - path: TeraTests/TeraComposerMediaOwnershipTests.swift + - path: TeraTests/TeraPublicationStageTests.swift - path: TeraTests/TeraPublicationEvidenceTests.swift - path: TeraTests/TeraPublicationTargetTests.swift - path: TeraTests/TeraPublicationRetryTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -6,7 +6,7 @@ "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "bfc57ed1f259dad8dcff7ec061b0889942a27bfdc9f9f01ebb631e25b715267b", - "xcode_project_sha256": "83c8c22a4f8eaf0f1f944073e67593af38c89fbdbb0bd27278ff1c128715c7fd" + "xcode_project_sha256": "4a9be0d06c4581252fc3a9eb32949fa2974ee4c7c6ceca38f6ed47710f6e535c" }, "disposition": "unsigned", "platforms": [ diff --git a/test-fixtures/swiftlint-maintainability-baseline.v1.json b/test-fixtures/swiftlint-maintainability-baseline.v1.json @@ -1 +1 @@ -[{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"line":21,"character":3,"file":"Tera/App/TeraAppModel.swift"},"severity":"error"},"text":" init("},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 373 lines","location":{"line":31,"character":7,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1089","location":{"line":1089,"character":1,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 675 lines","location":{"line":3,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"actor TeraRuntimeClient {"},{"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 70 lines","location":{"line":353,"character":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 749","location":{"line":749,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1085","location":{"line":1085,"character":1,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","location":{"line":4,"character":7,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 702","location":{"line":702,"character":1,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","location":{"line":78,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","location":{"line":148,"character":5,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 759","location":{"line":759,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","location":{"line":39,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"actor TeraSessionStore {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","location":{"line":125,"character":11,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","location":{"line":452,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","location":{"line":4,"character":1,"file":"Tera/Views/TeraAddView.swift"},"severity":"error"},"text":"struct TeraAddView: View {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","location":{"line":365,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","location":{"line":629,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 607 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 394 lines","location":{"line":682,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"actor AddBackend: TeraRuntimeBackend {"},{"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":{"line":1020,"character":3,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":" func makeStatus("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1232","location":{"line":1232,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","location":{"line":7,"character":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"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 94 lines","location":{"line":45,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"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 53 lines","location":{"line":212,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","location":{"line":4,"character":7,"file":"TeraTests/TeraRuntimeClientTests.swift"},"severity":"error"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"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 61 lines","location":{"line":7,"character":5,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 622","location":{"line":622,"character":1,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","location":{"line":207,"character":9,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 486","location":{"line":486,"character":1,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","location":{"line":4,"character":7,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"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 65 lines","location":{"line":62,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"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 66 lines","location":{"line":166,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"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 54 lines","location":{"line":239,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","location":{"line":566,"character":11,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"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 67 lines","location":{"line":1601,"character":10,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" static func environment("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 2077","location":{"line":2077,"character":1,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"}"}] +[{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Initializer body should span 50 lines or less excluding comments and whitespace: currently spans 52 lines","location":{"line":21,"character":3,"file":"Tera/App/TeraAppModel.swift"},"severity":"error"},"text":" init("},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 373 lines","location":{"line":31,"character":7,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"final class TeraGeneratedRuntimeBackend: TeraRuntimeBackend, @unchecked Sendable {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1089","location":{"line":1089,"character":1,"file":"Tera/Runtime/TeraGeneratedRuntimeBackend.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 675 lines","location":{"line":3,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"actor TeraRuntimeClient {"},{"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 70 lines","location":{"line":353,"character":3,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":" func changes(bufferCapacity: Int = 16) async throws -> AsyncStream<TeraRuntimeChange> {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 749","location":{"line":749,"character":1,"file":"Tera/Runtime/TeraRuntimeClient.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1029","location":{"line":1029,"character":1,"file":"Tera/Runtime/TeraRuntimeModels.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 491 lines","location":{"line":4,"character":7,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"final class TeraAddStore: ObservableObject {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 702","location":{"line":702,"character":1,"file":"Tera/State/TeraAddStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 468 lines","location":{"line":78,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"actor TeraConfigurationStore {"},{"violation":{"ruleDescription":"Function bodies should not span too many lines","ruleIdentifier":"function_body_length","ruleName":"Function Body Length","reason":"Function body should span 100 lines or less excluding comments and whitespace: currently spans 109 lines","location":{"line":148,"character":5,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":" func load() throws -> TeraAppConfiguration {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 759","location":{"line":759,"character":1,"file":"Tera/State/TeraConfigurationStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 390 lines","location":{"line":39,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"actor TeraSessionStore {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 13","location":{"line":125,"character":11,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":" private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 452","location":{"line":452,"character":1,"file":"Tera/State/TeraSessionStore.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 342 lines","location":{"line":4,"character":1,"file":"Tera/Views/TeraAddView.swift"},"severity":"error"},"text":"struct TeraAddView: View {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Struct body should span 250 lines or less excluding comments and whitespace: currently spans 259 lines","location":{"line":365,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"struct TeraSettingsView: View {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 629","location":{"line":629,"character":1,"file":"Tera/Views/TeraSupportingViews.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 607 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"final class TeraAddStoreTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 350 lines or less excluding comments and whitespace: currently spans 394 lines","location":{"line":682,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"actor AddBackend: TeraRuntimeBackend {"},{"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":{"line":1020,"character":3,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":" func makeStatus("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 1232","location":{"line":1232,"character":1,"file":"TeraTests/TeraAddStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 307 lines","location":{"line":7,"character":7,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationTests: XCTestCase {"},{"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 94 lines","location":{"line":45,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testQualificationRejectsAmbiguousOrUnsafeHarnessValues() {"},{"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 53 lines","location":{"line":212,"character":3,"file":"TeraTests/TeraRemoteQualificationTests.swift"},"severity":"error"},"text":" func testAuthorizationEvidenceIsRedactedEphemeralAndRelaunchCleaned() throws {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 250 lines or less excluding comments and whitespace: currently spans 298 lines","location":{"line":4,"character":7,"file":"TeraTests/TeraRuntimeClientTests.swift"},"severity":"error"},"text":"final class TeraRuntimeClientTests: XCTestCase {"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 580 lines","location":{"line":6,"character":7,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"final class TeraStateMigrationTests: XCTestCase {"},{"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 61 lines","location":{"line":7,"character":5,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":" func testRelayValidationMatchesRuntimeProfiles() throws {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 622","location":{"line":622,"character":1,"file":"TeraTests/TeraStateMigrationTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Actor body should span 250 lines or less excluding comments and whitespace: currently spans 252 lines","location":{"line":207,"character":9,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"private actor SupportingBackend: TeraRuntimeBackend {"},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 400 lines or less: currently contains 486","location":{"line":486,"character":1,"file":"TeraTests/TeraSupportingStoreTests.swift"},"severity":"error"},"text":"}"},{"violation":{"ruleDescription":"Type bodies should not span too many lines","ruleIdentifier":"type_body_length","ruleName":"Type Body Length","reason":"Class body should span 350 lines or less excluding comments and whitespace: currently spans 1365 lines","location":{"line":4,"character":7,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"final class TeraRemoteQualificationUITests: XCTestCase {"},{"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 65 lines","location":{"line":62,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialFiveFlowScenario() throws {"},{"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 66 lines","location":{"line":166,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testLocalSocialDeterministicPersonas() throws {"},{"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 54 lines","location":{"line":239,"character":3,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" func testRemoteBlossomUploadAndRecovery() throws {"},{"violation":{"ruleDescription":"Complexity of function bodies should be limited.","ruleIdentifier":"cyclomatic_complexity","ruleName":"Cyclomatic Complexity","reason":"Function should have complexity 10 or less; currently complexity is 18","location":{"line":566,"character":11,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" private func expectedAddFieldLabel(_ identifier: String, type: String) -> String {"},{"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 67 lines","location":{"line":1601,"character":10,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":" static func environment("},{"violation":{"ruleDescription":"Files should not span too many lines.","ruleIdentifier":"file_length","ruleName":"File Length","reason":"File should contain 1000 lines or less: currently contains 2077","location":{"line":2077,"character":1,"file":"TeraUITests/TeraRemoteQualificationUITests.swift"},"severity":"error"},"text":"}"}]