diff --git a/.github/workflows/xcode-build.yml b/.github/workflows/xcode-build.yml
index 9e105c2..66e2045 100644
--- a/.github/workflows/xcode-build.yml
+++ b/.github/workflows/xcode-build.yml
@@ -26,15 +26,36 @@ jobs:
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO
- - name: Run unit tests on macOS
+ - name: Run unit and UI tests on macOS
run: |
xcodebuild test \
-project potassiumProvider.xcodeproj \
-scheme potassiumProvider \
-destination 'platform=macOS' \
+ -resultBundlePath "$RUNNER_TEMP/macos-tests.xcresult" \
MACOSX_DEPLOYMENT_TARGET=26.4 \
CODE_SIGNING_ALLOWED=NO
+ - name: Run Stability profile unit tests on macOS
+ if: ${{ !cancelled() }}
+ run: |
+ xcodebuild test \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -destination 'platform=macOS' \
+ -resultBundlePath "$RUNNER_TEMP/macos-stability-tests.xcresult" \
+ MACOSX_DEPLOYMENT_TARGET=26.4 \
+ CODE_SIGNING_ALLOWED=NO
+
+ - name: Retain Mac test result bundles
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: macos-test-results
+ path: ${{ runner.temp }}/macos*-tests.xcresult
+ retention-days: 14
+ if-no-files-found: warn
+
ios:
name: iOS Simulator
runs-on: macos-26
@@ -58,8 +79,18 @@ jobs:
-scheme potassiumProvider \
-destination 'platform=iOS Simulator,OS=26.5,name=iPhone 17' \
-only-testing:potassiumProviderTests \
+ -resultBundlePath "$RUNNER_TEMP/ios-tests.xcresult" \
CODE_SIGNING_ALLOWED=NO
+ - name: Retain iOS test result bundle
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: ios-test-results
+ path: ${{ runner.temp }}/ios-tests.xcresult
+ retention-days: 14
+ if-no-files-found: warn
+
visionos:
name: visionOS
runs-on: macos-26
@@ -83,4 +114,14 @@ jobs:
-scheme potassiumProvider \
-destination 'platform=visionOS Simulator,OS=26.5,name=Apple Vision Pro' \
-only-testing:potassiumProviderTests \
+ -resultBundlePath "$RUNNER_TEMP/visionos-tests.xcresult" \
CODE_SIGNING_ALLOWED=NO
+
+ - name: Retain visionOS test result bundle
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: visionos-test-results
+ path: ${{ runner.temp }}/visionos-tests.xcresult
+ retention-days: 14
+ if-no-files-found: warn
diff --git a/AGENTS.md b/AGENTS.md
index 95fe12e..907c69b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,7 +13,7 @@ File Provider experience.
`potassiumProviderFileProvider`, `potassiumProviderActions`,
`potassiumProviderTests`, and `potassiumProviderUITests`
- Supported validation platforms: iOS Simulator, macOS, and visionOS
-- Dependencies: `SQLite.swift` and `potassiumChannel` package products
+- Dependencies: `InfomaniakConcurrency`, `SQLite.swift`, and `potassiumChannel` package products
`PotassiumChannelCore`, `PotassiumKDrive`, and `PotassiumOAuth`
- Tests: Swift Testing for unit tests, XCTest for UI tests
@@ -152,6 +152,15 @@ app.
- Keep networking behavior testable with mocks, fixtures, or injectable
clients. Live checks must be explicit, locally guarded, and kept out of the
default test path.
+- Provision new Stability Lab roots below the server-created `Private` folder,
+ verifying its stable identity and drive-root parent; do not assume the drive
+ root accepts creates. Never use the `Private` folder itself as a disposable root.
+- The current account is an operator-authorized lab account. Stability may reuse
+ its existing OAuth Keychain login as well as manually entered tokens; never
+ extract a credential into a command, fixture, environment variable, or log.
+- Live Finder runs defer permanent deletion by default and continue later scenarios.
+ Use `--include-permanent-deletion` only when requested; retain exact-item
+ confirmation and never count deferred deletion as full-suite acceptance.
- Prefer typed request/response flows from Potassium products over app-local
ad hoc HTTP construction.
@@ -207,6 +216,9 @@ app.
- Use Swift Testing (`import Testing`) for new unit tests unless the work is in
existing XCTest UI test targets.
- Use XCTest only for UI automation or when extending existing XCTest files.
+- Record the actual OS version/build and Xcode version with integration-test
+ evidence. Rerun native Finder integration after macOS upgrades before claiming
+ compatibility; an SDK or deployment target is not a tested runtime version.
- Run the relevant `xcodebuild build` or `xcodebuild test` command before
describing implementation work as complete, including Mac and visionOS
destinations when changes affect runtime behavior. If validation cannot be
diff --git a/Config/potassiumProviderInfo.plist b/Config/potassiumProviderInfo.plist
index 1bf4619..f3af7cd 100644
--- a/Config/potassiumProviderInfo.plist
+++ b/Config/potassiumProviderInfo.plist
@@ -20,6 +20,8 @@
LSApplicationCategoryType
public.app-category.productivity
+ NSAppleEventsUsageDescription
+ The opt-in Stability Lab uses Finder Automation only to exercise its verified disposable File Provider root.
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleURLTypes
diff --git a/Config/potassiumProviderStability.entitlements b/Config/potassiumProviderStability.entitlements
new file mode 100644
index 0000000..28b6801
--- /dev/null
+++ b/Config/potassiumProviderStability.entitlements
@@ -0,0 +1,18 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.net.weavee.potassiumProvider
+
+ com.apple.security.automation.apple-events
+
+ com.apple.security.network.client
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)net.weavee.potassiumProvider
+
+
+
diff --git a/PotassiumProviderCore/FileProviderBackgroundWork.swift b/PotassiumProviderCore/FileProviderBackgroundWork.swift
new file mode 100644
index 0000000..396f122
--- /dev/null
+++ b/PotassiumProviderCore/FileProviderBackgroundWork.swift
@@ -0,0 +1,56 @@
+import Foundation
+import Synchronization
+
+/// Owns work launched after a File Provider callback has been acknowledged.
+/// `invalidate()` is synchronous because the system's instance invalidation
+/// callback must cancel outstanding work before it returns. The mutex protects
+/// task registration only; operations and cancellation handlers run outside it.
+public final class FileProviderBackgroundWork: Sendable {
+ private struct State {
+ var invalidated = false
+ var tasks: [UUID: Task] = [:]
+ }
+ private let state = Mutex(State())
+
+ public init() {}
+
+ /// Returns false once this instance has been invalidated. A new provider
+ /// instance must own a new scope even when it shares the same process.
+ @discardableResult
+ public func start(_ operation: @escaping @Sendable () async -> Void) -> Bool {
+ state.withLock { state in
+ guard !state.invalidated else { return false }
+ let identifier = UUID()
+ state.tasks[identifier] = Task {
+ // Registration holds the same mutex, so an immediately
+ // finishing task cannot remove itself before insertion.
+ guard self.canBegin(identifier) else { return }
+ defer { self.finished(identifier) }
+ await operation()
+ }
+ return true
+ }
+ }
+
+ public func invalidate() {
+ let tasks = state.withLock { state in
+ state.invalidated = true
+ let tasks = Array(state.tasks.values)
+ state.tasks.removeAll()
+ return tasks
+ }
+ // Task cancellation can synchronously invoke a handler. Never call
+ // it while holding the registration mutex.
+ for task in tasks { task.cancel() }
+ }
+
+ var registeredTaskCount: Int { state.withLock { $0.tasks.count } }
+
+ private func canBegin(_ identifier: UUID) -> Bool {
+ state.withLock { !$0.invalidated && $0.tasks[identifier] != nil && !Task.isCancelled }
+ }
+
+ private func finished(_ identifier: UUID) {
+ state.withLock { $0.tasks[identifier] = nil }
+ }
+}
diff --git a/PotassiumProviderCore/FileProviderOperationLifecycle.swift b/PotassiumProviderCore/FileProviderOperationLifecycle.swift
index e99046e..7c041e7 100644
--- a/PotassiumProviderCore/FileProviderOperationLifecycle.swift
+++ b/PotassiumProviderCore/FileProviderOperationLifecycle.swift
@@ -6,15 +6,34 @@ public actor FileProviderOperationLifecycle {
public nonisolated let progress: Progress
private let cancellationCompletion: @Sendable () -> Void
+ private let diagnosticSpanTask: Task?
private var task: Task?
private var isFinished = false
public init(
progress: Progress,
+ diagnosticSource: ProviderDiagnosticSource = .fileProviderExtension,
+ diagnosticOperation: ProviderDiagnosticOperation? = nil,
+ diagnosticFieldShape: [ProviderDiagnosticField] = [],
+ diagnosticItemIdentifier: String? = nil,
+ diagnosticRecorder: (any ProviderDiagnosticRecording)? = nil,
cancellationCompletion: @escaping @Sendable () -> Void
) {
self.progress = progress
self.cancellationCompletion = cancellationCompletion
+ if let diagnosticOperation {
+ self.diagnosticSpanTask = Task {
+ await ProviderDiagnosticSpan.start(
+ itemIdentifier: diagnosticItemIdentifier,
+ source: diagnosticSource,
+ operation: diagnosticOperation,
+ fieldShape: diagnosticFieldShape,
+ recorder: diagnosticRecorder
+ )
+ }
+ } else {
+ self.diagnosticSpanTask = nil
+ }
progress.isCancellable = true
progress.isPausable = false
progress.cancellationHandler = { [weak self] in
@@ -34,24 +53,38 @@ public actor FileProviderOperationLifecycle {
private func begin(
_ operation: @escaping @Sendable (FileProviderOperationLifecycle) async -> Void
- ) {
+ ) async {
guard isFinished == false else { return }
guard progress.isCancelled == false else {
- cancel()
+ await cancel()
+ return
+ }
+ let diagnosticSpan = await diagnosticSpanTask?.value
+ guard isFinished == false else { return }
+ guard progress.isCancelled == false else {
+ await cancel()
return
}
task = Task {
- await operation(self)
+ if let diagnosticSpan {
+ await diagnosticSpan.withCorrelation {
+ await operation(self)
+ }
+ } else {
+ await operation(self)
+ }
}
}
@discardableResult
public func finish(
markProgressComplete: Bool,
+ diagnosticError: (any Error)? = nil,
+ diagnosticItemMetadataAlias: UUID? = nil,
_ completion: @escaping @Sendable () -> Void
- ) -> Bool {
+ ) async -> Bool {
if progress.isCancelled {
- cancel()
+ await cancel()
return false
}
@@ -62,11 +95,19 @@ public actor FileProviderOperationLifecycle {
if markProgressComplete, progress.totalUnitCount > 0 {
progress.completedUnitCount = progress.totalUnitCount
}
+ if let diagnosticSpanTask {
+ let diagnosticSpan = await diagnosticSpanTask.value
+ if let diagnosticError {
+ await diagnosticSpan.fail(error: diagnosticError)
+ } else {
+ await diagnosticSpan.complete(statusClass: .success, itemMetadataAlias: diagnosticItemMetadataAlias)
+ }
+ }
completion()
return true
}
- public func cancel() {
+ public func cancel() async {
guard isFinished == false else { return }
isFinished = true
let task = task
@@ -76,6 +117,10 @@ public actor FileProviderOperationLifecycle {
progress.cancel()
}
task?.cancel()
+ if let diagnosticSpanTask {
+ let diagnosticSpan = await diagnosticSpanTask.value
+ await diagnosticSpan.cancel()
+ }
cancellationCompletion()
}
}
diff --git a/PotassiumProviderCore/KDriveContextActions.swift b/PotassiumProviderCore/KDriveContextActions.swift
index cc41ccb..4932622 100644
--- a/PotassiumProviderCore/KDriveContextActions.swift
+++ b/PotassiumProviderCore/KDriveContextActions.swift
@@ -3,6 +3,7 @@ import Foundation
public struct KDriveShareLinkConfiguration: Equatable, Sendable {
public enum Access: String, CaseIterable, Equatable, Sendable {
case `public`
+ case inherit
case password
}
@@ -45,6 +46,18 @@ public struct KDriveShareLinkConfiguration: Equatable, Sendable {
public func isValid(preservingPasswordFor existingAccess: Access?) -> Bool {
isValid || (access == .password && existingAccess == .password)
}
+
+ /// Compare the settings kDrive reports after a write. Passwords are not
+ /// returned by the service, and expiration is transported in whole seconds.
+ /// A successful HTTP response alone does not prove that these values stuck.
+ public func hasSameReportedSettings(as other: Self) -> Bool {
+ access == other.access &&
+ validUntil.map { $0.timeIntervalSince1970.rounded(.towardZero) } ==
+ other.validUntil.map { $0.timeIntervalSince1970.rounded(.towardZero) } &&
+ allowsDownload == other.allowsDownload && allowsComments == other.allowsComments &&
+ allowsEditing == other.allowsEditing && allowsAccessRequests == other.allowsAccessRequests &&
+ showsFileInformation == other.showsFileInformation && showsStatistics == other.showsStatistics
+ }
}
public struct KDriveShareLinkSummary: Equatable, Sendable {
@@ -101,7 +114,7 @@ public struct KDriveFileVersionPage: Equatable, Sendable {
public protocol KDriveContextActionProviding: Sendable {
func setFavorite(driveID: Int, fileID: Int, isFavorite: Bool) async throws
- func duplicateItem(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem
+ func duplicateItem(driveID: Int, fileID: Int, name: String) async throws -> KDriveRemoteItem
func trashedItem(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem
func existingFileIDs(driveID: Int, fileIDs: [Int]) async throws -> Set
func restoreTrashedItem(driveID: Int, fileID: Int, destinationParentID: Int) async throws
@@ -131,6 +144,8 @@ public enum KDriveContextActionError: Error, Equatable, LocalizedError, Sendable
case invalidShareLinkURL
case passwordRequired
case restoredItemUnavailable
+ case unsupportedShareLinkAccess
+ case shareLinkSettingsNotApplied
public var errorDescription: String? {
switch self {
@@ -140,6 +155,10 @@ public enum KDriveContextActionError: Error, Equatable, LocalizedError, Sendable
return "Enter a password for the protected share link."
case .restoredItemUnavailable:
return "kDrive restored the version, but its metadata is not available yet."
+ case .unsupportedShareLinkAccess:
+ return "kDrive returned an unsupported share-link access policy."
+ case .shareLinkSettingsNotApplied:
+ return "kDrive did not apply all requested sharing settings. The current server settings are shown; review them before sharing the link."
}
}
}
@@ -215,6 +234,21 @@ public struct KDriveContextActionExecution: Equatable, Sendable {
}
}
+public enum KDriveDuplicateNamePolicy {
+ /// Produces an explicit, human-readable destination name. The duplicate
+ /// endpoint's empty-body/server-selected naming behavior is not part of the
+ /// pinned public contract, so callers never rely on it.
+ public static func duplicateName(for sourceName: String) -> String {
+ let path = sourceName as NSString
+ let pathExtension = path.pathExtension
+ let baseName = path.deletingPathExtension
+ guard pathExtension.isEmpty == false, baseName.isEmpty == false else {
+ return "\(sourceName) copy"
+ }
+ return "\(baseName) copy.\(pathExtension)"
+ }
+}
+
/// Coordinates action-specific remote calls and returns the exact provider
/// containers that must be invalidated after the server mutation succeeds.
public struct KDriveContextActionCoordinator: Sendable {
@@ -246,7 +280,13 @@ public struct KDriveContextActionCoordinator: Sendable {
return try await setFavorite(false, fileID: fileID, action: action)
case .duplicate:
try Task.checkCancellation()
- let duplicate = try await actions.duplicateItem(driveID: driveID, fileID: fileID)
+ let source = try await remote.item(driveID: driveID, fileID: fileID)
+ try Task.checkCancellation()
+ let duplicate = try await actions.duplicateItem(
+ driveID: driveID,
+ fileID: fileID,
+ name: KDriveDuplicateNamePolicy.duplicateName(for: source.name)
+ )
let authoritativeDuplicate = try await remote.item(driveID: driveID, fileID: duplicate.id)
return KDriveContextActionExecution(
action: action,
diff --git a/PotassiumProviderCore/KDriveCreationExecutor.swift b/PotassiumProviderCore/KDriveCreationExecutor.swift
new file mode 100644
index 0000000..7bb31c9
--- /dev/null
+++ b/PotassiumProviderCore/KDriveCreationExecutor.swift
@@ -0,0 +1,17 @@
+import FileProvider
+
+/// The plaintext create callback's production routing boundary. A reconciliation
+/// hint does not authorize overwriting a same-name server item. File replay uses
+/// the coordinator's deterministic upload identity; directory reconciliation is
+/// still an explicit CR-009 limitation.
+public enum KDriveCreationExecutor {
+ public static func execute(isDirectory: Bool, options: NSFileProviderCreateItemOptions,
+ createDirectory: @Sendable () async throws -> KDriveRemoteItem,
+ createFile: @Sendable () async throws -> KDriveRemoteItem) async throws -> KDriveRemoteItem {
+ try Task.checkCancellation()
+ // Preserve the existing policy for both ordinary and mayAlreadyExist
+ // callbacks. Matching a name alone cannot establish remote identity.
+ if isDirectory { return try await createDirectory() }
+ return try await createFile()
+ }
+}
diff --git a/PotassiumProviderCore/KDriveItemMetadataLookup.swift b/PotassiumProviderCore/KDriveItemMetadataLookup.swift
new file mode 100644
index 0000000..700639e
--- /dev/null
+++ b/PotassiumProviderCore/KDriveItemMetadataLookup.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+public enum KDriveItemMetadataLookupError: Error, Equatable {
+ case notFound, identityMismatch, trashLookupUnavailable
+}
+
+/// Resolves an existing identity for metadata, including items in Trash.
+/// Content and mutation preflight must continue to use active-item metadata.
+public enum KDriveItemMetadataLookup {
+ public struct Result: Equatable, Sendable {
+ public let item: KDriveRemoteItem
+ public let isTrashed: Bool
+ }
+
+ public static func resolve(
+ driveID: Int,
+ fileID: Int,
+ active: @Sendable () async throws -> KDriveRemoteItem,
+ trashed: @Sendable () async throws -> KDriveRemoteItem
+ ) async throws -> Result {
+ try Task.checkCancellation()
+ let item: KDriveRemoteItem
+ let isTrashed: Bool
+ do {
+ item = try await active()
+ isTrashed = false
+ } catch {
+ try Task.checkCancellation()
+ guard KDriveRemoteErrorClassifier.isNotFound(error) else { throw error }
+ do {
+ item = try await trashed()
+ isTrashed = true
+ } catch {
+ try Task.checkCancellation()
+ // Only absence from both identity endpoints establishes noSuchItem.
+ guard KDriveRemoteErrorClassifier.isNotFound(error) else { throw error }
+ throw KDriveItemMetadataLookupError.notFound
+ }
+ }
+ try Task.checkCancellation()
+ guard item.id == fileID, item.driveID == driveID else {
+ throw KDriveItemMetadataLookupError.identityMismatch
+ }
+ return Result(item: item, isTrashed: isTrashed)
+ }
+}
diff --git a/PotassiumProviderCore/KDriveModificationExecutor.swift b/PotassiumProviderCore/KDriveModificationExecutor.swift
new file mode 100644
index 0000000..27b3e4a
--- /dev/null
+++ b/PotassiumProviderCore/KDriveModificationExecutor.swift
@@ -0,0 +1,73 @@
+import FileProvider
+import Foundation
+
+/// The production plaintext modify callback sequence. Content I/O stays injectable
+/// so the extension can retain its transfer permit, progress, and staging boundary.
+public struct KDriveModificationExecutor: Sendable {
+ public struct Result: Sendable {
+ public let item: KDriveRemoteItem?
+ public let remainingFields: NSFileProviderItemFields
+ public let affectedParentIDs: Set
+ public let trashed: Bool
+ }
+
+ private let coordinator: KDriveMutationCoordinator
+ private let lookup: @Sendable (Int) async throws -> KDriveRemoteItem
+
+ public init(coordinator: KDriveMutationCoordinator,
+ lookup: @escaping @Sendable (Int) async throws -> KDriveRemoteItem) {
+ self.coordinator = coordinator
+ self.lookup = lookup
+ }
+
+ public func execute(fileID: Int, filename: String, baseVersion: KDriveItemBaseVersion,
+ fields: NSFileProviderItemFields, destinationParentID: Int?,
+ requestsTrash: Bool, modificationDate: Date?, hasContents: Bool,
+ applyContents: @Sendable () async throws -> KDriveContentMutationResult) async throws -> Result {
+ try Task.checkCancellation()
+ // Reject malformed callbacks before applying any metadata changes.
+ if fields.contains(.contents), !hasContents { throw NSFileProviderError(.cannotSynchronize) }
+ var remaining = fields
+ var updated: KDriveRemoteItem?
+ var parents = Set([KDriveItemMetadataVersion(data: baseVersion.metadataVersion)?.parentID].compactMap { $0 })
+ if fields.contains(.parentItemIdentifier), !requestsTrash {
+ guard let destinationParentID else { throw NSFileProviderError(.cannotSynchronize) }
+ updated = try await coordinator.moveItem(fileID: fileID, baseMetadataVersion: baseVersion.metadataVersion,
+ destinationParentID: destinationParentID, name: fields.contains(.filename) ? filename : nil)
+ remaining.subtract([.parentItemIdentifier, .filename])
+ parents.insert(destinationParentID)
+ } else if fields.contains(.filename) {
+ updated = try await coordinator.renameItem(fileID: fileID, baseMetadataVersion: baseVersion.metadataVersion, name: filename)
+ remaining.remove(.filename)
+ }
+ if let updated { parents.insert(updated.parentID) }
+ try Task.checkCancellation()
+ if fields.contains(.contents) {
+ updated = try await applyContents().item
+ remaining.subtract([.contents, .contentModificationDate])
+ } else if fields.contains(.contentModificationDate), let modificationDate {
+ updated = try await coordinator.updateModificationDate(fileID: fileID, date: modificationDate)
+ remaining.remove(.contentModificationDate)
+ }
+ if let updated { parents.insert(updated.parentID) }
+ try Task.checkCancellation()
+ if requestsTrash {
+ let original = try await coordinator.trashItem(fileID: fileID, baseVersion: baseVersion)
+ parents.insert(original.parentID)
+ var trashedItem = original
+ if let updated, updated.id != fileID {
+ trashedItem = try await coordinator.trashItem(fileID: updated.id,
+ baseVersion: KDriveItemBaseVersion(contentVersion: updated.contentVersion, metadataVersion: updated.metadataVersion))
+ }
+ remaining.remove(.parentItemIdentifier)
+ // nil tells the replicated provider to delete the local replica.
+ // Retain managed Trash metadata, including the identity/bytes of a
+ // preserved local conflict copy when both versions were trashed.
+ return Result(item: trashedItem, remainingFields: remaining, affectedParentIDs: parents, trashed: true)
+ }
+ let result: KDriveRemoteItem
+ if let updated { result = updated } else { result = try await lookup(fileID) }
+ if result.isDirectory { parents.insert(result.id) }
+ return Result(item: result, remainingFields: remaining, affectedParentIDs: parents, trashed: false)
+ }
+}
diff --git a/PotassiumProviderCore/KDriveMutationCoordinator.swift b/PotassiumProviderCore/KDriveMutationCoordinator.swift
index 93c9496..a2c6cfd 100644
--- a/PotassiumProviderCore/KDriveMutationCoordinator.swift
+++ b/PotassiumProviderCore/KDriveMutationCoordinator.swift
@@ -139,6 +139,7 @@ public struct KDriveMutationCoordinator: Sendable {
private let conflictDate: @Sendable () -> Date
private let conflictTimeZone: @Sendable () -> TimeZone
private let contentConflictObserver: ContentConflictObserver?
+ private let trashedItemLookup: @Sendable (Int) async throws -> KDriveRemoteItem
public init(
configuration: ProviderDomainConfiguration,
@@ -147,7 +148,8 @@ public struct KDriveMutationCoordinator: Sendable {
conflictDeviceName: @escaping @Sendable () -> String = { "This Mac" },
conflictDate: @escaping @Sendable () -> Date = { Date() },
conflictTimeZone: @escaping @Sendable () -> TimeZone = { .current },
- contentConflictObserver: ContentConflictObserver? = nil
+ contentConflictObserver: ContentConflictObserver? = nil,
+ trashedItemLookup: (@Sendable (Int) async throws -> KDriveRemoteItem)? = nil
) {
self.configuration = configuration
self.remote = remote
@@ -156,6 +158,15 @@ public struct KDriveMutationCoordinator: Sendable {
self.conflictDate = conflictDate
self.conflictTimeZone = conflictTimeZone
self.contentConflictObserver = contentConflictObserver
+ self.trashedItemLookup = trashedItemLookup ?? { fileID in
+ if let actions = remote as? any KDriveContextActionProviding {
+ return try await actions.trashedItem(driveID: configuration.driveID, fileID: fileID)
+ }
+ // Compatibility for metadata-only adapters. Production kDrive
+ // supplies the typed Trash endpoint above; active 404 is not proof
+ // that a provider-managed trashed identity has been deleted.
+ return try await remote.item(driveID: configuration.driveID, fileID: fileID)
+ }
}
public func createFile(
@@ -225,6 +236,11 @@ public struct KDriveMutationCoordinator: Sendable {
// the exact local bytes remain available for a later retry or recovery.
let stagedURL = try await conflictStager.stageConflictContents(contents, itemIdentifier: itemIdentifier)
let contentHash = KDriveMutationIdentity.contentHash(contents)
+ #if STABILITY
+ if configuration.purpose == .stabilityLab {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: String(fileID), point: .beforeContentPreflight)
+ }
+ #endif
let latestItem = try await remote.item(driveID: configuration.driveID, fileID: fileID)
guard let baseVersion = KDriveItemContentVersion(data: baseContentVersion),
baseVersion.authoritativelyMatches(latestItem),
@@ -254,6 +270,11 @@ public struct KDriveMutationCoordinator: Sendable {
expectedETag,
contentHash,
])
+ #if STABILITY
+ if configuration.purpose == .stabilityLab {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: String(fileID))
+ }
+ #endif
let operation = try remote.replaceFileOperation(
driveID: configuration.driveID,
fileID: fileID,
@@ -308,6 +329,11 @@ public struct KDriveMutationCoordinator: Sendable {
name: String
) async throws -> KDriveRemoteItem {
let latestItem = try await remote.item(driveID: configuration.driveID, fileID: fileID)
+ #if STABILITY
+ if configuration.purpose == .stabilityLab {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: String(fileID), point: .afterRenamePreflight)
+ }
+ #endif
if latestItem.name == name {
return latestItem
}
@@ -331,6 +357,11 @@ public struct KDriveMutationCoordinator: Sendable {
name: String?
) async throws -> KDriveRemoteItem {
let latestItem = try await remote.item(driveID: configuration.driveID, fileID: fileID)
+ #if STABILITY
+ if configuration.purpose == .stabilityLab {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: String(fileID), point: .afterMovePreflight)
+ }
+ #endif
let baseName = KDriveItemMetadataVersion(data: baseMetadataVersion)?.name
let desiredName = name ?? latestItem.name
if latestItem.parentID == destinationParentID, latestItem.name == desiredName {
@@ -348,6 +379,15 @@ public struct KDriveMutationCoordinator: Sendable {
}
public func updateModificationDate(fileID: Int, date: Date) async throws -> KDriveRemoteItem {
+ let current = try await remote.item(driveID: configuration.driveID, fileID: fileID)
+ if current.isDirectory {
+ // The server owns directory timestamps and rejects last-modified
+ // writes for directories. Apple's modifyItem contract propagates
+ // a returned authoritative field to disk when it is not pending.
+ // Resolve the automatic local directory mtime change to that value;
+ // never report a file-only HTTP mutation as if it had succeeded.
+ return current
+ }
try await remote.updateModificationDate(
driveID: configuration.driveID,
fileID: fileID,
@@ -363,7 +403,7 @@ public struct KDriveMutationCoordinator: Sendable {
}
public func deleteTrashedItem(fileID: Int, baseVersion: KDriveItemBaseVersion) async throws -> KDriveRemoteItem {
- let latestItem = try await remote.item(driveID: configuration.driveID, fileID: fileID)
+ let latestItem = try await trashedItemLookup(fileID)
guard KDriveVersionConflictResolver.itemVersionMatchesAllowingMetadataTimestampDrift(
contentVersion: baseVersion.contentVersion,
metadataVersion: baseVersion.metadataVersion,
diff --git a/PotassiumProviderCore/KDriveRemoteService.swift b/PotassiumProviderCore/KDriveRemoteService.swift
index dd73b98..9b8dfad 100644
--- a/PotassiumProviderCore/KDriveRemoteService.swift
+++ b/PotassiumProviderCore/KDriveRemoteService.swift
@@ -162,21 +162,104 @@ public enum KDriveUploadConflictStrategy: String, Sendable {
case rename
}
+public enum KDriveDirectUploadError: Error, Equatable, LocalizedError, Sendable {
+ case fileSizeUnavailable
+ case requiresUploadSession(maximumByteCount: Int)
+
+ public var errorDescription: String? {
+ switch self {
+ case .fileSizeUnavailable:
+ return "The file size could not be verified before direct upload."
+ case .requiresUploadSession(let maximumByteCount):
+ return "Files larger than \(maximumByteCount) bytes require a session-backed upload."
+ }
+ }
+
+ public var recoverySuggestion: String? {
+ switch self {
+ case .fileSizeUnavailable:
+ return "Keep the callback source available and retry once its size can be verified."
+ case .requiresUploadSession:
+ return "Keep the local content and retry after session-backed uploads are available."
+ }
+ }
+
+ public var recovery: KDriveRemoteAPIRejectionRecovery {
+ .cannotSynchronize
+ }
+
+ public var diagnosticCategory: KDriveProviderActivityErrorCategory {
+ .validation
+ }
+
+ public var diagnosticSummary: String {
+ switch self {
+ case .fileSizeUnavailable:
+ return "The callback file size could not be verified before direct upload."
+ case .requiresUploadSession:
+ return "The direct-upload size limit requires a session-backed transfer."
+ }
+ }
+}
+
+/// Loads callback content only after a cheap file-size preflight. The service
+/// validates the resulting `Data` again before constructing a request, closing
+/// the file-size/read race without first mapping an unsupported large file into
+/// the File Provider extension's address space.
+public enum KDriveDirectUploadContentLoader {
+ public static func loadContents(at fileURL: URL) throws -> Data {
+ guard let declaredByteCount = try? fileURL.resourceValues(
+ forKeys: [.fileSizeKey]
+ ).fileSize else {
+ throw KDriveDirectUploadError.fileSizeUnavailable
+ }
+ try PotassiumKDriveService.validateDirectUploadByteCount(declaredByteCount)
+
+ let contents = try Data(contentsOf: fileURL, options: .mappedIfSafe)
+ try PotassiumKDriveService.validateDirectUploadByteCount(contents.count)
+ return contents
+ }
+}
+
public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemoteProviding, KDriveContextActionProviding {
private let apiClient: InfomaniakAPIClient
private let driveClient: InfomaniakAPIClient
private let service: KDriveService
+ private let diagnosticRecorder: (any ProviderDiagnosticRecording)?
+ private let diagnosticSource: ProviderDiagnosticSource
/// kDrive advanced-listing routes reject `etag` and `files.etag` with HTTP
/// 422. Direct metadata and ordinary directory listings remain the source
/// of authoritative ETags for content mutations.
- private static let advancedDirectoryListingIncludedResources = "files.capabilities"
+ private static let advancedDirectoryListingIncludedResources = "files.capabilities,files.is_favorite"
+
+ /// Favorite state is optional unless explicitly included. Omitting it hides
+ /// both favorite actions and can erase known state during reconciliation.
+ private static let itemIncludedResources = "etag,is_favorite"
+
+ /// Maximum `total_size` accepted by kDrive's direct upload endpoint.
+ /// Larger transfers must use the upload-session API and are rejected before
+ /// a request operation is constructed.
+ public static let directUploadMaximumByteCount = 1_000_000_000
+
+ public static func validateDirectUploadByteCount(_ byteCount: Int) throws {
+ guard byteCount >= 0 else {
+ throw KDriveDirectUploadError.fileSizeUnavailable
+ }
+ guard byteCount <= directUploadMaximumByteCount else {
+ throw KDriveDirectUploadError.requiresUploadSession(
+ maximumByteCount: directUploadMaximumByteCount
+ )
+ }
+ }
public init(
bearerToken: String,
apiBaseURL: URL = ProviderConstants.apiBaseURL,
driveBaseURL: URL = ProviderConstants.driveBaseURL,
- session: URLSession = .shared
+ session: URLSession = .shared,
+ diagnosticRecorder: (any ProviderDiagnosticRecording)? = nil,
+ diagnosticSource: ProviderDiagnosticSource = .app
) {
self.apiClient = InfomaniakAPIClient(
configuration: APIClientConfiguration(baseURL: apiBaseURL, bearerToken: bearerToken),
@@ -187,10 +270,12 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
session: session
)
self.service = KDriveService(client: apiClient)
+ self.diagnosticRecorder = diagnosticRecorder
+ self.diagnosticSource = diagnosticSource
}
public func listDrives() async throws -> [KDriveDriveSummary] {
- try await performNetworkOperation("listDrives") {
+ try await performNetworkOperation(.listDrives) {
let response = try await performDriveDiscoveryRequest(
endpoint: "/2/drive/init"
) {
@@ -214,27 +299,30 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func item(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
- try await performNetworkOperation("item") {
- try await service.getFile(driveId: driveID, fileId: fileID, with: "etag").data.remoteItem
+ try await performNetworkOperation(.itemLookup) {
+ try await service.getFile(driveId: driveID, fileId: fileID, with: Self.itemIncludedResources).data.remoteItem
}
}
public func listDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) async throws -> KDriveItemPage {
- try await performNetworkOperation("listDirectory") {
+ try await performNetworkOperation(
+ .listDirectory,
+ additionalOptionShape: cursor == nil ? [] : [.paginationCursor]
+ ) {
let options = ListKDriveDirectoryFilesOptions(cursor: cursor, limit: limit, orderBy: ["name"], order: "asc")
let response: CursorPaginatedInfomaniakResponse<[KDriveFileItem]>
do {
response = try await service.listDirectoryFiles(
driveId: driveID,
fileId: folderID,
- with: "etag",
+ with: Self.itemIncludedResources,
options: options
)
} catch APIClientError.unacceptableStatusCode(422, _, _) {
response = try await service.listDirectoryFiles(
driveId: driveID,
fileId: folderID,
- with: nil,
+ with: "is_favorite",
options: options
)
}
@@ -246,7 +334,11 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
let orderBy = ["type", "name"]
let orderFor = ["type": "asc", "name": "asc"]
- return try await performNetworkOperation("listAdvancedDirectory") {
+ return try await performNetworkOperation(
+ .listAdvancedDirectory,
+ routeTemplate: cursor == nil ? .listAdvancedDirectory : .continueAdvancedDirectory,
+ additionalOptionShape: cursor == nil ? [] : [.paginationCursor]
+ ) {
let response: CursorPaginatedInfomaniakResponse
if let cursor {
response = try await service.continueAdvancedDirectoryListing(
@@ -286,10 +378,13 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func listTrash(driveID: Int, cursor: String?, limit: Int) async throws -> KDriveItemPage {
- try await performNetworkOperation("listTrash") {
+ try await performNetworkOperation(
+ .listTrash,
+ additionalOptionShape: cursor == nil ? [] : [.paginationCursor]
+ ) {
let response = try await service.listTrashFiles(
driveId: driveID,
- with: "etag",
+ with: Self.itemIncludedResources,
options: ListKDriveTrashOptions(cursor: cursor, limit: limit, orderBy: ["name"], order: "asc")
)
return KDriveItemPage(items: response.data.map(\.remoteItem), nextCursor: response.cursor, hasMore: response.hasMore)
@@ -297,11 +392,11 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func listWorkingSetRelevantItems(driveID: Int, latestLimit: Int) async throws -> [KDriveRemoteItem] {
- try await performNetworkOperation("listWorkingSetRelevantItems") {
- let latest = try await service.listLastModifiedFiles(driveId: driveID, with: "etag", limit: latestLimit).data
- let favorites = try await service.listFavoriteFiles(driveId: driveID, with: "etag", limit: latestLimit).data
- let myShared = try await service.listMySharedFiles(driveId: driveID, with: "etag", limit: latestLimit).data
- let sharedWithMe = try await service.listSharedWithMeFiles(driveId: driveID, with: "etag", limit: latestLimit).data
+ try await performNetworkOperation(.listWorkingSetRelevantItems) {
+ let latest = try await service.listLastModifiedFiles(driveId: driveID, with: Self.itemIncludedResources, limit: latestLimit).data
+ let favorites = try await service.listFavoriteFiles(driveId: driveID, with: Self.itemIncludedResources, limit: latestLimit).data
+ let myShared = try await service.listMySharedFiles(driveId: driveID, with: Self.itemIncludedResources, limit: latestLimit).data
+ let sharedWithMe = try await service.listSharedWithMeFiles(driveId: driveID, with: Self.itemIncludedResources, limit: latestLimit).data
var itemsByID: [Int: KDriveRemoteItem] = [:]
for item in latest + favorites + myShared + sharedWithMe {
itemsByID[item.id] = item.remoteItem
@@ -319,10 +414,12 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
since: Date
) async throws -> [KDrivePartialActivityResult] {
guard fileIDs.isEmpty == false else { return [] }
- return try await performNetworkOperation("listPartialActivities") {
+ return try await performNetworkOperation(.listPartialActivities) {
let response = try await service.listPartialFileActivities(
driveId: driveID,
- with: "file,file.etag",
+ // Partial listing supports the file expansion. A nested etag
+ // expansion is not part of this route's upstream contract.
+ with: "file",
options: ListKDrivePartialFileActivitiesOptions(
actions: [
"file_create", "file_delete", "file_trash", "file_restore",
@@ -355,19 +452,34 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
public func downloadFileOperation(driveID: Int, fileID: Int) throws -> KDriveTransferOperation {
let operation = try service.downloadFile(driveId: driveID, fileId: fileID)
+ let diagnosticSpan = makeDeferredDiagnosticSpan(for: .downloadFile)
return KDriveTransferOperation(
progress: operation.progress,
value: {
- try await performNetworkOperation("downloadFile") {
+ let span = await diagnosticSpan.resolve()
+ let progressTask = Self.trackProgress(
+ operation.progress,
+ with: span
+ )
+ defer { progressTask.cancel() }
+ return try await performNetworkOperation(
+ .downloadFile,
+ existingSpan: span
+ ) {
try await operation.value
}
},
- cancellation: operation.cancel
+ cancellation: {
+ operation.cancel()
+ Task {
+ await diagnosticSpan.cancel()
+ }
+ }
)
}
public func thumbnail(driveID: Int, fileID: Int, width: Int?, height: Int?) async throws -> Data {
- try await performNetworkOperation("thumbnail") {
+ try await performNetworkOperation(.thumbnail) {
try await service.getFileThumbnail(
driveId: driveID,
fileId: fileID,
@@ -408,11 +520,12 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
clientToken: String? = nil,
contentHash: String? = nil
) throws -> KDriveTransferOperation {
+ try Self.validateDirectUploadByteCount(contents.count)
let operation = try service.uploadFile(
driveId: driveID,
data: contents,
options: UploadKDriveFileOptions(
- with: "etag",
+ with: Self.itemIncludedResources,
clientToken: clientToken,
conflict: conflictStrategy.rawValue,
directoryId: parentID,
@@ -421,14 +534,32 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
totalChunkHash: contentHash
)
)
+ let diagnosticSpan = makeDeferredDiagnosticSpan(
+ for: .uploadFile,
+ additionalOptionShape: Self.diagnosticConflictOption(conflictStrategy)
+ )
return KDriveTransferOperation(
progress: operation.progress,
value: {
- try await performNetworkOperation("uploadFile") {
+ let span = await diagnosticSpan.resolve()
+ let progressTask = Self.trackProgress(
+ operation.progress,
+ with: span
+ )
+ defer { progressTask.cancel() }
+ return try await performNetworkOperation(
+ .uploadFile,
+ existingSpan: span
+ ) {
try await operation.value.data.remoteItem
}
},
- cancellation: operation.cancel
+ cancellation: {
+ operation.cancel()
+ Task {
+ await diagnosticSpan.cancel()
+ }
+ }
)
}
@@ -461,11 +592,12 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
contents: Data,
lastModifiedAt: Date?
) throws -> KDriveTransferOperation {
+ try Self.validateDirectUploadByteCount(contents.count)
let operation = try service.uploadFile(
driveId: driveID,
data: contents,
options: UploadKDriveFileOptions(
- with: "etag",
+ with: Self.itemIncludedResources,
ifMatch: expectedETag,
clientToken: clientToken,
fileId: fileID,
@@ -473,19 +605,34 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
totalChunkHash: contentHash
)
)
+ let diagnosticSpan = makeDeferredDiagnosticSpan(for: .replaceFile)
return KDriveTransferOperation(
progress: operation.progress,
value: {
- try await performNetworkOperation("replaceFile") {
+ let span = await diagnosticSpan.resolve()
+ let progressTask = Self.trackProgress(
+ operation.progress,
+ with: span
+ )
+ defer { progressTask.cancel() }
+ return try await performNetworkOperation(
+ .replaceFile,
+ existingSpan: span
+ ) {
try await operation.value.data.remoteItem
}
},
- cancellation: operation.cancel
+ cancellation: {
+ operation.cancel()
+ Task {
+ await diagnosticSpan.cancel()
+ }
+ }
)
}
public func createDirectory(driveID: Int, parentID: Int, name: String) async throws -> KDriveRemoteItem {
- try await performNetworkOperation("createDirectory") {
+ try await performNetworkOperation(.createDirectory) {
try await service.createDirectory(
driveId: driveID,
fileId: parentID,
@@ -495,13 +642,13 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func renameItem(driveID: Int, fileID: Int, name: String) async throws {
- _ = try await performNetworkOperation("renameItem") {
+ _ = try await performNetworkOperation(.renameItem) {
try await service.renameFile(driveId: driveID, fileId: fileID, options: RenameKDriveFileOptions(name: name))
}
}
public func moveItem(driveID: Int, fileID: Int, destinationParentID: Int, name: String?) async throws {
- _ = try await performNetworkOperation("moveItem") {
+ _ = try await performNetworkOperation(.moveItem) {
try await service.moveFile(
driveId: driveID,
fileId: fileID,
@@ -512,7 +659,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func updateModificationDate(driveID: Int, fileID: Int, date: Date) async throws {
- _ = try await performNetworkOperation("updateModificationDate") {
+ _ = try await performNetworkOperation(.updateModificationDate) {
try await service.updateFileLastModified(
driveId: driveID,
fileId: fileID,
@@ -522,19 +669,19 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func trashItem(driveID: Int, fileID: Int) async throws {
- _ = try await performNetworkOperation("trashItem") {
+ _ = try await performNetworkOperation(.trashItem) {
try await service.trashFileV2(driveId: driveID, fileId: fileID)
}
}
public func deleteTrashedItem(driveID: Int, fileID: Int) async throws {
- _ = try await performNetworkOperation("deleteTrashedItem") {
+ _ = try await performNetworkOperation(.deleteTrashedItem) {
try await service.removeTrashedFile(driveId: driveID, fileId: fileID)
}
}
public func setFavorite(driveID: Int, fileID: Int, isFavorite: Bool) async throws {
- _ = try await performNetworkOperation(isFavorite ? "favoriteItem" : "unfavoriteItem") {
+ _ = try await performNetworkOperation(.favoriteItem) {
if isFavorite {
try await service.favoriteFile(driveId: driveID, fileId: fileID)
} else {
@@ -543,21 +690,25 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
}
- public func duplicateItem(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
- try await performNetworkOperation("duplicateItem") {
- try await service.duplicateFile(driveId: driveID, fileId: fileID).data.remoteItem
+ public func duplicateItem(driveID: Int, fileID: Int, name: String) async throws -> KDriveRemoteItem {
+ try await performNetworkOperation(.duplicateItem) {
+ try await service.duplicateFile(
+ driveId: driveID,
+ fileId: fileID,
+ options: DuplicateKDriveFileOptions(name: name)
+ ).data.remoteItem
}
}
public func trashedItem(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
- try await performNetworkOperation("trashedItem") {
+ try await performNetworkOperation(.trashedItem) {
try await service.getTrashedFile(driveId: driveID, fileId: fileID).data.remoteItem
}
}
public func existingFileIDs(driveID: Int, fileIDs: [Int]) async throws -> Set {
guard fileIDs.isEmpty == false else { return [] }
- return try await performNetworkOperation("existingFileIDs") {
+ return try await performNetworkOperation(.existingFileIDs) {
Set(try await service.checkFilesExistence(driveId: driveID, fileIds: fileIDs).data.lazy
.filter(\.result)
.map(\.id))
@@ -565,7 +716,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func restoreTrashedItem(driveID: Int, fileID: Int, destinationParentID: Int) async throws {
- _ = try await performNetworkOperation("restoreTrashedItem") {
+ _ = try await performNetworkOperation(.restoreTrashedItem) {
try await service.restoreTrashedFile(
driveId: driveID,
fileId: fileID,
@@ -575,14 +726,47 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func shareLink(driveID: Int, fileID: Int) async throws -> KDriveShareLinkSummary? {
+ let span = await ProviderDiagnosticSpan.start(
+ source: diagnosticSource,
+ operation: .shareLink,
+ routeTemplate: Self.diagnosticRoute(for: .shareLink),
+ optionShape: Self.diagnosticOptions(for: .shareLink),
+ recorder: diagnosticRecorder
+ )
+ let correlationID = span.correlationID.uuidString
+ let startedAt = Date()
+ ProviderLog.network.debug("network start operation(\(ProviderDiagnosticOperation.shareLink.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public))")
do {
- return try await performNetworkOperation("shareLink") {
+ let summary = try await span.withCorrelation {
try Self.shareLinkSummary(
try await service.getFileShareLink(driveId: driveID, fileId: fileID).data
)
}
+ await span.complete(statusClass: .success)
+ let durationMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
+ ProviderLog.network.info("network success operation(\(ProviderDiagnosticOperation.shareLink.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public))")
+ return summary
} catch APIClientError.unacceptableStatusCode(let statusCode, _, _) where statusCode == 404 {
+ // Absence is the documented optional result for this adapter, not
+ // a failed provider operation.
+ await span.complete(statusClass: .success)
+ let durationMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
+ ProviderLog.network.info("network success operation(\(ProviderDiagnosticOperation.shareLink.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public))")
return nil
+ } catch {
+ let durationMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
+ let statusCode = KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode
+ let nsError = error as NSError
+ ProviderLog.network.error("network failure operation(\(ProviderDiagnosticOperation.shareLink.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public)) httpStatusCode(\(statusCode ?? 0, privacy: .public)) errorDomain(\(nsError.domain, privacy: .public)) errorCode(\(nsError.code, privacy: .public))")
+ if ProviderDiagnosticErrorClassifier.classify(error) == .cancellation {
+ await span.cancel()
+ } else {
+ await span.fail(
+ error: error,
+ statusClass: statusCode.map(ProviderDiagnosticStatusClass.init)
+ )
+ }
+ throw error
}
}
@@ -594,7 +778,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
guard configuration.isValid else {
throw KDriveContextActionError.passwordRequired
}
- return try await performNetworkOperation("createShareLink") {
+ return try await performNetworkOperation(.createShareLink) {
let link = try await service.createFileShareLink(
driveId: driveID,
fileId: fileID,
@@ -609,12 +793,35 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
fileID: Int,
configuration: KDriveShareLinkConfiguration
) async throws -> KDriveShareLinkSummary {
- _ = try await performNetworkOperation("updateShareLink") {
- try await service.updateFileShareLink(
+ // Update options are a patch. Avoid reapplying unchanged access/editing
+ // policy while changing an independent setting. Expiration is also
+ // plan-gated: null is needed only when clearing an existing date.
+ guard let current = try await shareLink(driveID: driveID, fileID: fileID) else {
+ throw KDriveContextActionError.invalidShareLinkURL
+ }
+ if configuration.hasSameReportedSettings(as: current.configuration),
+ configuration.access != .password || configuration.password == nil {
+ return current
+ }
+ let clearsExpiration = configuration.validUntil == nil && current.configuration.validUntil != nil
+ _ = try await performNetworkOperation(.updateShareLink) {
+ let options = Self.updateShareLinkOptions(configuration, current: current.configuration)
+ let potassiumRequest = try KDriveRequests.updateFileShareLink(
driveId: driveID,
fileId: fileID,
- options: Self.updateShareLinkOptions(configuration)
+ options: options
+ )
+ let body = try JSONEncoder().encode(
+ UpdateShareLinkRequestBody(options: options, clearsExpiration: clearsExpiration)
+ )
+ let request = APIRequest>(
+ method: potassiumRequest.method,
+ path: potassiumRequest.path,
+ queryParameters: potassiumRequest.queryParameters,
+ headers: potassiumRequest.headers,
+ body: body
)
+ return try await apiClient.send(request)
}
guard let link = try await shareLink(driveID: driveID, fileID: fileID) else {
throw KDriveContextActionError.invalidShareLinkURL
@@ -623,7 +830,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
public func deleteShareLink(driveID: Int, fileID: Int) async throws {
- _ = try await performNetworkOperation("deleteShareLink") {
+ _ = try await performNetworkOperation(.deleteShareLink) {
try await service.deleteFileShareLink(driveId: driveID, fileId: fileID)
}
}
@@ -634,7 +841,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
page: Int,
pageSize: Int
) async throws -> KDriveFileVersionPage {
- try await performNetworkOperation("fileVersions") {
+ try await performNetworkOperation(.fileVersions) {
let response = try await service.listFileVersions(
driveId: driveID,
fileId: fileID,
@@ -668,7 +875,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
destinationParentID: Int,
name: String
) async throws -> KDriveRemoteItem {
- let restoredID = try await performNetworkOperation("restoreFileVersion") {
+ let restoredID = try await performNetworkOperation(.restoreFileVersion) {
try await service.restoreFileVersionToDirectory(
driveId: driveID,
fileId: fileID,
@@ -697,18 +904,21 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
private static func updateShareLinkOptions(
- _ configuration: KDriveShareLinkConfiguration
+ _ configuration: KDriveShareLinkConfiguration,
+ current: KDriveShareLinkConfiguration
) -> UpdateKDriveFileShareLinkOptions {
UpdateKDriveFileShareLinkOptions(
+ // Omission/null inherits can_edit; it does not preserve comments.
canComment: configuration.allowsComments,
- canDownload: configuration.allowsDownload,
- canEdit: configuration.allowsEditing,
- canRequestAccess: configuration.allowsAccessRequests,
- canSeeInfo: configuration.showsFileInformation,
- canSeeStats: configuration.showsStatistics,
+ canDownload: configuration.allowsDownload == current.allowsDownload ? nil : configuration.allowsDownload,
+ canEdit: configuration.allowsEditing == current.allowsEditing ? nil : configuration.allowsEditing,
+ canRequestAccess: configuration.allowsAccessRequests == current.allowsAccessRequests ? nil : configuration.allowsAccessRequests,
+ canSeeInfo: configuration.showsFileInformation == current.showsFileInformation ? nil : configuration.showsFileInformation,
+ canSeeStats: configuration.showsStatistics == current.showsStatistics ? nil : configuration.showsStatistics,
password: configuration.access == .password ? configuration.password : nil,
- right: configuration.access.rawValue,
- validUntil: configuration.validUntil.map(unixTimestamp)
+ right: configuration.access == current.access ? nil : configuration.access.rawValue,
+ validUntil: configuration.validUntil.map(unixTimestamp) == current.validUntil.map(unixTimestamp)
+ ? nil : configuration.validUntil.map(unixTimestamp)
)
}
@@ -716,7 +926,9 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
guard let url = URL(string: link.url) else {
throw KDriveContextActionError.invalidShareLinkURL
}
- let access = KDriveShareLinkConfiguration.Access(rawValue: link.right) ?? .public
+ guard let access = KDriveShareLinkConfiguration.Access(rawValue: link.right) else {
+ throw KDriveContextActionError.unsupportedShareLinkAccess
+ }
return KDriveShareLinkSummary(
url: url,
configuration: KDriveShareLinkConfiguration(
@@ -733,6 +945,44 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
)
}
+ /// potassiumChannel 0.3.0 uses synthesized optional encoding for share
+ /// updates, which omits a nil `valid_until`. The endpoint defines that
+ /// field as nullable, so this adapter deliberately encodes JSON null to
+ /// clear an existing expiration while retaining the pinned typed route.
+ private struct UpdateShareLinkRequestBody: Encodable {
+ let options: UpdateKDriveFileShareLinkOptions
+ let clearsExpiration: Bool
+
+ private enum CodingKeys: String, CodingKey {
+ case canComment = "can_comment"
+ case canDownload = "can_download"
+ case canEdit = "can_edit"
+ case canRequestAccess = "can_request_access"
+ case canSeeInfo = "can_see_info"
+ case canSeeStats = "can_see_stats"
+ case password
+ case right
+ case validUntil = "valid_until"
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encodeIfPresent(options.canComment, forKey: .canComment)
+ try container.encodeIfPresent(options.canDownload, forKey: .canDownload)
+ try container.encodeIfPresent(options.canEdit, forKey: .canEdit)
+ try container.encodeIfPresent(options.canRequestAccess, forKey: .canRequestAccess)
+ try container.encodeIfPresent(options.canSeeInfo, forKey: .canSeeInfo)
+ try container.encodeIfPresent(options.canSeeStats, forKey: .canSeeStats)
+ try container.encodeIfPresent(options.password, forKey: .password)
+ try container.encodeIfPresent(options.right, forKey: .right)
+ if let validUntil = options.validUntil {
+ try container.encode(validUntil, forKey: .validUntil)
+ } else if clearsExpiration {
+ try container.encodeNil(forKey: .validUntil)
+ }
+ }
+ }
+
private static func displayName(for user: KDriveUser) -> String {
if let displayName = user.displayName, displayName.isEmpty == false {
return displayName
@@ -743,7 +993,7 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
private func performDriveDiscoveryRequest(
endpoint: String,
- _ work: () async throws -> Value
+ _ work: @Sendable () async throws -> Value
) async throws -> Value {
ProviderLog.network.debug(
"drive discovery request endpoint(\(endpoint, privacy: .public))"
@@ -765,28 +1015,164 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
}
}
- private func performNetworkOperation(
- _ operation: String,
- _ work: () async throws -> Value
+ private func performNetworkOperation(
+ _ operation: ProviderDiagnosticOperation,
+ routeTemplate: ProviderDiagnosticRouteTemplate? = nil,
+ additionalOptionShape: [ProviderDiagnosticOption] = [],
+ existingSpan: ProviderDiagnosticSpan? = nil,
+ _ work: @Sendable () async throws -> Value
) async throws -> Value {
- let correlationID = UUID().uuidString
+ let span: ProviderDiagnosticSpan
+ if let existingSpan {
+ span = existingSpan
+ } else {
+ span = await ProviderDiagnosticSpan.start(
+ source: diagnosticSource,
+ operation: operation,
+ routeTemplate: routeTemplate ?? Self.diagnosticRoute(for: operation),
+ optionShape: Self.diagnosticOptions(for: operation) + additionalOptionShape,
+ recorder: diagnosticRecorder
+ )
+ }
+ let correlationID = span.correlationID.uuidString
let startedAt = Date()
- ProviderLog.network.debug("network start operation(\(operation, privacy: .public)) correlationID(\(correlationID, privacy: .public))")
+ ProviderLog.network.debug("network start operation(\(operation.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public))")
do {
- let value = try await work()
+ let value = try await span.withCorrelation(operation: work)
let durationMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
- ProviderLog.network.info("network success operation(\(operation, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public))")
+ ProviderLog.network.info("network success operation(\(operation.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public))")
+ await span.complete(statusClass: .success)
return value
} catch {
let durationMilliseconds = Int(Date().timeIntervalSince(startedAt) * 1_000)
let statusCode = KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode
let nsError = error as NSError
- ProviderLog.network.error("network failure operation(\(operation, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public)) httpStatusCode(\(statusCode ?? 0, privacy: .public)) errorDomain(\(nsError.domain, privacy: .public)) errorCode(\(nsError.code, privacy: .public))")
+ ProviderLog.network.error("network failure operation(\(operation.rawValue, privacy: .public)) correlationID(\(correlationID, privacy: .public)) durationMilliseconds(\(durationMilliseconds, privacy: .public)) httpStatusCode(\(statusCode ?? 0, privacy: .public)) errorDomain(\(nsError.domain, privacy: .public)) errorCode(\(nsError.code, privacy: .public))")
+ if ProviderDiagnosticErrorClassifier.classify(error) == .cancellation {
+ await span.cancel()
+ } else {
+ await span.fail(
+ error: error,
+ statusClass: statusCode.map(ProviderDiagnosticStatusClass.init)
+ )
+ }
throw error
}
}
+ private func makeDeferredDiagnosticSpan(
+ for operation: ProviderDiagnosticOperation,
+ routeTemplate: ProviderDiagnosticRouteTemplate? = nil,
+ additionalOptionShape: [ProviderDiagnosticOption] = []
+ ) -> DeferredProviderDiagnosticSpan {
+ DeferredProviderDiagnosticSpan(
+ source: diagnosticSource,
+ operation: operation,
+ routeTemplate: routeTemplate ?? Self.diagnosticRoute(for: operation),
+ optionShape: Self.diagnosticOptions(for: operation) + additionalOptionShape,
+ recorder: diagnosticRecorder
+ )
+ }
+
+ static func trackProgress(
+ _ progress: Progress,
+ with span: ProviderDiagnosticSpan
+ ) -> Task {
+ Task {
+ while Task.isCancelled == false {
+ let total = progress.totalUnitCount
+ if total > 0 {
+ // URLSession progress can contain weighted children. Its
+ // parent unit count stays zero while those children make
+ // real progress; fractionCompleted includes their work.
+ await span.progress(fractionCompleted: progress.fractionCompleted)
+ }
+ do {
+ try await Task.sleep(for: .milliseconds(250))
+ } catch {
+ return
+ }
+ }
+ }
+ }
+
+ private static func diagnosticRoute(
+ for operation: ProviderDiagnosticOperation
+ ) -> ProviderDiagnosticRouteTemplate? {
+ switch operation {
+ case .listDrives: .driveDiscovery
+ case .itemLookup: .item
+ case .listDirectory: .listDirectory
+ case .listAdvancedDirectory: .listAdvancedDirectory
+ case .listTrash: .trash
+ case .listPartialActivities: .partialActivities
+ case .downloadFile: .download
+ case .thumbnail: .thumbnail
+ case .uploadFile, .replaceFile: .upload
+ case .createDirectory: .createDirectory
+ case .renameItem: .rename
+ case .moveItem: .move
+ case .trashItem: .trashItem
+ case .deleteTrashedItem: .deleteTrashedItem
+ case .favoriteItem: .favorite
+ case .duplicateItem: .duplicate
+ case .trashedItem: .trashedItem
+ case .existingFileIDs: .existingFileIDs
+ case .restoreTrashedItem: .restoreTrash
+ case .shareLink, .createShareLink, .updateShareLink, .deleteShareLink:
+ .shareLink
+ case .fileVersions: .versions
+ case .restoreFileVersion: .restoreVersion
+ default: nil
+ }
+ }
+
+ private static func diagnosticOptions(
+ for operation: ProviderDiagnosticOperation
+ ) -> [ProviderDiagnosticOption] {
+ switch operation {
+ case .itemLookup:
+ [.includeETag]
+ case .listDirectory, .listTrash:
+ [.pageLimit, .orderByName, .includeETag]
+ case .listAdvancedDirectory:
+ [.pageLimit, .orderByTypeThenName, .includeCapabilities]
+ case .listWorkingSetRelevantItems:
+ [.pageLimit, .includeETag]
+ case .listPartialActivities:
+ [.activityBatch, .includeETag]
+ case .downloadFile:
+ [.cancellableTransfer]
+ case .thumbnail:
+ [.thumbnailDimensions, .cancellableTransfer]
+ case .uploadFile:
+ [.includeETag, .clientToken, .contentHash, .lastModifiedAt, .cancellableTransfer]
+ case .replaceFile:
+ [.includeETag, .conditionalETag, .stableFileID, .clientToken, .contentHash, .lastModifiedAt, .cancellableTransfer]
+ case .moveItem:
+ [.destinationParent, .optionalName, .conflictRename]
+ case .restoreTrashedItem, .restoreFileVersion:
+ [.destinationParent]
+ case .createShareLink, .updateShareLink:
+ [.shareConfiguration]
+ case .fileVersions:
+ [.versionPagination]
+ default:
+ []
+ }
+ }
+
+ private static func diagnosticConflictOption(
+ _ strategy: KDriveUploadConflictStrategy
+ ) -> [ProviderDiagnosticOption] {
+ switch strategy {
+ case .error: [.conflictError]
+ case .rename: [.conflictRename]
+ case .version: [.conflictVersion]
+ }
+ }
+
private static func unixTimestamp(_ date: Date) -> Int {
Int(date.timeIntervalSince1970)
}
@@ -794,11 +1180,24 @@ public struct PotassiumKDriveService: KDriveFileProviding, KDriveWorkingSetRemot
public enum KDriveRemoteErrorClassifier {
public static func apiRejection(from error: Error) -> KDriveRemoteAPIRejection? {
- guard case let APIClientError.unacceptableStatusCode(statusCode, body, _) = error else {
+ guard case let APIClientError.unacceptableStatusCode(statusCode, body, metadata) = error else {
return nil
}
- return KDriveRemoteAPIRejection(statusCode: statusCode, responseBody: body)
+ return KDriveRemoteAPIRejection(
+ statusCode: statusCode,
+ responseBody: body,
+ retryAfterSeconds: parsedRetryAfterSeconds(metadata.retryAfter)
+ )
+ }
+
+ private static func parsedRetryAfterSeconds(_ value: String?) -> Int? {
+ guard let value,
+ let seconds = Int(value.trimmingCharacters(in: .whitespacesAndNewlines)),
+ seconds >= 0 else {
+ return nil
+ }
+ return seconds
}
public static func isInvalidCursor(_ error: Error) -> Bool {
@@ -842,16 +1241,21 @@ public enum KDriveRemoteErrorClassifier {
public struct KDriveRemoteAPIRejection: Equatable, Sendable {
public let statusCode: Int
public let responseBody: String
+ public let retryAfterSeconds: Int?
- public init(statusCode: Int, responseBody: String) {
+ public init(statusCode: Int, responseBody: String, retryAfterSeconds: Int? = nil) {
self.statusCode = statusCode
self.responseBody = responseBody
+ self.retryAfterSeconds = retryAfterSeconds
}
public var recovery: KDriveRemoteAPIRejectionRecovery {
if statusCode == 401 {
return .notAuthenticated
}
+ if statusCode == 408 || statusCode == 429 {
+ return .serverUnreachable
+ }
if isInsufficientQuota {
return .insufficientQuota
}
@@ -895,6 +1299,66 @@ public enum KDriveRemoteAPIRejectionRecovery: Equatable, Sendable {
case cannotSynchronize
}
+/// Defers diagnostic start until a lazy transfer is consumed or cancelled.
+/// This preserves the transfer API's lazy contract and prevents abandoned
+/// operation values from leaving permanent start-only records.
+private actor DeferredProviderDiagnosticSpan {
+ private let source: ProviderDiagnosticSource
+ private let operation: ProviderDiagnosticOperation
+ private let routeTemplate: ProviderDiagnosticRouteTemplate?
+ private let optionShape: [ProviderDiagnosticOption]
+ private let recorder: (any ProviderDiagnosticRecording)?
+ private var span: ProviderDiagnosticSpan?
+ private var inFlightSpan: Task?
+
+ init(
+ source: ProviderDiagnosticSource,
+ operation: ProviderDiagnosticOperation,
+ routeTemplate: ProviderDiagnosticRouteTemplate?,
+ optionShape: [ProviderDiagnosticOption],
+ recorder: (any ProviderDiagnosticRecording)?
+ ) {
+ self.source = source
+ self.operation = operation
+ self.routeTemplate = routeTemplate
+ self.optionShape = optionShape
+ self.recorder = recorder
+ }
+
+ func resolve() async -> ProviderDiagnosticSpan {
+ if let span {
+ return span
+ }
+ if let inFlightSpan {
+ return await inFlightSpan.value
+ }
+ let source = source
+ let operation = operation
+ let routeTemplate = routeTemplate
+ let optionShape = optionShape
+ let recorder = recorder
+ let creation = Task {
+ await ProviderDiagnosticSpan.start(
+ source: source,
+ operation: operation,
+ routeTemplate: routeTemplate,
+ optionShape: optionShape,
+ recorder: recorder
+ )
+ }
+ inFlightSpan = creation
+ let created = await creation.value
+ span = created
+ inFlightSpan = nil
+ return created
+ }
+
+ func cancel() async {
+ let span = await resolve()
+ await span.cancel()
+ }
+}
+
private struct KDriveInitPayload: Decodable, Sendable {
let drives: [KDriveInitDrive]
}
diff --git a/PotassiumProviderCore/ProviderActionItemResolver.swift b/PotassiumProviderCore/ProviderActionItemResolver.swift
new file mode 100644
index 0000000..bfd503d
--- /dev/null
+++ b/PotassiumProviderCore/ProviderActionItemResolver.swift
@@ -0,0 +1,121 @@
+import FileProvider
+import Foundation
+
+/// Translates FileProviderUI's opaque selection through the system namespace.
+/// Never interprets a macOS document ID, a filename, or a local path as a server ID.
+public struct ProviderActionItemResolver: Sendable {
+ public typealias Completion = @Sendable (Result) -> Void
+ public struct Binding: Sendable {
+ public let itemIdentifier: String
+ public let domainIdentifier: String
+ public init(itemIdentifier: String, domainIdentifier: String) {
+ self.itemIdentifier = itemIdentifier
+ self.domainIdentifier = domainIdentifier
+ }
+ }
+
+ private let visibleURL: @Sendable (String, @escaping Completion) -> Void
+ private let identifier: @Sendable (URL, @escaping Completion) -> Void
+
+ public init(
+ visibleURL: @escaping @Sendable (String, @escaping Completion) -> Void,
+ identifier: @escaping @Sendable (URL, @escaping Completion) -> Void
+ ) {
+ self.visibleURL = visibleURL
+ self.identifier = identifier
+ }
+
+ public func resolve(
+ _ selection: String, domainIdentifier: String, engine: ProviderEncryptionMode,
+ timeout: Duration = .seconds(90)
+ ) async throws -> String {
+ try Task.checkCancellation()
+ guard engine != .opaqueVaultV1 else { throw ProviderActionItemResolutionError.invalidIdentifier }
+ let deadline = StabilityDeadline(budget: timeout)
+ do {
+ let url = try await StabilityCallbackWaiter().wait(timeout: deadline.remaining()) { completion in
+ visibleURL(selection, completion)
+ }
+ try Task.checkCancellation()
+ guard url.isFileURL else { throw ProviderActionItemResolutionError.unavailable }
+ // No contents are read and no URL is retained. Balance the security scope
+ // while asking the system to resolve the URL back into its provider domain.
+ let accessed = url.startAccessingSecurityScopedResource()
+ defer { if accessed { url.stopAccessingSecurityScopedResource() } }
+ let binding = try await StabilityCallbackWaiter().wait(timeout: deadline.remaining()) { completion in
+ identifier(url, completion)
+ }
+ try Task.checkCancellation()
+ guard binding.domainIdentifier == domainIdentifier else { throw ProviderActionItemResolutionError.domainMismatch }
+ let resolved = try Self.validate(binding.itemIdentifier, engine: engine)
+ if let canonicalSelection = try? Self.validate(selection, engine: engine), canonicalSelection != resolved {
+ throw ProviderActionItemResolutionError.invalidIdentifier
+ }
+ return resolved
+ } catch is CancellationError {
+ throw CancellationError()
+ } catch StabilityDeadlineError.expired {
+ throw ProviderActionItemResolutionError.timedOut
+ } catch let error as ProviderActionItemResolutionError {
+ throw error
+ } catch {
+ // System errors can contain user-visible URLs and private identifiers.
+ throw ProviderActionItemResolutionError.unavailable
+ }
+ }
+
+ public static func validate(_ identifier: String, engine: ProviderEncryptionMode) throws -> String {
+ switch engine {
+ case .legacyPlaintext:
+ guard let parsed = try? KDriveItemIdentifier(rawValue: identifier),
+ parsed == .root || parsed.fileID != nil else { throw ProviderActionItemResolutionError.invalidIdentifier }
+ case .opaqueVaultV2:
+ guard VaultItemIdentifier(fileProviderIdentifier: identifier) != nil else { throw ProviderActionItemResolutionError.invalidIdentifier }
+ case .opaqueVaultV1:
+ throw ProviderActionItemResolutionError.invalidIdentifier
+ }
+ return identifier
+ }
+
+ public static func resolve(
+ _ selection: NSFileProviderItemIdentifier, configuration: ProviderDomainConfiguration
+ ) async throws -> NSFileProviderItemIdentifier {
+ #if os(macOS)
+ let domain = NSFileProviderDomain(identifier: .init(configuration.domainIdentifier), displayName: configuration.displayName)
+ guard let manager = NSFileProviderManager(for: domain) else { throw ProviderActionItemResolutionError.unavailable }
+ let resolver = Self(visibleURL: { selection, completion in
+ manager.getUserVisibleURL(for: .init(selection)) { url, error in
+ if let error { completion(.failure(error)) }
+ else if let url { completion(.success(url)) }
+ else { completion(.failure(ProviderActionItemResolutionError.unavailable)) }
+ }
+ }, identifier: { url, completion in
+ NSFileProviderManager.getIdentifierForUserVisibleFile(at: url) { item, domain, error in
+ if let error { completion(.failure(error)) }
+ else if let item, let domain {
+ completion(.success(.init(itemIdentifier: item.rawValue, domainIdentifier: domain.rawValue)))
+ } else { completion(.failure(ProviderActionItemResolutionError.unavailable)) }
+ }
+ })
+ let identifier = try await resolver.resolve(selection.rawValue, domainIdentifier: configuration.domainIdentifier, engine: configuration.encryptionMode)
+ return .init(identifier)
+ #else
+ // Replicated iOS extensions cannot obtain user-visible URLs. Files supplies
+ // provider identifiers; reject unknown identifiers without a path fallback.
+ try Task.checkCancellation()
+ return try .init(validate(selection.rawValue, engine: configuration.encryptionMode))
+ #endif
+ }
+}
+
+public enum ProviderActionItemResolutionError: Error, Equatable, LocalizedError, Sendable {
+ case invalidIdentifier, domainMismatch, unavailable, timedOut
+ public var errorDescription: String? {
+ switch self {
+ case .invalidIdentifier, .domainMismatch, .unavailable:
+ "The selected item could not be identified in this File Provider domain. Close this action and select the item again."
+ case .timedOut:
+ "Identifying the selected item timed out. Close this action and try again."
+ }
+ }
+}
diff --git a/PotassiumProviderCore/ProviderActionRuntime.swift b/PotassiumProviderCore/ProviderActionRuntime.swift
index b3f1609..032c83b 100644
--- a/PotassiumProviderCore/ProviderActionRuntime.swift
+++ b/PotassiumProviderCore/ProviderActionRuntime.swift
@@ -31,6 +31,10 @@ public struct ProviderActionRuntime: Sendable {
throw ProviderActionRuntimeError.configurationUnavailable
}
+ guard configuration.isCompatible(with: .current) else {
+ throw ProviderActionRuntimeError.configurationUnavailable
+ }
+
let tokenStore = KeychainOAuthTokenStore(accessGroup: ProviderConstants.keychainAccessGroup)
guard configuration.encryptionMode != .opaqueVaultV1 else {
throw ProviderActionRuntimeError.configurationUnavailable
@@ -49,9 +53,11 @@ public struct ProviderActionRuntime: Sendable {
try await tokenStore.saveToken(token, accountIdentifier: configuration.accountIdentifier)
}
- let service = PotassiumKDriveService(bearerToken: token.accessToken)
- let eventStore = try? KDriveProviderEventSQLiteStore(
- appGroupIdentifier: ProviderConstants.appGroupIdentifier
+ let eventStore = try? ProviderEventStoreFactory.makeDefault()
+ let service = PotassiumKDriveService(
+ bearerToken: token.accessToken,
+ diagnosticRecorder: eventStore as? any ProviderDiagnosticRecording,
+ diagnosticSource: .actionExtension
)
let encryptedVault: (any EncryptedVaultProviding)?
if configuration.encryptionMode == .opaqueVaultV2 {
diff --git a/PotassiumProviderCore/ProviderDiagnosticSpan.swift b/PotassiumProviderCore/ProviderDiagnosticSpan.swift
new file mode 100644
index 0000000..9491e67
--- /dev/null
+++ b/PotassiumProviderCore/ProviderDiagnosticSpan.swift
@@ -0,0 +1,488 @@
+import Darwin
+import FileProvider
+import Foundation
+@preconcurrency import SQLite
+
+/// Propagates a privacy-safe correlation identifier through structured tasks.
+///
+/// The value is deliberately limited to a UUID. Nested diagnostics can be
+/// correlated without carrying item identifiers, paths, URLs, or other private
+/// request context through the logging API.
+public enum ProviderDiagnosticCorrelationContext {
+ @TaskLocal public static var current: UUID?
+ @TaskLocal public static var parentSpanID: UUID?
+ @TaskLocal public static var subjectAlias: UUID?
+
+ public static func withCorrelation(
+ _ correlationID: UUID,
+ operation: @Sendable () async throws -> Result
+ ) async rethrows -> Result {
+ try await $current.withValue(correlationID, operation: operation)
+ }
+}
+
+/// Classifies only stable error categories. Error descriptions, domains, and
+/// user-info values are never copied into a diagnostic event.
+public enum ProviderDiagnosticErrorClassifier {
+ public static func classify(_ error: any Error) -> ProviderDiagnosticErrorClass {
+ classifyOriginal(originalError(error))
+ }
+
+ private static func classifyOriginal(_ error: any Error) -> ProviderDiagnosticErrorClass {
+ if error is CancellationError {
+ return .cancellation
+ }
+
+ if sqliteCode(error) != nil || error is DomainConfigurationStoreError { return .storage }
+ if error is DecodingError { return .validation }
+
+ if let snapshotError = error as? KDriveSnapshotStoreError {
+ switch snapshotError {
+ case .staleSnapshot: return .concurrentSnapshot
+ case .expiredGeneration, .invalidPageToken: return .invalidCursor
+ case .missingAppGroupContainer: return .storage
+ }
+ }
+
+ if let rejection = KDriveRemoteErrorClassifier.apiRejection(from: error) {
+ switch rejection.statusCode {
+ case 401: return .authentication
+ case 403: return .permission
+ case 404: return .notFound
+ case 408, 429: return .network
+ case 409, 412: return .conflict
+ case 422: return .validation
+ case 507: return .quota
+ case 500...599: return .server
+ default: return .unknown
+ }
+ }
+
+ if error is KDriveMutationConflictError {
+ return .conflict
+ }
+
+ if error is KDriveDirectUploadError {
+ return .validation
+ }
+
+ let cocoaError = error as NSError
+ if cocoaError.domain == NSFileProviderErrorDomain,
+ let code = NSFileProviderError.Code(rawValue: cocoaError.code) {
+ return classifyFileProviderCode(code)
+ }
+ switch cocoaError.domain {
+ case NSURLErrorDomain:
+ return classifyURLCode(cocoaError.code)
+ case NSCocoaErrorDomain:
+ return classifyCocoaCode(cocoaError.code)
+ case NSPOSIXErrorDomain:
+ return classifyPOSIXCode(cocoaError.code)
+ default:
+ return .unknown
+ }
+ }
+
+ /// Our fallback mapping wraps an otherwise unknown error as XPC reply
+ /// invalid. Inspect only that bounded cause chain; never retain user-info.
+ fileprivate static func originalError(_ error: any Error) -> any Error {
+ var result = error
+ for _ in 0..<4 {
+ let value = result as NSError
+ guard value.domain == NSCocoaErrorDomain, value.code == NSXPCConnectionReplyInvalid,
+ let underlying = value.userInfo[NSUnderlyingErrorKey] as? any Error else { break }
+ result = underlying
+ }
+ return result
+ }
+
+ fileprivate static func sqliteCode(_ error: any Error) -> Int? {
+ guard let result = error as? SQLite.Result else { return nil }
+ switch result {
+ case .error(_, let code, _): return Int(code)
+ case .extendedError(_, let code, _): return Int(code)
+ }
+ }
+
+ private static func classifyFileProviderCode(
+ _ code: NSFileProviderError.Code
+ ) -> ProviderDiagnosticErrorClass {
+ switch code {
+ case .notAuthenticated:
+ return .authentication
+ case .filenameCollision, .localVersionConflictingWithServer:
+ return .conflict
+ case .syncAnchorExpired:
+ return .invalidCursor
+ case .insufficientQuota:
+ return .quota
+ case .serverUnreachable, .providerDomainTemporarilyUnavailable:
+ return .network
+ case .noSuchItem, .providerDomainNotFound, .versionNoLongerAvailable:
+ return .notFound
+ case .deletionRejected, .directoryNotEmpty, .excludedFromSync,
+ .domainDisabled, .nonEvictable, .nonEvictableChildren,
+ .unsyncedEdits:
+ return .permission
+ case .cannotSynchronize, .providerNotFound, .providerTranslocated,
+ .olderExtensionVersionRunning, .newerExtensionVersionFound,
+ .applicationExtensionNotFound:
+ return .synchronization
+ @unknown default:
+ return .unknown
+ }
+ }
+
+ private static func classifyURLCode(_ rawCode: Int) -> ProviderDiagnosticErrorClass {
+ switch URLError.Code(rawValue: rawCode) {
+ case .cancelled, .userCancelledAuthentication:
+ return .cancellation
+ case .userAuthenticationRequired:
+ return .authentication
+ case .fileDoesNotExist:
+ return .notFound
+ case .noPermissionsToReadFile:
+ return .permission
+ case .badURL, .unsupportedURL:
+ return .validation
+ default:
+ // A recognized URL-loading domain is safe to classify broadly as
+ // network failure without retaining its URL or response details.
+ return .network
+ }
+ }
+
+ private static func classifyCocoaCode(_ code: Int) -> ProviderDiagnosticErrorClass {
+ switch code {
+ case NSUserCancelledError:
+ return .cancellation
+ case NSFileNoSuchFileError, NSFileReadNoSuchFileError:
+ return .notFound
+ case NSFileReadNoPermissionError, NSFileWriteNoPermissionError:
+ return .permission
+ case NSFileWriteOutOfSpaceError:
+ return .storage
+ default:
+ return .unknown
+ }
+ }
+
+ private static func classifyPOSIXCode(_ code: Int) -> ProviderDiagnosticErrorClass {
+ if code == Int(ECANCELED) {
+ return .cancellation
+ }
+ if code == Int(ENOENT) {
+ return .notFound
+ }
+ if code == Int(EACCES) || code == Int(EPERM) {
+ return .permission
+ }
+ if code == Int(ENOSPC) || code == Int(EDQUOT) {
+ return .storage
+ }
+ if code == Int(EINVAL) {
+ return .validation
+ }
+ return .unknown
+ }
+}
+
+/// Converts File Provider's option set to a closed, value-free diagnostic
+/// shape. The caller supplies only whether a parent change targets the virtual
+/// trash container; no item identifier is retained.
+public enum ProviderDiagnosticFieldClassifier {
+ public static func classify(
+ _ fields: NSFileProviderItemFields,
+ isTrashDestination: Bool = false
+ ) -> [ProviderDiagnosticField] {
+ var result: [ProviderDiagnosticField] = []
+ if fields.contains(.contents) { result.append(.contents) }
+ if fields.contains(.filename) { result.append(.filename) }
+ if fields.contains(.parentItemIdentifier) {
+ result.append(.parent)
+ if isTrashDestination { result.append(.trash) }
+ }
+ if fields.contains(.lastUsedDate) { result.append(.lastUsedDate) }
+ if fields.contains(.tagData) { result.append(.tagData) }
+ if fields.contains(.favoriteRank) { result.append(.favoriteRank) }
+ if fields.contains(.creationDate) { result.append(.creationDate) }
+ if fields.contains(.contentModificationDate) {
+ result.append(.contentModificationDate)
+ }
+ if fields.contains(.fileSystemFlags) { result.append(.fileSystemFlags) }
+ if fields.contains(.extendedAttributes) { result.append(.extendedAttributes) }
+ if fields.contains(.typeAndCreator) { result.append(.typeAndCreator) }
+ return result
+ }
+}
+
+/// A concurrency-safe diagnostic lifecycle shared by callback and network
+/// instrumentation.
+///
+/// Construction is intentionally asynchronous so the `.started` event is
+/// attempted exactly once before the span is returned. Recording is
+/// best-effort: a missing, unavailable, or failed diagnostic sink never changes
+/// provider behavior. Actor isolation makes the first terminal call win even
+/// when completion, failure, and cancellation race.
+public actor ProviderDiagnosticSpan {
+ /// Prevents corrupt or implausibly large elapsed values from entering a run
+ /// bundle if a span survives an unusually long process lifetime.
+ public static let maximumDurationMilliseconds = 7 * 24 * 60 * 60 * 1_000
+
+ /// Progress is deliberately coarse so diagnostics capture behavior rather
+ /// than byte counts or other workload-specific values.
+ public static let progressBucketWidth = 10
+
+ public nonisolated let correlationID: UUID
+ public nonisolated let spanID: UUID
+ public nonisolated let parentSpanID: UUID?
+ public nonisolated let subjectAlias: UUID?
+ public nonisolated let source: ProviderDiagnosticSource
+ public nonisolated let operation: ProviderDiagnosticOperation
+ public nonisolated let fieldShape: [ProviderDiagnosticField]
+ public nonisolated let routeTemplate: ProviderDiagnosticRouteTemplate?
+ public nonisolated let optionShape: [ProviderDiagnosticOption]
+ public nonisolated let startedAt: Date
+
+ private let recorder: (any ProviderDiagnosticRecording)?
+ private let clock: ContinuousClock
+ private let startedInstant: ContinuousClock.Instant
+ private var startedWasEmitted = false
+ private var terminalWasEmitted = false
+ private var lastProgressPercentBucket: Int?
+
+ private init(
+ correlationID: UUID,
+ subjectAlias: UUID?,
+ source: ProviderDiagnosticSource,
+ operation: ProviderDiagnosticOperation,
+ fieldShape: [ProviderDiagnosticField],
+ routeTemplate: ProviderDiagnosticRouteTemplate?,
+ optionShape: [ProviderDiagnosticOption],
+ startedAt: Date,
+ recorder: (any ProviderDiagnosticRecording)?
+ ) {
+ self.correlationID = correlationID
+ self.spanID = UUID()
+ self.parentSpanID = ProviderDiagnosticCorrelationContext.parentSpanID
+ self.subjectAlias = subjectAlias
+ self.source = source
+ self.operation = operation
+ self.fieldShape = Array(Set(fieldShape)).sorted { $0.rawValue < $1.rawValue }
+ self.routeTemplate = routeTemplate
+ self.optionShape = Array(Set(optionShape)).sorted { $0.rawValue < $1.rawValue }
+ self.startedAt = startedAt
+ self.recorder = recorder
+ let clock = ContinuousClock()
+ self.clock = clock
+ self.startedInstant = clock.now
+ }
+
+ public static func start(
+ correlationID: UUID? = nil,
+ itemIdentifier: String? = nil,
+ source: ProviderDiagnosticSource,
+ operation: ProviderDiagnosticOperation,
+ fieldShape: [ProviderDiagnosticField] = [],
+ routeTemplate: ProviderDiagnosticRouteTemplate? = nil,
+ optionShape: [ProviderDiagnosticOption] = [],
+ recorder: (any ProviderDiagnosticRecording)? = nil
+ ) async -> ProviderDiagnosticSpan {
+ let inheritedCorrelationID: UUID?
+ if let taskCorrelationID = ProviderDiagnosticCorrelationContext.current {
+ inheritedCorrelationID = taskCorrelationID
+ } else {
+ #if STABILITY
+ inheritedCorrelationID = try? ProviderEventStoreFactory.activeFinderStepCorrelation()
+ #else
+ inheritedCorrelationID = nil
+ #endif
+ }
+ let span = ProviderDiagnosticSpan(
+ correlationID: correlationID ?? inheritedCorrelationID ?? UUID(),
+ subjectAlias: StabilityDiagnosticIdentity.activeAlias(for: itemIdentifier) ?? ProviderDiagnosticCorrelationContext.subjectAlias,
+ source: source,
+ operation: operation,
+ fieldShape: fieldShape,
+ routeTemplate: routeTemplate,
+ optionShape: optionShape,
+ startedAt: Date(),
+ recorder: recorder
+ )
+ await span.emitStarted()
+ return span
+ }
+
+ /// Runs nested work with this span's correlation identifier. A nested span
+ /// that does not specify an identifier inherits it automatically.
+ public nonisolated func withCorrelation(
+ operation: @Sendable () async throws -> Result
+ ) async rethrows -> Result {
+ try await ProviderDiagnosticCorrelationContext.$parentSpanID.withValue(spanID) {
+ try await ProviderDiagnosticCorrelationContext.$subjectAlias.withValue(subjectAlias) {
+ try await ProviderDiagnosticCorrelationContext.withCorrelation(correlationID, operation: operation)
+ }
+ }
+ }
+
+ public func progress(fractionCompleted: Double) async {
+ guard terminalWasEmitted == false else { return }
+ let bucket = Self.progressBucket(for: fractionCompleted)
+ guard bucket != lastProgressPercentBucket else { return }
+ lastProgressPercentBucket = bucket
+ await emit(
+ phase: .progress,
+ durationMilliseconds: elapsedMilliseconds(),
+ progressPercentBucket: bucket
+ )
+ }
+
+ public func checkpoint(
+ hasCursor: Bool? = nil,
+ hasMore: Bool? = nil,
+ hasAnchor: Bool? = nil,
+ errorClass: ProviderDiagnosticErrorClass? = nil
+ ) async {
+ guard terminalWasEmitted == false else { return }
+ await emit(
+ phase: .checkpoint,
+ errorClass: errorClass,
+ durationMilliseconds: elapsedMilliseconds(),
+ hasCursor: hasCursor,
+ hasMore: hasMore,
+ hasAnchor: hasAnchor
+ )
+ }
+
+ public func complete(
+ statusClass: ProviderDiagnosticStatusClass? = nil,
+ hasCursor: Bool? = nil,
+ hasMore: Bool? = nil,
+ hasAnchor: Bool? = nil,
+ itemMetadataAlias: UUID? = nil
+ ) async {
+ guard beginTerminalEmission() else { return }
+ await emit(
+ phase: .completed,
+ itemMetadataAlias: itemMetadataAlias,
+ statusClass: statusClass,
+ durationMilliseconds: elapsedMilliseconds(),
+ progressPercentBucket: 100,
+ hasCursor: hasCursor,
+ hasMore: hasMore,
+ hasAnchor: hasAnchor
+ )
+ }
+
+ public func fail(
+ error: any Error,
+ statusClass: ProviderDiagnosticStatusClass? = nil
+ ) async {
+ guard beginTerminalEmission() else { return }
+ await emit(
+ phase: .failed,
+ statusClass: statusClass,
+ errorClass: ProviderDiagnosticErrorClassifier.classify(error),
+ errorCode: Self.safeCode(error),
+ validationFields: ProviderDiagnosticValidationField.classify(error),
+ durationMilliseconds: elapsedMilliseconds()
+ )
+ }
+
+ public func cancel() async {
+ guard beginTerminalEmission() else { return }
+ await emit(
+ phase: .cancelled,
+ errorClass: .cancellation,
+ durationMilliseconds: elapsedMilliseconds()
+ )
+ }
+
+ private func emitStarted() async {
+ guard startedWasEmitted == false else { return }
+ startedWasEmitted = true
+ await emit(phase: .started, occurredAt: startedAt)
+ }
+
+ private func beginTerminalEmission() -> Bool {
+ guard terminalWasEmitted == false else { return false }
+ terminalWasEmitted = true
+ return true
+ }
+
+ private func emit(
+ phase: ProviderDiagnosticPhase,
+ itemMetadataAlias: UUID? = nil,
+ occurredAt: Date = Date(),
+ statusClass: ProviderDiagnosticStatusClass? = nil,
+ errorClass: ProviderDiagnosticErrorClass? = nil,
+ errorCode: Int? = nil,
+ validationFields: [ProviderDiagnosticValidationField]? = nil,
+ durationMilliseconds: Int? = nil,
+ progressPercentBucket: Int? = nil,
+ hasCursor: Bool? = nil,
+ hasMore: Bool? = nil,
+ hasAnchor: Bool? = nil
+ ) async {
+ guard let recorder else { return }
+ let event = ProviderDiagnosticEvent(
+ occurredAt: occurredAt,
+ spanID: spanID,
+ parentSpanID: parentSpanID,
+ subjectAlias: subjectAlias,
+ itemMetadataAlias: itemMetadataAlias,
+ processInstanceID: StabilityDiagnosticIdentity.processInstanceID,
+ processCodeHash: StabilityDiagnosticIdentity.processCodeHash,
+ errorCode: errorCode,
+ validationFields: validationFields,
+ correlationID: correlationID,
+ source: source,
+ operation: operation,
+ phase: phase,
+ fieldShape: fieldShape,
+ routeTemplate: routeTemplate,
+ optionShape: optionShape,
+ statusClass: statusClass,
+ errorClass: errorClass,
+ durationMilliseconds: durationMilliseconds,
+ progressPercentBucket: progressPercentBucket,
+ hasCursor: hasCursor,
+ hasMore: hasMore,
+ hasAnchor: hasAnchor
+ )
+ do {
+ try await recorder.recordDiagnostic(event)
+ } catch {
+ // Diagnostics are evidence, never part of callback or API success.
+ }
+ }
+
+ private static func safeCode(_ error: any Error) -> Int? {
+ let original = ProviderDiagnosticErrorClassifier.originalError(error)
+ if let code = ProviderDiagnosticErrorClassifier.sqliteCode(original) { return code }
+ if let rejection = KDriveRemoteErrorClassifier.apiRejection(from: original) { return rejection.statusCode }
+ let value = original as NSError
+ return [NSFileProviderErrorDomain, NSURLErrorDomain, NSCocoaErrorDomain, NSPOSIXErrorDomain].contains(value.domain) ? value.code : nil
+ }
+
+ private func elapsedMilliseconds() -> Int {
+ let components = startedInstant.duration(to: clock.now).components
+ let milliseconds = Double(components.seconds) * 1_000
+ + Double(components.attoseconds) / 1_000_000_000_000_000
+ guard milliseconds.isFinite else {
+ return Self.maximumDurationMilliseconds
+ }
+ return Int(min(
+ Double(Self.maximumDurationMilliseconds),
+ max(0, milliseconds.rounded(.down))
+ ))
+ }
+
+ private static func progressBucket(for fractionCompleted: Double) -> Int {
+ guard fractionCompleted.isFinite else { return 0 }
+ let boundedFraction = min(1, max(0, fractionCompleted))
+ let percent = Int((boundedFraction * 100).rounded(.down))
+ guard percent < 100 else { return 100 }
+ return (percent / progressBucketWidth) * progressBucketWidth
+ }
+}
diff --git a/PotassiumProviderCore/ProviderDiagnosticValidationField.swift b/PotassiumProviderCore/ProviderDiagnosticValidationField.swift
new file mode 100644
index 0000000..c5dc7f1
--- /dev/null
+++ b/PotassiumProviderCore/ProviderDiagnosticValidationField.swift
@@ -0,0 +1,41 @@
+import Foundation
+
+/// Only known request-field classes leave the response parser. Messages, field
+/// values, unrecognized keys, and indices never enter a diagnostic bundle.
+public enum ProviderDiagnosticValidationField: String, Codable, Sendable {
+ case includedResources, actions, files, fromDate, modificationDate
+
+ public static func classify(_ error: any Error) -> [Self]? {
+ guard let rejection = KDriveRemoteErrorClassifier.apiRejection(from: error), [400, 422].contains(rejection.statusCode),
+ let data = rejection.responseBody.data(using: .utf8), data.count <= 64 * 1024,
+ let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
+ var fields = Set()
+ func field(_ key: String) -> Self? {
+ switch key.split(separator: ".").first {
+ case "with": return .includedResources
+ case "actions": return .actions
+ case "files": return .files
+ case "from_date": return .fromDate
+ case "last_modified_at": return .modificationDate
+ default: return nil
+ }
+ }
+ func visit(_ value: Any, depth: Int) {
+ guard depth < 6 else { return }
+ if let array = value as? [Any] {
+ for child in array.prefix(200) { visit(child, depth: depth + 1) }
+ return
+ }
+ guard let object = value as? [String: Any] else { return }
+ for (key, child) in object {
+ if let known = field(key) { fields.insert(known) }
+ if ["field", "parameter"].contains(key), let name = child as? String, let known = field(name) {
+ fields.insert(known)
+ }
+ visit(child, depth: depth + 1)
+ }
+ }
+ visit(root, depth: 0)
+ return fields.isEmpty ? nil : fields.sorted { $0.rawValue < $1.rawValue }
+ }
+}
diff --git a/PotassiumProviderCore/ProviderDomainConfiguration.swift b/PotassiumProviderCore/ProviderDomainConfiguration.swift
index 7764812..3eb5377 100644
--- a/PotassiumProviderCore/ProviderDomainConfiguration.swift
+++ b/PotassiumProviderCore/ProviderDomainConfiguration.swift
@@ -39,6 +39,31 @@ public enum ProviderEncryptionMode: String, Codable, Equatable, Sendable {
}
}
+/// Distinguishes ordinary user domains from the single disposable domain used
+/// by the opt-in Stability Lab. Legacy records decode as `.ordinary`.
+public enum ProviderDomainPurpose: String, Codable, Equatable, Sendable {
+ case ordinary
+ case stabilityLab
+}
+
+/// Local ownership evidence for a disposable Stability Lab root. Remote IDs
+/// are private operational state and must never be copied into diagnostics.
+public struct ProviderStabilityLabConfiguration: Codable, Equatable, Sendable {
+ public var driveRootFileID: Int
+ public var markerFileID: Int
+ public var ownershipMarker: StabilityLabOwnershipMarker
+
+ public init(
+ driveRootFileID: Int,
+ markerFileID: Int,
+ ownershipMarker: StabilityLabOwnershipMarker
+ ) {
+ self.driveRootFileID = driveRootFileID
+ self.markerFileID = markerFileID
+ self.ownershipMarker = ownershipMarker
+ }
+}
+
public struct ProviderVaultConfiguration: Codable, Equatable, Sendable {
public var vaultIdentifier: VaultIdentifier
public var vaultRootFileID: Int
@@ -201,6 +226,8 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
public var knownFolderLayout: ProviderKnownFolderLayout
public var encryptionMode: ProviderEncryptionMode
public var vault: ProviderVaultConfiguration?
+ public var purpose: ProviderDomainPurpose
+ public var stabilityLab: ProviderStabilityLabConfiguration?
public var createdAt: Date
public var updatedAt: Date
@@ -214,6 +241,8 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
knownFolderLayout: ProviderKnownFolderLayout = .machineNamespace,
encryptionMode: ProviderEncryptionMode = .legacyPlaintext,
vault: ProviderVaultConfiguration? = nil,
+ purpose: ProviderDomainPurpose = .ordinary,
+ stabilityLab: ProviderStabilityLabConfiguration? = nil,
createdAt: Date = Date(),
updatedAt: Date = Date()
) {
@@ -226,6 +255,8 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
self.knownFolderLayout = knownFolderLayout
self.encryptionMode = encryptionMode
self.vault = vault
+ self.purpose = purpose
+ self.stabilityLab = stabilityLab
self.createdAt = createdAt
self.updatedAt = updatedAt
}
@@ -235,6 +266,44 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
return trimmedDriveName.isEmpty ? "kDrive" : trimmedDriveName
}
+ /// Fails closed when a Stability Lab record is incomplete or its locally
+ /// persisted ownership evidence no longer identifies this exact domain.
+ public var hasConsistentPurposeConfiguration: Bool {
+ switch purpose {
+ case .ordinary:
+ return stabilityLab == nil
+ case .stabilityLab:
+ guard encryptionMode == .legacyPlaintext,
+ vault == nil,
+ rootFileID > 0,
+ let stabilityLab,
+ stabilityLab.driveRootFileID == ProviderConstants.defaultRootFileID,
+ stabilityLab.markerFileID > 0 else {
+ return false
+ }
+ return stabilityLab.ownershipMarker.driveID == driveID
+ && stabilityLab.ownershipMarker.rootFileID == rootFileID
+ && rootFileID != stabilityLab.driveRootFileID
+ && markerFileIDIsDistinct(stabilityLab.markerFileID, from: stabilityLab)
+ }
+ }
+
+ public func isCompatible(with profile: ProviderRuntimeProfile) -> Bool {
+ switch profile {
+ case .standard:
+ return purpose == .ordinary && hasConsistentPurposeConfiguration
+ case .stability:
+ return purpose == .stabilityLab && hasConsistentPurposeConfiguration
+ }
+ }
+
+ private func markerFileIDIsDistinct(
+ _ markerFileID: Int,
+ from stabilityLab: ProviderStabilityLabConfiguration
+ ) -> Bool {
+ markerFileID != rootFileID && markerFileID != stabilityLab.driveRootFileID
+ }
+
@discardableResult
public mutating func normalizeFinderDisplayName(updatedAt: Date = Date()) -> Bool {
let normalizedDisplayName = Self.finderDisplayName(forDriveName: driveName)
@@ -257,6 +326,8 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
case knownFolderLayout
case encryptionMode
case vault
+ case purpose
+ case stabilityLab
case createdAt
case updatedAt
}
@@ -283,6 +354,14 @@ public struct ProviderDomainConfiguration: Codable, Equatable, Identifiable, Sen
ProviderVaultConfiguration.self,
forKey: .vault
)
+ purpose = try container.decodeIfPresent(
+ ProviderDomainPurpose.self,
+ forKey: .purpose
+ ) ?? .ordinary
+ stabilityLab = try container.decodeIfPresent(
+ ProviderStabilityLabConfiguration.self,
+ forKey: .stabilityLab
+ )
createdAt = try container.decode(Date.self, forKey: .createdAt)
updatedAt = try container.decode(Date.self, forKey: .updatedAt)
}
diff --git a/PotassiumProviderCore/ProviderErrorMapping.swift b/PotassiumProviderCore/ProviderErrorMapping.swift
new file mode 100644
index 0000000..ba02630
--- /dev/null
+++ b/PotassiumProviderCore/ProviderErrorMapping.swift
@@ -0,0 +1,299 @@
+import FileProvider
+import Foundation
+import OSLog
+
+/// The same mapping is used by production callbacks and deterministic conflict tests.
+public struct ProviderErrorMapping {
+ public let mappedError: Error
+ public let diagnostic: KDriveProviderActivityErrorDiagnostic
+}
+
+public func providerErrorMapping(_ error: Error) -> ProviderErrorMapping {
+ if let fileProviderError = error as? NSFileProviderError {
+ let nsError = fileProviderError as NSError
+ ProviderLog.runtime.debug("preserve FileProvider error code(\(nsError.code, privacy: .public)): \(nsError.localizedDescription, privacy: .public)")
+ return ProviderErrorMapping(
+ mappedError: fileProviderError,
+ diagnostic: providerDiagnostic(
+ category: providerActivityErrorCategory(originalError: nsError),
+ originalError: error,
+ mappedError: fileProviderError
+ )
+ )
+ }
+
+ if let apiRejection = KDriveRemoteErrorClassifier.apiRejection(from: error) {
+ let mappedError = fileProviderError(for: apiRejection.recovery)
+ let mappedNSError = mappedError as NSError
+ ProviderLog.runtime.error("map API rejection HTTP \(apiRejection.statusCode, privacy: .public) to \(mappedNSError.domain, privacy: .public) code(\(mappedNSError.code, privacy: .public))")
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .api,
+ originalError: error,
+ mappedError: mappedError,
+ diagnosticSummary: apiRejection.diagnosticSummary
+ )
+ )
+ }
+
+ if let directUploadError = error as? KDriveDirectUploadError {
+ let mappedError = fileProviderError(for: directUploadError.recovery)
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: directUploadError.diagnosticCategory,
+ originalError: error,
+ mappedError: mappedError,
+ diagnosticSummary: directUploadError.diagnosticSummary
+ )
+ )
+ }
+
+ if let mutationConflictError = error as? KDriveMutationConflictError {
+ switch mutationConflictError {
+ case .staleVersion:
+ ProviderLog.runtime.error("map stale mutation version to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
+ let mappedError = staleMutationVersionError()
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .mutationConflict,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ case .localContentConflict:
+ let mappedError = NSFileProviderError(.localVersionConflictingWithServer)
+ ProviderLog.runtime.error("map fail-on-conflict upload to localVersionConflictingWithServer")
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .mutationConflict,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+ }
+
+ if error is KDriveListingValidationError {
+ ProviderLog.runtime.error("map listing validation failure to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
+ let mappedError = NSFileProviderError(.cannotSynchronize)
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .listing,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+
+ if let snapshotStoreError = error as? KDriveSnapshotStoreError,
+ case .staleSnapshot = snapshotStoreError {
+ ProviderLog.runtime.error("map stale snapshot write to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
+ let mappedError = NSFileProviderError(.cannotSynchronize)
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .snapshot,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+
+ if error is VaultCryptoError ||
+ error is VaultJournalError ||
+ error is VaultLocalStoreError ||
+ error is VaultProvisioningError {
+ let mappedError = NSFileProviderError(.cannotSynchronize)
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .storage,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+
+ if let vaultError = error as? EncryptedVaultError {
+ let mappedError: NSFileProviderError
+ switch vaultError {
+ case .missingKey:
+ mappedError = NSFileProviderError(.notAuthenticated)
+ case .itemNotFound:
+ mappedError = NSFileProviderError(.noSuchItem)
+ case .syncAnchorExpired:
+ mappedError = NSFileProviderError(.syncAnchorExpired)
+ default:
+ mappedError = NSFileProviderError(.cannotSynchronize)
+ }
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .storage,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+
+ let nsError = error as NSError
+ if nsError.domain == NSURLErrorDomain {
+ ProviderLog.runtime.error("map URL error \(nsError.code, privacy: .public) to serverUnreachable: \(nsError.localizedDescription, privacy: .public)")
+ let mappedError = NSFileProviderError(.serverUnreachable)
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: .network,
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+ }
+
+ if nsError.domain == NSCocoaErrorDomain || nsError.domain == NSFileProviderErrorDomain {
+ ProviderLog.runtime.error("preserve Cocoa/FileProvider error \(nsError.domain, privacy: .public) code(\(nsError.code, privacy: .public)): \(nsError.localizedDescription, privacy: .public)")
+ return ProviderErrorMapping(
+ mappedError: error,
+ diagnostic: providerDiagnostic(
+ category: providerActivityErrorCategory(originalError: nsError),
+ originalError: error,
+ mappedError: error
+ )
+ )
+ }
+
+ ProviderLog.runtime.error("wrap unexpected error as XPC reply invalid: \(error.localizedDescription, privacy: .public)")
+ let mappedError = NSError(
+ domain: NSCocoaErrorDomain,
+ code: NSXPCConnectionReplyInvalid,
+ userInfo: [NSUnderlyingErrorKey: error]
+ )
+ return ProviderErrorMapping(
+ mappedError: mappedError,
+ diagnostic: providerDiagnostic(
+ category: providerActivityErrorCategory(originalError: nsError),
+ originalError: error,
+ mappedError: mappedError
+ )
+ )
+}
+
+public func providerError(_ error: Error) -> Error {
+ providerErrorMapping(error).mappedError
+}
+
+public func shouldRecordGenericFailure(for error: Error) -> Bool {
+ if error is CancellationError { return false }
+ if error is KDriveMutationConflictError { return false }
+
+ let nsError = error as NSError
+ return nsError.domain != NSCocoaErrorDomain || nsError.code != NSUserCancelledError
+}
+
+public func providerActivityKindForRuntimeLoadFailure(_ error: Error) -> KDriveProviderActivityKind {
+ let nsError = error as NSError
+ if nsError.domain == NSFileProviderErrorDomain,
+ nsError.code == NSFileProviderError.notAuthenticated.rawValue {
+ return .authentication
+ }
+ if let apiRejection = KDriveRemoteErrorClassifier.apiRejection(from: error),
+ apiRejection.recovery == .notAuthenticated {
+ return .authentication
+ }
+ if error is KDriveOAuthError || error is KeychainTokenStoreError {
+ return .authentication
+ }
+ return .runtimeLoading
+}
+
+private func providerDiagnostic(
+ category: KDriveProviderActivityErrorCategory,
+ originalError: Error,
+ mappedError: Error,
+ diagnosticSummary: String? = nil
+) -> KDriveProviderActivityErrorDiagnostic {
+ let originalNSError = originalError as NSError
+ let mappedNSError = mappedError as NSError
+ let providerCode = mappedNSError.domain == NSFileProviderErrorDomain ? mappedNSError.code : nil
+ let recoverySuggestion = mappedNSError.localizedRecoverySuggestion
+ ?? (originalError as? LocalizedError)?.recoverySuggestion
+
+ return KDriveProviderActivityErrorDiagnostic(
+ errorCategory: category,
+ providerErrorCode: providerCode,
+ underlyingErrorDomain: originalNSError.domain,
+ underlyingErrorCode: originalNSError.code,
+ recoverySuggestion: recoverySuggestion,
+ diagnosticSummary: diagnosticSummary ?? providerDiagnosticSummary(category: category)
+ )
+}
+
+private func fileProviderError(for recovery: KDriveRemoteAPIRejectionRecovery) -> Error {
+ switch recovery {
+ case .notAuthenticated:
+ return NSFileProviderError(.notAuthenticated)
+ case .serverUnreachable:
+ return NSFileProviderError(.serverUnreachable)
+ case .insufficientQuota:
+ return NSFileProviderError(.insufficientQuota)
+ case .cannotSynchronize:
+ return NSFileProviderError(.cannotSynchronize)
+ }
+}
+
+private func providerActivityErrorCategory(originalError nsError: NSError) -> KDriveProviderActivityErrorCategory {
+ if nsError.domain == NSURLErrorDomain {
+ return .network
+ }
+ if nsError.domain == NSFileProviderErrorDomain {
+ if nsError.code == NSFileProviderError.notAuthenticated.rawValue {
+ return .authentication
+ }
+ return .fileProvider
+ }
+ if nsError.domain == NSCocoaErrorDomain {
+ return .storage
+ }
+ return .unknown
+}
+
+private func providerDiagnosticSummary(category: KDriveProviderActivityErrorCategory) -> String {
+ switch category {
+ case .authentication:
+ return "Authentication is unavailable or needs to be refreshed."
+ case .network:
+ return "A network request failed before the operation could complete."
+ case .api:
+ return "The remote API rejected the operation."
+ case .fileProvider:
+ return "File Provider returned a recoverable provider error."
+ case .listing:
+ return "The remote listing response could not be safely applied."
+ case .snapshot:
+ return "Local sync snapshot state could not be updated safely."
+ case .storage:
+ return "Local storage returned an error."
+ case .validation:
+ return "Input or remote state failed validation."
+ case .mutationConflict:
+ return "The remote item changed before the local mutation could be applied."
+ case .unknown:
+ return "An unexpected provider error occurred."
+ }
+}
+
+private func staleMutationVersionError() -> Error {
+ NSError(
+ domain: NSFileProviderErrorDomain,
+ code: NSFileProviderError.cannotSynchronize.rawValue,
+ userInfo: [
+ NSLocalizedDescriptionKey: "The item changed on the server before the local mutation could be applied.",
+ NSLocalizedRecoverySuggestionErrorKey: "Refresh the folder and retry the change."
+ ]
+ )
+}
diff --git a/PotassiumProviderCore/ProviderLogging.swift b/PotassiumProviderCore/ProviderLogging.swift
index 51af785..0f75fa2 100644
--- a/PotassiumProviderCore/ProviderLogging.swift
+++ b/PotassiumProviderCore/ProviderLogging.swift
@@ -31,7 +31,14 @@ public enum ProviderLog {
public static let export = logger(.export)
public static func logger(_ category: ProviderLogCategory) -> Logger {
+ #if STABILITY
+ // Existing development logs predate the Stability privacy contract and
+ // include identifiers and localized error text at many call sites.
+ // Stability uses the closed-schema JSONL recorder exclusively.
+ Logger(OSLog.disabled)
+ #else
Logger(subsystem: ProviderConstants.logSubsystem, category: category.rawValue)
+ #endif
}
}
diff --git a/PotassiumProviderCore/SQLiteSnapshotStore.swift b/PotassiumProviderCore/SQLiteSnapshotStore.swift
index c4a9d49..5732d63 100644
--- a/PotassiumProviderCore/SQLiteSnapshotStore.swift
+++ b/PotassiumProviderCore/SQLiteSnapshotStore.swift
@@ -1,6 +1,10 @@
import Foundation
@preconcurrency import SQLite
+/// Snapshot saves and working-set writes reserve the SQLite writer before reading
+/// their predicates. A deferred transaction cannot wait when upgrading a WAL snapshot
+/// while another connection owns the writer; the existing busy timeout applies
+/// to the initial immediate reservation instead. No network work holds that lock.
public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotStatisticsProviding, KDriveWorkingSetStateStoring {
private let databaseURL: URL
private let database: Connection
@@ -84,7 +88,7 @@ public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotSta
containerIdentifier: String,
condition: KDriveSnapshotSaveCondition
) throws {
- try database.transaction {
+ try database.transaction(.immediate) {
try saveSnapshot(
snapshot,
domainIdentifier: domainIdentifier,
@@ -498,7 +502,7 @@ public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotSta
minimumInterval: TimeInterval
) throws -> Bool {
var claimed = false
- try database.transaction {
+ try database.transaction(.immediate) {
let query = WorkingSetSchema.pollState.filter(WorkingSetSchema.domainIdentifier == domainIdentifier)
if let row = try database.pluck(query) {
if let lastAttempt = row[WorkingSetSchema.lastPollAttemptAt],
@@ -525,11 +529,25 @@ public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotSta
containerSnapshotUpdates: [KDriveWorkingSetContainerSnapshotUpdate],
items: [KDriveRemoteItem],
changes: KDriveSnapshotChangeSet,
- completedAt: Date
+ completedAt: Date,
+ condition: KDriveWorkingSetCommitCondition = .unconditional
) throws -> KDriveWorkingSetSnapshot {
var committedSnapshot: KDriveWorkingSetSnapshot?
- try database.transaction {
+ try database.transaction(.immediate) {
+ if case .matchingAnchor(let expected) = condition,
+ try workingSetSnapshot(domainIdentifier: domainIdentifier)?.anchor != expected {
+ throw KDriveSnapshotStoreError.staleSnapshot(domainIdentifier: domainIdentifier, containerIdentifier: "working-set")
+ }
for update in containerSnapshotUpdates {
+ if let current = try snapshot(domainIdentifier: domainIdentifier, containerIdentifier: update.containerIdentifier),
+ !update.condition.accepts(current),
+ update.snapshot.isSameAdvancedListingResult(as: current) {
+ // Enumeration already committed this exact server result.
+ // Preserve its newer local anchor/generation; do not write
+ // the stale prepared snapshot over it. The working-set
+ // change batch still commits atomically below.
+ continue
+ }
try saveSnapshot(
update.snapshot,
domainIdentifier: domainIdentifier,
@@ -566,6 +584,42 @@ public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotSta
return committedSnapshot!
}
+ /// Merge a confirmed mutation without advancing remote cursors or poll time.
+ /// If another writer changed this item since the caller read it, retain that
+ /// state and let normal reconciliation resolve ordering; never overwrite it.
+ public func publishKnownWorkingSetItem(_ item: KDriveRemoteItem, replacing expectedItem: KDriveRemoteItem?,
+ domainIdentifier: String, recordedAt: Date) throws -> Bool {
+ var published = false
+ try database.transaction(.immediate) {
+ let query = WorkingSetSchema.pollState.filter(WorkingSetSchema.domainIdentifier == domainIdentifier)
+ let row = try database.pluck(query)
+ let previous = try workingSetSnapshot(domainIdentifier: domainIdentifier)
+ let current = previous?.items.first { $0.id == item.id }
+ guard current != item else { published = true; return }
+ guard current == expectedItem else { return }
+ let oldAnchor = previous?.anchor ?? UUID().uuidString, newAnchor = UUID().uuidString
+ let items = ((previous?.items ?? []).filter { $0.id != item.id } + [item]).sorted { $0.id < $1.id }
+ try database.run(WorkingSetSchema.changeBatches.insert(
+ WorkingSetSchema.domainIdentifier <- domainIdentifier,
+ WorkingSetSchema.anchorBefore <- oldAnchor,
+ WorkingSetSchema.anchorAfter <- newAnchor,
+ WorkingSetSchema.updatedItemsJSON <- try Self.encode([item]),
+ WorkingSetSchema.deletedItemIDsJSON <- try Self.encode([Int]()),
+ WorkingSetSchema.changeCompletedAt <- recordedAt.timeIntervalSince1970
+ ))
+ try database.run(WorkingSetSchema.pollState.insert(or: .replace,
+ WorkingSetSchema.domainIdentifier <- domainIdentifier,
+ WorkingSetSchema.workingSetAnchor <- newAnchor,
+ WorkingSetSchema.workingSetItemsJSON <- try Self.encode(items),
+ WorkingSetSchema.lastPollAttemptAt <- row?[WorkingSetSchema.lastPollAttemptAt],
+ WorkingSetSchema.lastSuccessfulPollAt <- row?[WorkingSetSchema.lastSuccessfulPollAt]
+ ))
+ try trimWorkingSetChangeBatches(domainIdentifier: domainIdentifier, retaining: 32)
+ published = true
+ }
+ return published
+ }
+
private func saveSnapshot(
_ snapshot: KDriveSnapshot,
domainIdentifier: String,
@@ -831,13 +885,13 @@ public actor KDriveSnapshotSQLiteStore: KDriveSnapshotStoring, KDriveSnapshotSta
}
private static func configure(_ database: Connection) throws {
- try database.execute("PRAGMA journal_mode=WAL")
+ // Opening an existing WAL database can overlap another connection's
+ // exclusive cleanup/recovery lock. Cover the first database query too.
try database.execute("PRAGMA busy_timeout=5000")
+ try database.execute("PRAGMA journal_mode=WAL")
}
private static func createTables(on database: Connection) throws {
- try KDriveProviderEventSQLiteStore.createTables(on: database)
-
try database.run(Schema.containerSnapshots.create(ifNotExists: true) { table in
table.column(Schema.domainIdentifier)
table.column(Schema.containerIdentifier)
diff --git a/PotassiumProviderCore/StabilityActionPanelIdentity.swift b/PotassiumProviderCore/StabilityActionPanelIdentity.swift
new file mode 100644
index 0000000..235305e
--- /dev/null
+++ b/PotassiumProviderCore/StabilityActionPanelIdentity.swift
@@ -0,0 +1,20 @@
+#if STABILITY
+import Foundation
+
+/// Disk-backed run discovery must not execute in SwiftUI layout or a Combine
+/// subscriber on the main actor. The bounded waiter also ignores late disk replies.
+public enum StabilityActionPanelIdentity {
+ public static func resolve(
+ for identifier: String,
+ timeout: Duration = .seconds(90),
+ lookup: @escaping @Sendable (String) -> UUID? = { StabilityDiagnosticIdentity.activeAlias(for: $0) }
+ ) async throws -> UUID? {
+ try Task.checkCancellation()
+ return try await StabilityCallbackWaiter().wait(timeout: timeout) { completion in
+ Task.detached(priority: .utility) {
+ completion(.success(lookup(identifier)))
+ }
+ }
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityConflictBarrier.swift b/PotassiumProviderCore/StabilityConflictBarrier.swift
new file mode 100644
index 0000000..f9905fe
--- /dev/null
+++ b/PotassiumProviderCore/StabilityConflictBarrier.swift
@@ -0,0 +1,164 @@
+#if STABILITY
+import Foundation
+
+public enum StabilityConflictSchedulingPoint: String, Codable, Sendable {
+ case beforeContentPreflight
+ case afterContentPreflight
+ case afterRenamePreflight
+ case afterMovePreflight
+}
+
+public enum StabilityLiveConflictCase: String, Codable, CaseIterable, Sendable {
+ case contentBeforePreflight = "content-before-preflight"
+ case contentAfterPreflight = "content-after-preflight"
+ case renameRename = "rename-rename"
+ case moveMove = "move-move"
+ case editRename = "edit-rename"
+ case editMove = "edit-move"
+
+ public var scenario: StabilityFinderScenario {
+ switch self {
+ case .contentAfterPreflight: .concurrentRemotePreserveBoth
+ case .renameRename: .rename
+ case .moveMove: .move
+ case .contentBeforePreflight, .editRename, .editMove: .editAndUpload
+ }
+ }
+ public var schedulingPoint: StabilityConflictSchedulingPoint {
+ switch self {
+ case .contentBeforePreflight: .beforeContentPreflight
+ case .renameRename: .afterRenamePreflight
+ case .moveMove: .afterMovePreflight
+ default: .afterContentPreflight
+ }
+ }
+}
+
+/// Changes scheduling only. Each arm has a unique attempt identity; release and
+/// arrival files from prior cases cannot satisfy a later race.
+public enum StabilityConflictBarrier {
+ public struct Ticket: Codable, Equatable, Sendable {
+ public let runID: UUID
+ public let caseID: StabilityLiveConflictCase
+ public let attemptID: UUID
+ public let subject: UUID
+ public let correlationID: UUID
+ public let point: StabilityConflictSchedulingPoint
+ }
+
+ private struct CompetingMutation: Codable {
+ let schemaVersion: UInt16
+ let ticket: Ticket
+ let metadataAlias: UUID
+ }
+
+ /// Records independent server read-back while this exact attempt is held.
+ /// An accepted asynchronous mutation response alone is not this evidence.
+ public static func recordVerifiedCompetingMutation(_ ticket: Ticket, run: StabilityRunHandle,
+ itemIdentifier: String, metadataAlias: UUID) throws {
+ try recordVerifiedCompetingMutation(ticket, run: run, itemIdentifier: itemIdentifier,
+ metadataAlias: metadataAlias, activeRunID: StabilityDiagnosticIdentity.activeRun()?.runID)
+ }
+ static func recordVerifiedCompetingMutation(_ ticket: Ticket, run: StabilityRunHandle,
+ itemIdentifier: String, metadataAlias: UUID, activeRunID: UUID?) throws {
+ guard activeRunID == run.runID, try request(run) == ticket, reached(ticket, run: run),
+ !settled(ticket, run: run), ticket.subject == StabilityDiagnosticIdentity.alias(for: itemIdentifier, runID: run.runID)
+ else { throw CancellationError() }
+ let evidence = CompetingMutation(schemaVersion: 1, ticket: ticket, metadataAlias: metadataAlias)
+ try SecurePOSIXFile.createExclusively(JSONEncoder().encode(evidence),
+ at: path(ticket, "competitor-verified.json", run), permissions: 0o400)
+ }
+ public static func competingMutationVerified(_ ticket: Ticket, run: StabilityRunHandle) -> Bool {
+ guard ticket.runID == run.runID, !SecurePOSIXFile.isRegularFile(path(ticket, "cancelled", run)),
+ let data = try? SecurePOSIXFile.read(path(ticket, "competitor-verified.json", run), maximumBytes: 4096),
+ let evidence = try? JSONDecoder().decode(CompetingMutation.self, from: data) else { return false }
+ return evidence.schemaVersion == 1 && evidence.ticket == ticket
+ }
+
+ @discardableResult
+ public static func arm(run: StabilityRunHandle, itemIdentifier: String, correlationID: UUID,
+ caseID: StabilityLiveConflictCase = .contentAfterPreflight) throws -> Ticket {
+ try arm(run: run, itemIdentifier: itemIdentifier, correlationID: correlationID,
+ caseID: caseID, activeRunID: StabilityDiagnosticIdentity.activeRun()?.runID)
+ }
+ @discardableResult
+ static func arm(run: StabilityRunHandle, itemIdentifier: String, correlationID: UUID,
+ caseID: StabilityLiveConflictCase = .contentAfterPreflight, activeRunID: UUID?) throws -> Ticket {
+ guard activeRunID == run.runID else { throw CancellationError() }
+ return try SecurePOSIXFile.withLock(at: run.directoryURL.appendingPathComponent("conflict-gate.lock"), operation: LOCK_EX) {
+ if let old = try request(run), !settled(old, run: run) { throw CancellationError() }
+ let ticket = Ticket(runID: run.runID, caseID: caseID, attemptID: UUID(),
+ subject: StabilityDiagnosticIdentity.alias(for: itemIdentifier, runID: run.runID),
+ correlationID: correlationID, point: caseID.schedulingPoint)
+ try SecurePOSIXFile.replaceAtomically(JSONEncoder().encode(ticket),
+ at: run.directoryURL.appendingPathComponent("conflict-request.json"), permissions: 0o600)
+ return ticket
+ }
+ }
+ public static func reached(run: StabilityRunHandle) -> Bool {
+ guard let ticket = try? request(run) else { return false }
+ return reached(ticket, run: run)
+ }
+ public static func reached(_ ticket: Ticket, run: StabilityRunHandle) -> Bool {
+ ticket.runID == run.runID && SecurePOSIXFile.isRegularFile(path(ticket, "reached", run))
+ }
+ public static func release(run: StabilityRunHandle) throws {
+ try release(run: run, activeRunID: StabilityDiagnosticIdentity.activeRun()?.runID)
+ }
+ public static func release(_ ticket: Ticket, run: StabilityRunHandle) throws {
+ try release(ticket, run: run, activeRunID: StabilityDiagnosticIdentity.activeRun()?.runID)
+ }
+ static func release(run: StabilityRunHandle, activeRunID: UUID?) throws {
+ guard let ticket = try request(run) else { throw CancellationError() }
+ try release(ticket, run: run, activeRunID: activeRunID)
+ }
+ static func release(_ ticket: Ticket, run: StabilityRunHandle, activeRunID: UUID?) throws {
+ guard activeRunID == run.runID, ticket.runID == run.runID, try request(run) == ticket else { throw CancellationError() }
+ let url = path(ticket, "release", run)
+ if !SecurePOSIXFile.isRegularFile(url) { try SecurePOSIXFile.createExclusively(Data(), at: url, permissions: 0o600) }
+ }
+ public static func arriveIfArmed(itemIdentifier: String, point: StabilityConflictSchedulingPoint = .afterContentPreflight) async throws {
+ try await arriveIfArmed(itemIdentifier: itemIdentifier, correlationID: ProviderDiagnosticCorrelationContext.current,
+ point: point, activeRun: { try StabilityDiagnosticIdentity.activeRun() })
+ }
+ static func arriveIfArmed(itemIdentifier: String, correlationID: UUID?,
+ point: StabilityConflictSchedulingPoint = .afterContentPreflight,
+ budget: Duration = .seconds(90),
+ activeRun: @Sendable () throws -> StabilityRunHandle?) async throws {
+ try Task.checkCancellation()
+ guard let run = try activeRun(), let ticket = try request(run) else { return }
+ guard ticket.runID == run.runID, ticket.point == point,
+ ticket.subject == StabilityDiagnosticIdentity.alias(for: itemIdentifier, runID: run.runID),
+ ticket.correlationID == correlationID else { return }
+ if SecurePOSIXFile.isRegularFile(path(ticket, "cancelled", run)) { throw CancellationError() }
+ if SecurePOSIXFile.isRegularFile(path(ticket, "release", run)) { return }
+ // Only one callback attempt can claim this gate. An overlapping attempt
+ // must fail rather than bypassing the held mutation and changing the race.
+ try SecurePOSIXFile.createExclusively(Data(), at: path(ticket, "reached", run), permissions: 0o600)
+ let deadline = ContinuousClock.now.advanced(by: budget)
+ do {
+ while !SecurePOSIXFile.isRegularFile(path(ticket, "release", run)) {
+ try Task.checkCancellation()
+ guard ContinuousClock.now < deadline, try activeRun()?.runID == run.runID,
+ try request(run) == ticket else { throw CancellationError() }
+ try await Task.sleep(for: min(.milliseconds(100), ContinuousClock.now.duration(to: deadline)))
+ }
+ try Task.checkCancellation()
+ } catch {
+ try? SecurePOSIXFile.createExclusively(Data(), at: path(ticket, "cancelled", run), permissions: 0o600)
+ throw error
+ }
+ }
+ private static func request(_ run: StabilityRunHandle) throws -> Ticket? {
+ let url = run.directoryURL.appendingPathComponent("conflict-request.json")
+ guard SecurePOSIXFile.isRegularFile(url) else { return nil }
+ return try JSONDecoder().decode(Ticket.self, from: SecurePOSIXFile.read(url, maximumBytes: 4096))
+ }
+ private static func settled(_ ticket: Ticket, run: StabilityRunHandle) -> Bool {
+ SecurePOSIXFile.isRegularFile(path(ticket, "release", run)) || SecurePOSIXFile.isRegularFile(path(ticket, "cancelled", run))
+ }
+ private static func path(_ ticket: Ticket, _ phase: String, _ run: StabilityRunHandle) -> URL {
+ run.directoryURL.appendingPathComponent("conflict-" + ticket.attemptID.uuidString.lowercased() + "-" + phase)
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityConflictProfile.swift b/PotassiumProviderCore/StabilityConflictProfile.swift
new file mode 100644
index 0000000..2359179
--- /dev/null
+++ b/PotassiumProviderCore/StabilityConflictProfile.swift
@@ -0,0 +1,49 @@
+#if STABILITY
+import Foundation
+
+/// A targeted conflict result is never a sixteen-scenario Finder certificate.
+/// The original report remains complete, with non-selected scenarios explicitly
+/// skipped; this immutable manifest declares the narrower selection.
+public struct StabilityConflictProfile: Codable, Equatable, Sendable {
+ public let schemaVersion: UInt16
+ public let runID: UUID
+ public let selectedCase: StabilityLiveConflictCase
+ public let extensionLaunchMode: StabilityExtensionLaunchMode?
+
+ public init(runID: UUID, selectedCase: StabilityLiveConflictCase, extensionLaunchMode: StabilityExtensionLaunchMode? = nil) {
+ schemaVersion = 3; self.runID = runID; self.selectedCase = selectedCase
+ self.extensionLaunchMode = extensionLaunchMode
+ }
+
+ public func validate(report: StabilityFinderRunReport, ticket: StabilityConflictBarrier.Ticket?,
+ reached: Bool, released: Bool, competingMutationVerified: Bool = false,
+ diagnostics: [ProviderDiagnosticEvent]) throws {
+ guard [1, 2, 3].contains(schemaVersion) else { throw StabilityLiveEvidenceError.missingConflict }
+ for step in report.stepResults where step.scenario != selectedCase.scenario {
+ guard step.outcome == .skipped(.notSelectedForConflictProfile) ||
+ step.outcome == .skipped(.preflightFailure) || step.outcome == .skipped(.preflightCheckpoint) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ }
+ guard let step = report.stepResults.first(where: { $0.scenario == selectedCase.scenario }) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ guard step.outcome == .passed else { return }
+ guard schemaVersion < 3 || competingMutationVerified else { throw StabilityLiveEvidenceError.missingConflict }
+ guard report.preflightResults.allSatisfy({ $0.outcome == .passed }),
+ let ticket, reached, released, ticket.runID == runID, ticket.caseID == selectedCase,
+ ticket.point == selectedCase.schedulingPoint, ticket.correlationID == step.correlationID,
+ let proof = step.liveEvidence, proof.conflictBarrierReached, proof.subjects.contains(ticket.subject),
+ diagnostics.contains(where: { $0.correlationID == step.correlationID && $0.subjectAlias == ticket.subject &&
+ $0.source == .fileProviderExtension && $0.operation == .modifyItem && $0.phase == .completed }) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ if selectedCase == .contentBeforePreflight {
+ guard diagnostics.contains(where: { $0.correlationID == step.correlationID && $0.subjectAlias == ticket.subject &&
+ $0.source == .fileProviderExtension && $0.operation == .uploadFile && $0.phase == .completed }) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ }
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityDeadline.swift b/PotassiumProviderCore/StabilityDeadline.swift
new file mode 100644
index 0000000..0981753
--- /dev/null
+++ b/PotassiumProviderCore/StabilityDeadline.swift
@@ -0,0 +1,55 @@
+import Foundation
+
+/// Monotonic scenario budget. Operator pauses extend the deadline only on resume.
+public struct StabilityDeadline: Sendable {
+ private var deadline: ContinuousClock.Instant
+ private var pausedAt: ContinuousClock.Instant?
+ public init(budget: Duration, now: ContinuousClock.Instant = .now) { deadline = now.advanced(by: budget) }
+ public mutating func pause(now: ContinuousClock.Instant = .now) {
+ if pausedAt == nil { pausedAt = now }
+ }
+ public mutating func resume(now: ContinuousClock.Instant = .now) {
+ if let pausedAt { deadline = deadline.advanced(by: pausedAt.duration(to: now)) }
+ pausedAt = nil
+ }
+ public func remaining(now: ContinuousClock.Instant = .now) -> Duration {
+ max(.zero, (pausedAt ?? now).duration(to: deadline))
+ }
+}
+
+public enum StabilityDeadlineError: Error { case expired, waiterAlreadyInUse }
+
+/// Bounds callback APIs that do not expose cancellation. A late system callback
+/// is ignored after the waiter expires; it cannot resume the continuation twice.
+public actor StabilityCallbackWaiter {
+ private var continuation: CheckedContinuation?
+ private var result: Result?
+ private var timer: Task?
+ public init() {}
+
+ public func wait(timeout: Duration = .seconds(90), register: @Sendable (@escaping @Sendable (Result) -> Void) -> Void) async throws -> Value {
+ guard continuation == nil else { throw StabilityDeadlineError.waiterAlreadyInUse }
+ return try await withTaskCancellationHandler {
+ try await withCheckedThrowingContinuation { continuation in
+ if let result { continuation.resume(with: result); return }
+ self.continuation = continuation
+ timer = Task {
+ do { try await Task.sleep(for: timeout) } catch { return }
+ self.finish(.failure(StabilityDeadlineError.expired))
+ }
+ register { value in Task { await self.finish(value) } }
+ }
+ } onCancel: {
+ Task { await self.finish(.failure(CancellationError())) }
+ }
+ }
+
+ private func finish(_ value: Result) {
+ guard result == nil else { return }
+ result = value
+ timer?.cancel()
+ timer = nil
+ continuation?.resume(with: value)
+ continuation = nil
+ }
+}
diff --git a/PotassiumProviderCore/StabilityDiagnosticIdentity.swift b/PotassiumProviderCore/StabilityDiagnosticIdentity.swift
new file mode 100644
index 0000000..7091bea
--- /dev/null
+++ b/PotassiumProviderCore/StabilityDiagnosticIdentity.swift
@@ -0,0 +1,85 @@
+import CryptoKit
+import Foundation
+#if os(macOS)
+import Security
+#endif
+
+/// Process identity and run-local aliases contain no raw item or account identifiers.
+public enum StabilityDiagnosticIdentity {
+ public static let processInstanceID = UUID()
+ public static let processCodeHash: String? = {
+ #if os(macOS)
+ var code: SecCode?
+ guard SecCodeCopySelf([], &code) == errSecSuccess, let code else { return nil }
+ var staticCode: SecStaticCode?
+ guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { return nil }
+ return codeHash(staticCode)
+ #else
+ return nil
+ #endif
+ }()
+
+ #if os(macOS)
+ public static func codeHash(at url: URL) -> String? {
+ var code: SecStaticCode?
+ guard SecStaticCodeCreateWithPath(url as CFURL, [], &code) == errSecSuccess, let code else { return nil }
+ return codeHash(code)
+ }
+
+ public static func codeHash(forProcessIdentifier pid: Int32) -> String? {
+ var code: SecCode?
+ let attributes = [kSecGuestAttributePid as String: NSNumber(value: pid)] as CFDictionary
+ guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &code) == errSecSuccess, let code else { return nil }
+ var staticCode: SecStaticCode?
+ guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { return nil }
+ return codeHash(staticCode)
+ }
+
+ private static func codeHash(_ code: SecStaticCode) -> String? {
+ var information: CFDictionary?
+ guard SecCodeCopySigningInformation(code, [], &information) == errSecSuccess,
+ let dictionary = information as? [String: Any],
+ let hash = dictionary[kSecCodeInfoUnique as String] as? Data else { return nil }
+ return hash.map { String(format: "%02x", $0) }.joined()
+ }
+ #endif
+
+ public static func alias(for identifier: String, runID: UUID) -> UUID {
+ let bytes = Array(SHA256.hash(data: Data((runID.uuidString + ":item:" + identifier).utf8)).prefix(16))
+ return UUID(uuid: (bytes[0],bytes[1],bytes[2],bytes[3],bytes[4],bytes[5],bytes[6],bytes[7],
+ bytes[8],bytes[9],bytes[10],bytes[11],bytes[12],bytes[13],bytes[14],bytes[15]))
+ }
+
+ public static func activeAlias(for identifier: String?) -> UUID? {
+ #if STABILITY
+ guard let identifier, let run = try? activeRun() else { return nil }
+ return alias(for: identifier, runID: run.runID)
+ #else
+ return nil
+ #endif
+ }
+
+ /// Commits to an item's identity, name, parent, and size without exporting
+ /// their values. The run salt prevents comparison across evidence bundles.
+ public static func metadataAlias(for item: KDriveRemoteItem, runID: UUID) -> UUID {
+ let fields = [String(item.id), String(item.parentID), item.name, item.size.map(String.init) ?? "nil"]
+ let encoded = fields.map { "\($0.utf8.count):\($0)" }.joined()
+ return alias(for: "metadata:" + encoded, runID: runID)
+ }
+
+ public static func activeMetadataAlias(for item: KDriveRemoteItem?) -> UUID? {
+ #if STABILITY
+ guard let item, let run = try? activeRun() else { return nil }
+ return metadataAlias(for: item, runID: run.runID)
+ #else
+ return nil
+ #endif
+ }
+
+ public static func activeRun() throws -> StabilityRunHandle? {
+ guard let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ProviderConstants.appGroupIdentifier) else {
+ throw ProviderDiagnosticStoreError.missingAppGroupContainer(ProviderConstants.appGroupIdentifier)
+ }
+ return try StabilityRunLocator.activeRun(rootDirectoryURL: container.appendingPathComponent("StabilityRuns"))
+ }
+}
diff --git a/PotassiumProviderCore/StabilityDiagnosticSettlement.swift b/PotassiumProviderCore/StabilityDiagnosticSettlement.swift
new file mode 100644
index 0000000..99ab4af
--- /dev/null
+++ b/PotassiumProviderCore/StabilityDiagnosticSettlement.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+/// A local recorder fence, independent of server polling and Retry-After.
+/// Every span must close exactly once and the stream must then remain quiet.
+public struct StabilityDiagnosticSettlement: Sendable {
+ private let quietPeriod: Duration
+ private var eventCount: Int?
+ private var lastEventID: UUID?
+ private var quietSince: ContinuousClock.Instant?
+
+ public init(quietPeriod: Duration = .seconds(1)) {
+ self.quietPeriod = quietPeriod
+ }
+
+ public mutating func observe(_ events: [ProviderDiagnosticEvent], at now: ContinuousClock.Instant = .now) -> Bool {
+ if eventCount != events.count || lastEventID != events.last?.id {
+ eventCount = events.count
+ lastEventID = events.last?.id
+ quietSince = nil
+ }
+ let spans = Dictionary(grouping: events.filter { $0.spanID != nil }, by: { $0.spanID! })
+ guard spans.values.allSatisfy({ span in
+ span.filter { $0.phase == .started }.count == 1 &&
+ span.filter { [.completed, .failed, .cancelled].contains($0.phase) }.count == 1
+ }) else {
+ quietSince = nil
+ return false
+ }
+ guard let quietSince else { self.quietSince = now; return false }
+ return quietSince.duration(to: now) >= quietPeriod
+ }
+}
diff --git a/PotassiumProviderCore/StabilityDiagnosticTail.swift b/PotassiumProviderCore/StabilityDiagnosticTail.swift
new file mode 100644
index 0000000..1e9c4e2
--- /dev/null
+++ b/PotassiumProviderCore/StabilityDiagnosticTail.swift
@@ -0,0 +1,99 @@
+#if os(macOS) && STABILITY
+import Darwin
+import Foundation
+
+public enum StabilityDiagnosticTailError: Error {
+ case replacedOrTruncated, unhealthyWriter
+}
+
+/// A run-local scheduling aid. Register before the UI action to avoid replaying
+/// historical callbacks. Certification still validates the entire sealed bundle.
+/// Use on one actor; this cursor deliberately does not conform to Sendable.
+public final class StabilityDiagnosticTail {
+ private let url: URL
+ private let descriptor: Int32
+ private let device: dev_t
+ private let inode: ino_t
+ private var offset: Int
+ private var anchor: Data
+ private let decoder: JSONDecoder
+
+ public init(eventsURL: URL) throws {
+ let fd = Darwin.open(eventsURL.path, O_RDONLY | O_NOFOLLOW)
+ guard fd >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ do {
+ try SecurePOSIXFile.requireRegularFile(fd)
+ guard flock(fd, LOCK_SH) == 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { flock(fd, LOCK_UN) }
+ var status = stat()
+ guard fstat(fd, &status) == 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ let baseline = try SecurePOSIXFile.read(fd, maximumBytes: StabilityRunCoordinator.defaultMaximumTotalBytes)
+ let end = baseline.lastIndex(of: 0x0A).map { $0 + 1 } ?? 0
+ url = eventsURL
+ descriptor = fd
+ device = status.st_dev
+ inode = status.st_ino
+ offset = end
+ anchor = Data(baseline[.. [ProviderDiagnosticEvent] {
+ guard SecurePOSIXFile.pathKind(url.deletingLastPathComponent()
+ .appendingPathComponent("diagnostic-health.failed")) == .missing else {
+ throw StabilityDiagnosticTailError.unhealthyWriter
+ }
+ guard flock(descriptor, LOCK_SH | LOCK_NB) == 0 else {
+ if errno == EWOULDBLOCK { return [] }
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ defer { flock(descriptor, LOCK_UN) }
+ var status = stat(), current = stat()
+ guard fstat(descriptor, &status) == 0, lstat(url.path, ¤t) == 0,
+ current.st_mode & S_IFMT == S_IFREG, current.st_dev == device, current.st_ino == inode,
+ status.st_size >= offset else { throw StabilityDiagnosticTailError.replacedOrTruncated }
+ guard try read(count: anchor.count, at: offset - anchor.count) == anchor else {
+ throw StabilityDiagnosticTailError.replacedOrTruncated
+ }
+ guard status.st_size <= StabilityRunCoordinator.defaultMaximumTotalBytes else {
+ throw ProviderDiagnosticStoreError.fileTooLarge
+ }
+ let count = min(Int(status.st_size) - offset, 1_024 * 1_024)
+ guard count > 0 else { return [] }
+ let data = try read(count: count, at: offset)
+ guard let newline = data.lastIndex(of: 0x0A) else {
+ guard count < 1_024 * 1_024 else { throw ProviderDiagnosticStoreError.fileTooLarge }
+ return []
+ }
+ let complete = Data(data[...newline])
+ let events = try StabilityRunCoordinator.decodeDiagnosticEvents(from: complete, decoder: decoder)
+ offset += complete.count
+ anchor = try read(count: min(offset, 64), at: max(0, offset - 64))
+ return events
+ }
+
+ private func read(count: Int, at offset: Int) throws -> Data {
+ guard count > 0 else { return Data() }
+ var data = Data(count: count)
+ try data.withUnsafeMutableBytes { bytes in
+ var consumed = 0
+ while consumed < count {
+ let result = pread(descriptor, bytes.baseAddress!.advanced(by: consumed), count - consumed, off_t(offset + consumed))
+ if result < 0, errno == EINTR { continue }
+ guard result > 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ consumed += result
+ }
+ }
+ return data
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityDiagnostics.swift b/PotassiumProviderCore/StabilityDiagnostics.swift
new file mode 100644
index 0000000..8e258cf
--- /dev/null
+++ b/PotassiumProviderCore/StabilityDiagnostics.swift
@@ -0,0 +1,2058 @@
+import CryptoKit
+import Darwin
+import Foundation
+
+public enum ProviderRuntimeProfile: String, Codable, Equatable, Sendable {
+ case standard
+ case stability
+
+ public static var current: ProviderRuntimeProfile {
+ #if STABILITY
+ .stability
+ #else
+ .standard
+ #endif
+ }
+}
+
+public enum ProviderDiagnosticSource: String, Codable, Equatable, Sendable {
+ case app
+ case fileProviderExtension
+ case actionExtension
+ case finderRunner
+}
+
+public enum ProviderDiagnosticOperation: String, Codable, CaseIterable, Equatable, Sendable {
+ case runtimeLoad
+ case runtimeInitialize
+ case runtimeInvalidate
+ case enumeratorInitialize
+ case enumeratorInvalidate
+ case itemLookup
+ case enumerateItems
+ case enumerateChanges
+ case currentSyncAnchor
+ case fetchContents
+ case createItem
+ case modifyItem
+ case deleteItem
+ case materializedItemsChanged
+ case thumbnail
+ case listDrives
+ case listDirectory
+ case listAdvancedDirectory
+ case listTrash
+ case listWorkingSetRelevantItems
+ case listPartialActivities
+ case downloadFile
+ case uploadFile
+ case replaceFile
+ case createDirectory
+ case renameItem
+ case moveItem
+ case updateModificationDate
+ case trashItem
+ case deleteTrashedItem
+ case favoriteItem
+ case duplicateItem
+ case trashedItem
+ case existingFileIDs
+ case restoreTrashedItem
+ case shareLink
+ case createShareLink
+ case updateShareLink
+ case deleteShareLink
+ case fileVersions
+ case restoreFileVersion
+ case workingSetRefresh
+ case knownFolderLocations
+ case labPreflight
+ case labProvision
+ case labReset
+ case finderScenario
+}
+
+public enum ProviderDiagnosticPhase: String, Codable, Equatable, Sendable {
+ case started
+ case progress
+ case completed
+ case failed
+ case cancelled
+ case checkpoint
+}
+
+public enum ProviderDiagnosticField: String, Codable, CaseIterable, Equatable, Sendable {
+ case contents
+ case filename
+ case parent
+ case contentModificationDate
+ case creationDate
+ case extendedAttributes
+ case favoriteRank
+ case fileSystemFlags
+ case lastUsedDate
+ case tagData
+ case trash
+ case typeAndCreator
+}
+
+public enum ProviderDiagnosticRouteTemplate: String, Codable, CaseIterable, Equatable, Sendable {
+ case driveDiscovery = "GET /2/drive/init"
+ case item = "GET /3/drive/{drive_id}/files/{file_id}"
+ case listDirectory = "GET /3/drive/{drive_id}/files/{file_id}/files"
+ case listAdvancedDirectory = "GET /3/drive/{drive_id}/files/{file_id}/listing"
+ case continueAdvancedDirectory = "GET /3/drive/{drive_id}/files/{file_id}/listing/continue"
+ case partialActivities = "POST /3/drive/{drive_id}/files/listing/partial"
+ case trash = "GET /3/drive/{drive_id}/trash"
+ case download = "GET /2/drive/{drive_id}/files/{file_id}/download"
+ case thumbnail = "GET /2/drive/{drive_id}/files/{file_id}/thumbnail"
+ case upload = "POST /3/drive/{drive_id}/upload"
+ case createDirectory = "POST /3/drive/{drive_id}/files/{file_id}/directory"
+ case rename = "POST /2/drive/{drive_id}/files/{file_id}/rename"
+ case move = "POST /3/drive/{drive_id}/files/{file_id}/move/{destination_id}"
+ case trashItem = "DELETE /2/drive/{drive_id}/files/{file_id}"
+ case deleteTrashedItem = "DELETE /2/drive/{drive_id}/trash/{file_id}"
+ case favorite = "POST /2/drive/{drive_id}/files/{file_id}/favorite"
+ case duplicate = "POST /3/drive/{drive_id}/files/{file_id}/duplicate"
+ case trashedItem = "GET /2/drive/{drive_id}/trash/{file_id}"
+ case existingFileIDs = "POST /2/drive/{drive_id}/files/existence"
+ case restoreTrash = "POST /2/drive/{drive_id}/trash/{file_id}/restore"
+ case shareLink = "/2/drive/{drive_id}/files/{file_id}/link"
+ case versions = "GET /3/drive/{drive_id}/files/{file_id}/versions"
+ case restoreVersion = "POST /3/drive/{drive_id}/files/{file_id}/versions/{version_id}/restore/{destination_id}"
+}
+
+public enum ProviderDiagnosticOption: String, Codable, CaseIterable, Equatable, Sendable {
+ case paginationCursor
+ case pageLimit
+ case orderByName
+ case orderByTypeThenName
+ case includeETag
+ case includeCapabilities
+ case conditionalETag
+ case stableFileID
+ case clientToken
+ case contentHash
+ case conflictError
+ case conflictRename
+ case conflictVersion
+ case destinationParent
+ case optionalName
+ case lastModifiedAt
+ case cancellableTransfer
+ case thumbnailDimensions
+ case activityBatch
+ case shareConfiguration
+ case versionPagination
+}
+
+public enum ProviderDiagnosticStatusClass: String, Codable, Equatable, Sendable {
+ case informational
+ case success
+ case redirection
+ case clientError
+ case serverError
+ case transportError
+
+ public init(httpStatusCode: Int) {
+ switch httpStatusCode {
+ case 100..<200: self = .informational
+ case 200..<300: self = .success
+ case 300..<400: self = .redirection
+ case 400..<500: self = .clientError
+ default: self = .serverError
+ }
+ }
+}
+
+public enum ProviderDiagnosticErrorClass: String, Codable, Equatable, Sendable {
+ case concurrentSnapshot
+ case authentication
+ case cancellation
+ case conflict
+ case invalidCursor
+ case network
+ case notFound
+ case permission
+ case quota
+ case server
+ case storage
+ case synchronization
+ case validation
+ case unknown
+}
+
+/// A value-only diagnostic record. Every textual field is a closed enum so a
+/// caller cannot accidentally persist a token, URL, item name, path, body, or
+/// account identifier.
+public struct ProviderDiagnosticEvent: Codable, Equatable, Sendable {
+ public static let schemaVersion = 3
+
+ public let schemaVersion: Int
+ public let id: UUID
+ public let occurredAt: Date
+ public let spanID: UUID?
+ public let parentSpanID: UUID?
+ public let subjectAlias: UUID?
+ public let itemMetadataAlias: UUID?
+ public let processInstanceID: UUID?
+ public let processCodeHash: String?
+ public let errorCode: Int?
+ public let validationFields: [ProviderDiagnosticValidationField]?
+ public let correlationID: UUID
+ public let source: ProviderDiagnosticSource
+ public let operation: ProviderDiagnosticOperation
+ public let phase: ProviderDiagnosticPhase
+ public let fieldShape: [ProviderDiagnosticField]
+ public let routeTemplate: ProviderDiagnosticRouteTemplate?
+ public let optionShape: [ProviderDiagnosticOption]
+ public let statusClass: ProviderDiagnosticStatusClass?
+ public let errorClass: ProviderDiagnosticErrorClass?
+ public let durationMilliseconds: Int?
+ public let progressPercentBucket: Int?
+ public let hasCursor: Bool?
+ public let hasMore: Bool?
+ public let hasAnchor: Bool?
+
+ public init(
+ id: UUID = UUID(),
+ occurredAt: Date = Date(),
+ spanID: UUID? = nil,
+ parentSpanID: UUID? = nil,
+ subjectAlias: UUID? = nil,
+ itemMetadataAlias: UUID? = nil,
+ processInstanceID: UUID? = nil,
+ processCodeHash: String? = nil,
+ errorCode: Int? = nil,
+ validationFields: [ProviderDiagnosticValidationField]? = nil,
+ correlationID: UUID,
+ source: ProviderDiagnosticSource,
+ operation: ProviderDiagnosticOperation,
+ phase: ProviderDiagnosticPhase,
+ fieldShape: [ProviderDiagnosticField] = [],
+ routeTemplate: ProviderDiagnosticRouteTemplate? = nil,
+ optionShape: [ProviderDiagnosticOption] = [],
+ statusClass: ProviderDiagnosticStatusClass? = nil,
+ errorClass: ProviderDiagnosticErrorClass? = nil,
+ durationMilliseconds: Int? = nil,
+ progressPercentBucket: Int? = nil,
+ hasCursor: Bool? = nil,
+ hasMore: Bool? = nil,
+ hasAnchor: Bool? = nil
+ ) {
+ self.schemaVersion = Self.schemaVersion
+ self.id = id
+ self.occurredAt = occurredAt
+ self.spanID = spanID
+ self.parentSpanID = parentSpanID
+ self.subjectAlias = subjectAlias
+ self.itemMetadataAlias = itemMetadataAlias
+ self.processInstanceID = processInstanceID
+ self.processCodeHash = processCodeHash.flatMap { value in
+ value.count == 40 && value.allSatisfy(\.isHexDigit) ? value.lowercased() : nil
+ }
+ self.errorCode = errorCode
+ self.validationFields = validationFields
+ self.correlationID = correlationID
+ self.source = source
+ self.operation = operation
+ self.phase = phase
+ self.fieldShape = Array(Set(fieldShape)).sorted { $0.rawValue < $1.rawValue }
+ self.routeTemplate = routeTemplate
+ self.optionShape = Array(Set(optionShape)).sorted { $0.rawValue < $1.rawValue }
+ self.statusClass = statusClass
+ self.errorClass = errorClass
+ self.durationMilliseconds = durationMilliseconds.map { max(0, $0) }
+ self.progressPercentBucket = progressPercentBucket.map { min(100, max(0, $0)) }
+ self.hasCursor = hasCursor
+ self.hasMore = hasMore
+ self.hasAnchor = hasAnchor
+ }
+}
+
+public protocol ProviderDiagnosticRecording: Sendable {
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) async throws
+}
+
+public struct StabilityRunManifest: Codable, Equatable, Sendable {
+ public static let schemaVersion = 1
+
+ public let schemaVersion: Int
+ public let runID: UUID
+ public let startedAt: Date
+ public let buildRevision: String?
+
+ public init(runID: UUID, startedAt: Date = Date(), buildRevision: String?) {
+ self.schemaVersion = Self.schemaVersion
+ self.runID = runID
+ self.startedAt = startedAt
+ self.buildRevision = Self.safeRevision(buildRevision)
+ }
+
+ private static func safeRevision(_ revision: String?) -> String? {
+ guard let revision else { return nil }
+ let safe = revision.lowercased().filter { $0.isHexDigit || $0 == "-" }.prefix(64)
+ return safe.isEmpty ? nil : String(safe)
+ }
+}
+
+public struct StabilityRunSummary: Codable, Equatable, Sendable {
+ public static let schemaVersion = 1
+
+ public let schemaVersion: Int
+ public let finishedAt: Date
+ public let assertionCount: Int
+ public let failedAssertionCount: Int
+ public let checkpointCount: Int
+
+ public init(
+ finishedAt: Date = Date(),
+ assertionCount: Int,
+ failedAssertionCount: Int,
+ checkpointCount: Int
+ ) {
+ self.schemaVersion = Self.schemaVersion
+ self.finishedAt = finishedAt
+ self.assertionCount = max(0, assertionCount)
+ self.failedAssertionCount = max(0, failedAssertionCount)
+ self.checkpointCount = max(0, checkpointCount)
+ }
+}
+
+public struct StabilityRunHandle: Codable, Equatable, Sendable {
+ public let runID: UUID
+ public let directoryURL: URL
+
+ public var manifestURL: URL { directoryURL.appendingPathComponent("run.json") }
+ public var eventsURL: URL { directoryURL.appendingPathComponent("events.jsonl") }
+ public var observationsURL: URL { directoryURL.appendingPathComponent("api-observations.jsonl") }
+ public var assertionsURL: URL { directoryURL.appendingPathComponent("assertions.jsonl") }
+ public var finderReportURL: URL { directoryURL.appendingPathComponent("finder-report.json") }
+ public var finderAbandonedURL: URL { directoryURL.appendingPathComponent("finder-abandoned.json") }
+ fileprivate var finderOwnerURL: URL { directoryURL.appendingPathComponent("finder-owner.json") }
+ fileprivate var finderStepURL: URL { directoryURL.appendingPathComponent("finder-step.json") }
+ public var summaryURL: URL { directoryURL.appendingPathComponent("summary.json") }
+}
+
+public struct StabilityOwnedRunHandle: Equatable, Sendable {
+ public let run: StabilityRunHandle
+ fileprivate let ownershipToken: UUID
+}
+
+public actor StabilityRunCoordinator {
+ public static let defaultMaximumRunCount = 20
+ public static let defaultMaximumTotalBytes = 250 * 1_024 * 1_024
+ private static let maximumFinderReportBytes = 1 * 1_024 * 1_024
+
+ private let rootDirectoryURL: URL
+ private let encoder: JSONEncoder
+ private let decoder: JSONDecoder
+ private let processIdentifier: Int32
+
+ public init(rootDirectoryURL: URL, processIdentifier: Int32 = getpid()) {
+ self.rootDirectoryURL = rootDirectoryURL
+ self.encoder = Self.makeEncoder()
+ self.decoder = Self.makeDecoder()
+ self.processIdentifier = processIdentifier
+ }
+
+ public init(
+ appGroupIdentifier: String = ProviderConstants.appGroupIdentifier,
+ processIdentifier: Int32 = getpid()
+ ) throws {
+ guard let containerURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: appGroupIdentifier
+ ) else {
+ throw ProviderDiagnosticStoreError.missingAppGroupContainer(appGroupIdentifier)
+ }
+ self.init(
+ rootDirectoryURL: containerURL.appendingPathComponent("StabilityRuns", isDirectory: true),
+ processIdentifier: processIdentifier
+ )
+ }
+
+ public func startRun(buildRevision: String? = nil) throws -> StabilityRunHandle {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ if let active = try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL) {
+ guard SecurePOSIXFile.pathKind(active.finderAbandonedURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runAbandonedByFinderRunner(active.runID)
+ }
+ return active
+ }
+
+ try SecurePOSIXFile.ensureDirectory(runsDirectoryURL)
+ let runID = UUID()
+ let directoryURL = runsDirectoryURL.appendingPathComponent(runID.uuidString.lowercased(), isDirectory: true)
+ try SecurePOSIXFile.createDirectoryExclusively(directoryURL)
+ let handle = StabilityRunHandle(runID: runID, directoryURL: directoryURL)
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(StabilityRunManifest(runID: runID, buildRevision: buildRevision)),
+ at: handle.manifestURL,
+ permissions: 0o400
+ )
+ for url in [handle.eventsURL, handle.observationsURL, handle.assertionsURL] {
+ try SecurePOSIXFile.createExclusively(Data(), at: url, permissions: 0o600)
+ }
+ try SecurePOSIXFile.replaceAtomically(
+ encoder.encode(ActiveRunPointer(runID: runID)),
+ at: activeRunURL,
+ permissions: 0o600
+ )
+ return handle
+ }
+ }
+
+ /// Starts a new run that only its creator may finish. Unlike `startRun`,
+ /// this rejects a pre-existing active run so a command cannot repurpose a
+ /// UI-owned bundle. The owner marker is retained after failures so partial
+ /// evidence cannot be finalized as an ordinary successful run. A separate,
+ /// explicit stale-owner recovery transition preserves an abandonment
+ /// marker before releasing the active-run lease.
+ public func startOwnedRun(buildRevision: String? = nil) throws -> StabilityOwnedRunHandle {
+ guard processIdentifier > 0 else {
+ throw ProviderDiagnosticStoreError.malformedRecord
+ }
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL) == nil else {
+ throw ProviderDiagnosticStoreError.runAlreadyActive
+ }
+ try SecurePOSIXFile.ensureDirectory(runsDirectoryURL)
+ let runID = UUID()
+ let token = UUID()
+ let directoryURL = runsDirectoryURL.appendingPathComponent(
+ runID.uuidString.lowercased(),
+ isDirectory: true
+ )
+ try SecurePOSIXFile.createDirectoryExclusively(directoryURL)
+ let handle = StabilityRunHandle(runID: runID, directoryURL: directoryURL)
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(StabilityRunManifest(runID: runID, buildRevision: buildRevision)),
+ at: handle.manifestURL,
+ permissions: 0o400
+ )
+ for url in [handle.eventsURL, handle.observationsURL, handle.assertionsURL] {
+ try SecurePOSIXFile.createExclusively(Data(), at: url, permissions: 0o600)
+ }
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(FinderRunOwnership(
+ token: token,
+ processIdentifier: processIdentifier
+ )),
+ at: handle.finderOwnerURL,
+ permissions: 0o400
+ )
+ try SecurePOSIXFile.replaceAtomically(
+ encoder.encode(ActiveRunPointer(runID: runID)),
+ at: activeRunURL,
+ permissions: 0o600
+ )
+ return StabilityOwnedRunHandle(run: handle, ownershipToken: token)
+ }
+ }
+
+ @discardableResult
+ public func finishRun(runID: UUID, summary: StabilityRunSummary) throws -> StabilityRunHandle {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ let handle = StabilityRunHandle(
+ runID: runID,
+ directoryURL: runsDirectoryURL.appendingPathComponent(runID.uuidString.lowercased(), isDirectory: true)
+ )
+ guard SecurePOSIXFile.isRegularFile(handle.manifestURL) else {
+ throw ProviderDiagnosticStoreError.runNotFound(runID)
+ }
+ guard SecurePOSIXFile.pathKind(handle.finderOwnerURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runOwnedByFinderRunner(runID)
+ }
+ guard SecurePOSIXFile.pathKind(handle.finderAbandonedURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runAbandonedByFinderRunner(runID)
+ }
+ guard SecurePOSIXFile.pathKind(handle.summaryURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runAlreadyFinished(runID)
+ }
+ let wasActive = try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)?.runID == runID
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(summary),
+ at: handle.summaryURL,
+ permissions: 0o400
+ )
+
+ if wasActive {
+ try SecurePOSIXFile.removeRegularFile(activeRunURL)
+ }
+ return handle
+ }
+ }
+
+ @discardableResult
+ public func finishOwnedRun(
+ _ ownedRun: StabilityOwnedRunHandle,
+ summary: StabilityRunSummary
+ ) throws -> StabilityRunHandle {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ let handle = ownedRun.run
+ guard try StabilityRunLocator.activeRunUnlocked(
+ rootDirectoryURL: rootDirectoryURL
+ )?.runID == handle.runID,
+ SecurePOSIXFile.isRegularFile(handle.manifestURL),
+ SecurePOSIXFile.isRegularFile(handle.finderReportURL),
+ SecurePOSIXFile.pathKind(handle.finderAbandonedURL) == .missing,
+ SecurePOSIXFile.pathKind(handle.finderStepURL) == .missing,
+ SecurePOSIXFile.pathKind(handle.summaryURL) == .missing,
+ try Self.matchesOwnership(ownedRun, decoder: decoder) else {
+ throw ProviderDiagnosticStoreError.runNotFound(handle.runID)
+ }
+ let report = try decoder.decode(
+ StabilityFinderRunReport.self,
+ from: SecurePOSIXFile.read(
+ handle.finderReportURL,
+ maximumBytes: Self.maximumFinderReportBytes
+ )
+ )
+ let expectedAssertionCount = report.stepResults.reduce(into: 0) {
+ $0 += $1.assertions.count
+ }
+ let expectedFailedAssertionCount = report.stepResults.reduce(into: 0) { count, step in
+ count += step.assertions.filter {
+ if case .failed = $0.outcome { return true }
+ return false
+ }.count
+ }
+ let expectedCheckpointCount = report.preflightSummary.checkpointed
+ + report.stepSummary.checkpointed
+ guard summary.assertionCount == expectedAssertionCount,
+ summary.failedAssertionCount == expectedFailedAssertionCount,
+ summary.checkpointCount == expectedCheckpointCount else {
+ throw ProviderDiagnosticStoreError.finderSummaryMismatch
+ }
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(summary),
+ at: handle.summaryURL,
+ permissions: 0o400
+ )
+ try SecurePOSIXFile.removeRegularFile(activeRunURL)
+ try SecurePOSIXFile.removeRegularFile(handle.finderOwnerURL)
+ return handle
+ }
+ }
+
+ /// Abandons an incomplete Finder run only after its recorded owner process
+ /// is no longer alive. Evidence remains on disk and is marked incomplete;
+ /// the active pointer is removed so a new run or reversible lab reset can
+ /// proceed. This operation never mutates remote data.
+ @discardableResult
+ public func abandonStaleOwnedRun() throws -> StabilityRunHandle {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard let handle = try StabilityRunLocator.activeRunUnlocked(
+ rootDirectoryURL: rootDirectoryURL
+ ) else {
+ throw ProviderDiagnosticStoreError.noStaleFinderRun
+ }
+
+ switch SecurePOSIXFile.pathKind(handle.finderAbandonedURL) {
+ case .regularFile:
+ break
+ case .missing:
+ guard SecurePOSIXFile.isRegularFile(handle.finderOwnerURL) else {
+ throw ProviderDiagnosticStoreError.noStaleFinderRun
+ }
+ let ownership = try decoder.decode(
+ FinderRunOwnership.self,
+ from: SecurePOSIXFile.read(handle.finderOwnerURL, maximumBytes: 4 * 1_024)
+ )
+ guard ownership.processIdentifier > 0 else {
+ throw ProviderDiagnosticStoreError.malformedRecord
+ }
+ errno = 0
+ if kill(ownership.processIdentifier, 0) == 0 || errno == EPERM {
+ throw ProviderDiagnosticStoreError.finderOwnerProcessStillRunning
+ }
+ guard errno == ESRCH else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(FinderRunAbandonment()),
+ at: handle.finderAbandonedURL,
+ permissions: 0o400
+ )
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+
+ switch SecurePOSIXFile.pathKind(handle.finderStepURL) {
+ case .missing:
+ break
+ case .regularFile:
+ try SecurePOSIXFile.removeRegularFile(handle.finderStepURL)
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ switch SecurePOSIXFile.pathKind(handle.finderOwnerURL) {
+ case .missing:
+ break
+ case .regularFile:
+ try SecurePOSIXFile.removeRegularFile(handle.finderOwnerURL)
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ try SecurePOSIXFile.removeRegularFile(activeRunURL)
+ return handle
+ }
+ }
+
+ public func beginFinderStep(
+ ownedRun: StabilityOwnedRunHandle,
+ correlationID: UUID
+ ) throws {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try StabilityRunLocator.activeRunUnlocked(
+ rootDirectoryURL: rootDirectoryURL
+ )?.runID == ownedRun.run.runID,
+ try Self.matchesOwnership(ownedRun, decoder: decoder),
+ SecurePOSIXFile.pathKind(ownedRun.run.summaryURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ guard SecurePOSIXFile.pathKind(ownedRun.run.finderStepURL) == .missing else {
+ throw ProviderDiagnosticStoreError.finderStepAlreadyActive
+ }
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(FinderStepPointer(correlationID: correlationID)),
+ at: ownedRun.run.finderStepURL,
+ permissions: 0o400
+ )
+ }
+ }
+
+ public func endFinderStep(
+ ownedRun: StabilityOwnedRunHandle,
+ correlationID: UUID
+ ) throws {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try Self.matchesOwnership(ownedRun, decoder: decoder),
+ SecurePOSIXFile.isRegularFile(ownedRun.run.finderStepURL) else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ let pointer = try decoder.decode(
+ FinderStepPointer.self,
+ from: SecurePOSIXFile.read(
+ ownedRun.run.finderStepURL,
+ maximumBytes: 4 * 1_024
+ )
+ )
+ guard pointer.correlationID == correlationID else {
+ throw ProviderDiagnosticStoreError.finderStepCorrelationMismatch
+ }
+ try SecurePOSIXFile.removeRegularFile(ownedRun.run.finderStepURL)
+ }
+ }
+
+ public func activeRun() throws -> StabilityRunHandle? {
+ try StabilityRunLocator.activeRun(rootDirectoryURL: rootDirectoryURL)
+ }
+
+ #if STABILITY
+ public func selectConflictProfile(_ selectedCase: StabilityLiveConflictCase, ownedRun: StabilityOwnedRunHandle,
+ extensionLaunchMode: StabilityExtensionLaunchMode? = nil) throws {
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try Self.matchesOwnership(ownedRun, decoder: decoder),
+ try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)?.runID == ownedRun.run.runID else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ try SecurePOSIXFile.createExclusively(encoder.encode(StabilityConflictProfile(runID: ownedRun.run.runID, selectedCase: selectedCase, extensionLaunchMode: extensionLaunchMode)),
+ at: ownedRun.run.directoryURL.appendingPathComponent("conflict-profile.json"), permissions: 0o400)
+ }
+ }
+ public func requireExtensionLaunch(_ mode: StabilityExtensionLaunchMode, ownedRun: StabilityOwnedRunHandle) throws {
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try Self.matchesOwnership(ownedRun, decoder: decoder),
+ try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)?.runID == ownedRun.run.runID else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ try SecurePOSIXFile.createExclusively(encoder.encode(StabilityExtensionLaunchRequest(runID: ownedRun.run.runID, mode: mode)),
+ at: ownedRun.run.directoryURL.appendingPathComponent("extension-launch-request.json"), permissions: 0o400)
+ }
+ }
+
+ public func recordExtensionLaunch(_ evidence: StabilityExtensionLaunchEvidence, ownedRun: StabilityOwnedRunHandle) throws {
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard evidence.runID == ownedRun.run.runID, try Self.matchesOwnership(ownedRun, decoder: decoder),
+ try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)?.runID == evidence.runID,
+ SecurePOSIXFile.pathKind(ownedRun.run.finderReportURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ try SecurePOSIXFile.createExclusively(encoder.encode(evidence),
+ at: ownedRun.run.directoryURL.appendingPathComponent("extension-launch.json"), permissions: 0o400)
+ }
+ }
+ #endif
+
+ /// Retains a rejected candidate without authorizing finalization or changing
+ /// the accepted report. Error descriptions and external payloads are omitted.
+ public func recordFinderEvidenceRejection(
+ _ rejection: StabilityFinderEvidenceRejection, ownedRun: StabilityOwnedRunHandle
+ ) throws {
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ guard try Self.matchesOwnership(ownedRun, decoder: decoder),
+ SecurePOSIXFile.pathKind(ownedRun.run.summaryURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runNotFound(ownedRun.run.runID)
+ }
+ try SecurePOSIXFile.createExclusively(encoder.encode(rejection),
+ at: ownedRun.run.directoryURL.appendingPathComponent("finder-evidence-rejected.json"), permissions: 0o400)
+ }
+ }
+
+ /// Assembles closed Finder assertion and API-observation evidence. Each
+ /// file replacement is atomic; the immutable report is the commit marker
+ /// and can be written only once. Run summary sealing remains a separate
+ /// final lifecycle transition.
+ public func writeFinderEvidence(
+ ownedRun: StabilityOwnedRunHandle,
+ report: StabilityFinderRunReport,
+ observations: [StabilityFinderAPIObservation]
+ ) throws {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ let handle = ownedRun.run
+ guard try StabilityRunLocator.activeRunUnlocked(
+ rootDirectoryURL: rootDirectoryURL
+ )?.runID == handle.runID,
+ SecurePOSIXFile.isRegularFile(handle.manifestURL),
+ SecurePOSIXFile.pathKind(handle.summaryURL) == .missing,
+ SecurePOSIXFile.pathKind(handle.finderStepURL) == .missing,
+ try Self.matchesOwnership(ownedRun, decoder: decoder) else {
+ throw ProviderDiagnosticStoreError.runNotFound(handle.runID)
+ }
+ guard SecurePOSIXFile.pathKind(handle.finderReportURL) == .missing else {
+ throw ProviderDiagnosticStoreError.finderEvidenceAlreadyFinalized(handle.runID)
+ }
+ guard SecurePOSIXFile.pathKind(handle.directoryURL.appendingPathComponent("diagnostic-health.failed")) == .missing else {
+ throw ProviderDiagnosticStoreError.malformedRecord
+ }
+ try Self.validateFinderObservations(observations, report: report)
+ let timeline = try Self.readDiagnosticEvents(from: handle.eventsURL, decoder: decoder).sorted {
+ $0.occurredAt == $1.occurredAt ? $0.id.uuidString < $1.id.uuidString : $0.occurredAt < $1.occurredAt
+ }
+ try Self.validateFinderDiagnostics(timeline, report: report)
+ #if STABILITY
+ let profileURL = handle.directoryURL.appendingPathComponent("conflict-profile.json")
+ if SecurePOSIXFile.isRegularFile(profileURL) {
+ let profile = try decoder.decode(StabilityConflictProfile.self, from: SecurePOSIXFile.read(profileURL, maximumBytes: 4096))
+ guard profile.runID == handle.runID else { throw StabilityLiveEvidenceError.missingConflict }
+ let requestURL = handle.directoryURL.appendingPathComponent("conflict-request.json")
+ let ticket = SecurePOSIXFile.isRegularFile(requestURL)
+ ? try JSONDecoder().decode(StabilityConflictBarrier.Ticket.self, from: SecurePOSIXFile.read(requestURL, maximumBytes: 4096)) : nil
+ let released = ticket.map { SecurePOSIXFile.isRegularFile(handle.directoryURL.appendingPathComponent("conflict-" + $0.attemptID.uuidString.lowercased() + "-release")) } ?? false
+ try profile.validate(report: report, ticket: ticket,
+ reached: ticket.map { StabilityConflictBarrier.reached($0, run: handle) } ?? false,
+ released: released,
+ competingMutationVerified: ticket.map { StabilityConflictBarrier.competingMutationVerified($0, run: handle) } ?? false,
+ diagnostics: timeline)
+ if let mode = profile.extensionLaunchMode, report.stepSummary.passed > 0 {
+ let launch = try decoder.decode(StabilityExtensionLaunchEvidence.self,
+ from: SecurePOSIXFile.read(handle.directoryURL.appendingPathComponent("extension-launch.json"), maximumBytes: 4096))
+ guard launch.mode == mode else { throw StabilityLiveEvidenceError.wrongExtensionBuild }
+ try launch.validate(runID: handle.runID, report: report, diagnostics: timeline)
+ }
+ } else if report.stepResults.contains(where: { $0.outcome == .skipped(.notSelectedForConflictProfile) }) {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ if report.schemaVersion >= StabilityFinderRunReport.liveSchemaVersion,
+ report.stepResults.contains(where: { $0.scenario == .concurrentRemotePreserveBoth && $0.outcome == .passed }) {
+ let ticket = try JSONDecoder().decode(StabilityConflictBarrier.Ticket.self,
+ from: SecurePOSIXFile.read(handle.directoryURL.appendingPathComponent("conflict-request.json"), maximumBytes: 4096))
+ guard StabilityConflictBarrier.competingMutationVerified(ticket, run: handle) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ }
+ let launchRequestURL = handle.directoryURL.appendingPathComponent("extension-launch-request.json")
+ if SecurePOSIXFile.isRegularFile(launchRequestURL) {
+ let request = try decoder.decode(StabilityExtensionLaunchRequest.self, from: SecurePOSIXFile.read(launchRequestURL, maximumBytes: 4096))
+ guard request.runID == handle.runID else { throw StabilityLiveEvidenceError.wrongExtensionBuild }
+ let launchURL = handle.directoryURL.appendingPathComponent("extension-launch.json")
+ let evidence = SecurePOSIXFile.isRegularFile(launchURL)
+ ? try decoder.decode(StabilityExtensionLaunchEvidence.self, from: SecurePOSIXFile.read(launchURL, maximumBytes: 4096)) : nil
+ try request.validate(evidence: evidence, report: report, diagnostics: timeline)
+ }
+ #endif
+ if report.schemaVersion >= StabilityFinderRunReport.liveSchemaVersion {
+ try SecurePOSIXFile.replaceAtomically(try encoder.encode(timeline),
+ at: handle.directoryURL.appendingPathComponent("diagnostic-timeline.json"), permissions: 0o400)
+ }
+
+ let assertionRecords = report.preflightResults.map {
+ FinderAssertionRecord.preflight($0)
+ } + report.stepResults.map {
+ FinderAssertionRecord.step($0)
+ }
+ try SecurePOSIXFile.replaceAtomically(
+ try Self.jsonLines(assertionRecords, encoder: encoder),
+ at: handle.assertionsURL,
+ permissions: 0o400
+ )
+ try SecurePOSIXFile.replaceAtomically(
+ try Self.jsonLines(observations, encoder: encoder),
+ at: handle.observationsURL,
+ permissions: 0o400
+ )
+ try SecurePOSIXFile.createExclusively(
+ encoder.encode(report),
+ at: handle.finderReportURL,
+ permissions: 0o400
+ )
+ }
+ }
+
+ /// Holds the cross-process run lifecycle lock for the full duration of a
+ /// destructive Stability Lab reset. A run cannot start between the reset
+ /// preflight and its final reversible trash operation.
+ public func withInactiveRunLease(
+ _ body: @Sendable () async throws -> T
+ ) async throws -> T {
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try await SecurePOSIXFile.withAsyncLock(
+ at: coordinatorLockURL,
+ operation: LOCK_EX
+ ) {
+ guard try StabilityRunLocator.activeRunUnlocked(
+ rootDirectoryURL: rootDirectoryURL
+ ) == nil else {
+ throw ProviderDiagnosticStoreError.runAlreadyActive
+ }
+ return try await body()
+ }
+ }
+
+ @discardableResult
+ public func pruneCompletedRuns(
+ maximumRunCount: Int = defaultMaximumRunCount,
+ maximumTotalBytes: Int = defaultMaximumTotalBytes
+ ) throws -> [UUID] {
+ guard SecurePOSIXFile.pathKind(runsDirectoryURL) != .missing else { return [] }
+ try SecurePOSIXFile.ensureDirectory(rootDirectoryURL)
+ return try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_EX) {
+ let activeRunID = try StabilityRunLocator.activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)?.runID
+ let directories = try FileManager.default.contentsOfDirectory(
+ at: runsDirectoryURL,
+ includingPropertiesForKeys: nil,
+ options: [.skipsHiddenFiles]
+ )
+ var completed: [(handle: StabilityRunHandle, finishedAt: Date, size: Int)] = []
+ for directory in directories {
+ guard SecurePOSIXFile.pathKind(directory) == .directory,
+ let runID = UUID(uuidString: directory.lastPathComponent),
+ runID != activeRunID else { continue }
+ let handle = StabilityRunHandle(runID: runID, directoryURL: directory)
+ guard SecurePOSIXFile.isRegularFile(handle.manifestURL),
+ SecurePOSIXFile.isRegularFile(handle.summaryURL) else { continue }
+ let summary = try decoder.decode(
+ StabilityRunSummary.self,
+ from: SecurePOSIXFile.read(handle.summaryURL, maximumBytes: 64 * 1_024)
+ )
+ completed.append((handle, summary.finishedAt, try Self.directorySize(directory)))
+ }
+ completed.sort { $0.finishedAt > $1.finishedAt }
+
+ var retainedCount = 0
+ var retainedBytes = 0
+ var removed: [UUID] = []
+ for run in completed {
+ let fitsCount = retainedCount < max(0, maximumRunCount)
+ let fitsBytes = run.size <= max(0, maximumTotalBytes - retainedBytes)
+ if fitsCount && fitsBytes {
+ retainedCount += 1
+ retainedBytes += run.size
+ } else {
+ try FileManager.default.removeItem(at: run.handle.directoryURL)
+ try SecurePOSIXFile.synchronizeDirectory(runsDirectoryURL)
+ removed.append(run.handle.runID)
+ }
+ }
+ return removed
+ }
+ }
+
+ private var runsDirectoryURL: URL {
+ rootDirectoryURL.appendingPathComponent("runs", isDirectory: true)
+ }
+
+ private var activeRunURL: URL {
+ rootDirectoryURL.appendingPathComponent("current-run.json")
+ }
+
+ private var coordinatorLockURL: URL {
+ rootDirectoryURL.appendingPathComponent("coordinator.lock")
+ }
+
+ private static func directorySize(_ directoryURL: URL) throws -> Int {
+ guard SecurePOSIXFile.pathKind(directoryURL) == .directory,
+ let enumerator = FileManager.default.enumerator(
+ at: directoryURL,
+ includingPropertiesForKeys: nil,
+ options: [.skipsHiddenFiles]
+ ) else { return 0 }
+ var size = 0
+ for case let url as URL in enumerator {
+ switch SecurePOSIXFile.pathKind(url) {
+ case .regularFile:
+ size += try SecurePOSIXFile.fileSize(url)
+ case .directory:
+ continue
+ case .missing, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ }
+ return size
+ }
+
+ private static func validateFinderObservations(
+ _ observations: [StabilityFinderAPIObservation],
+ report: StabilityFinderRunReport
+ ) throws {
+ let steps = Dictionary(uniqueKeysWithValues: report.stepResults.map {
+ ($0.scenario, $0)
+ })
+ var seen: Set = []
+ for observation in observations {
+ let key = FinderObservationKey(
+ scenario: observation.scenario,
+ phase: observation.phase
+ )
+ guard seen.insert(key).inserted else {
+ throw StabilityFinderEvidenceValidationError.duplicateObservation(
+ scenario: observation.scenario,
+ phase: observation.phase
+ )
+ }
+ guard steps[observation.scenario]?.correlationID == observation.correlationID else {
+ throw StabilityFinderEvidenceValidationError.observationCorrelationMismatch(
+ observation.scenario
+ )
+ }
+ }
+
+ for step in report.stepResults where step.outcome == .passed {
+ for phase in [
+ StabilityFinderAPIObservationPhase.baseline,
+ .postcondition,
+ ] {
+ guard let observation = observations.first(where: {
+ $0.scenario == step.scenario && $0.phase == phase
+ }) else {
+ throw StabilityFinderEvidenceValidationError.missingPassedObservation(
+ scenario: step.scenario,
+ phase: phase
+ )
+ }
+ guard observation.outcome == .passed else {
+ throw StabilityFinderEvidenceValidationError.invalidPassedObservation(
+ step.scenario
+ )
+ }
+ }
+ }
+ }
+
+ public static func readDiagnosticEvents(
+ from eventsURL: URL,
+ decoder suppliedDecoder: JSONDecoder? = nil
+ ) throws -> [ProviderDiagnosticEvent] {
+ let data = try LockedJSONLFile.read(from: eventsURL, maximumBytes: defaultMaximumTotalBytes)
+ return try decodeDiagnosticEvents(from: data, decoder: suppliedDecoder)
+ }
+
+ static func decodeDiagnosticEvents(from data: Data, decoder suppliedDecoder: JSONDecoder? = nil) throws -> [ProviderDiagnosticEvent] {
+ let decoder = suppliedDecoder ?? makeDecoder()
+ let endsInNewline = data.last == 0x0A || data.isEmpty
+ let lines = data.split(separator: 0x0A, omittingEmptySubsequences: false)
+ var diagnostics: [ProviderDiagnosticEvent] = []
+ for (offset, line) in lines.enumerated() where line.isEmpty == false {
+ if endsInNewline == false, offset == lines.count - 1 {
+ break
+ }
+ let record: JSONLRecord
+ do {
+ record = try decoder.decode(JSONLRecord.self, from: Data(line))
+ } catch {
+ throw ProviderDiagnosticStoreError.corruptRecord(line: offset + 1)
+ }
+ guard record.schemaVersion == JSONLRecord.schemaVersion else {
+ throw ProviderDiagnosticStoreError.unsupportedSchemaVersion(record.schemaVersion)
+ }
+ if case .diagnostic(let event) = try record.payload {
+ diagnostics.append(event)
+ }
+ }
+ return diagnostics
+ }
+
+ private static func validateFinderDiagnostics(
+ _ diagnostics: [ProviderDiagnosticEvent],
+ report: StabilityFinderRunReport
+ ) throws {
+ for step in report.stepResults where step.outcome == .passed {
+ if report.schemaVersion >= StabilityFinderRunReport.liveSchemaVersion {
+ try StabilityLiveEvidenceValidator.validate(step: step, diagnostics: diagnostics)
+ continue
+ }
+ guard let requirement = step.scenario.diagnosticRequirement,
+ requirement.operationGroups.isEmpty == false,
+ requirement.operationGroups.allSatisfy({ operationGroup in
+ operationGroup.isEmpty == false && diagnostics.contains(where: {
+ $0.correlationID == step.correlationID
+ && $0.source == requirement.source
+ && operationGroup.contains($0.operation)
+ && $0.phase == .completed
+ })
+ }) else {
+ throw StabilityFinderEvidenceValidationError.missingDiagnosticEvidence(
+ step.scenario
+ )
+ }
+ }
+ }
+
+ private static func jsonLines(
+ _ values: [T],
+ encoder: JSONEncoder
+ ) throws -> Data {
+ var data = Data()
+ for value in values {
+ data.append(try encoder.encode(value))
+ data.append(0x0A)
+ }
+ return data
+ }
+
+ private static func matchesOwnership(
+ _ ownedRun: StabilityOwnedRunHandle,
+ decoder: JSONDecoder
+ ) throws -> Bool {
+ guard SecurePOSIXFile.isRegularFile(ownedRun.run.finderOwnerURL) else { return false }
+ let ownership = try decoder.decode(
+ FinderRunOwnership.self,
+ from: SecurePOSIXFile.read(ownedRun.run.finderOwnerURL, maximumBytes: 4 * 1_024)
+ )
+ return ownership.token == ownedRun.ownershipToken
+ }
+
+ fileprivate static func makeEncoder() -> JSONEncoder {
+ let encoder = JSONEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ encoder.outputFormatting = [.sortedKeys]
+ return encoder
+ }
+
+ fileprivate static func makeDecoder() -> JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+ return decoder
+ }
+}
+
+private struct ActiveRunPointer: Codable {
+ let runID: UUID
+}
+
+private struct FinderRunOwnership: Codable {
+ let token: UUID
+ let processIdentifier: Int32
+}
+
+private struct FinderRunAbandonment: Codable {
+ static let schemaVersion = 1
+
+ let schemaVersion: Int
+ let abandonedAt: Date
+
+ init(abandonedAt: Date = Date()) {
+ self.schemaVersion = Self.schemaVersion
+ self.abandonedAt = abandonedAt
+ }
+}
+
+private struct FinderStepPointer: Codable {
+ let correlationID: UUID
+}
+
+private struct FinderObservationKey: Hashable {
+ let scenario: StabilityFinderScenario
+ let phase: StabilityFinderAPIObservationPhase
+}
+
+private enum FinderAssertionRecord: Codable {
+ case preflight(StabilityFinderPreflightResult)
+ case step(StabilityFinderStepResult)
+}
+
+public enum StabilityRunLocator {
+ public static func activeRun(rootDirectoryURL: URL) throws -> StabilityRunHandle? {
+ switch SecurePOSIXFile.pathKind(rootDirectoryURL) {
+ case .missing:
+ return nil
+ case .directory:
+ break
+ case .regularFile, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ return try SecurePOSIXFile.withLock(
+ at: rootDirectoryURL.appendingPathComponent("coordinator.lock"),
+ operation: LOCK_SH
+ ) {
+ try activeRunUnlocked(rootDirectoryURL: rootDirectoryURL)
+ }
+ }
+
+ public static func activeFinderStepCorrelation(
+ rootDirectoryURL: URL
+ ) throws -> UUID? {
+ guard SecurePOSIXFile.pathKind(rootDirectoryURL) == .directory else { return nil }
+ return try SecurePOSIXFile.withLock(
+ at: rootDirectoryURL.appendingPathComponent("coordinator.lock"),
+ operation: LOCK_SH
+ ) {
+ guard let run = try activeRunUnlocked(rootDirectoryURL: rootDirectoryURL) else {
+ return nil
+ }
+ switch SecurePOSIXFile.pathKind(run.finderStepURL) {
+ case .missing:
+ return nil
+ case .regularFile:
+ return try StabilityRunCoordinator.makeDecoder().decode(
+ FinderStepPointer.self,
+ from: SecurePOSIXFile.read(run.finderStepURL, maximumBytes: 4 * 1_024)
+ ).correlationID
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ }
+ }
+
+ fileprivate static func activeRunUnlocked(rootDirectoryURL: URL) throws -> StabilityRunHandle? {
+ let pointerURL = rootDirectoryURL.appendingPathComponent("current-run.json")
+ switch SecurePOSIXFile.pathKind(pointerURL) {
+ case .missing:
+ return nil
+ case .regularFile:
+ break
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.invalidActiveRunPointer
+ }
+ let pointer = try StabilityRunCoordinator.makeDecoder().decode(
+ ActiveRunPointer.self,
+ from: SecurePOSIXFile.read(pointerURL, maximumBytes: 64 * 1_024)
+ )
+ let directoryURL = rootDirectoryURL
+ .appendingPathComponent("runs", isDirectory: true)
+ .appendingPathComponent(pointer.runID.uuidString.lowercased(), isDirectory: true)
+ let handle = StabilityRunHandle(runID: pointer.runID, directoryURL: directoryURL)
+ guard SecurePOSIXFile.pathKind(directoryURL) == .directory,
+ SecurePOSIXFile.isRegularFile(handle.manifestURL) else {
+ throw ProviderDiagnosticStoreError.invalidActiveRunPointer
+ }
+ switch SecurePOSIXFile.pathKind(handle.summaryURL) {
+ case .missing:
+ break
+ case .regularFile:
+ // A crash after the immutable summary was created but before the
+ // active pointer was unlinked leaves a recoverable stale pointer.
+ return nil
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.invalidActiveRunPointer
+ }
+ let manifest = try StabilityRunCoordinator.makeDecoder().decode(
+ StabilityRunManifest.self,
+ from: SecurePOSIXFile.read(handle.manifestURL, maximumBytes: 64 * 1_024)
+ )
+ guard manifest.runID == pointer.runID else {
+ throw ProviderDiagnosticStoreError.invalidActiveRunPointer
+ }
+ return handle
+ }
+}
+
+public actor KDriveProviderEventJSONLStore: KDriveProviderEventStoring,
+ KDriveProviderEventTimelinePaging,
+ KDriveProviderEventStatisticsProviding,
+ KDriveProviderEventObserving,
+ KDriveProviderEventPruning,
+ KDriveProviderEventExporting,
+ ProviderDiagnosticRecording
+{
+ private let eventsURL: URL
+ private let encoder: JSONEncoder
+ private let decoder: JSONDecoder
+ private let runID: UUID
+ private let domainHashSalt: Data
+ private let summaryURL: URL
+ private let coordinatorLockURL: URL
+ private let maximumEventBytes: Int
+
+ public init(
+ runDirectoryURL: URL,
+ maximumEventBytes: Int = StabilityRunCoordinator.defaultMaximumTotalBytes
+ ) throws {
+ guard SecurePOSIXFile.pathKind(runDirectoryURL) == .directory else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ let manifestURL = runDirectoryURL.appendingPathComponent("run.json")
+ let manifest = try StabilityRunCoordinator.makeDecoder().decode(
+ StabilityRunManifest.self,
+ from: SecurePOSIXFile.read(manifestURL, maximumBytes: 64 * 1_024)
+ )
+ guard runDirectoryURL.lastPathComponent.caseInsensitiveCompare(manifest.runID.uuidString) == .orderedSame else {
+ throw ProviderDiagnosticStoreError.invalidActiveRunPointer
+ }
+ self.eventsURL = runDirectoryURL.appendingPathComponent("events.jsonl")
+ self.encoder = StabilityRunCoordinator.makeEncoder()
+ self.decoder = StabilityRunCoordinator.makeDecoder()
+ self.runID = manifest.runID
+ self.domainHashSalt = Data(manifest.runID.uuidString.lowercased().utf8)
+ self.summaryURL = runDirectoryURL.appendingPathComponent("summary.json")
+ self.coordinatorLockURL = runDirectoryURL
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ .appendingPathComponent("coordinator.lock")
+ self.maximumEventBytes = max(0, maximumEventBytes)
+ switch SecurePOSIXFile.pathKind(eventsURL) {
+ case .regularFile:
+ break
+ case .missing:
+ try SecurePOSIXFile.createExclusively(Data(), at: eventsURL, permissions: 0o600)
+ case .directory, .other:
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ }
+
+ public func saveConflict(_ event: KDriveConflictEvent) throws {
+ try append(.conflict(sanitize(event)))
+ }
+
+ public func recordActivity(_ event: KDriveProviderActivityEvent) throws {
+ try append(.activity(sanitize(event)))
+ }
+
+ public func recordDiagnostic(_ event: ProviderDiagnosticEvent) throws {
+ try append(.diagnostic(event))
+ }
+
+ public func recentConflicts(domainIdentifier: String?, limit: Int = 100) throws -> [KDriveConflictEvent] {
+ let domainHash = domainIdentifier.map(domainHash)
+ return Array(try replay().conflicts.values
+ .filter { domainHash == nil || $0.domainIdentifier == domainHash }
+ .sorted { $0.detectedAt > $1.detectedAt }
+ .prefix(max(0, limit)))
+ }
+
+ public func recentActivity(domainIdentifier: String?, limit: Int = 100) throws -> [KDriveProviderActivityEvent] {
+ try recentActivity(domainIdentifier: domainIdentifier, outcome: nil, limit: limit)
+ }
+
+ public func recentActivity(
+ domainIdentifier: String?,
+ outcome: KDriveProviderActivityOutcome?,
+ limit: Int = 100
+ ) throws -> [KDriveProviderActivityEvent] {
+ let domainHash = domainIdentifier.map(domainHash)
+ return Array(try replay().activity.values
+ .filter { event in
+ (domainHash == nil || event.domainIdentifier == domainHash)
+ && (outcome == nil || event.outcome == outcome)
+ }
+ .sorted { $0.occurredAt > $1.occurredAt }
+ .prefix(max(0, limit)))
+ }
+
+ public func removeActivityAndResolvedConflicts(domainIdentifier: String? = nil) throws {
+ try append(.clearActivityAndResolvedConflicts(domainIdentifier.map(domainHash)))
+ }
+
+ public func removeEvents(domainIdentifier: String) throws {
+ try append(.removeDomain(domainHash(domainIdentifier)))
+ }
+
+ public func pruneActivityEvents(maximumCount _: Int) throws {
+ // Stability bundles are immutable evidence. Retention removes only
+ // complete run directories through StabilityRunCoordinator.
+ }
+
+ public func timelinePage(
+ filter: KDriveProviderTimelineFilter,
+ before cursor: KDriveProviderTimelineCursor?,
+ limit: Int
+ ) throws -> KDriveProviderTimelinePage {
+ guard limit > 0 else {
+ return KDriveProviderTimelinePage(entries: [], nextCursor: nil, hasMore: false)
+ }
+ let state = try replay()
+ var entries = state.conflicts.values.map(KDriveProviderTimelineEntry.conflict)
+ entries += state.activity.values
+ .filter { $0.relatedConflictID == nil }
+ .filter { filter == .allActivity || $0.outcome == .failure }
+ .map(KDriveProviderTimelineEntry.activity)
+ entries.sort(by: Self.isNewer)
+ if let cursor {
+ entries = entries.filter { Self.isEntry($0, before: cursor) }
+ }
+ let pageSize = max(0, limit)
+ let pageEntries = Array(entries.prefix(pageSize))
+ let hasMore = entries.count > pageEntries.count
+ return KDriveProviderTimelinePage(
+ entries: pageEntries,
+ nextCursor: hasMore ? pageEntries.last?.cursor : nil,
+ hasMore: hasMore
+ )
+ }
+
+ public func eventStatistics(
+ domainIdentifiers: Set
+ ) throws -> [KDriveProviderEventDomainStatistics] {
+ let requested = Dictionary(uniqueKeysWithValues: domainIdentifiers.map { (domainHash($0), $0) })
+ let state = try replay()
+ return requested.sorted { $0.value < $1.value }.map { domainHash, originalDomain in
+ let conflicts = state.conflicts.values.filter { $0.domainIdentifier == domainHash }
+ let activity = state.activity.values.filter { $0.domainIdentifier == domainHash }
+ return KDriveProviderEventDomainStatistics(
+ domainIdentifier: originalDomain,
+ unresolvedConflictCount: conflicts.filter { $0.resolutionState == .unresolved }.count,
+ blockedConflictCount: conflicts.filter { $0.resolutionState == .blockedRetryable }.count,
+ failedConflictCount: conflicts.filter { $0.resolutionState == .failed }.count,
+ resolvedConflictCount: conflicts.filter { $0.resolutionState == .automaticallyResolved }.count,
+ recentFailureCount: activity.filter { $0.outcome == .failure }.count,
+ recentSuccessCount: activity.filter { $0.outcome == .success }.count,
+ latestConflictAt: conflicts.map { $0.resolvedAt ?? $0.detectedAt }.max(),
+ latestActivityAt: activity.map(\.occurredAt).max()
+ )
+ }
+ }
+
+ public func supportLogData(domainIdentifier: String? = nil) throws -> Data {
+ let log = KDriveProviderSupportLog(
+ activity: try recentActivity(domainIdentifier: domainIdentifier, limit: .max),
+ conflicts: try recentConflicts(domainIdentifier: domainIdentifier, limit: .max)
+ )
+ let exportEncoder = StabilityRunCoordinator.makeEncoder()
+ exportEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ return try exportEncoder.encode(log)
+ }
+
+ public func eventChanges(pollInterval: TimeInterval = 1) async -> AsyncStream {
+ let url = eventsURL
+ // Establish the subscription before returning it. If the worker takes
+ // its baseline later, an intervening append becomes invisible.
+ let initial = Self.fileFingerprint(url)
+ return AsyncStream { continuation in
+ let task = Task.detached {
+ var previous = initial
+ while Task.isCancelled == false {
+ try? await Task.sleep(for: .seconds(max(0.05, pollInterval)))
+ let current = Self.fileFingerprint(url)
+ if current != previous {
+ previous = current
+ continuation.yield()
+ }
+ }
+ continuation.finish()
+ }
+ continuation.onTermination = { _ in task.cancel() }
+ }
+ }
+
+ private func append(_ payload: JSONLRecord.Payload) throws {
+ var data = try encoder.encode(JSONLRecord(payload: payload))
+ data.append(0x0A)
+ try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_SH) {
+ guard SecurePOSIXFile.pathKind(summaryURL) == .missing else {
+ throw ProviderDiagnosticStoreError.runAlreadyFinished(runID)
+ }
+ do {
+ try LockedJSONLFile.append(data, to: eventsURL, maximumBytes: maximumEventBytes)
+ } catch {
+ // Persist a content-free health latch: a later successful write cannot hide a gap.
+ try? SecurePOSIXFile.createExclusively(Data(), at: eventsURL.deletingLastPathComponent().appendingPathComponent("diagnostic-health.failed"), permissions: 0o400)
+ throw error
+ }
+ }
+ }
+
+ private func replay() throws -> ReplayState {
+ let data = try SecurePOSIXFile.withLock(at: coordinatorLockURL, operation: LOCK_SH) {
+ try LockedJSONLFile.read(from: eventsURL, maximumBytes: maximumEventBytes)
+ }
+ let endsInNewline = data.last == 0x0A || data.isEmpty
+ let lines = data.split(separator: 0x0A, omittingEmptySubsequences: false)
+ var state = ReplayState()
+ for (offset, line) in lines.enumerated() where line.isEmpty == false {
+ if endsInNewline == false, offset == lines.count - 1 {
+ break
+ }
+ let record: JSONLRecord
+ do {
+ record = try decoder.decode(JSONLRecord.self, from: Data(line))
+ } catch {
+ throw ProviderDiagnosticStoreError.corruptRecord(line: offset + 1)
+ }
+ guard record.schemaVersion == JSONLRecord.schemaVersion else {
+ throw ProviderDiagnosticStoreError.unsupportedSchemaVersion(record.schemaVersion)
+ }
+ state.apply(try record.payload)
+ }
+ return state
+ }
+
+ private func sanitize(_ event: KDriveProviderActivityEvent) -> KDriveProviderActivityEvent {
+ KDriveProviderActivityEvent(
+ id: event.id,
+ occurredAt: event.occurredAt,
+ domainIdentifier: domainHash(event.domainIdentifier),
+ driveID: 0,
+ kind: event.kind,
+ scope: event.scope,
+ outcome: event.outcome,
+ severity: event.severity,
+ itemIdentifier: nil,
+ itemName: nil,
+ itemPath: nil,
+ summary: "\(Self.displayName(event.kind)) \(Self.outcomeDescription(event.outcome)).",
+ relatedConflictID: event.relatedConflictID,
+ diagnostic: event.errorCategory.map {
+ KDriveProviderActivityErrorDiagnostic(
+ errorCategory: $0,
+ providerErrorCode: event.providerErrorCode,
+ underlyingErrorDomain: Self.safeErrorDomain(event.underlyingErrorDomain),
+ underlyingErrorCode: event.underlyingErrorCode,
+ recoverySuggestion: nil,
+ diagnosticSummary: nil
+ )
+ },
+ correlationID: Self.safeUUIDString(event.correlationID),
+ durationMilliseconds: event.durationMilliseconds.map { max(0, $0) },
+ networkOperation: event.networkOperation.flatMap(Self.safeNetworkOperation),
+ httpStatusCode: event.httpStatusCode,
+ remoteRequestID: nil
+ )
+ }
+
+ private func sanitize(_ event: KDriveConflictEvent) -> KDriveConflictEvent {
+ KDriveConflictEvent(
+ id: event.id,
+ detectedAt: event.detectedAt,
+ resolvedAt: event.resolvedAt,
+ domainIdentifier: domainHash(event.domainIdentifier),
+ driveID: 0,
+ operation: event.operation,
+ originalItemIdentifier: nil,
+ originalItemName: nil,
+ originalItemPath: nil,
+ conflictItemIdentifier: nil,
+ conflictItemName: nil,
+ conflictItemPath: nil,
+ resolutionState: event.resolutionState,
+ automaticallyResolved: event.automaticallyResolved,
+ resolutionKind: event.resolutionKind,
+ resolutionSummary: "Conflict \(event.resolutionState.rawValue).",
+ stagedUploadRelativePath: nil
+ )
+ }
+
+ private func domainHash(_ value: String) -> String {
+ var input = domainHashSalt
+ input.append(contentsOf: value.utf8)
+ let digest = SHA256.hash(data: input)
+ return "domain-" + digest.prefix(8).map { String(format: "%02x", $0) }.joined()
+ }
+
+ private static func safeUUIDString(_ value: String?) -> String? {
+ value.flatMap(UUID.init(uuidString:))?.uuidString.lowercased()
+ }
+
+ private static func safeErrorDomain(_ value: String?) -> String? {
+ let allowed = [NSCocoaErrorDomain, NSURLErrorDomain, "NSFileProviderErrorDomain"]
+ return value.flatMap { allowed.contains($0) ? $0 : nil }
+ }
+
+ private static func safeNetworkOperation(_ value: String) -> String? {
+ let allowed = Set(ProviderDiagnosticOperation.allCases.map(\.rawValue))
+ return allowed.contains(value) ? value : nil
+ }
+
+ private static func displayName(_ kind: KDriveProviderActivityKind) -> String {
+ switch kind {
+ case .syncAnchor: "Sync anchor"
+ case .fetchContents: "Fetch contents"
+ case .metadataLookup: "Metadata lookup"
+ case .runtimeLoading: "Runtime loading"
+ case .driveDiscovery: "Drive discovery"
+ case .domainManagement: "Domain management"
+ case .shareLink: "Share link"
+ case .versionRestore: "Version restore"
+ default: kind.rawValue.capitalized
+ }
+ }
+
+ private static func outcomeDescription(_ outcome: KDriveProviderActivityOutcome) -> String {
+ switch outcome {
+ case .success: "succeeded"
+ case .failure: "failed"
+ }
+ }
+
+ private static func isNewer(_ lhs: KDriveProviderTimelineEntry, _ rhs: KDriveProviderTimelineEntry) -> Bool {
+ if lhs.date != rhs.date { return lhs.date > rhs.date }
+ if lhs.cursor.kind != rhs.cursor.kind { return lhs.cursor.kind.rawValue > rhs.cursor.kind.rawValue }
+ return lhs.cursor.eventID.uuidString > rhs.cursor.eventID.uuidString
+ }
+
+ private static func isEntry(_ entry: KDriveProviderTimelineEntry, before cursor: KDriveProviderTimelineCursor) -> Bool {
+ if entry.date != cursor.date { return entry.date < cursor.date }
+ if entry.cursor.kind != cursor.kind { return entry.cursor.kind.rawValue < cursor.kind.rawValue }
+ return entry.cursor.eventID.uuidString < cursor.eventID.uuidString
+ }
+
+ private struct FileFingerprint: Equatable, Sendable {
+ let size: off_t
+ let modifiedSeconds: Int
+ let modifiedNanoseconds: Int
+ }
+
+ private nonisolated static func fileFingerprint(_ url: URL) -> FileFingerprint? {
+ var status = stat()
+ guard lstat(url.path, &status) == 0, status.st_mode & S_IFMT == S_IFREG else {
+ return nil
+ }
+ return FileFingerprint(
+ size: status.st_size,
+ modifiedSeconds: status.st_mtimespec.tv_sec,
+ modifiedNanoseconds: status.st_mtimespec.tv_nsec
+ )
+ }
+}
+
+private struct ReplayState {
+ var activity: [UUID: KDriveProviderActivityEvent] = [:]
+ var conflicts: [UUID: KDriveConflictEvent] = [:]
+ var diagnostics: [UUID: ProviderDiagnosticEvent] = [:]
+
+ mutating func apply(_ payload: JSONLRecord.Payload) {
+ switch payload {
+ case .activity(let event):
+ activity[event.id] = event
+ case .conflict(let event):
+ conflicts[event.id] = event
+ case .diagnostic(let event):
+ diagnostics[event.id] = event
+ case .removeDomain(let domainIdentifier):
+ activity = activity.filter { $0.value.domainIdentifier != domainIdentifier }
+ conflicts = conflicts.filter { $0.value.domainIdentifier != domainIdentifier }
+ case .clearActivityAndResolvedConflicts(let domainIdentifier):
+ activity = activity.filter { _, event in
+ domainIdentifier != nil && event.domainIdentifier != domainIdentifier
+ }
+ conflicts = conflicts.filter { _, event in
+ let matchesDomain = domainIdentifier == nil || event.domainIdentifier == domainIdentifier
+ return matchesDomain == false || event.resolutionState != .automaticallyResolved
+ }
+ }
+ }
+}
+
+private struct JSONLRecord: Codable {
+ static let schemaVersion = 1
+
+ enum Kind: String, Codable {
+ case activity
+ case conflict
+ case diagnostic
+ case removeDomain
+ case clearActivityAndResolvedConflicts
+ }
+
+ enum Payload {
+ case activity(KDriveProviderActivityEvent)
+ case conflict(KDriveConflictEvent)
+ case diagnostic(ProviderDiagnosticEvent)
+ case removeDomain(String)
+ case clearActivityAndResolvedConflicts(String?)
+ }
+
+ let schemaVersion: Int
+ let kind: Kind
+ let activity: KDriveProviderActivityEvent?
+ let conflict: KDriveConflictEvent?
+ let diagnostic: ProviderDiagnosticEvent?
+ let domainIdentifier: String?
+
+ init(payload: Payload) {
+ self.schemaVersion = Self.schemaVersion
+ switch payload {
+ case .activity(let event):
+ self.kind = .activity
+ self.activity = event
+ self.conflict = nil
+ self.diagnostic = nil
+ self.domainIdentifier = nil
+ case .conflict(let event):
+ self.kind = .conflict
+ self.activity = nil
+ self.conflict = event
+ self.diagnostic = nil
+ self.domainIdentifier = nil
+ case .diagnostic(let event):
+ self.kind = .diagnostic
+ self.activity = nil
+ self.conflict = nil
+ self.diagnostic = event
+ self.domainIdentifier = nil
+ case .removeDomain(let domainIdentifier):
+ self.kind = .removeDomain
+ self.activity = nil
+ self.conflict = nil
+ self.diagnostic = nil
+ self.domainIdentifier = domainIdentifier
+ case .clearActivityAndResolvedConflicts(let domainIdentifier):
+ self.kind = .clearActivityAndResolvedConflicts
+ self.activity = nil
+ self.conflict = nil
+ self.diagnostic = nil
+ self.domainIdentifier = domainIdentifier
+ }
+ }
+
+ var payload: Payload {
+ get throws {
+ switch kind {
+ case .activity:
+ guard let activity else { throw ProviderDiagnosticStoreError.malformedRecord }
+ return .activity(activity)
+ case .conflict:
+ guard let conflict else { throw ProviderDiagnosticStoreError.malformedRecord }
+ return .conflict(conflict)
+ case .diagnostic:
+ guard let diagnostic else { throw ProviderDiagnosticStoreError.malformedRecord }
+ return .diagnostic(diagnostic)
+ case .removeDomain:
+ guard let domainIdentifier else { throw ProviderDiagnosticStoreError.malformedRecord }
+ return .removeDomain(domainIdentifier)
+ case .clearActivityAndResolvedConflicts:
+ return .clearActivityAndResolvedConflicts(domainIdentifier)
+ }
+ }
+ }
+}
+
+private enum LockedJSONLFile {
+ static func append(_ data: Data, to url: URL, maximumBytes: Int) throws {
+ let descriptor = Darwin.open(url.path, O_RDWR | O_APPEND | O_NOFOLLOW)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try SecurePOSIXFile.requireRegularFile(descriptor)
+ guard flock(descriptor, LOCK_EX) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ defer { flock(descriptor, LOCK_UN) }
+ try truncateInterruptedTail(descriptor)
+ let existingBytes = try SecurePOSIXFile.fileSize(descriptor)
+ guard data.count <= maximumBytes,
+ existingBytes <= maximumBytes - data.count else {
+ throw ProviderDiagnosticStoreError.runCapacityExceeded
+ }
+ try data.withUnsafeBytes { buffer in
+ guard let baseAddress = buffer.baseAddress else { return }
+ var written = 0
+ while written < buffer.count {
+ let result = Darwin.write(descriptor, baseAddress.advanced(by: written), buffer.count - written)
+ guard result > 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ written += result
+ }
+ }
+ guard fsync(descriptor) == 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ }
+
+ private static func truncateInterruptedTail(_ descriptor: Int32) throws {
+ var status = stat()
+ guard fstat(descriptor, &status) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard status.st_size > 0 else { return }
+
+ var finalByte: UInt8 = 0
+ guard pread(descriptor, &finalByte, 1, status.st_size - 1) == 1 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard finalByte != 0x0A else { return }
+
+ var scanEnd = status.st_size
+ var buffer = [UInt8](repeating: 0, count: 64 * 1_024)
+ var retainedLength: off_t = 0
+ while scanEnd > 0 {
+ let count = Int(min(off_t(buffer.count), scanEnd))
+ let offset = scanEnd - off_t(count)
+ let readCount = pread(descriptor, &buffer, count, offset)
+ guard readCount == count else {
+ if readCount < 0, errno == EINTR { continue }
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ if let newline = buffer[.. Data {
+ let descriptor = Darwin.open(url.path, O_RDONLY | O_NOFOLLOW)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try SecurePOSIXFile.requireRegularFile(descriptor)
+ guard flock(descriptor, LOCK_SH) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ defer { flock(descriptor, LOCK_UN) }
+ return try SecurePOSIXFile.read(descriptor, maximumBytes: maximumBytes)
+ }
+}
+
+enum SecurePOSIXFile {
+ enum PathKind: Equatable {
+ case missing
+ case regularFile
+ case directory
+ case other
+ }
+
+ static func pathKind(_ url: URL) -> PathKind {
+ var status = stat()
+ guard lstat(url.path, &status) == 0 else {
+ return errno == ENOENT ? .missing : .other
+ }
+ switch status.st_mode & S_IFMT {
+ case S_IFREG: return .regularFile
+ case S_IFDIR: return .directory
+ default: return .other
+ }
+ }
+
+ static func isRegularFile(_ url: URL) -> Bool {
+ pathKind(url) == .regularFile
+ }
+
+ static func ensureDirectory(_ url: URL) throws {
+ let created = pathKind(url) == .missing
+ if created {
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ }
+ guard pathKind(url) == .directory else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ guard chmod(url.path, 0o700) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ if created {
+ try synchronizeDirectory(url.deletingLastPathComponent())
+ }
+ }
+
+ static func createDirectoryExclusively(_ url: URL) throws {
+ guard mkdir(url.path, 0o700) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard pathKind(url) == .directory else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ try synchronizeDirectory(url.deletingLastPathComponent())
+ }
+
+ static func createExclusively(_ data: Data, at url: URL, permissions: mode_t) throws {
+ let descriptor = Darwin.open(url.path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, permissions)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try requireRegularFile(descriptor)
+ guard fchmod(descriptor, permissions) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ try write(data, to: descriptor)
+ guard fsync(descriptor) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ try synchronizeDirectory(url.deletingLastPathComponent())
+ }
+
+ static func replaceAtomically(_ data: Data, at url: URL, permissions: mode_t) throws {
+ let temporaryURL = url.deletingLastPathComponent()
+ .appendingPathComponent(".\(url.lastPathComponent).\(UUID().uuidString.lowercased()).tmp")
+ do {
+ try createExclusively(data, at: temporaryURL, permissions: permissions)
+ guard rename(temporaryURL.path, url.path) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ try synchronizeDirectory(url.deletingLastPathComponent())
+ } catch {
+ if pathKind(temporaryURL) == .regularFile {
+ _ = unlink(temporaryURL.path)
+ }
+ throw error
+ }
+ }
+
+ static func withLock(at url: URL, operation: Int32, _ body: () throws -> T) throws -> T {
+ let descriptor = Darwin.open(url.path, O_RDWR | O_CREAT | O_NOFOLLOW, 0o600)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try requireRegularFile(descriptor)
+ guard fchmod(descriptor, 0o600) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard flock(descriptor, operation) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ defer { flock(descriptor, LOCK_UN) }
+ return try body()
+ }
+
+ static func withAsyncLock(
+ at url: URL,
+ operation: Int32,
+ _ body: @Sendable () async throws -> T
+ ) async throws -> T {
+ let descriptor = Darwin.open(url.path, O_RDWR | O_CREAT | O_NOFOLLOW, 0o600)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try requireRegularFile(descriptor)
+ guard fchmod(descriptor, 0o600) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard flock(descriptor, operation) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ defer { flock(descriptor, LOCK_UN) }
+ return try await body()
+ }
+
+ static func read(_ url: URL, maximumBytes: Int) throws -> Data {
+ let descriptor = Darwin.open(url.path, O_RDONLY | O_NOFOLLOW)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ try requireRegularFile(descriptor)
+ return try read(descriptor, maximumBytes: maximumBytes)
+ }
+
+ static func read(_ descriptor: Int32, maximumBytes: Int) throws -> Data {
+ var status = stat()
+ guard fstat(descriptor, &status) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard status.st_size >= 0, status.st_size <= off_t(max(0, maximumBytes)) else {
+ throw ProviderDiagnosticStoreError.fileTooLarge
+ }
+ var data = Data()
+ data.reserveCapacity(Int(status.st_size))
+ var buffer = [UInt8](repeating: 0, count: 64 * 1_024)
+ while true {
+ let count = Darwin.read(descriptor, &buffer, buffer.count)
+ if count == 0 { break }
+ if count < 0 {
+ if errno == EINTR { continue }
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard data.count <= maximumBytes - count else {
+ throw ProviderDiagnosticStoreError.fileTooLarge
+ }
+ data.append(contentsOf: buffer.prefix(count))
+ }
+ return data
+ }
+
+ static func requireRegularFile(_ descriptor: Int32) throws {
+ var status = stat()
+ guard fstat(descriptor, &status) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard status.st_mode & S_IFMT == S_IFREG else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ }
+
+ static func fileSize(_ url: URL) throws -> Int {
+ var status = stat()
+ guard lstat(url.path, &status) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard status.st_mode & S_IFMT == S_IFREG, status.st_size >= 0 else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ return Int(status.st_size)
+ }
+
+ static func fileSize(_ descriptor: Int32) throws -> Int {
+ var status = stat()
+ guard fstat(descriptor, &status) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ guard status.st_mode & S_IFMT == S_IFREG, status.st_size >= 0 else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ return Int(status.st_size)
+ }
+
+ static func removeRegularFile(_ url: URL) throws {
+ guard pathKind(url) == .regularFile else {
+ throw ProviderDiagnosticStoreError.unsafeFileType
+ }
+ guard unlink(url.path) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ try synchronizeDirectory(url.deletingLastPathComponent())
+ }
+
+ static func synchronizeDirectory(_ url: URL) throws {
+ let descriptor = Darwin.open(url.path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW)
+ guard descriptor >= 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ defer { Darwin.close(descriptor) }
+ guard fsync(descriptor) == 0 else {
+ throw ProviderDiagnosticStoreError.fileSystemError(errno)
+ }
+ }
+
+ private static func write(_ data: Data, to descriptor: Int32) throws {
+ try data.withUnsafeBytes { buffer in
+ guard let baseAddress = buffer.baseAddress else { return }
+ var written = 0
+ while written < buffer.count {
+ let result = Darwin.write(descriptor, baseAddress.advanced(by: written), buffer.count - written)
+ if result < 0, errno == EINTR { continue }
+ guard result > 0 else { throw ProviderDiagnosticStoreError.fileSystemError(errno) }
+ written += result
+ }
+ }
+ }
+}
+
+public enum ProviderEventStoreFactory {
+ public static func activeFinderStepCorrelation(
+ appGroupIdentifier: String = ProviderConstants.appGroupIdentifier
+ ) throws -> UUID? {
+ guard let containerURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: appGroupIdentifier
+ ) else {
+ throw ProviderDiagnosticStoreError.missingAppGroupContainer(appGroupIdentifier)
+ }
+ return try StabilityRunLocator.activeFinderStepCorrelation(
+ rootDirectoryURL: containerURL.appendingPathComponent("StabilityRuns", isDirectory: true)
+ )
+ }
+
+ public static func makeDefault(
+ profile: ProviderRuntimeProfile = .current,
+ appGroupIdentifier: String = ProviderConstants.appGroupIdentifier
+ ) throws -> (any KDriveProviderEventStoring)? {
+ switch profile {
+ case .standard:
+ return try KDriveProviderEventSQLiteStore(appGroupIdentifier: appGroupIdentifier)
+ case .stability:
+ guard let containerURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: appGroupIdentifier
+ ) else {
+ throw ProviderDiagnosticStoreError.missingAppGroupContainer(appGroupIdentifier)
+ }
+ let root = containerURL.appendingPathComponent("StabilityRuns", isDirectory: true)
+ guard let run = try StabilityRunLocator.activeRun(rootDirectoryURL: root) else { return nil }
+ return try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+ }
+ }
+
+ public static func make(
+ profile: ProviderRuntimeProfile,
+ standardDatabaseURL: URL,
+ stabilityRootDirectoryURL: URL
+ ) throws -> (any KDriveProviderEventStoring)? {
+ switch profile {
+ case .standard:
+ return try KDriveProviderEventSQLiteStore(databaseURL: standardDatabaseURL)
+ case .stability:
+ guard let run = try StabilityRunLocator.activeRun(rootDirectoryURL: stabilityRootDirectoryURL) else {
+ return nil
+ }
+ return try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+ }
+ }
+}
+
+public enum ProviderDiagnosticStoreError: Error, Equatable, LocalizedError, Sendable {
+ case missingAppGroupContainer(String)
+ case couldNotCreateEventFile
+ case unsafeFileType
+ case fileTooLarge
+ case runCapacityExceeded
+ case corruptRecord(line: Int)
+ case unsupportedSchemaVersion(Int)
+ case malformedRecord
+ case invalidActiveRunPointer
+ case runNotFound(UUID)
+ case runAlreadyFinished(UUID)
+ case runAlreadyActive
+ case runOwnedByFinderRunner(UUID)
+ case runAbandonedByFinderRunner(UUID)
+ case finderOwnerProcessStillRunning
+ case noStaleFinderRun
+ case finderEvidenceAlreadyFinalized(UUID)
+ case finderSummaryMismatch
+ case finderStepAlreadyActive
+ case finderStepCorrelationMismatch
+ case fileSystemError(Int32)
+
+ public var errorDescription: String? {
+ switch self {
+ case .missingAppGroupContainer(let identifier):
+ "The shared app-group container '\(identifier)' is unavailable."
+ case .couldNotCreateEventFile:
+ "The Stability event file could not be created."
+ case .unsafeFileType:
+ "A Stability run path is not a regular file or directory."
+ case .fileTooLarge:
+ "A Stability run file exceeds the supported size limit."
+ case .runCapacityExceeded:
+ "The active Stability event file reached its configured capacity. Finish the run before recording more evidence."
+ case .corruptRecord(let line):
+ "The Stability event stream is corrupt at complete line \(line)."
+ case .unsupportedSchemaVersion(let version):
+ "The Stability event stream uses unsupported schema version \(version)."
+ case .malformedRecord:
+ "The Stability event stream contains a malformed record."
+ case .invalidActiveRunPointer:
+ "The active Stability run pointer is stale or invalid."
+ case .runNotFound:
+ "The Stability run does not exist."
+ case .runAlreadyFinished:
+ "The Stability run is already complete."
+ case .runAlreadyActive:
+ "A Stability run is active."
+ case .runOwnedByFinderRunner:
+ "The active Stability run is owned by the Finder runner."
+ case .runAbandonedByFinderRunner:
+ "The Stability run was abandoned by the Finder runner and cannot be finalized."
+ case .finderOwnerProcessStillRunning:
+ "The Stability Finder run owner process is still running."
+ case .noStaleFinderRun:
+ "There is no stale Stability Finder run to recover."
+ case .finderEvidenceAlreadyFinalized:
+ "The Stability Finder evidence is already finalized."
+ case .finderSummaryMismatch:
+ "The Stability Finder summary does not match its immutable report."
+ case .finderStepAlreadyActive:
+ "A Stability Finder step is already active."
+ case .finderStepCorrelationMismatch:
+ "The active Stability Finder step correlation does not match."
+ case .fileSystemError(let code):
+ "The Stability event file operation failed with errno \(code)."
+ }
+ }
+}
diff --git a/PotassiumProviderCore/StabilityExtensionLaunchEvidence.swift b/PotassiumProviderCore/StabilityExtensionLaunchEvidence.swift
new file mode 100644
index 0000000..a38055d
--- /dev/null
+++ b/PotassiumProviderCore/StabilityExtensionLaunchEvidence.swift
@@ -0,0 +1,113 @@
+#if STABILITY
+import Foundation
+
+public enum StabilityExtensionLaunchMode: String, Codable, Sendable {
+ case fresh
+ case running
+}
+
+/// Declares the lifecycle requirement for the original sixteen-scenario runner.
+/// Conflict selections carry the same requirement in their profile manifest.
+public struct StabilityExtensionLaunchRequest: Codable, Equatable, Sendable {
+ public let schemaVersion: UInt16
+ public let runID: UUID
+ public let mode: StabilityExtensionLaunchMode
+ public init(runID: UUID, mode: StabilityExtensionLaunchMode) { schemaVersion = 1; self.runID = runID; self.mode = mode }
+
+ public func validate(evidence: StabilityExtensionLaunchEvidence?, report: StabilityFinderRunReport,
+ diagnostics: [ProviderDiagnosticEvent]) throws {
+ guard schemaVersion == 1 else { throw StabilityLiveEvidenceError.wrongExtensionBuild }
+ guard report.stepSummary.passed > 0 else { return }
+ guard let evidence, evidence.mode == mode else { throw StabilityLiveEvidenceError.wrongExtensionBuild }
+ try evidence.validate(runID: runID, report: report, diagnostics: diagnostics)
+ }
+}
+
+/// Kernel birth time, signed build and diagnostic process identity jointly
+/// distinguish a new extension process from one that remained alive throughout.
+/// Replicated-provider objects may be recreated inside that same process.
+public struct StabilityExtensionLaunchEvidence: Codable, Equatable, Sendable {
+ public let schemaVersion: UInt16
+ public let runID: UUID
+ public let mode: StabilityExtensionLaunchMode
+ public let recordingStartedAtMicroseconds: Int64
+ public let preparedAtMicroseconds: Int64
+ public let processStartedAtMicroseconds: Int64
+ public var recordingStartedAt: Date { Date(timeIntervalSince1970: Double(recordingStartedAtMicroseconds) / 1_000_000) }
+ public var preparedAt: Date { Date(timeIntervalSince1970: Double(preparedAtMicroseconds) / 1_000_000) }
+ public var processStartedAt: Date { Date(timeIntervalSince1970: Double(processStartedAtMicroseconds) / 1_000_000) }
+ public let processInstanceID: UUID
+ public let expectedCodeHash: String
+
+ public init(runID: UUID, mode: StabilityExtensionLaunchMode, recordingStartedAt: Date, preparedAt: Date,
+ processStartedAt: Date, processInstanceID: UUID, expectedCodeHash: String) {
+ schemaVersion = 2; self.runID = runID; self.mode = mode
+ recordingStartedAtMicroseconds = Int64(recordingStartedAt.timeIntervalSince1970 * 1_000_000)
+ preparedAtMicroseconds = Int64(preparedAt.timeIntervalSince1970 * 1_000_000)
+ processStartedAtMicroseconds = Int64(processStartedAt.timeIntervalSince1970 * 1_000_000)
+ self.processInstanceID = processInstanceID; self.expectedCodeHash = expectedCodeHash
+ }
+
+ public func validate(runID: UUID, report: StabilityFinderRunReport,
+ diagnostics: [ProviderDiagnosticEvent]) throws {
+ guard [1, 2].contains(schemaVersion), self.runID == runID, !expectedCodeHash.isEmpty,
+ floor(recordingStartedAt.timeIntervalSince1970) == floor(report.startedAt.timeIntervalSince1970),
+ preparedAt >= recordingStartedAt, preparedAt < report.finishedAt else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ let observed = diagnostics.filter {
+ $0.source == .fileProviderExtension &&
+ (schemaVersion == 2 && mode == .running || $0.occurredAt >= preparedAt)
+ }
+ guard !observed.isEmpty, observed.allSatisfy({ $0.processInstanceID == processInstanceID &&
+ $0.processCodeHash == expectedCodeHash }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ if schemaVersion == 1 {
+ // Historical proofs keep their original, stricter object-lifetime
+ // interpretation. A rejected old bundle is never upgraded in place.
+ guard !observed.contains(where: { $0.operation == .runtimeInvalidate }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ } else {
+ try validateReplicatedInstanceEvents(observed)
+ }
+ switch mode {
+ case .fresh:
+ guard processStartedAt >= preparedAt,
+ let initialized = observed.filter({ $0.operation == .runtimeInitialize && $0.phase == .completed }).min(by: { $0.occurredAt < $1.occurredAt }),
+ let mutation = observed.filter({ $0.operation == .modifyItem && $0.phase == .started }).min(by: { $0.occurredAt < $1.occurredAt }),
+ initialized.occurredAt <= mutation.occurredAt else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ case .running:
+ guard processStartedAt < recordingStartedAt,
+ (schemaVersion == 2 || !diagnostics.contains(where: {
+ $0.source == .fileProviderExtension && $0.operation == .runtimeInitialize
+ })),
+ diagnostics.contains(where: { $0.source == .fileProviderExtension && $0.occurredAt < preparedAt &&
+ $0.phase == .completed && $0.processInstanceID == processInstanceID && $0.processCodeHash == expectedCodeHash }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ }
+ }
+
+ private func validateReplicatedInstanceEvents(_ events: [ProviderDiagnosticEvent]) throws {
+ let lifecycle = events.filter { [.runtimeInitialize, .runtimeInvalidate].contains($0.operation) }
+ guard lifecycle.allSatisfy({ $0.spanID != nil }) else { throw StabilityLiveEvidenceError.pendingOperations }
+ let spans = Dictionary(grouping: lifecycle, by: { $0.spanID! })
+ for span in spans.values {
+ guard !span.contains(where: { [.failed, .cancelled].contains($0.phase) }) else {
+ throw StabilityLiveEvidenceError.unexpectedFailure
+ }
+ let starts = span.filter { $0.phase == .started }
+ let ends = span.filter { $0.phase == .completed }
+ guard starts.count == 1, ends.count == 1,
+ starts[0].operation == ends[0].operation,
+ starts[0].occurredAt <= ends[0].occurredAt else {
+ throw StabilityLiveEvidenceError.pendingOperations
+ }
+ }
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityFinderEvidence.swift b/PotassiumProviderCore/StabilityFinderEvidence.swift
new file mode 100644
index 0000000..9eb97ba
--- /dev/null
+++ b/PotassiumProviderCore/StabilityFinderEvidence.swift
@@ -0,0 +1,150 @@
+import Foundation
+
+public enum StabilityFinderAPIObservationPhase: String, Codable, Equatable, Hashable, Sendable {
+ case baseline
+ case postcondition
+}
+
+public enum StabilityFinderAPIObservationOutcome: String, Codable, Equatable, Sendable {
+ case passed
+ case failed
+ case checkpoint
+ case notEvaluated
+}
+
+/// Closed, value-only server evidence. It deliberately has no field capable of
+/// retaining an item name, path, URL, account identity, remote identifier, or
+/// response body.
+public struct StabilityFinderAPIObservation: Codable, Equatable, Sendable {
+ public static let schemaVersion: UInt16 = 1
+
+ public let schemaVersion: UInt16
+ public let scenario: StabilityFinderScenario
+ public let correlationID: UUID
+ public let phase: StabilityFinderAPIObservationPhase
+ public let outcome: StabilityFinderAPIObservationOutcome
+ public let recordedAt: Date
+ public let hasMore: Bool?
+ public let itemCountBucket: UInt16?
+
+ public init(
+ scenario: StabilityFinderScenario,
+ correlationID: UUID,
+ phase: StabilityFinderAPIObservationPhase,
+ outcome: StabilityFinderAPIObservationOutcome,
+ recordedAt: Date = Date(),
+ hasMore: Bool? = nil,
+ itemCount: Int? = nil
+ ) {
+ self.schemaVersion = Self.schemaVersion
+ self.scenario = scenario
+ self.correlationID = correlationID
+ self.phase = phase
+ self.outcome = outcome
+ self.recordedAt = recordedAt
+ self.hasMore = hasMore
+ self.itemCountBucket = itemCount.map(Self.bucket)
+ }
+
+ private static func bucket(_ count: Int) -> UInt16 {
+ switch max(0, count) {
+ case 0: 0
+ case 1: 1
+ case 2...5: 5
+ case 6...10: 10
+ case 11...25: 25
+ case 26...50: 50
+ case 51...100: 100
+ default: 101
+ }
+ }
+}
+
+public enum StabilityFinderEvidenceValidationError: Error, Equatable, Sendable {
+ case duplicateObservation(
+ scenario: StabilityFinderScenario,
+ phase: StabilityFinderAPIObservationPhase
+ )
+ case observationCorrelationMismatch(StabilityFinderScenario)
+ case missingPassedObservation(
+ scenario: StabilityFinderScenario,
+ phase: StabilityFinderAPIObservationPhase
+ )
+ case invalidPassedObservation(StabilityFinderScenario)
+ case missingDiagnosticEvidence(StabilityFinderScenario)
+}
+
+/// A closed requirement used when a Finder step is sealed. A passed step must
+/// have one successful terminal diagnostic for every operation group; the
+/// operations inside a group are alternatives. Synthetic report observations
+/// alone are never sufficient.
+public struct StabilityFinderDiagnosticRequirement: Equatable, Sendable {
+ public let source: ProviderDiagnosticSource
+ /// Every group must have one matching completed event. Operations within
+ /// a group are alternatives, while separate groups are conjunctive.
+ public let operationGroups: [Set]
+
+ public init(
+ source: ProviderDiagnosticSource,
+ operations: Set
+ ) {
+ self.source = source
+ self.operationGroups = [operations]
+ }
+
+ public init(
+ source: ProviderDiagnosticSource,
+ operationGroups: [Set]
+ ) {
+ self.source = source
+ self.operationGroups = operationGroups
+ }
+}
+
+public extension StabilityFinderScenario {
+ var diagnosticRequirement: StabilityFinderDiagnosticRequirement? {
+ switch self {
+ case .enumerationAndChangeAnchors:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operationGroups: [
+ [.enumerateItems],
+ [.enumerateChanges, .currentSyncAnchor],
+ ]
+ )
+ case .hydrate, .download:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operations: [.fetchContents]
+ )
+ case .evict:
+ StabilityFinderDiagnosticRequirement(
+ source: .finderRunner,
+ operations: [.itemLookup]
+ )
+ case .fileCreate, .directoryCreate:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operations: [.createItem]
+ )
+ case .editAndUpload, .rename, .move, .concurrentRemotePreserveBoth:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operations: [.modifyItem]
+ )
+ case .trash:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operations: [.modifyItem]
+ )
+ case .workingSetRefresh:
+ StabilityFinderDiagnosticRequirement(
+ source: .fileProviderExtension,
+ operations: [.workingSetRefresh, .enumerateItems, .enumerateChanges]
+ )
+ case .restore, .permanentDeletion, .cancellationAndProgress,
+ .supportedContextualActions:
+ nil
+ }
+ }
+}
diff --git a/PotassiumProviderCore/StabilityFinderEvidenceRejection.swift b/PotassiumProviderCore/StabilityFinderEvidenceRejection.swift
new file mode 100644
index 0000000..c1d06af
--- /dev/null
+++ b/PotassiumProviderCore/StabilityFinderEvidenceRejection.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+/// Diagnostic candidate only. This file is never an acceptance commit marker.
+public struct StabilityFinderEvidenceRejection: Codable, Sendable {
+ public let schemaVersion: Int
+ public let eligibleForAcceptance: Bool
+ public let evidenceError: StabilityLiveEvidenceError?
+ public let errorClass: ProviderDiagnosticErrorClass
+ public let report: StabilityFinderRunReport
+ public let observations: [StabilityFinderAPIObservation]
+
+ public init(report: StabilityFinderRunReport, observations: [StabilityFinderAPIObservation], error: Error) {
+ schemaVersion = 1
+ eligibleForAcceptance = false
+ evidenceError = error as? StabilityLiveEvidenceError
+ errorClass = ProviderDiagnosticErrorClassifier.classify(error)
+ self.report = report
+ self.observations = observations
+ }
+}
diff --git a/PotassiumProviderCore/StabilityFinderRunModel.swift b/PotassiumProviderCore/StabilityFinderRunModel.swift
new file mode 100644
index 0000000..898c9c6
--- /dev/null
+++ b/PotassiumProviderCore/StabilityFinderRunModel.swift
@@ -0,0 +1,626 @@
+import Foundation
+
+/// The fixed scenario order used by the macOS Finder stability runner.
+///
+/// Raw values are durable report vocabulary. They intentionally describe only
+/// operation classes and never carry item names, paths, URLs, or identifiers.
+public enum StabilityFinderScenario: String, CaseIterable, Codable, Equatable, Hashable, Sendable {
+ case enumerationAndChangeAnchors
+ case hydrate
+ case evict
+ case download
+ case fileCreate
+ case directoryCreate
+ case editAndUpload
+ case rename
+ case move
+ case trash
+ case restore
+ case permanentDeletion
+ case concurrentRemotePreserveBoth
+ case cancellationAndProgress
+ case workingSetRefresh
+ case supportedContextualActions
+}
+
+public enum StabilityFinderPreflightCheck: String, CaseIterable, Codable, Equatable, Hashable, Sendable {
+ case accessibilityPermission
+ case finderAutomationPermission
+ case fileProviderRegistration
+ case fileProviderConsent
+ case stabilityLabSafety
+ case screenRecordingPermission
+}
+
+/// A pause that needs operator or OS-owned UI action, not a product failure.
+public enum StabilityFinderCheckpointReason: String, Codable, Equatable, Hashable, Sendable {
+ case accessibilityConsentRequired
+ case screenRecordingConsentRequired
+ case finderAutomationConsentRequired
+ case fileProviderConsentRequired
+ case variableContextualUI
+ case scopedRestoreWorkflowRequired
+ case scopedPermanentDeletionWorkflowRequired
+ case finderCancellationRequired
+}
+
+public enum StabilityFinderPreflightFailure: String, Codable, Equatable, Hashable, Sendable {
+ case permissionStateUnavailable
+ case finderUnavailable
+ case fileProviderNotRegistered
+ case stabilityLabRejected
+}
+
+public enum StabilityFinderPreflightSkipReason: String, Codable, Equatable, Hashable, Sendable {
+ case blockedByEarlierPreflight
+}
+
+public enum StabilityFinderPreflightOutcome: Codable, Equatable, Sendable {
+ case passed
+ case failed(StabilityFinderPreflightFailure)
+ case checkpoint(StabilityFinderCheckpointReason)
+ case skipped(StabilityFinderPreflightSkipReason)
+}
+
+public struct StabilityFinderPreflightResult: Codable, Equatable, Sendable {
+ public let check: StabilityFinderPreflightCheck
+ public let outcome: StabilityFinderPreflightOutcome
+ public let recordedAt: Date
+
+ public init(
+ check: StabilityFinderPreflightCheck,
+ outcome: StabilityFinderPreflightOutcome,
+ recordedAt: Date
+ ) {
+ self.check = check
+ self.outcome = outcome
+ self.recordedAt = recordedAt
+ }
+}
+
+public enum StabilityFinderAssertionClass: String, CaseIterable, Codable, Equatable, Hashable, Sendable {
+ case finderVisible
+ case serverAuthoritative
+}
+
+public enum StabilityFinderAssertionFailure: String, Codable, Equatable, Hashable, Sendable {
+ case stateMismatch
+ case stateUnavailable
+ case timedOut
+ case diagnosticCorrelationMissing
+}
+
+public enum StabilityFinderAssertionNotEvaluatedReason: String, Codable, Equatable, Hashable, Sendable {
+ case operationDidNotReachAssertion
+ case checkpointReached
+ case stepSkipped
+}
+
+public enum StabilityFinderAssertionOutcome: Codable, Equatable, Sendable {
+ case passed
+ case failed(StabilityFinderAssertionFailure)
+ case notEvaluated(StabilityFinderAssertionNotEvaluatedReason)
+}
+
+public struct StabilityFinderAssertionResult: Codable, Equatable, Sendable {
+ public let assertionClass: StabilityFinderAssertionClass
+ public let outcome: StabilityFinderAssertionOutcome
+
+ public init(
+ assertionClass: StabilityFinderAssertionClass,
+ outcome: StabilityFinderAssertionOutcome
+ ) {
+ self.assertionClass = assertionClass
+ self.outcome = outcome
+ }
+}
+
+public enum StabilityFinderStepFailure: String, Codable, Equatable, Hashable, Sendable {
+ case operationFailed
+ case timedOut
+ case cancellationContractViolated
+ case progressContractViolated
+ case assertionFailed
+}
+
+public enum StabilityFinderStepSkipReason: String, Codable, Equatable, Hashable, Sendable {
+ case preflightFailure
+ case preflightCheckpoint
+ case earlierStepFailure
+ case earlierStepCheckpoint
+ case notSelectedForConflictProfile
+ case permanentDeletionNotSelected
+}
+
+/// Exactly one case is persisted for every planned step.
+public enum StabilityFinderStepOutcome: Codable, Equatable, Sendable {
+ case passed
+ case failed(StabilityFinderStepFailure)
+ case checkpoint(StabilityFinderCheckpointReason)
+ case skipped(StabilityFinderStepSkipReason)
+}
+
+public struct StabilityFinderStepResult: Codable, Equatable, Sendable {
+ public let sequenceNumber: UInt16
+ public let scenario: StabilityFinderScenario
+ public let correlationID: UUID
+ public let startedAt: Date
+ public let finishedAt: Date
+ public let durationMilliseconds: UInt64
+ public let outcome: StabilityFinderStepOutcome
+ public let assertions: [StabilityFinderAssertionResult]
+ public let liveEvidence: StabilityLiveStepEvidence?
+
+ public init(
+ sequenceNumber: UInt16,
+ scenario: StabilityFinderScenario,
+ correlationID: UUID,
+ startedAt: Date,
+ finishedAt: Date,
+ outcome: StabilityFinderStepOutcome,
+ assertions: [StabilityFinderAssertionResult],
+ liveEvidence: StabilityLiveStepEvidence? = nil
+ ) {
+ self.sequenceNumber = sequenceNumber
+ self.scenario = scenario
+ self.correlationID = correlationID
+ self.startedAt = startedAt
+ self.finishedAt = finishedAt
+ self.durationMilliseconds = Self.milliseconds(from: startedAt, to: finishedAt)
+ self.outcome = outcome
+ self.assertions = assertions
+ self.liveEvidence = liveEvidence
+ }
+
+ private static func milliseconds(from startedAt: Date, to finishedAt: Date) -> UInt64 {
+ guard finishedAt >= startedAt else {
+ return 0
+ }
+ return UInt64((finishedAt.timeIntervalSince(startedAt) * 1_000).rounded())
+ }
+}
+
+/// Immutable aggregate counts. `total` is derived so it cannot drift from the
+/// four terminal buckets.
+public struct StabilityFinderOutcomeSummary: Codable, Equatable, Sendable {
+ public let passed: Int
+ public let failed: Int
+ public let checkpointed: Int
+ public let skipped: Int
+
+ public var total: Int {
+ passed + failed + checkpointed + skipped
+ }
+
+ fileprivate init(passed: Int, failed: Int, checkpointed: Int, skipped: Int) {
+ self.passed = passed
+ self.failed = failed
+ self.checkpointed = checkpointed
+ self.skipped = skipped
+ }
+}
+
+public enum StabilityFinderRunValidationError: Error, Codable, Equatable, Sendable {
+ case unsupportedSchemaVersion(UInt16)
+ case invalidRunInterval
+ case invalidRunDuration
+ case summaryMismatch
+ case duplicatePreflightResult(StabilityFinderPreflightCheck)
+ case missingPreflightResult(StabilityFinderPreflightCheck)
+ case preflightOutOfSequence(
+ expected: StabilityFinderPreflightCheck,
+ actual: StabilityFinderPreflightCheck
+ )
+ case invalidPreflightOutcome(check: StabilityFinderPreflightCheck)
+ case preflightTimestampOutOfBounds(StabilityFinderPreflightCheck)
+ case duplicateStepResult(StabilityFinderScenario)
+ case missingStepResult(StabilityFinderScenario)
+ case stepOutOfSequence(expected: StabilityFinderScenario, actual: StabilityFinderScenario)
+ case invalidSequenceNumber(scenario: StabilityFinderScenario, expected: UInt16, actual: UInt16)
+ case duplicateCorrelationID(UUID)
+ case invalidStepInterval(StabilityFinderScenario)
+ case invalidStepDuration(StabilityFinderScenario)
+ case stepTimestampOutOfBounds(StabilityFinderScenario)
+ case stepTimestampOutOfSequence(
+ previous: StabilityFinderScenario,
+ current: StabilityFinderScenario
+ )
+ case duplicateAssertion(
+ scenario: StabilityFinderScenario,
+ assertionClass: StabilityFinderAssertionClass
+ )
+ case missingAssertion(
+ scenario: StabilityFinderScenario,
+ assertionClass: StabilityFinderAssertionClass
+ )
+ case assertionOutcomeMismatch(StabilityFinderScenario)
+ case invalidStepDeferral(StabilityFinderScenario)
+ case invalidStepCheckpoint(
+ scenario: StabilityFinderScenario,
+ reason: StabilityFinderCheckpointReason
+ )
+}
+
+/// A validated, terminal report. Initialization succeeds only when every
+/// required preflight and scenario has one ordered result and every scenario
+/// has both Finder-visible and server-authoritative assertion records.
+public struct StabilityFinderRunReport: Codable, Equatable, Sendable {
+ public static let currentSchemaVersion: UInt16 = 1
+ public static let liveSchemaVersion: UInt16 = 2
+ public static let selectiveSchemaVersion: UInt16 = 3
+
+ /// A completed selection, never full sixteen-scenario acceptance. Evidence
+ /// must still be validated and sealed before publishing this status.
+ public var hasOnlyDeferredPermanentDeletion: Bool {
+ preflightResults.allSatisfy { $0.outcome == .passed } &&
+ stepResults.allSatisfy {
+ $0.outcome == ($0.scenario == .permanentDeletion ? .skipped(.permanentDeletionNotSelected) : .passed)
+ }
+ }
+
+ public let schemaVersion: UInt16
+ public let correlationID: UUID
+ public let startedAt: Date
+ public let finishedAt: Date
+ public let durationMilliseconds: UInt64
+ public let preflightResults: [StabilityFinderPreflightResult]
+ public let stepResults: [StabilityFinderStepResult]
+ public let preflightSummary: StabilityFinderOutcomeSummary
+ public let stepSummary: StabilityFinderOutcomeSummary
+
+ public init(
+ schemaVersion: UInt16 = Self.currentSchemaVersion,
+ correlationID: UUID,
+ startedAt: Date,
+ finishedAt: Date,
+ preflightResults: [StabilityFinderPreflightResult],
+ stepResults: [StabilityFinderStepResult]
+ ) throws {
+ guard [Self.currentSchemaVersion, Self.liveSchemaVersion, Self.selectiveSchemaVersion].contains(schemaVersion) else {
+ throw StabilityFinderRunValidationError.unsupportedSchemaVersion(schemaVersion)
+ }
+ guard finishedAt >= startedAt else {
+ throw StabilityFinderRunValidationError.invalidRunInterval
+ }
+
+ try Self.validatePreflight(
+ preflightResults,
+ schemaVersion: schemaVersion,
+ runStartedAt: startedAt,
+ runFinishedAt: finishedAt
+ )
+ try Self.validateSteps(
+ stepResults,
+ schemaVersion: schemaVersion,
+ runStartedAt: startedAt,
+ runFinishedAt: finishedAt
+ )
+
+ self.schemaVersion = schemaVersion
+ self.correlationID = correlationID
+ self.startedAt = startedAt
+ self.finishedAt = finishedAt
+ self.durationMilliseconds = Self.milliseconds(from: startedAt, to: finishedAt)
+ self.preflightResults = preflightResults
+ self.stepResults = stepResults
+ self.preflightSummary = Self.summary(for: preflightResults)
+ self.stepSummary = Self.summary(for: stepResults)
+ }
+
+ private enum CodingKeys: String, CodingKey, Codable, Equatable, Sendable {
+ case schemaVersion
+ case correlationID
+ case startedAt
+ case finishedAt
+ case durationMilliseconds
+ case preflightResults
+ case stepResults
+ case preflightSummary
+ case stepSummary
+ }
+
+ public init(from decoder: any Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ let schemaVersion = try container.decode(UInt16.self, forKey: .schemaVersion)
+ let correlationID = try container.decode(UUID.self, forKey: .correlationID)
+ let startedAt = try container.decode(Date.self, forKey: .startedAt)
+ let finishedAt = try container.decode(Date.self, forKey: .finishedAt)
+ let encodedDuration = try container.decode(UInt64.self, forKey: .durationMilliseconds)
+ let preflightResults = try container.decode(
+ [StabilityFinderPreflightResult].self,
+ forKey: .preflightResults
+ )
+ let stepResults = try container.decode(
+ [StabilityFinderStepResult].self,
+ forKey: .stepResults
+ )
+ let encodedPreflightSummary = try container.decode(
+ StabilityFinderOutcomeSummary.self,
+ forKey: .preflightSummary
+ )
+ let encodedStepSummary = try container.decode(
+ StabilityFinderOutcomeSummary.self,
+ forKey: .stepSummary
+ )
+
+ guard [Self.currentSchemaVersion, Self.liveSchemaVersion, Self.selectiveSchemaVersion].contains(schemaVersion) else {
+ throw StabilityFinderRunValidationError.unsupportedSchemaVersion(schemaVersion)
+ }
+ guard finishedAt >= startedAt else {
+ throw StabilityFinderRunValidationError.invalidRunInterval
+ }
+ try Self.validatePreflight(
+ preflightResults,
+ schemaVersion: schemaVersion,
+ runStartedAt: startedAt,
+ runFinishedAt: finishedAt
+ )
+ try Self.validateSteps(
+ stepResults,
+ schemaVersion: schemaVersion,
+ runStartedAt: startedAt,
+ runFinishedAt: finishedAt
+ )
+ let decodedDuration = Self.milliseconds(from: startedAt, to: finishedAt)
+ guard Self.duration(encodedDuration, isPlausibleFor: decodedDuration) else {
+ throw StabilityFinderRunValidationError.invalidRunDuration
+ }
+ let preflightSummary = Self.summary(for: preflightResults)
+ let stepSummary = Self.summary(for: stepResults)
+ guard encodedPreflightSummary == preflightSummary,
+ encodedStepSummary == stepSummary else {
+ throw StabilityFinderRunValidationError.summaryMismatch
+ }
+
+ self.schemaVersion = schemaVersion
+ self.correlationID = correlationID
+ self.startedAt = startedAt
+ self.finishedAt = finishedAt
+ self.durationMilliseconds = encodedDuration
+ self.preflightResults = preflightResults
+ self.stepResults = stepResults
+ self.preflightSummary = preflightSummary
+ self.stepSummary = stepSummary
+ }
+
+ public func encode(to encoder: any Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+ try container.encode(schemaVersion, forKey: .schemaVersion)
+ try container.encode(correlationID, forKey: .correlationID)
+ try container.encode(startedAt, forKey: .startedAt)
+ try container.encode(finishedAt, forKey: .finishedAt)
+ try container.encode(durationMilliseconds, forKey: .durationMilliseconds)
+ try container.encode(preflightResults, forKey: .preflightResults)
+ try container.encode(stepResults, forKey: .stepResults)
+ try container.encode(preflightSummary, forKey: .preflightSummary)
+ try container.encode(stepSummary, forKey: .stepSummary)
+ }
+
+ private static func validatePreflight(
+ _ results: [StabilityFinderPreflightResult],
+ schemaVersion: UInt16,
+ runStartedAt: Date,
+ runFinishedAt: Date
+ ) throws {
+ var seen = Set()
+ for result in results where !seen.insert(result.check).inserted {
+ throw StabilityFinderRunValidationError.duplicatePreflightResult(result.check)
+ }
+ let required = StabilityFinderPreflightCheck.allCases.filter { schemaVersion >= Self.liveSchemaVersion || seen.contains(.screenRecordingPermission) || $0 != .screenRecordingPermission }
+ for check in required where !seen.contains(check) {
+ throw StabilityFinderRunValidationError.missingPreflightResult(check)
+ }
+ for (expected, actual) in zip(required, results) {
+ guard expected == actual.check else {
+ throw StabilityFinderRunValidationError.preflightOutOfSequence(
+ expected: expected,
+ actual: actual.check
+ )
+ }
+ guard actual.recordedAt >= runStartedAt,
+ actual.recordedAt <= runFinishedAt else {
+ throw StabilityFinderRunValidationError.preflightTimestampOutOfBounds(
+ actual.check
+ )
+ }
+ try validatePreflightOutcome(actual.outcome, for: actual.check)
+ }
+ }
+
+ private static func validatePreflightOutcome(
+ _ outcome: StabilityFinderPreflightOutcome,
+ for check: StabilityFinderPreflightCheck
+ ) throws {
+ guard case let .checkpoint(reason) = outcome else {
+ return
+ }
+ let isValid = switch (check, reason) {
+ case (.screenRecordingPermission, .screenRecordingConsentRequired),
+ (.accessibilityPermission, .accessibilityConsentRequired),
+ (.finderAutomationPermission, .finderAutomationConsentRequired),
+ (.fileProviderConsent, .fileProviderConsentRequired):
+ true
+ default:
+ false
+ }
+ guard isValid else {
+ throw StabilityFinderRunValidationError.invalidPreflightOutcome(check: check)
+ }
+ }
+
+ private static func validateSteps(
+ _ results: [StabilityFinderStepResult],
+ schemaVersion: UInt16,
+ runStartedAt: Date,
+ runFinishedAt: Date
+ ) throws {
+ var seenScenarios = Set()
+ for result in results where !seenScenarios.insert(result.scenario).inserted {
+ throw StabilityFinderRunValidationError.duplicateStepResult(result.scenario)
+ }
+ for scenario in StabilityFinderScenario.allCases where !seenScenarios.contains(scenario) {
+ throw StabilityFinderRunValidationError.missingStepResult(scenario)
+ }
+
+ var seenCorrelations = Set()
+ var previousResult: StabilityFinderStepResult?
+ for (index, result) in results.enumerated() {
+ if result.outcome == .skipped(.permanentDeletionNotSelected),
+ schemaVersion < Self.selectiveSchemaVersion || result.scenario != .permanentDeletion {
+ throw StabilityFinderRunValidationError.invalidStepDeferral(result.scenario)
+ }
+ let expectedScenario = StabilityFinderScenario.allCases[index]
+ guard result.scenario == expectedScenario else {
+ throw StabilityFinderRunValidationError.stepOutOfSequence(
+ expected: expectedScenario,
+ actual: result.scenario
+ )
+ }
+ let expectedSequence = UInt16(index + 1)
+ guard result.sequenceNumber == expectedSequence else {
+ throw StabilityFinderRunValidationError.invalidSequenceNumber(
+ scenario: result.scenario,
+ expected: expectedSequence,
+ actual: result.sequenceNumber
+ )
+ }
+ guard seenCorrelations.insert(result.correlationID).inserted else {
+ throw StabilityFinderRunValidationError.duplicateCorrelationID(result.correlationID)
+ }
+ guard result.finishedAt >= result.startedAt else {
+ throw StabilityFinderRunValidationError.invalidStepInterval(result.scenario)
+ }
+ guard result.startedAt >= runStartedAt,
+ result.finishedAt <= runFinishedAt else {
+ throw StabilityFinderRunValidationError.stepTimestampOutOfBounds(result.scenario)
+ }
+ if let previousResult, result.startedAt < previousResult.finishedAt {
+ throw StabilityFinderRunValidationError.stepTimestampOutOfSequence(
+ previous: previousResult.scenario,
+ current: result.scenario
+ )
+ }
+ let expectedDuration = milliseconds(from: result.startedAt, to: result.finishedAt)
+ guard duration(result.durationMilliseconds, isPlausibleFor: expectedDuration) else {
+ throw StabilityFinderRunValidationError.invalidStepDuration(result.scenario)
+ }
+ try validateAssertions(for: result)
+ try validateCheckpoint(for: result)
+ previousResult = result
+ }
+ }
+
+ private static func validateCheckpoint(for result: StabilityFinderStepResult) throws {
+ guard case let .checkpoint(reason) = result.outcome else { return }
+ let expectedReason: StabilityFinderCheckpointReason? = switch result.scenario {
+ case .restore:
+ .scopedRestoreWorkflowRequired
+ case .permanentDeletion:
+ .scopedPermanentDeletionWorkflowRequired
+ case .cancellationAndProgress:
+ .finderCancellationRequired
+ case .supportedContextualActions:
+ .variableContextualUI
+ default:
+ nil
+ }
+ guard reason == expectedReason else {
+ throw StabilityFinderRunValidationError.invalidStepCheckpoint(
+ scenario: result.scenario,
+ reason: reason
+ )
+ }
+ }
+
+ private static func validateAssertions(for result: StabilityFinderStepResult) throws {
+ var seen = Set()
+ for assertion in result.assertions where !seen.insert(assertion.assertionClass).inserted {
+ throw StabilityFinderRunValidationError.duplicateAssertion(
+ scenario: result.scenario,
+ assertionClass: assertion.assertionClass
+ )
+ }
+ for assertionClass in StabilityFinderAssertionClass.allCases where !seen.contains(assertionClass) {
+ throw StabilityFinderRunValidationError.missingAssertion(
+ scenario: result.scenario,
+ assertionClass: assertionClass
+ )
+ }
+
+ let outcomes = result.assertions.map(\.outcome)
+ let isConsistent = switch result.outcome {
+ case .passed:
+ outcomes.allSatisfy { $0 == .passed }
+ case .failed:
+ outcomes.contains { outcome in
+ if case .failed = outcome { return true }
+ return false
+ } || outcomes.allSatisfy { $0 == .notEvaluated(.operationDidNotReachAssertion) }
+ case .checkpoint:
+ outcomes.allSatisfy { $0 == .notEvaluated(.checkpointReached) }
+ case .skipped:
+ outcomes.allSatisfy { $0 == .notEvaluated(.stepSkipped) }
+ }
+ guard isConsistent else {
+ throw StabilityFinderRunValidationError.assertionOutcomeMismatch(result.scenario)
+ }
+ }
+
+ private static func summary(
+ for results: [StabilityFinderPreflightResult]
+ ) -> StabilityFinderOutcomeSummary {
+ var passed = 0
+ var failed = 0
+ var checkpointed = 0
+ var skipped = 0
+ for result in results {
+ switch result.outcome {
+ case .passed: passed += 1
+ case .failed: failed += 1
+ case .checkpoint: checkpointed += 1
+ case .skipped: skipped += 1
+ }
+ }
+ return StabilityFinderOutcomeSummary(
+ passed: passed,
+ failed: failed,
+ checkpointed: checkpointed,
+ skipped: skipped
+ )
+ }
+
+ private static func summary(
+ for results: [StabilityFinderStepResult]
+ ) -> StabilityFinderOutcomeSummary {
+ var passed = 0
+ var failed = 0
+ var checkpointed = 0
+ var skipped = 0
+ for result in results {
+ switch result.outcome {
+ case .passed: passed += 1
+ case .failed: failed += 1
+ case .checkpoint: checkpointed += 1
+ case .skipped: skipped += 1
+ }
+ }
+ return StabilityFinderOutcomeSummary(
+ passed: passed,
+ failed: failed,
+ checkpointed: checkpointed,
+ skipped: skipped
+ )
+ }
+
+ private static func milliseconds(from startedAt: Date, to finishedAt: Date) -> UInt64 {
+ UInt64((finishedAt.timeIntervalSince(startedAt) * 1_000).rounded())
+ }
+
+ /// The run-bundle encoder uses ISO-8601 timestamps, which omit fractional
+ /// seconds. The stored duration keeps millisecond precision, so decoding
+ /// may legitimately differ from the timestamps by at most 999 ms.
+ private static func duration(_ duration: UInt64, isPlausibleFor expected: UInt64) -> Bool {
+ let difference = duration >= expected ? duration - expected : expected - duration
+ return difference <= 999
+ }
+}
diff --git a/PotassiumProviderCore/StabilityLabRemoteCoordinator.swift b/PotassiumProviderCore/StabilityLabRemoteCoordinator.swift
new file mode 100644
index 0000000..7601925
--- /dev/null
+++ b/PotassiumProviderCore/StabilityLabRemoteCoordinator.swift
@@ -0,0 +1,439 @@
+import Foundation
+
+/// Remote identifiers and ownership proof for one disposable Stability Lab.
+/// This value intentionally contains no display names, paths, URLs, or account
+/// identifiers and must not itself be written to diagnostics.
+public struct StabilityLabRemoteConfiguration: Equatable, Sendable {
+ public let driveID: Int
+ public let driveRootFileID: Int
+ public let rootFileID: Int
+ public let ownershipMarkerFileID: Int
+ public let ownershipMarker: StabilityLabOwnershipMarker
+
+ public init(
+ driveID: Int,
+ driveRootFileID: Int,
+ rootFileID: Int,
+ ownershipMarkerFileID: Int,
+ ownershipMarker: StabilityLabOwnershipMarker
+ ) {
+ self.driveID = driveID
+ self.driveRootFileID = driveRootFileID
+ self.rootFileID = rootFileID
+ self.ownershipMarkerFileID = ownershipMarkerFileID
+ self.ownershipMarker = ownershipMarker
+ }
+}
+
+public struct StabilityLabRemoteResetResult: Equatable, Sendable {
+ public let trashedImmediateChildCount: Int
+
+ public init(trashedImmediateChildCount: Int) {
+ self.trashedImmediateChildCount = trashedImmediateChildCount
+ }
+}
+
+public typealias StabilityLabRegisteredDomainsProvider =
+ @Sendable () async throws -> [StabilityLabRegisteredDomain]
+
+public enum StabilityLabRemoteCoordinatorError: Error, Equatable, Sendable {
+ case invalidDriveRootIdentity
+ case driveAccessNotVerified
+ case ordinaryDomainRegistered
+ case unsupportedRegisteredDomain
+ case stabilityLabDomainAlreadyRegistered
+ case invalidProvisionedRoot
+ case invalidOwnershipMarkerFile
+ case ownershipMarkerEncodingFailed
+ case ownershipMarkerDecodingFailed
+ case ownershipMarkerMismatch
+ case invalidMaximumRootChildren
+ case incompleteDirectoryPage
+ case repeatedDirectoryCursor
+ case maximumRootChildrenExceeded(limit: Int)
+ case resetTargetIdentityChanged
+}
+
+extension StabilityLabRemoteCoordinatorError: LocalizedError {
+ public var errorDescription: String? {
+ switch self {
+ case .invalidDriveRootIdentity:
+ "The selected Stability Lab drive root is invalid."
+ case .driveAccessNotVerified:
+ "The selected Stability Lab drive must be an available internal drive."
+ case .ordinaryDomainRegistered:
+ "Remove ordinary File Provider domains before provisioning a Stability Lab."
+ case .unsupportedRegisteredDomain:
+ "Stability Lab supports only legacy plaintext domains."
+ case .stabilityLabDomainAlreadyRegistered:
+ "A Stability Lab domain is already registered."
+ case .invalidProvisionedRoot:
+ "The server did not return the expected top-level Stability Lab folder."
+ case .invalidOwnershipMarkerFile:
+ "The server did not return the expected Stability Lab ownership-marker file."
+ case .ownershipMarkerEncodingFailed:
+ "The Stability Lab ownership marker could not be encoded."
+ case .ownershipMarkerDecodingFailed:
+ "The Stability Lab ownership marker could not be decoded."
+ case .ownershipMarkerMismatch:
+ "The remote Stability Lab ownership marker does not match local configuration."
+ case .invalidMaximumRootChildren:
+ "The configured Stability Lab reset limit is invalid."
+ case .incompleteDirectoryPage:
+ "The Stability Lab root listing ended without a continuation cursor."
+ case .repeatedDirectoryCursor:
+ "The Stability Lab root listing repeated a continuation cursor."
+ case .maximumRootChildrenExceeded(let limit):
+ "The Stability Lab root contains more than the reset limit of \(limit) immediate items."
+ case .resetTargetIdentityChanged:
+ "A Stability Lab reset target changed after planning; no mutation was performed for it."
+ }
+ }
+}
+
+/// Performs the narrowly scoped remote lifecycle for a disposable Stability
+/// Lab. Credentials and domain registration remain outside this actor. All
+/// destructive execution is derived from `StabilityLabSafety.planReset` and
+/// uses trash, never permanent deletion.
+public actor StabilityLabRemoteCoordinator {
+ private static let markerFileName = ".potassium-stability-lab.json"
+ private static let folderNamePrefix = "Potassium Stability Lab"
+ private static let listingPageSize = 200
+
+ private let remote: any KDriveFileProviding
+ private let makeUUID: @Sendable () -> UUID
+ private let now: @Sendable () -> Date
+
+ public init(
+ remote: any KDriveFileProviding,
+ makeUUID: @escaping @Sendable () -> UUID = UUID.init,
+ now: @escaping @Sendable () -> Date = Date.init
+ ) {
+ self.remote = remote
+ self.makeUUID = makeUUID
+ self.now = now
+ }
+
+ /// Creates a uniquely named direct child of the explicit drive root and
+ /// uploads a fixed-name ownership marker inside it. A partial provisioning
+ /// result is left untouched for manual recovery; this method never rolls
+ /// back with a destructive remote mutation.
+ public func provision(
+ driveID: Int,
+ driveRootFileID: Int,
+ privateParentFileID: Int? = nil,
+ registeredDomainsProvider: StabilityLabRegisteredDomainsProvider
+ ) async throws -> StabilityLabRemoteConfiguration {
+ try validateProvisioningDomains(try await registeredDomainsProvider())
+ guard driveID > 0,
+ driveRootFileID == ProviderConstants.defaultRootFileID else {
+ throw StabilityLabRemoteCoordinatorError.invalidDriveRootIdentity
+ }
+ guard try await hasInternalDriveAccess(driveID) else {
+ throw StabilityLabRemoteCoordinatorError.driveAccessNotVerified
+ }
+
+ let driveRoot = try await remote.item(driveID: driveID, fileID: driveRootFileID)
+ guard driveRoot.id == driveRootFileID,
+ driveRoot.driveID == driveID,
+ driveRoot.isDirectory else {
+ throw StabilityLabRemoteCoordinatorError.invalidDriveRootIdentity
+ }
+
+ if let privateParentFileID {
+ try await verifyPrivateParent(driveID: driveID, fileID: privateParentFileID, driveRootFileID: driveRootFileID)
+ }
+ let parentID = privateParentFileID ?? driveRootFileID
+ try validateProvisioningDomains(try await registeredDomainsProvider())
+ let identifier = makeUUID()
+ let root = try await remote.createDirectory(
+ driveID: driveID,
+ parentID: parentID,
+ name: "\(Self.folderNamePrefix) \(identifier.uuidString)"
+ )
+ guard root.id > 0,
+ root.id != driveRootFileID,
+ root.driveID == driveID,
+ root.parentID == parentID,
+ root.id != parentID,
+ root.isDirectory else {
+ throw StabilityLabRemoteCoordinatorError.invalidProvisionedRoot
+ }
+
+ try validateProvisioningDomains(try await registeredDomainsProvider())
+ let marker = StabilityLabOwnershipMarker(
+ identifier: identifier,
+ driveID: driveID,
+ rootFileID: root.id,
+ createdAt: now(),
+ parentFileID: privateParentFileID
+ )
+ let markerData = try encodeMarker(marker)
+ let uploadedMarker = try await remote.uploadFile(
+ driveID: driveID,
+ parentID: root.id,
+ fileName: Self.markerFileName,
+ contents: markerData,
+ lastModifiedAt: marker.createdAt,
+ conflictStrategy: .error,
+ clientToken: KDriveMutationIdentity.clientToken([
+ "stability-lab-marker-create",
+ identifier.uuidString
+ ]),
+ contentHash: KDriveMutationIdentity.contentHash(markerData)
+ )
+ guard uploadedMarker.id > 0,
+ uploadedMarker.id != root.id,
+ uploadedMarker.id != driveRootFileID,
+ uploadedMarker.driveID == driveID,
+ uploadedMarker.parentID == root.id,
+ uploadedMarker.isDirectory == false else {
+ throw StabilityLabRemoteCoordinatorError.invalidOwnershipMarkerFile
+ }
+ try validateProvisioningDomains(try await registeredDomainsProvider())
+
+ let configuration = StabilityLabRemoteConfiguration(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ rootFileID: root.id,
+ ownershipMarkerFileID: uploadedMarker.id,
+ ownershipMarker: marker
+ )
+ let finalizedObservation = try await observe(configuration: configuration)
+ guard finalizedObservation.hasVerifiedLabOwnership else {
+ throw StabilityLabRemoteCoordinatorError.driveAccessNotVerified
+ }
+ return configuration
+ }
+
+ /// Fetches both server objects and downloads the marker before returning
+ /// root evidence. Remote names and paths are deliberately discarded.
+ public func observe(
+ configuration: StabilityLabRemoteConfiguration
+ ) async throws -> StabilityLabRootObservation {
+ let hasInternalAccess = try await hasInternalDriveAccess(configuration.driveID)
+ guard configuration.driveRootFileID == ProviderConstants.defaultRootFileID else {
+ throw StabilityLabRemoteCoordinatorError.invalidDriveRootIdentity
+ }
+ if let parentID = configuration.ownershipMarker.parentFileID {
+ try await verifyPrivateParent(driveID: configuration.driveID, fileID: parentID, driveRootFileID: configuration.driveRootFileID)
+ }
+ let root = try await remote.item(
+ driveID: configuration.driveID,
+ fileID: configuration.rootFileID
+ )
+ guard root.id == configuration.rootFileID,
+ root.driveID == configuration.driveID,
+ root.parentID == (configuration.ownershipMarker.parentFileID ?? configuration.driveRootFileID),
+ root.id != configuration.ownershipMarker.parentFileID,
+ root.isDirectory else {
+ throw StabilityLabRemoteCoordinatorError.invalidProvisionedRoot
+ }
+
+ let markerItem = try await remote.item(
+ driveID: configuration.driveID,
+ fileID: configuration.ownershipMarkerFileID
+ )
+ guard markerItem.id == configuration.ownershipMarkerFileID,
+ markerItem.driveID == configuration.driveID,
+ markerItem.parentID == configuration.rootFileID,
+ markerItem.isDirectory == false else {
+ throw StabilityLabRemoteCoordinatorError.invalidOwnershipMarkerFile
+ }
+
+ let markerData = try await remote.downloadFile(
+ driveID: configuration.driveID,
+ fileID: configuration.ownershipMarkerFileID
+ )
+ let observedMarker: StabilityLabOwnershipMarker
+ do {
+ observedMarker = try JSONDecoder().decode(
+ StabilityLabOwnershipMarker.self,
+ from: markerData
+ )
+ } catch {
+ throw StabilityLabRemoteCoordinatorError.ownershipMarkerDecodingFailed
+ }
+ guard observedMarker == configuration.ownershipMarker else {
+ throw StabilityLabRemoteCoordinatorError.ownershipMarkerMismatch
+ }
+
+ return StabilityLabRootObservation(
+ driveID: root.driveID,
+ fileID: root.id,
+ parentFileID: root.parentID,
+ driveRootFileID: configuration.driveRootFileID,
+ hasVerifiedLabOwnership: hasInternalAccess,
+ ownershipMarker: observedMarker,
+ verifiedPrivateParentFileID: configuration.ownershipMarker.parentFileID
+ )
+ }
+
+ private func verifyPrivateParent(driveID: Int, fileID: Int, driveRootFileID: Int) async throws {
+ let parent = try await remote.item(driveID: driveID, fileID: fileID)
+ guard fileID > 0, fileID != driveRootFileID, parent.id == fileID,
+ parent.driveID == driveID, parent.parentID == driveRootFileID,
+ parent.isDirectory, parent.name == KDrivePrivateDirectoryResolver.directoryName else {
+ throw StabilityLabRemoteCoordinatorError.invalidProvisionedRoot
+ }
+ }
+
+ /// Fully consumes ordinary directory-listing pagination. A missing or
+ /// repeated cursor and inventories larger than the configured bound fail
+ /// closed rather than producing partial reset evidence.
+ public func inventory(
+ configuration: StabilityLabRemoteConfiguration,
+ policy: StabilityLabResetPolicy = StabilityLabResetPolicy()
+ ) async throws -> StabilityLabRootInventory {
+ guard policy.maximumRootChildren > 0 else {
+ throw StabilityLabRemoteCoordinatorError.invalidMaximumRootChildren
+ }
+ let (maximumListedChildren, overflow) = policy.maximumRootChildren.addingReportingOverflow(1)
+ guard overflow == false else {
+ throw StabilityLabRemoteCoordinatorError.invalidMaximumRootChildren
+ }
+
+ var cursor: String?
+ var seenCursors: Set = []
+ var children: [StabilityLabRootChild] = []
+
+ while true {
+ let page = try await remote.listDirectory(
+ driveID: configuration.driveID,
+ folderID: configuration.rootFileID,
+ cursor: cursor,
+ limit: Self.listingPageSize
+ )
+ children.append(contentsOf: page.items.map { item in
+ StabilityLabRootChild(fileID: item.id, parentFileID: item.parentID)
+ })
+ guard children.count <= maximumListedChildren else {
+ throw StabilityLabRemoteCoordinatorError.maximumRootChildrenExceeded(
+ limit: policy.maximumRootChildren
+ )
+ }
+
+ guard page.hasMore else {
+ return StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: configuration.ownershipMarkerFileID,
+ children: children
+ )
+ }
+ guard let nextCursor = page.nextCursor?.trimmingCharacters(in: .whitespacesAndNewlines),
+ nextCursor.isEmpty == false else {
+ throw StabilityLabRemoteCoordinatorError.incompleteDirectoryPage
+ }
+ guard seenCursors.insert(nextCursor).inserted else {
+ throw StabilityLabRemoteCoordinatorError.repeatedDirectoryCursor
+ }
+ cursor = nextCursor
+ }
+ }
+
+ /// Plans from complete evidence, then revalidates the root, marker, and
+ /// each immediate child directly before moving that child to trash.
+ public func reset(
+ configuration: StabilityLabRemoteConfiguration,
+ configuredEncryptionMode: ProviderEncryptionMode,
+ registeredDomainsProvider: StabilityLabRegisteredDomainsProvider,
+ confirmation: StabilityLabResetConfirmation,
+ policy: StabilityLabResetPolicy = StabilityLabResetPolicy()
+ ) async throws -> StabilityLabRemoteResetResult {
+ let registeredDomains = try await registeredDomainsProvider()
+ let root = try await observe(configuration: configuration)
+ let preflightInput = StabilityLabPreflightInput(
+ expectedMarker: configuration.ownershipMarker,
+ expectedOwnershipMarkerFileID: configuration.ownershipMarkerFileID,
+ configuredEncryptionMode: configuredEncryptionMode,
+ root: root,
+ registeredDomains: registeredDomains
+ )
+ let rootInventory = try await inventory(configuration: configuration, policy: policy)
+ let plan = try StabilityLabSafety.planReset(
+ input: preflightInput,
+ inventory: rootInventory,
+ confirmation: confirmation,
+ policy: policy
+ )
+
+ var trashedCount = 0
+ for action in plan.actions {
+ let targetFileID: Int
+ switch action {
+ case .trashImmediateChild(let fileID):
+ targetFileID = fileID
+ }
+
+ let freshRoot = try await observe(configuration: configuration)
+ let freshRegisteredDomains = try await registeredDomainsProvider()
+ let freshPreflight = StabilityLabSafety.preflight(
+ StabilityLabPreflightInput(
+ expectedMarker: configuration.ownershipMarker,
+ expectedOwnershipMarkerFileID: configuration.ownershipMarkerFileID,
+ configuredEncryptionMode: configuredEncryptionMode,
+ root: freshRoot,
+ registeredDomains: freshRegisteredDomains
+ )
+ )
+ guard freshPreflight.isAllowed else {
+ throw StabilityLabResetPlanningError.preflightRejected(freshPreflight.issues)
+ }
+
+ let target = try await remote.item(
+ driveID: configuration.driveID,
+ fileID: targetFileID
+ )
+ guard target.id == targetFileID,
+ target.driveID == configuration.driveID,
+ target.parentID == configuration.rootFileID,
+ target.id != configuration.rootFileID,
+ target.id != configuration.driveRootFileID,
+ target.id != configuration.ownershipMarkerFileID else {
+ throw StabilityLabRemoteCoordinatorError.resetTargetIdentityChanged
+ }
+
+ try await remote.trashItem(
+ driveID: configuration.driveID,
+ fileID: targetFileID
+ )
+ trashedCount += 1
+ }
+
+ return StabilityLabRemoteResetResult(trashedImmediateChildCount: trashedCount)
+ }
+
+ private func validateProvisioningDomains(
+ _ registeredDomains: [StabilityLabRegisteredDomain]
+ ) throws {
+ if registeredDomains.contains(where: { $0.purpose == .ordinary }) {
+ throw StabilityLabRemoteCoordinatorError.ordinaryDomainRegistered
+ }
+ if registeredDomains.contains(where: { $0.encryptionMode != .legacyPlaintext }) {
+ throw StabilityLabRemoteCoordinatorError.unsupportedRegisteredDomain
+ }
+ if registeredDomains.contains(where: { $0.purpose == .stabilityLab }) {
+ throw StabilityLabRemoteCoordinatorError.stabilityLabDomainAlreadyRegistered
+ }
+ }
+
+ private func hasInternalDriveAccess(_ driveID: Int) async throws -> Bool {
+ let matchingDrives = try await remote.listDrives().filter { drive in
+ drive.id == driveID
+ }
+ guard matchingDrives.count == 1, let drive = matchingDrives.first else {
+ return false
+ }
+ return drive.isUsableInternalDrive && drive.isInMaintenance == false
+ }
+
+ private func encodeMarker(_ marker: StabilityLabOwnershipMarker) throws -> Data {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.sortedKeys]
+ do {
+ return try encoder.encode(marker)
+ } catch {
+ throw StabilityLabRemoteCoordinatorError.ownershipMarkerEncodingFailed
+ }
+ }
+}
diff --git a/PotassiumProviderCore/StabilityLabSafety.swift b/PotassiumProviderCore/StabilityLabSafety.swift
new file mode 100644
index 0000000..926039d
--- /dev/null
+++ b/PotassiumProviderCore/StabilityLabSafety.swift
@@ -0,0 +1,487 @@
+import Foundation
+
+/// A non-secret marker written to the disposable Stability Lab root and kept
+/// locally with the lab configuration. Matching both copies is a prerequisite
+/// for any reset plan.
+public struct StabilityLabOwnershipMarker: Codable, Equatable, Sendable {
+ public static let currentSchemaVersion: UInt16 = 1
+
+ public var schemaVersion: UInt16
+ public var identifier: UUID
+ public var driveID: Int
+ public var rootFileID: Int
+ public var createdAt: Date
+ /// Nil preserves legacy top-level labs; new labs bind their verified Private parent.
+ public var parentFileID: Int?
+
+ public init(
+ schemaVersion: UInt16 = Self.currentSchemaVersion,
+ identifier: UUID = UUID(),
+ driveID: Int,
+ rootFileID: Int,
+ createdAt: Date = Date(),
+ parentFileID: Int? = nil
+ ) {
+ self.schemaVersion = schemaVersion
+ self.identifier = identifier
+ self.driveID = driveID
+ self.rootFileID = rootFileID
+ self.createdAt = Date(timeIntervalSince1970: floor(createdAt.timeIntervalSince1970))
+ self.parentFileID = parentFileID
+ }
+ private enum CodingKeys: String, CodingKey {
+ case schemaVersion, identifier, driveID, rootFileID, createdAt, parentFileID
+ }
+
+ /// Domain configuration uses ISO-8601 seconds, whereas legacy remote marker
+ /// JSON uses Foundation numeric dates. Normalize both without changing any
+ /// identity field or requiring a write to an existing remote marker.
+ public init(from decoder: Decoder) throws {
+ let values = try decoder.container(keyedBy: CodingKeys.self)
+ self.init(schemaVersion: try values.decode(UInt16.self, forKey: .schemaVersion),
+ identifier: try values.decode(UUID.self, forKey: .identifier),
+ driveID: try values.decode(Int.self, forKey: .driveID),
+ rootFileID: try values.decode(Int.self, forKey: .rootFileID),
+ createdAt: try values.decode(Date.self, forKey: .createdAt),
+ parentFileID: try values.decodeIfPresent(Int.self, forKey: .parentFileID))
+ }
+
+}
+
+public enum StabilityLabRegisteredDomainPurpose: String, Codable, Equatable, Sendable {
+ case ordinary
+ case stabilityLab
+}
+
+/// The minimum registered-domain evidence required by the lab safety gate.
+/// Display names and account identifiers are intentionally excluded.
+public struct StabilityLabRegisteredDomain: Equatable, Sendable {
+ public var purpose: StabilityLabRegisteredDomainPurpose
+ public var driveID: Int
+ public var rootFileID: Int
+ public var encryptionMode: ProviderEncryptionMode
+ public var ownershipMarkerIdentifier: UUID?
+
+ public init(
+ purpose: StabilityLabRegisteredDomainPurpose,
+ driveID: Int,
+ rootFileID: Int,
+ encryptionMode: ProviderEncryptionMode,
+ ownershipMarkerIdentifier: UUID? = nil
+ ) {
+ self.purpose = purpose
+ self.driveID = driveID
+ self.rootFileID = rootFileID
+ self.encryptionMode = encryptionMode
+ self.ownershipMarkerIdentifier = ownershipMarkerIdentifier
+ }
+}
+
+/// Server-authoritative facts about the configured root. This type contains no
+/// names, paths, URLs, or account identifiers. Its numeric remote identifiers
+/// are still private operational state and must not be written to diagnostics.
+public struct StabilityLabRootObservation: Equatable, Sendable {
+ public var driveID: Int
+ public var fileID: Int
+ public var parentFileID: Int
+ public var driveRootFileID: Int
+ public var hasVerifiedLabOwnership: Bool
+ public var verifiedPrivateParentFileID: Int?
+ public var ownershipMarker: StabilityLabOwnershipMarker?
+
+ public init(
+ driveID: Int,
+ fileID: Int,
+ parentFileID: Int,
+ driveRootFileID: Int,
+ hasVerifiedLabOwnership: Bool,
+ ownershipMarker: StabilityLabOwnershipMarker?,
+ verifiedPrivateParentFileID: Int? = nil
+ ) {
+ self.driveID = driveID
+ self.fileID = fileID
+ self.parentFileID = parentFileID
+ self.driveRootFileID = driveRootFileID
+ self.hasVerifiedLabOwnership = hasVerifiedLabOwnership
+ self.verifiedPrivateParentFileID = verifiedPrivateParentFileID
+ self.ownershipMarker = ownershipMarker
+ }
+}
+
+public struct StabilityLabPreflightInput: Equatable, Sendable {
+ public var expectedMarker: StabilityLabOwnershipMarker
+ public var expectedOwnershipMarkerFileID: Int
+ public var configuredEncryptionMode: ProviderEncryptionMode
+ public var root: StabilityLabRootObservation
+ public var registeredDomains: [StabilityLabRegisteredDomain]
+
+ public init(
+ expectedMarker: StabilityLabOwnershipMarker,
+ expectedOwnershipMarkerFileID: Int,
+ configuredEncryptionMode: ProviderEncryptionMode,
+ root: StabilityLabRootObservation,
+ registeredDomains: [StabilityLabRegisteredDomain]
+ ) {
+ self.expectedMarker = expectedMarker
+ self.expectedOwnershipMarkerFileID = expectedOwnershipMarkerFileID
+ self.configuredEncryptionMode = configuredEncryptionMode
+ self.root = root
+ self.registeredDomains = registeredDomains
+ }
+}
+
+public enum StabilityLabPreflightIssue: String, Codable, Equatable, Hashable, Sendable {
+ case unsupportedOwnershipMarkerVersion
+ case invalidRootIdentity
+ case invalidOwnershipMarkerFileIdentity
+ case unsupportedEncryptedDomain
+ case ordinaryDomainRegistered
+ case driveRootSelected
+ case rootIdentityMismatch
+ case rootIsNotTopLevel
+ case rootOwnershipNotVerified
+ case ownershipMarkerMissing
+ case ownershipMarkerMismatch
+ case registeredLabDomainMissing
+ case registeredLabDomainMismatch
+ case multipleStabilityLabDomains
+}
+
+public struct StabilityLabPreflightResult: Equatable, Sendable {
+ public let issues: [StabilityLabPreflightIssue]
+
+ public var isAllowed: Bool {
+ issues.isEmpty
+ }
+
+ fileprivate init(issues: [StabilityLabPreflightIssue]) {
+ self.issues = issues
+ }
+}
+
+/// An explicit confirmation bound to one marker and root. A confirmation for a
+/// previous lab root cannot authorize a reset after reprovisioning.
+public struct StabilityLabResetConfirmation: Equatable, Sendable {
+ public static let requiredPhrase = "DELETE STABILITY LAB CONTENTS"
+
+ fileprivate let marker: StabilityLabOwnershipMarker
+
+ public init(
+ typedPhrase: String,
+ marker: StabilityLabOwnershipMarker
+ ) throws {
+ guard typedPhrase == Self.requiredPhrase else {
+ throw StabilityLabResetConfirmationError.typedPhraseMismatch
+ }
+ self.marker = marker
+ }
+}
+
+public enum StabilityLabResetConfirmationError: Error, Equatable, Sendable {
+ case typedPhraseMismatch
+}
+
+public struct StabilityLabRootChild: Equatable, Sendable {
+ public var fileID: Int
+ public var parentFileID: Int
+
+ public init(fileID: Int, parentFileID: Int) {
+ self.fileID = fileID
+ self.parentFileID = parentFileID
+ }
+}
+
+/// `isComplete` must be backed by a fully consumed server listing. Reset never
+/// plans from a partial page because the resulting evidence would be ambiguous.
+public struct StabilityLabRootInventory: Equatable, Sendable {
+ public var isComplete: Bool
+ /// The separately persisted marker-file identity that must appear exactly
+ /// once in the complete server-authoritative child listing.
+ public var ownershipMarkerFileID: Int?
+ public var children: [StabilityLabRootChild]
+
+ public init(
+ isComplete: Bool,
+ ownershipMarkerFileID: Int?,
+ children: [StabilityLabRootChild]
+ ) {
+ self.isComplete = isComplete
+ self.ownershipMarkerFileID = ownershipMarkerFileID
+ self.children = children
+ }
+}
+
+public struct StabilityLabResetPolicy: Equatable, Sendable {
+ public static let defaultMaximumRootChildren = 1_000
+
+ public var maximumRootChildren: Int
+
+ public init(maximumRootChildren: Int = Self.defaultMaximumRootChildren) {
+ self.maximumRootChildren = maximumRootChildren
+ }
+}
+
+public enum StabilityLabResetAction: Equatable, Sendable {
+ /// Trashes one item whose authoritative parent is the preserved lab root.
+ /// A directory may recursively contain lab-owned descendants, but the lab
+ /// root itself is not representable as a reset action.
+ case trashImmediateChild(fileID: Int)
+}
+
+public struct StabilityLabResetPlan: Equatable, Sendable {
+ public let ownershipMarkerIdentifier: UUID
+ public let preservedRootFileID: Int
+ public let preservedOwnershipMarkerFileID: Int
+ public let actions: [StabilityLabResetAction]
+
+ /// Kept explicit for command/UI assertions. The plan type has no root
+ /// deletion action, so this value cannot become true.
+ public var deletesRoot: Bool { false }
+
+ fileprivate init(
+ ownershipMarkerIdentifier: UUID,
+ preservedRootFileID: Int,
+ preservedOwnershipMarkerFileID: Int,
+ actions: [StabilityLabResetAction]
+ ) {
+ self.ownershipMarkerIdentifier = ownershipMarkerIdentifier
+ self.preservedRootFileID = preservedRootFileID
+ self.preservedOwnershipMarkerFileID = preservedOwnershipMarkerFileID
+ self.actions = actions
+ }
+}
+
+public enum StabilityLabResetPlanningError: Error, Equatable, Sendable {
+ case preflightRejected([StabilityLabPreflightIssue])
+ case confirmationDoesNotMatchRoot
+ case invalidMaximumRootChildren
+ case incompleteInventory
+ case maximumRootChildrenExceeded(limit: Int)
+ case ownershipMarkerFileEvidenceMissing
+ case ownershipMarkerFileEvidenceDuplicate
+ case ownershipMarkerFileEvidenceMismatch
+ case ownershipMarkerFileIsNotImmediateChild
+ case invalidContentIdentity
+ case rootIncludedInContents
+ case driveRootIncludedInContents
+ case contentIsNotImmediateChild
+ case duplicateContentIdentifier
+}
+
+extension StabilityLabResetConfirmationError: LocalizedError {
+ public var errorDescription: String? {
+ switch self {
+ case .typedPhraseMismatch:
+ "Type the exact destructive confirmation phrase before resetting the Stability Lab."
+ }
+ }
+}
+
+extension StabilityLabResetPlanningError: LocalizedError {
+ public var errorDescription: String? {
+ switch self {
+ case .preflightRejected:
+ "Stability Lab reset was rejected by the root safety preflight."
+ case .confirmationDoesNotMatchRoot:
+ "The destructive confirmation belongs to a different Stability Lab root."
+ case .invalidMaximumRootChildren:
+ "The configured Stability Lab reset limit is invalid."
+ case .incompleteInventory:
+ "The Stability Lab root inventory is incomplete."
+ case .maximumRootChildrenExceeded(let limit):
+ "The Stability Lab root contains more than the reset limit of \(limit) immediate items."
+ case .ownershipMarkerFileEvidenceMissing:
+ "The Stability Lab ownership-marker file is missing from the complete root inventory."
+ case .ownershipMarkerFileEvidenceDuplicate:
+ "The Stability Lab root inventory contains duplicate ownership-marker file evidence."
+ case .ownershipMarkerFileEvidenceMismatch:
+ "The Stability Lab root inventory identifies a different ownership-marker file."
+ case .ownershipMarkerFileIsNotImmediateChild:
+ "The Stability Lab ownership-marker file is not an immediate child of the lab root."
+ case .invalidContentIdentity:
+ "The Stability Lab root inventory contains an invalid item identity."
+ case .rootIncludedInContents:
+ "The Stability Lab root cannot be included in its reset contents."
+ case .driveRootIncludedInContents:
+ "The drive root cannot be included in Stability Lab reset contents."
+ case .contentIsNotImmediateChild:
+ "Every Stability Lab reset target must be an immediate child of the lab root."
+ case .duplicateContentIdentifier:
+ "The Stability Lab root inventory contains a duplicate item identity."
+ }
+ }
+}
+
+/// Pure safety decisions shared by the app command and tests. Remote listing,
+/// marker persistence, confirmation UI, and deletion execution stay injected
+/// outside this namespace.
+public enum StabilityLabSafety {
+ public static func preflight(
+ _ input: StabilityLabPreflightInput
+ ) -> StabilityLabPreflightResult {
+ var issues: [StabilityLabPreflightIssue] = []
+
+ func record(_ issue: StabilityLabPreflightIssue) {
+ if issues.contains(issue) == false {
+ issues.append(issue)
+ }
+ }
+
+ let marker = input.expectedMarker
+ let root = input.root
+
+ if marker.schemaVersion != StabilityLabOwnershipMarker.currentSchemaVersion {
+ record(.unsupportedOwnershipMarkerVersion)
+ }
+ if input.configuredEncryptionMode != .legacyPlaintext {
+ record(.unsupportedEncryptedDomain)
+ }
+ if marker.driveID <= 0 || marker.rootFileID <= 0 ||
+ root.driveID <= 0 || root.fileID <= 0 ||
+ root.driveRootFileID != ProviderConstants.defaultRootFileID {
+ record(.invalidRootIdentity)
+ }
+ if input.expectedOwnershipMarkerFileID <= 0 ||
+ input.expectedOwnershipMarkerFileID == root.fileID ||
+ input.expectedOwnershipMarkerFileID == root.driveRootFileID {
+ record(.invalidOwnershipMarkerFileIdentity)
+ }
+ if root.fileID == root.driveRootFileID || marker.rootFileID == root.driveRootFileID {
+ record(.driveRootSelected)
+ }
+ if root.driveID != marker.driveID || root.fileID != marker.rootFileID {
+ record(.rootIdentityMismatch)
+ }
+ let expectedParent = marker.parentFileID ?? root.driveRootFileID
+ if root.parentFileID != expectedParent || root.fileID == expectedParent ||
+ (marker.parentFileID != nil && root.verifiedPrivateParentFileID != expectedParent) {
+ record(.rootIsNotTopLevel)
+ }
+ if root.hasVerifiedLabOwnership == false {
+ record(.rootOwnershipNotVerified)
+ }
+
+ switch root.ownershipMarker {
+ case .none:
+ record(.ownershipMarkerMissing)
+ case .some(let observedMarker):
+ if observedMarker.schemaVersion != StabilityLabOwnershipMarker.currentSchemaVersion {
+ record(.unsupportedOwnershipMarkerVersion)
+ }
+ if observedMarker != marker {
+ record(.ownershipMarkerMismatch)
+ }
+ }
+
+ let stabilityDomains = input.registeredDomains.filter { domain in
+ if domain.purpose == .ordinary {
+ record(.ordinaryDomainRegistered)
+ }
+ if domain.encryptionMode != .legacyPlaintext {
+ record(.unsupportedEncryptedDomain)
+ }
+ return domain.purpose == .stabilityLab
+ }
+
+ if stabilityDomains.isEmpty {
+ record(.registeredLabDomainMissing)
+ } else if stabilityDomains.count > 1 {
+ record(.multipleStabilityLabDomains)
+ }
+ if stabilityDomains.contains(where: { domain in
+ domain.driveID != marker.driveID ||
+ domain.rootFileID != marker.rootFileID ||
+ domain.ownershipMarkerIdentifier != marker.identifier
+ }) {
+ record(.registeredLabDomainMismatch)
+ }
+
+ return StabilityLabPreflightResult(issues: issues)
+ }
+
+ public static func planReset(
+ input: StabilityLabPreflightInput,
+ inventory: StabilityLabRootInventory,
+ confirmation: StabilityLabResetConfirmation,
+ policy: StabilityLabResetPolicy = StabilityLabResetPolicy()
+ ) throws -> StabilityLabResetPlan {
+ let preflightResult = preflight(input)
+ guard preflightResult.isAllowed else {
+ throw StabilityLabResetPlanningError.preflightRejected(preflightResult.issues)
+ }
+ guard confirmation.marker == input.expectedMarker else {
+ throw StabilityLabResetPlanningError.confirmationDoesNotMatchRoot
+ }
+ guard policy.maximumRootChildren > 0 else {
+ throw StabilityLabResetPlanningError.invalidMaximumRootChildren
+ }
+ guard inventory.isComplete else {
+ throw StabilityLabResetPlanningError.incompleteInventory
+ }
+
+ guard let ownershipMarkerFileID = inventory.ownershipMarkerFileID else {
+ throw StabilityLabResetPlanningError.ownershipMarkerFileEvidenceMissing
+ }
+ guard ownershipMarkerFileID == input.expectedOwnershipMarkerFileID else {
+ throw StabilityLabResetPlanningError.ownershipMarkerFileEvidenceMismatch
+ }
+ let markerChildren = inventory.children.filter { child in
+ child.fileID == ownershipMarkerFileID
+ }
+ guard markerChildren.isEmpty == false else {
+ throw StabilityLabResetPlanningError.ownershipMarkerFileEvidenceMissing
+ }
+ guard markerChildren.count == 1 else {
+ throw StabilityLabResetPlanningError.ownershipMarkerFileEvidenceDuplicate
+ }
+
+ let rootFileID = input.root.fileID
+ guard markerChildren[0].parentFileID == rootFileID else {
+ throw StabilityLabResetPlanningError.ownershipMarkerFileIsNotImmediateChild
+ }
+
+ let deletableChildCount = inventory.children.count - markerChildren.count
+ guard deletableChildCount <= policy.maximumRootChildren else {
+ throw StabilityLabResetPlanningError.maximumRootChildrenExceeded(
+ limit: policy.maximumRootChildren
+ )
+ }
+
+ let driveRootFileID = input.root.driveRootFileID
+ var seenIdentifiers: Set = []
+ var contentIdentifiers: [Int] = []
+ contentIdentifiers.reserveCapacity(inventory.children.count)
+
+ for child in inventory.children {
+ if child.fileID == ownershipMarkerFileID {
+ continue
+ }
+ guard child.fileID > 0 else {
+ throw StabilityLabResetPlanningError.invalidContentIdentity
+ }
+ guard child.fileID != rootFileID else {
+ throw StabilityLabResetPlanningError.rootIncludedInContents
+ }
+ guard child.fileID != driveRootFileID else {
+ throw StabilityLabResetPlanningError.driveRootIncludedInContents
+ }
+ guard child.parentFileID == rootFileID else {
+ throw StabilityLabResetPlanningError.contentIsNotImmediateChild
+ }
+ guard seenIdentifiers.insert(child.fileID).inserted else {
+ throw StabilityLabResetPlanningError.duplicateContentIdentifier
+ }
+ contentIdentifiers.append(child.fileID)
+ }
+
+ let actions = contentIdentifiers
+ .sorted()
+ .map(StabilityLabResetAction.trashImmediateChild(fileID:))
+ return StabilityLabResetPlan(
+ ownershipMarkerIdentifier: input.expectedMarker.identifier,
+ preservedRootFileID: rootFileID,
+ preservedOwnershipMarkerFileID: ownershipMarkerFileID,
+ actions: actions
+ )
+ }
+}
diff --git a/PotassiumProviderCore/StabilityLaunchPreparationFailure.swift b/PotassiumProviderCore/StabilityLaunchPreparationFailure.swift
new file mode 100644
index 0000000..56b467b
--- /dev/null
+++ b/PotassiumProviderCore/StabilityLaunchPreparationFailure.swift
@@ -0,0 +1,39 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+public enum StabilityLaunchPreparationError: String, Error {
+ case initialProcessAbsent, initialProcessChanged
+}
+
+/// An unsuccessful preflight has no Finder report. Retain a closed diagnostic
+/// without sealing it or fabricating the missing scenario/lifecycle evidence.
+public struct StabilityLaunchPreparationFailure: Codable {
+ public enum Stage: String, Codable { case initialProcessObservation, labPreflight, launchPreparation, liveContext }
+ public let schemaVersion: UInt16
+ public let stage: Stage
+ public let occurredAt: Date
+ public let errorClass: ProviderDiagnosticErrorClass
+ public let errorCode: Int
+ public let reason: String
+ public let eligibleForAcceptance: Bool
+
+ public init(stage: Stage, error: any Error) {
+ schemaVersion = 1; self.stage = stage; occurredAt = Date()
+ errorClass = ProviderDiagnosticErrorClassifier.classify(error)
+ errorCode = (error as NSError).code
+ reason = (error as? StabilityLaunchPreparationError)?.rawValue
+ ?? (error is StabilityDeadlineError ? "deadline" : "unclassified")
+ eligibleForAcceptance = false
+ }
+
+ public static func record(stage: Stage, error: any Error, run: StabilityRunHandle) {
+ let failure = Self(stage: stage, error: error)
+ print("finder stability preparation failed: stage=\(stage.rawValue) reason=\(failure.reason) class=\(failure.errorClass.rawValue) code=\(failure.errorCode)")
+ do {
+ let encoder = JSONEncoder(); encoder.dateEncodingStrategy = .iso8601
+ try SecurePOSIXFile.createExclusively(encoder.encode(failure),
+ at: run.directoryURL.appendingPathComponent("launch-preparation-failed.json"), permissions: 0o400)
+ } catch { print("finder stability preparation failure evidence could not be retained") }
+ }
+}
+#endif
diff --git a/PotassiumProviderCore/StabilityLiveEvidence.swift b/PotassiumProviderCore/StabilityLiveEvidence.swift
new file mode 100644
index 0000000..80254cf
--- /dev/null
+++ b/PotassiumProviderCore/StabilityLiveEvidence.swift
@@ -0,0 +1,199 @@
+import Foundation
+
+public enum StabilityFailureOrigin: String, Codable, Sendable {
+ case automation, harness, provider, api, environment
+}
+
+public enum StabilityLiveFailureReason: String, Codable, Sendable {
+ case deadline, missingFixture, unsafeTarget, unverifiedBuild, uiUnavailable, selectionMismatch, windowMismatch
+ case missingTelemetry, screenshotUnavailable, operatorStopped, cancellationNotExercised, assertionFailed, remoteError, unsupportedControl, resourceBusy, unknown
+}
+
+/// Additional evidence required by the live UI report (version 2).
+/// Values name operation classes and run-local aliases, never file names or URLs.
+public struct StabilityLiveStepEvidence: Codable, Equatable, Sendable {
+ public var subjects: [UUID]
+ public var observedUIActions: Int
+ public var expectedExtensionCodeHash: String
+ public var expectedActionsCodeHash: String?
+ public var conflictBarrierReached: Bool
+ public var cancellationObserved: Bool
+ public var workingSetMemberObserved: Bool
+ public var expectedWorkingSetMetadataAlias: UUID?
+ public var failureOrigin: StabilityFailureOrigin?
+ public var failureReason: StabilityLiveFailureReason?
+ public var supportingSpanIDs: [UUID]?
+
+ public init(subjects: [UUID], observedUIActions: Int, expectedExtensionCodeHash: String,
+ conflictBarrierReached: Bool = false, cancellationObserved: Bool = false,
+ workingSetMemberObserved: Bool = false, failureOrigin: StabilityFailureOrigin? = nil,
+ failureReason: StabilityLiveFailureReason? = nil, supportingSpanIDs: [UUID]? = nil,
+ expectedActionsCodeHash: String? = nil, expectedWorkingSetMetadataAlias: UUID? = nil) {
+ self.subjects = subjects
+ self.observedUIActions = observedUIActions
+ self.expectedExtensionCodeHash = expectedExtensionCodeHash
+ self.expectedActionsCodeHash = expectedActionsCodeHash
+ self.conflictBarrierReached = conflictBarrierReached
+ self.cancellationObserved = cancellationObserved
+ self.workingSetMemberObserved = workingSetMemberObserved
+ self.expectedWorkingSetMetadataAlias = expectedWorkingSetMetadataAlias
+ self.failureOrigin = failureOrigin
+ self.failureReason = failureReason
+ self.supportingSpanIDs = supportingSpanIDs
+ }
+}
+
+public enum StabilityLiveEvidenceError: String, Error, Codable, Sendable {
+ case missingUIEvidence, missingSubject, missingCallback, wrongExtensionBuild
+ case contradictoryTerminal, missingConflict, missingCancellation, missingWorkingSet
+ case pendingOperations, missingSpanStart, unexpectedFailure, incompleteRun
+}
+
+public enum StabilityLiveEvidenceValidator {
+ public static func validate(step: StabilityFinderStepResult, diagnostics: [ProviderDiagnosticEvent]) throws {
+ guard step.outcome == .passed else { return }
+ guard let proof = step.liveEvidence, proof.observedUIActions > 0 else {
+ throw StabilityLiveEvidenceError.missingUIEvidence
+ }
+ guard !proof.subjects.isEmpty else { throw StabilityLiveEvidenceError.missingSubject }
+ let scoped = diagnostics.filter {
+ $0.correlationID == step.correlationID && $0.subjectAlias.map(proof.subjects.contains) == true
+ }
+ func withinStep(_ event: ProviderDiagnosticEvent) -> Bool {
+ // JSONL dates historically have whole-second precision. Correlation
+ // and subject identity remain mandatory at this rounded boundary.
+ event.occurredAt.timeIntervalSince1970 >= floor(step.startedAt.timeIntervalSince1970) &&
+ event.occurredAt.timeIntervalSince1970 <= ceil(step.finishedAt.timeIntervalSince1970)
+ }
+ let starts = scoped.filter { $0.phase == .started }
+ let allStartedIDs = Set(starts.compactMap(\.spanID))
+ let selectedIDs = Set(starts.filter(withinStep).compactMap(\.spanID))
+ let events = scoped.filter { event in
+ guard let spanID = event.spanID else { return withinStep(event) }
+ // Keep the entire selected span, including terminals during run
+ // settling and contradictory late terminals. An orphan in the step
+ // still fails, and a callback started before it cannot prove it.
+ return selectedIDs.contains(spanID) || (!allStartedIDs.contains(spanID) && withinStep(event))
+ }
+ let callbacks = events.filter { $0.source == .fileProviderExtension && Self.callbackOperations.contains($0.operation) }
+ guard !callbacks.isEmpty else { throw StabilityLiveEvidenceError.missingCallback }
+ guard proof.expectedExtensionCodeHash.count == 40,
+ callbacks.allSatisfy({ $0.processCodeHash == proof.expectedExtensionCodeHash && $0.processInstanceID != nil }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ let actionEvents = events.filter { $0.source == .actionExtension }
+ if !actionEvents.isEmpty {
+ guard let expected = proof.expectedActionsCodeHash, expected.count == 40,
+ actionEvents.allSatisfy({ $0.processCodeHash == expected && $0.processInstanceID != nil }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ }
+ for (_, span) in Dictionary(grouping: events.filter { $0.spanID != nil }, by: { $0.spanID! }) {
+ guard span.filter({ $0.phase == .started }).count == 1 else { throw StabilityLiveEvidenceError.missingSpanStart }
+ let terminals = span.filter { [.completed, .cancelled, .failed].contains($0.phase) }
+ guard terminals.count <= 1 else { throw StabilityLiveEvidenceError.contradictoryTerminal }
+ if terminals.isEmpty {
+ throw StabilityLiveEvidenceError.pendingOperations
+ }
+ for terminal in terminals where terminal.phase != .completed {
+ let expectedCancellation = step.scenario == .cancellationAndProgress && terminal.phase == .cancelled && [.fetchContents, .downloadFile].contains(terminal.operation)
+ let expectedConflict = step.scenario == .concurrentRemotePreserveBoth && terminal.errorClass == .conflict
+ let recoveredTrashLookup = isRecoveredTrashLookup(terminal, events: events)
+ guard expectedCancellation || expectedConflict || recoveredTrashLookup else {
+ throw StabilityLiveEvidenceError.unexpectedFailure
+ }
+ }
+ }
+ func completed(_ groups: [Set]) throws {
+ for group in groups {
+ guard events.contains(where: { group.contains($0.operation) && $0.phase == .completed &&
+ ($0.source == .fileProviderExtension || $0.source == .actionExtension) }) else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ }
+ }
+ switch step.scenario {
+ case .enumerationAndChangeAnchors:
+ try completed([[.enumerateItems], [.enumerateChanges, .currentSyncAnchor]])
+ case .hydrate, .download: try completed([[.fetchContents]])
+ case .evict: try completed([[.itemLookup, .materializedItemsChanged]])
+ case .fileCreate, .directoryCreate: try completed([[.createItem]])
+ case .editAndUpload, .rename, .move:
+ let field: ProviderDiagnosticField = step.scenario == .editAndUpload ? .contents :
+ step.scenario == .rename ? .filename : .parent
+ guard callbacks.contains(where: { $0.operation == .modifyItem && $0.phase == .completed &&
+ $0.fieldShape.contains(field) && !$0.fieldShape.contains(.trash) }) else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ case .trash:
+ guard callbacks.contains(where: { $0.operation == .modifyItem && $0.phase == .completed &&
+ $0.fieldShape.contains(.parent) && $0.fieldShape.contains(.trash) }) else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ case .restore:
+ guard callbacks.contains(where: { $0.phase == .completed &&
+ ($0.operation == .restoreTrashedItem || ($0.operation == .modifyItem &&
+ $0.fieldShape.contains(.parent) && !$0.fieldShape.contains(.trash))) }) else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ case .permanentDeletion: try completed([[.deleteItem]])
+ case .concurrentRemotePreserveBoth:
+ guard proof.conflictBarrierReached,
+ events.contains(where: { $0.errorClass == .conflict && $0.source == .fileProviderExtension }) else {
+ throw StabilityLiveEvidenceError.missingConflict
+ }
+ try completed([[.modifyItem], [.uploadFile]])
+ case .cancellationAndProgress:
+ guard proof.cancellationObserved,
+ let cancelled = callbacks.first(where: { $0.operation == .fetchContents && $0.phase == .cancelled }),
+ let spanID = cancelled.spanID,
+ events.contains(where: { $0.phase == .progress && ($0.progressPercentBucket ?? 0) > 0 &&
+ ($0.spanID == spanID || $0.parentSpanID == spanID) }) else {
+ throw StabilityLiveEvidenceError.missingCancellation
+ }
+ guard callbacks.contains(where: { $0.operation == .fetchContents && $0.phase == .completed &&
+ $0.subjectAlias == cancelled.subjectAlias && $0.spanID != spanID && $0.occurredAt >= cancelled.occurredAt }) else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ case .workingSetRefresh:
+ guard proof.workingSetMemberObserved else { throw StabilityLiveEvidenceError.missingWorkingSet }
+ try completed([[.workingSetRefresh]])
+ guard let expected = proof.expectedWorkingSetMetadataAlias,
+ callbacks.contains(where: { $0.operation == .workingSetRefresh && $0.phase == .completed &&
+ $0.itemMetadataAlias == expected && $0.parentSpanID.map { parentID in
+ callbacks.contains { $0.spanID == parentID && $0.phase == .completed &&
+ [.enumerateItems, .enumerateChanges].contains($0.operation) }
+ } == true }) else {
+ throw StabilityLiveEvidenceError.missingWorkingSet
+ }
+ case .supportedContextualActions:
+ guard callbacks.filter({ $0.operation == .favoriteItem && $0.phase == .completed && $0.parentSpanID == nil }).count >= 2 else {
+ throw StabilityLiveEvidenceError.missingCallback
+ }
+ try completed([[.favoriteItem], [.duplicateItem], [.createShareLink], [.updateShareLink], [.deleteShareLink], [.restoreFileVersion]])
+ }
+ }
+
+ /// A handled active-item 404 remains in the timeline. It is expected only
+ /// when the same callback resolves that exact subject from Trash successfully.
+ private static func isRecoveredTrashLookup(_ failure: ProviderDiagnosticEvent,
+ events: [ProviderDiagnosticEvent]) -> Bool {
+ guard failure.source == .fileProviderExtension, failure.operation == .itemLookup,
+ failure.phase == .failed, failure.errorClass == .notFound, failure.errorCode == 404,
+ let parent = failure.parentSpanID else { return false }
+ func matches(_ event: ProviderDiagnosticEvent) -> Bool {
+ event.source == failure.source && event.subjectAlias == failure.subjectAlias &&
+ event.processInstanceID == failure.processInstanceID &&
+ event.processCodeHash == failure.processCodeHash && event.phase == .completed &&
+ event.occurredAt >= failure.occurredAt
+ }
+ return events.contains { matches($0) && $0.spanID == parent && $0.operation == .itemLookup } &&
+ events.contains { matches($0) && $0.parentSpanID == parent && $0.operation == .trashedItem }
+ }
+
+ private static let callbackOperations: Set = [
+ .itemLookup, .enumerateItems, .enumerateChanges, .currentSyncAnchor, .fetchContents,
+ .createItem, .modifyItem, .deleteItem, .materializedItemsChanged, .workingSetRefresh,
+ .favoriteItem, .duplicateItem, .restoreTrashedItem,
+ ]
+}
diff --git a/PotassiumProviderCore/StabilityLiveStatus.swift b/PotassiumProviderCore/StabilityLiveStatus.swift
new file mode 100644
index 0000000..08e303b
--- /dev/null
+++ b/PotassiumProviderCore/StabilityLiveStatus.swift
@@ -0,0 +1,23 @@
+import Foundation
+
+public struct StabilityLiveStatus: Codable, Equatable, Sendable {
+ public enum State: String, Codable, Sendable { case preflight, awaitingPermissions, running, settling, finished, failed }
+ public let schemaVersion: UInt16
+ public let state: State
+ public let scenario: StabilityFinderScenario?
+ public let recordedAt: Date
+ public init(state: State, scenario: StabilityFinderScenario? = nil) {
+ schemaVersion = 1; self.state = state; self.scenario = scenario; recordedAt = Date()
+ }
+ public func write(to run: StabilityRunHandle) throws {
+ guard try StabilityDiagnosticIdentity.activeRun()?.runID == run.runID,
+ SecurePOSIXFile.pathKind(run.finderReportURL) == .missing else { throw CancellationError() }
+ try SecurePOSIXFile.replaceAtomically(JSONEncoder().encode(self),
+ at: run.directoryURL.appendingPathComponent("live-status.json"), permissions: 0o400)
+ }
+ public static func read(from run: StabilityRunHandle) throws -> Self? {
+ let url = run.directoryURL.appendingPathComponent("live-status.json")
+ guard SecurePOSIXFile.pathKind(url) != .missing else { return nil }
+ return try JSONDecoder().decode(Self.self, from: SecurePOSIXFile.read(url, maximumBytes: 4096))
+ }
+}
diff --git a/PotassiumProviderCore/StabilityRunConfinement.swift b/PotassiumProviderCore/StabilityRunConfinement.swift
new file mode 100644
index 0000000..ce21d2b
--- /dev/null
+++ b/PotassiumProviderCore/StabilityRunConfinement.swift
@@ -0,0 +1,26 @@
+import Foundation
+
+public enum StabilityRunConfinementError: Error, Equatable { case unownedTarget, identityDrift, invalidAncestry }
+
+public enum StabilityRunConfinement {
+ /// Re-fetch every link: a cached URL or previously observed parent is never
+ /// authority to mutate an item after it moved outside this run's subtree.
+ public static func verify(targetID: Int, runRootID: Int, labRootID: Int, driveID: Int,
+ ownedIDs: Set, lookup: @Sendable (Int) async throws -> KDriveRemoteItem) async throws {
+ guard targetID != labRootID, runRootID != labRootID, ownedIDs.contains(targetID), ownedIDs.contains(runRootID) else {
+ throw StabilityRunConfinementError.unownedTarget
+ }
+ var cursor = targetID, seen: Set = []
+ while seen.count < 16 && seen.insert(cursor).inserted {
+ let item = try await lookup(cursor)
+ guard item.id == cursor, item.driveID == driveID else { throw StabilityRunConfinementError.identityDrift }
+ if cursor == runRootID {
+ guard item.parentID == labRootID, item.isDirectory else { throw StabilityRunConfinementError.invalidAncestry }
+ return
+ }
+ guard ownedIDs.contains(item.parentID) else { throw StabilityRunConfinementError.invalidAncestry }
+ cursor = item.parentID
+ }
+ throw StabilityRunConfinementError.invalidAncestry
+ }
+}
diff --git a/PotassiumProviderCore/VaultModificationExecutor.swift b/PotassiumProviderCore/VaultModificationExecutor.swift
new file mode 100644
index 0000000..5d5b18d
--- /dev/null
+++ b/PotassiumProviderCore/VaultModificationExecutor.swift
@@ -0,0 +1,38 @@
+import FileProvider
+import Foundation
+
+/// Keeps combined fields and their acknowledgement in the same path used by the
+/// replicated extension. A trash transition must never discard pending contents.
+public enum VaultModificationExecutor {
+ public struct Result: Sendable {
+ public let item: VaultItem?
+ public let remainingFields: NSFileProviderItemFields
+ public let trashed: Bool
+ }
+
+ public static func execute(current: VaultItem, fields: NSFileProviderItemFields,
+ requestsTrash: Bool, hasContents: Bool,
+ baseContentRevision: VaultRevision, baseMetadataRevision: VaultRevision,
+ modify: @Sendable () async throws -> VaultItem,
+ trash: @Sendable (VaultRevision, VaultRevision) async throws -> Void) async throws -> Result {
+ try Task.checkCancellation()
+ if fields.contains(.contents), !hasContents { throw NSFileProviderError(.cannotSynchronize) }
+ var remaining = fields
+ let modifiedFields = fields.intersection([.contents, .filename, .parentItemIdentifier])
+ .subtracting(requestsTrash ? [.parentItemIdentifier] : [])
+ var updated = current
+ if !modifiedFields.isEmpty {
+ updated = try await modify()
+ remaining.subtract(modifiedFields)
+ if fields.contains(.contents) { remaining.remove(.contentModificationDate) }
+ }
+ if requestsTrash {
+ try Task.checkCancellation()
+ try await trash(modifiedFields.isEmpty ? baseContentRevision : updated.contentRevision,
+ modifiedFields.isEmpty ? baseMetadataRevision : updated.metadataRevision)
+ remaining.remove(.parentItemIdentifier)
+ return Result(item: nil, remainingFields: remaining, trashed: true)
+ }
+ return Result(item: updated, remainingFields: remaining, trashed: false)
+ }
+}
diff --git a/PotassiumProviderCore/WorkingSetSync.swift b/PotassiumProviderCore/WorkingSetSync.swift
index 816c58f..9859c8d 100644
--- a/PotassiumProviderCore/WorkingSetSync.swift
+++ b/PotassiumProviderCore/WorkingSetSync.swift
@@ -1,4 +1,5 @@
import Foundation
+import InfomaniakConcurrency
public struct KDriveMaterializedItem: Codable, Equatable, Sendable {
public let fileID: Int
@@ -28,6 +29,14 @@ public struct KDriveWorkingSetContainerSnapshotUpdate: Equatable, Sendable {
}
}
+extension KDriveSnapshot {
+ func isSameAdvancedListingResult(as other: KDriveSnapshot) -> Bool {
+ usesAdvancedListing && other.usesAdvancedListing && isFullyEnumerated && other.isFullyEnumerated &&
+ serverCursor != nil && serverCursor == other.serverCursor &&
+ items.sorted { $0.id < $1.id } == other.items.sorted { $0.id < $1.id }
+ }
+}
+
public struct KDrivePartialActivityResult: Equatable, Sendable {
public let fileID: Int
public let lastAction: String?
@@ -82,6 +91,11 @@ public struct KDriveWorkingSetChanges: Equatable, Sendable {
}
}
+public enum KDriveWorkingSetCommitCondition: Sendable {
+ case unconditional
+ case matchingAnchor(String?)
+}
+
public protocol KDriveWorkingSetStateStoring: Sendable {
func snapshot(domainIdentifier: String, containerIdentifier: String) async throws -> KDriveSnapshot?
func replaceMaterializedItems(
@@ -105,8 +119,37 @@ public protocol KDriveWorkingSetStateStoring: Sendable {
containerSnapshotUpdates: [KDriveWorkingSetContainerSnapshotUpdate],
items: [KDriveRemoteItem],
changes: KDriveSnapshotChangeSet,
- completedAt: Date
+ completedAt: Date,
+ condition: KDriveWorkingSetCommitCondition
) async throws -> KDriveWorkingSetSnapshot
+ func publishKnownWorkingSetItem(_ item: KDriveRemoteItem, replacing expectedItem: KDriveRemoteItem?,
+ domainIdentifier: String, recordedAt: Date) async throws -> Bool
+}
+
+public extension KDriveWorkingSetStateStoring {
+ func commitWorkingSetPoll(domainIdentifier: String, containerSnapshotUpdates: [KDriveWorkingSetContainerSnapshotUpdate],
+ items: [KDriveRemoteItem], changes: KDriveSnapshotChangeSet,
+ completedAt: Date) async throws -> KDriveWorkingSetSnapshot {
+ try await commitWorkingSetPoll(domainIdentifier: domainIdentifier, containerSnapshotUpdates: containerSnapshotUpdates,
+ items: items, changes: changes, completedAt: completedAt, condition: .unconditional)
+ }
+}
+
+/// Deliver committed journal entries before waiting for another remote crawl.
+/// Empty journals still refresh normally; an expired anchor stays expired.
+public enum KDriveWorkingSetChangeDelivery {
+ public static func changes(domainIdentifier: String, from anchor: String,
+ store: any KDriveWorkingSetStateStoring,
+ refresh: @escaping @Sendable () async throws -> Void) async throws -> KDriveWorkingSetChanges? {
+ try Task.checkCancellation()
+ if let cached = try await store.workingSetChanges(domainIdentifier: domainIdentifier, from: anchor),
+ !cached.changes.isEmpty { return cached }
+ // Keep the refresh inside the callback lifetime. Polling cooperatively
+ // stops when a newer journal supersedes its starting snapshot.
+ try await refresh()
+ try Task.checkCancellation()
+ return try await store.workingSetChanges(domainIdentifier: domainIdentifier, from: anchor)
+ }
}
public struct KDriveWorkingSetPollOutcome: Equatable, Sendable {
@@ -130,6 +173,7 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
public static let pollingInterval: TimeInterval = 60
public static let latestItemLimit = 200
public static let partialActivityBatchSize = 200
+ private static let maximumConcurrentContainerPolls = 4
private let domainIdentifier: String
private let driveID: Int
@@ -156,8 +200,18 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
public func poll(
now: Date = Date(),
- minimumInterval: TimeInterval = Self.pollingInterval
+ minimumInterval: TimeInterval = Self.pollingInterval,
+ coalescePendingMaterializationPolls: Bool = false,
+ onSnapshotRetry: @Sendable () async -> Void = {}
) async throws -> KDriveWorkingSetPollOutcome {
+ try await WorkingSetPollScheduling.shared.withPermit(domainIdentifier: domainIdentifier,
+ coalescePending: coalescePendingMaterializationPolls) {
+ try await pollExclusively(now: now, minimumInterval: minimumInterval, onSnapshotRetry: onSnapshotRetry)
+ }
+ }
+
+ private func pollExclusively(now: Date, minimumInterval: TimeInterval,
+ onSnapshotRetry: @Sendable () async -> Void) async throws -> KDriveWorkingSetPollOutcome {
let claimed = try await stateStore.claimWorkingSetPoll(
domainIdentifier: domainIdentifier,
now: now,
@@ -171,6 +225,33 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
)
}
+ // Enumeration can commit a newer container while the remote poll is
+ // in flight. Retry the complete read/prepare/transaction, never the
+ // stale write. Failed transactions leave every cursor and watermark
+ // unchanged. Keep the original throttle claim and bound contention.
+ for attempt in 0...2 {
+ try Task.checkCancellation()
+ do { return try await pollClaimed(now: now) }
+ catch WorkingSetPollSuperseded.newerJournal {
+ return KDriveWorkingSetPollOutcome(didPoll: false,
+ changes: KDriveSnapshotChangeSet(updatedItems: [], deletedItemIDs: []),
+ snapshot: try await stateStore.workingSetSnapshot(domainIdentifier: domainIdentifier))
+ }
+ catch let error as KDriveSnapshotStoreError where error == .staleSnapshot(
+ domainIdentifier: domainIdentifier, containerIdentifier: "working-set") {
+ return KDriveWorkingSetPollOutcome(didPoll: false,
+ changes: KDriveSnapshotChangeSet(updatedItems: [], deletedItemIDs: []),
+ snapshot: try await stateStore.workingSetSnapshot(domainIdentifier: domainIdentifier))
+ }
+ catch KDriveSnapshotStoreError.staleSnapshot where attempt < 2 {
+ await onSnapshotRetry()
+ try await Task.sleep(for: .milliseconds(250 * (attempt + 1)))
+ }
+ }
+ preconditionFailure("The final poll attempt returns or throws")
+ }
+
+ private func pollClaimed(now: Date) async throws -> KDriveWorkingSetPollOutcome {
let oldWorkingSet = try await stateStore.workingSetSnapshot(domainIdentifier: domainIdentifier)
let lastSuccessfulPoll = try await stateStore.lastSuccessfulWorkingSetPoll(domainIdentifier: domainIdentifier)
let materialized = try await stateStore.materializedItems(domainIdentifier: domainIdentifier)
@@ -185,8 +266,16 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
var deletedItemIDs = Set()
var containerSnapshotUpdates: [KDriveWorkingSetContainerSnapshotUpdate] = []
- for folderID in materializedContainerIDs.sorted() {
- let result = try await pollMaterializedContainer(folderID: folderID)
+ // Independent folders can be read together, but their cursors and the
+ // working-set journal still commit as one guarded transaction. Bound
+ // requests instead of making Finder wait for every folder serially.
+ let containerResults = try await materializedContainerIDs.sorted().concurrentMap(
+ customConcurrency: Self.maximumConcurrentContainerPolls
+ ) { folderID in
+ try await self.requireWorkingSetAnchor(oldWorkingSet?.anchor)
+ return try await self.pollMaterializedContainer(folderID: folderID)
+ }
+ for result in containerResults {
relevantItems.append(contentsOf: result.snapshot.items)
changedItems.append(contentsOf: result.changes.updatedItems)
deletedItemIDs.formUnion(result.changes.deletedItemIDs)
@@ -198,6 +287,7 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
let relevantFileIDs = Set(relevantItems.map(\.id)).union(materializedFileIDs)
let partialSince = lastSuccessfulPoll ?? Date(timeIntervalSince1970: 0)
for fileIDBatch in relevantFileIDs.sorted().chunked(maximumCount: Self.partialActivityBatchSize) {
+ try await requireWorkingSetAnchor(oldWorkingSet?.anchor)
let activities = try await workingSetRemote.listPartialActivities(
driveID: driveID,
fileIDs: fileIDBatch,
@@ -239,16 +329,27 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
updatedItems: updates,
deletedItemIDs: deletedItemIDs.sorted()
)
+ try await requireWorkingSetAnchor(oldWorkingSet?.anchor)
let snapshot = try await stateStore.commitWorkingSetPoll(
domainIdentifier: domainIdentifier,
containerSnapshotUpdates: containerSnapshotUpdates,
items: currentItems,
changes: changes,
- completedAt: now
+ completedAt: now,
+ condition: .matchingAnchor(oldWorkingSet?.anchor)
)
return KDriveWorkingSetPollOutcome(didPoll: true, changes: changes, snapshot: snapshot)
}
+ private func requireWorkingSetAnchor(_ expected: String?) async throws {
+ try Task.checkCancellation()
+ if try await stateStore.workingSetSnapshot(domainIdentifier: domainIdentifier)?.anchor != expected {
+ // Discard prepared container updates. The new journal is already
+ // durable; this poll must not overwrite it or advance watermarks.
+ throw WorkingSetPollSuperseded.newerJournal
+ }
+ }
+
private func currentItem(
forMoveOut activity: KDrivePartialActivityResult
) async throws -> KDriveRemoteItem? {
@@ -421,6 +522,61 @@ public struct KDriveWorkingSetPollCoordinator: Sendable {
}
}
+private enum WorkingSetPollSuperseded: Error { case newerJournal }
+
+/// Materialization notifications request an immediate poll while enumeration
+/// and the timer can also poll. The persisted interval is a throttle, not an
+/// in-flight lock (notably when the interval is zero). Serialize these callers
+/// per domain while retaining cancellation and the snapshot transaction guards.
+actor WorkingSetPollScheduling {
+ static let shared = WorkingSetPollScheduling()
+ private struct Entry {
+ let limiter = AsyncOperationLimiter(maxConcurrentOperations: 1)
+ var clients = 0
+ var requestNumber: UInt64 = 0
+ var completed: (coveredRequest: UInt64, outcome: KDriveWorkingSetPollOutcome)?
+ }
+ private var entries: [String: Entry] = [:]
+
+ func pendingRequestCount(domainIdentifier: String) -> Int { entries[domainIdentifier]?.clients ?? 0 }
+
+ func withPermit(domainIdentifier: String, coalescePending: Bool = false,
+ operation: @Sendable () async throws -> KDriveWorkingSetPollOutcome) async throws -> KDriveWorkingSetPollOutcome {
+ var entry = entries[domainIdentifier] ?? Entry()
+ entry.clients += 1
+ entry.requestNumber += 1
+ let requestNumber = entry.requestNumber
+ entries[domainIdentifier] = entry
+ defer {
+ if var remaining = entries[domainIdentifier] {
+ remaining.clients -= 1
+ entries[domainIdentifier] = remaining.clients == 0 ? nil : remaining
+ }
+ }
+ return try await entry.limiter.withPermit {
+ try await self.perform(domainIdentifier: domainIdentifier, requestNumber: requestNumber,
+ coalescePending: coalescePending, operation: operation)
+ }
+ }
+
+ private func perform(domainIdentifier: String, requestNumber: UInt64, coalescePending: Bool,
+ operation: @Sendable () async throws -> KDriveWorkingSetPollOutcome) async throws -> KDriveWorkingSetPollOutcome {
+ try Task.checkCancellation()
+ guard let entry = entries[domainIdentifier] else { throw CancellationError() }
+ if coalescePending, let completed = entry.completed, completed.coveredRequest >= requestNumber {
+ return KDriveWorkingSetPollOutcome(didPoll: false,
+ changes: KDriveSnapshotChangeSet(updatedItems: [], deletedItemIDs: []), snapshot: completed.outcome.snapshot)
+ }
+ // Every request in this batch persisted its materialized set before
+ // entering the queue. Only a successful poll which STARTS after those
+ // requests may satisfy them; arrivals during its I/O require another poll.
+ let coveredRequest = entry.requestNumber
+ let outcome = try await operation()
+ if outcome.didPoll { entries[domainIdentifier]?.completed = (coveredRequest, outcome) }
+ return outcome
+ }
+}
+
private extension Array {
func chunked(maximumCount: Int) -> [[Element]] {
guard maximumCount > 0, isEmpty == false else { return [] }
diff --git a/README.md b/README.md
index be6f217..f370764 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,8 @@ data.
- [File Provider Lifecycle](doc/FILE_PROVIDER_LIFECYCLE.md): Apple callbacks,
known-folder locations, mutations, enumeration, and SQLite touch points.
- [Contextual Actions](doc/CONTEXTUAL_ACTIONS.md): Finder/Files favorite,
- duplicate, restore, share-link, and version-history actions.
+ duplicate, restore, share-link, and version-history actions, including verification
+ of returned sharing settings before reporting success.
- [Listing And Versioning](doc/LISTING_AND_VERSIONING.md): how Apple
enumeration, sync anchors, kDrive listing APIs, SQLite caching, and item
versions fit together.
@@ -53,10 +54,16 @@ data.
recovery limits, and the mandatory maintenance procedure.
- [Conflicts](doc/CONFLICTS.md): conflict cases, current resolution behavior,
design context, risks, and safer future direction.
+- [Conflict testing](doc/CONFLICT_TESTING.md): deterministic two-client cases, production
+ callback coverage, vault replay, and independently selected live Finder races.
- [File Provider Cleanup](doc/FILE_PROVIDER_CLEANUP.md): local development
uninstall script, reset modes, stale registration repair, and safety boundary.
- [Testing And Development](doc/TESTING_AND_DEVELOPMENT.md): schemes,
- dependencies, commands, and local-state caveats.
+ dependencies, commands, and the opt-in live Finder suite using the saved
+ Keychain account and an isolated lab inside `Private`. Permanent deletion is
+ deferred by default; `--include-permanent-deletion` enables its confirmation.
+- [Stability implementation audit](doc/STABILITY_LOOP_AUDIT.md): retained live
+ failures, focused fixes, finalized validation results, and remaining gates.
## Project Shape
diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md
index 00197c7..536be78 100644
--- a/doc/ARCHITECTURE.md
+++ b/doc/ARCHITECTURE.md
@@ -55,8 +55,9 @@ flowchart LR
- `PotassiumProviderCore` owns typed provider models, persistence protocols,
OAuth utilities, and the `PotassiumKDriveService` adapter.
- `potassiumChannel` owns the typed request builders and service calls for
- Infomaniak APIs. The Xcode project requires the published 0.2 release line,
- while `Package.resolved` locks validated builds to potassiumChannel 0.2.0.
+ Infomaniak APIs. `Package.resolved` locks validated builds to tag `0.3.0` at
+ commit `db829f1f2bd8c2113a529c9c521bd5cdfb5ef4dc`; the version-pinned adapter
+ matrix in `doc/STABILITY_LOOP_AUDIT.md` must be rerun before changing it.
- The app group is the shared storage boundary between app and extension.
- The keychain access group is the shared credential boundary. Tokens are keyed
by local account identifier.
diff --git a/doc/CONFLICT_RESOLUTION_TRUTH_TABLE.md b/doc/CONFLICT_RESOLUTION_TRUTH_TABLE.md
index cc96e5f..e471b56 100644
--- a/doc/CONFLICT_RESOLUTION_TRUTH_TABLE.md
+++ b/doc/CONFLICT_RESOLUTION_TRUTH_TABLE.md
@@ -15,6 +15,511 @@ below are independently normative for their respective domain type.
## Merge Integration Audit Status
+2026-09-13 merge scope: the operator accepted the unapplied file-comments setting
+as a non-blocking limitation for this PR because comments are outside the critical
+path. CR-027 remains open as a follow-up; the app's warning, strict scenario failure,
+and retained evidence are unchanged. This decision does not establish full-suite
+acceptance or change CR-013 and the deferred permanent-deletion scenario.
+
+OS scope of the September 12 evidence: the live Finder/Actions and conflict runs
+used the same arm64 macOS 26.6.2 (25G83) host as the retained Mac unit results.
+Focused simulator results cover iOS 26.5 (23F73) and visionOS 26.5 (23O470), with
+Xcode 26.5 (17F42). Live reports do not embed OS metadata; see the audit's OS-version
+section for provenance and the required rerun after an OS upgrade. No compatibility
+with the next macOS release is established by these results.
+
+2026-09-12 continuation: fresh runs `c6d8e57e-ce9b-4179-b9c8-444835c00023`
+and `17d1e74b-8bb0-4672-89cf-fb84e3c7bf20` sealed 14 passed, one failed and
+permanent deletion unselected. Current warm run
+`07253623-3e2b-4e3b-8362-14eaa4f471be` also sealed 14 passed, one failed and
+deletion unselected. Cancellation, Trash restoration, working-set refresh, link
+disabling and historical-copy byte preservation all verified. Scenario 16 remains
+failed solely for unapplied comments. These results are not full-suite acceptance;
+CR-013 remains open.
+Detailed retained-run evidence and validation commands are in
+[STABILITY_LOOP_AUDIT.md](STABILITY_LOOP_AUDIT.md).
+
+- Four SQLite read-before-write paths now reserve the writer with `BEGIN IMMEDIATE`.
+ All four synthetic contention cases failed with SQLite code 5 before this change
+ and pass afterward. Existing timeouts, atomic rollback, snapshots and watermarks
+ remain guarded. No lock spans network work. CR-025 is mitigated; the exact earlier
+ live lock owner remains unobserved.
+- Plaintext Trash returns managed trashed metadata, preserving a local conflict
+ copy's identity/version when both items were trashed. The extension sets the
+ Trash parent/flag and excludes that result from active working-set publication.
+ Combined-field regressions and repeated fresh/warm Restore scenarios pass.
+ CR-026 is mitigated; this does not prove every earlier missing menu had one cause.
+- Finder panel binding deduplicates the same remote AX root across attested hosts,
+ requires its owned Finder host, and rejects distinct/ambiguous roots. One bounded
+ replacement of an owned window can recover missing provider commands. A genuine
+ Finder Copy Stop now cancels the provider fetch, followed by an independent
+ same-item recovery with exact local/server bytes. Fresh and warm runs verify this.
+- Share updates omit unchanged fields except explicit comment intent: documented
+ omitted/null `can_comment` inherits editing. They omit an already-absent expiry,
+ encode null only to clear an existing expiry, preserve explicit password rotation,
+ and skip an empty update after an authoritative read. No conditional write
+ guarantee is implied. The Actions view compares all reported settings before
+ success; password secrecy and whole-second date representation are respected.
+ Comments still reverted on generated fixtures under inherited and public access,
+ including a comments-only request. The temporary public comparison link was
+ disabled and independently re-read as absent. The strict scenario stays failed;
+ [SHARE_SETTINGS_REPRODUCTION.md](SHARE_SETTINGS_REPRODUCTION.md) records the
+ remaining service/integration question. CR-027 remains open.
+- Plaintext version history uses Form because the hosted List hid Restore even with
+ child containment. Manual installed-app validation exposed its stable button and
+ successfully restored a historical copy. Confirmations require one exact leaf
+ sheet, prompt, recovery message and enabled button pair. AXPress can report failure
+ after the restore API succeeds; the driver presses once, rejects a pre-existing
+ success, waits for a new success result, then requires independent server/byte
+ checks. It never repeats a possibly executed restore.
+- The warm working-set target's exact new metadata arrived within 17 seconds, but
+ through a callback started in the previous scenario. Its original correlation
+ correctly failed the gate. The runner now settles prior callbacks within a
+ separate 90-second preparation before creating the working-set fixture; the
+ scenario retains its 90-second budget. Metadata, parent-callback, correlation,
+ process and server checks remain unchanged. No event is relabelled. The current
+ warm run passed working-set refresh, mitigating CR-028.
+
+All six targeted conflict cases also pass in both fresh and already-running
+extension profiles on the current build (12 sealed independent passes); the exact
+run matrix is in the audit document. These do not replace the failing comments
+condition or authorize permanent deletion.
+
+The current full macOS Stability unit target passes 558 tests. Shared API,
+returned-setting and callback selections pass 24 tests each on iOS and visionOS
+Simulator; the final version form also builds on both platforms. The earlier
+operator-interrupted run and rejected evidence candidates remain retained and do
+not establish acceptance.
+
+The provisioning-corrected `27e5dc9` retry sealed 12 passed, three failed and
+deletion deferred. Mouse-driven menus failed before the Actions panel launched;
+a separate confined probe found another process receiving the intended Finder hit.
+Native input now verifies exact process/scope ancestry before mouse-down and treats
+obstruction as an environment failure, without traversing or capturing the other
+app. Nineteen focused regressions passed; complete live verification remains open.
+No mutation/conflict policy changes or CR-013 closure follow from this UI diagnosis.
+
+Actions access in the sealed `a961bf5` retry remained blocked before API work
+(13 passed, two failed, deletion deferred). Its embedded profile lacked the shared
+App Groups grant despite claiming it in the signature. Both extensions now enable
+profile registration; installed-bundle preflight rejects unauthorized group claims,
+identity mismatches and expired/malformed profiles. This fixes configuration and
+strengthens evidence qualification without changing sharing/conflict policy. The
+alias-timeout regression uses an explicit held-read gate after full Mac CI exposed
+its relative-sleep race. Live confirmation remains required; CR-013 stays open.
+
+The `2ebe661` retry encountered a confirmed macOS data-access prompt while the
+Actions view synchronously read its diagnostic run alias. The operator accepted
+it. Stability now resolves that alias off the UI actor with bounded/cancellable
+waiting, publishes it once, and keeps rendering/native AX binding free of file I/O.
+Loading can be dismissed without authorizing any mutation. Regressions cover
+UI-thread confinement, absent runs, deadline and cancellation. The retained run
+remains failed (13 passed, two failed, deletion deferred); contextual acceptance
+must be rerun after consent. No conflict/sharing policy or CR-013 state changes.
+
+Fresh `8af5313` live evidence confirms canonical selection resolution and visible
+share settings. The next divergence is harness discovery: FileProviderUI exposes
+this hosted panel through AXMainWindow with an empty AXWindows list, and the SwiftUI
+identifier is absent from the native tree. Stability now publishes the resolved
+alias on its AppKit root and includes the attested main window; unrelated windows
+remain rejected. No sharing/conflict policy changes, and CR-013 stays open. The
+bundle remains 13 passed, two failed, one deferred until complete UI/server evidence
+can verify contextual actions and cancellation.
+
+The current Actions extension exposed a macOS opaque document identifier directly
+to the kDrive parser in fresh run `8a4d7d89-48f1-4a54-81c5-f9266e0934cd`.
+The UI boundary now resolves through system URL/identifier APIs, checks the returned
+domain, and validates against the configured engine before loading or mutating.
+Wrong-domain, unresolved, virtual-container, timeout, cancellation and cross-engine
+regressions fail closed. Canonical identity also binds the Stability panel alias;
+its separate AX process requires the installed executable's path and code hash.
+No conflict policy changes. The run sealed 13 passed, two failed, deletion deferred;
+cancellation and live verification of the Actions correction remain open. CR-013
+remains open regardless of disposable-file outcomes.
+
+The toolbar path passed the first ten live scenarios but omitted provider Restore
+and favorite actions. It is now limited to the two verified built-in download
+commands; provider actions and selected deletion retain their item context menu.
+Parameterized routing regressions protect this distinction. Native session-level
+pointer delivery and cursor-position observations still require live verification.
+No missing command is replaced by broader selection or Empty Trash; CR-013 stays open.
+
+Granted event-posting permission and the revised pointer sequence did not resolve
+the subsequent missing-menu trace. The harness now prefers Finder's named Actions
+toolbar menu for the exact bound selection, with negative confinement regressions.
+Actual popup, command, server and callback evidence remain required. This is a UI
+routing change awaiting live verification; CR-013 remains open.
+
+A later trace narrowed the menu failure to posted pointer input with no visible
+popup, before any mutation. Native input now clears modifiers, moves before clicking
+and revalidates the target, with cancellation/release regressions. Ordinary UI waits
+remain capped at 90 seconds inside transfer scenarios. These harness changes need
+live verification and do not alter conflict policies or CR-013.
+
+A later fresh run failed before eviction/contextual command dispatch, with no
+current Actions callback. Its bundle stays failed; native cancellation was not
+exercised. Sanitized menu observations now distinguish anchor, popup and command
+selection failures. All four advanced cases create independent fixtures and may
+continue after an earlier failure, without changing the failed result or relaxing
+conflict/cancellation evidence. CR-013 and mutation policy remain unchanged.
+Popup selection excludes menu-bar commands and subordinate menus, with negative
+regressions for ambiguous roots. This harness correction does not change conflict
+or deletion policies; confirmation in a live run is still required.
+
+Fresh `765a651` evidence confirms favorite, unfavorite and duplicate UI/server
+operations after adding the optional metadata include. The final share panel was
+observed loading; the runner timed out without the Stability panel identifier.
+Process inspection identified an older Actions binary from a separate installed
+app, despite correct replicated-provider evidence. Preflight now rejects ambiguous
+Actions registrations; final code-hash validation remains mandatory. The next run
+must verify the current UI extension before attributing a panel defect to this
+branch. Cancellation's active progress ring was observed in Finder without a
+labeled Cancel button; a confined native click is now attempted only at actual
+intermediate progress and must still produce real cancellation evidence. Conflict
+policies, full acceptance requirements and CR-013 are unchanged.
+
+Fresh run `fdaa23ad-7080-4463-9cf0-456982831b03` sealed with 13 passed, two failed
+(cancellation and contextual actions), and deletion deferred. Working-set refresh
+passed. Computer use observed Share/History/Duplicate but no favorite actions for
+the generated fixture. Requests omitted optional favorite state while the activation
+rules correctly require known true/false. The adapter now includes `is_favorite`
+(or `files.is_favorite` on advanced listings), preserving nil when omitted. The
+ordinary-listing ETag fallback retains the favorite include. Parameterized request/
+mapping regressions cover known true, known false, and unknown without changing
+identity, names, parentage, contents, or the existing last-writer-wins favorite
+policy. Advanced ETag rejection and CR-013 remain unchanged; live proof of the
+favorite-action correction is pending.
+
+The timed `1fd42e7` live run passed 12 scenarios with deletion deferred; both
+transfers completed uncancelled despite Download Now returning in 0.57 seconds.
+The harness now observes appended local diagnostics at 50 ms without the API
+poller's 2/4/8/10-second backoff, confines progress to the callback's item/process/
+parent span, and stops looking for a cancel control after completion. Working-set
+and contextual checks continue independently with fresh fixtures after an earlier
+failure; that failure remains in the report. Regression coverage rejects unrelated
+or terminal-only progress, contradictory cancellation terminals, cursor rotation,
+missing telemetry, and historical replay. No mutation policy or CR-013 state changes. The lazy-download regression now checks the complete diagnostic lifecycle and monotonic intermediate progress instead of assuming at most one sampler tick; exact bytes, request identity and lazy start remain required.
+
+The next live run verified weighted transfer progress at 10–90%, but both transfers
+completed before cancellation could be invoked. Download Now now dispatches a
+confined native menu click and returns after popup dismissal, with transfer
+completion owned by diagnostic assertions. Sequence regressions forbid waiting
+for transfer completion in that dispatch path and retain result checks for other
+actions. Cancellation still requires actual Finder cancellation, exactly one
+callback terminal, no later success, and a subsequent successful download. This
+changes only harness scheduling; CR-013 and conflict policies are unchanged.
+
+The next live retry stopped before eviction because the harness required an
+advertised `AXShowMenu` action for a secondary-click route. Computer use confirmed
+the generated fixture's native menu remained available. The driver now uses the
+unique exact display-name field and its confined geometry, retaining independent
+URL/domain/selection checks. Negative target regressions reject ambiguous names
+and absent/out-of-window geometry. This is an automation correction; CR-013 and
+all mutation policies remain unchanged. The progress fix still awaits a live
+transfer attempt after this earlier failure.
+
+The first deletion-deferred live run passed the real preserve-both race but could
+not cancel either transfer: diagnostics showed only zero and terminal completion.
+Transfer diagnostic sampling now uses Foundation's `fractionCompleted`, including
+weighted child work, instead of dividing the parent's integer unit counts. The
+parent can remain at zero units while its child has transferred 40% or 90%; a new
+regression verifies both intermediate buckets and cancellation without a later
+success. This corrects progress evidence without simulating transfer or weakening
+cancellation acceptance. Focused validation finalized 25 passing tests on each
+Mac profile and each requested simulator, with zero failures/skips; the signed
+generic visionOS build passed. Live confirmation remains pending.
+
+2026-09-11 operator-directed selection: original Finder runs now defer permanent
+deletion before any re-trash or confirmation, then continue the independent
+preserve-both, transfer, working-set, and contextual-action scenarios. Explicit
+`--include-permanent-deletion` retains exact-item confirmation and `deleteItem`
+evidence. Report schema 3 records an untested deletion, never a pass; schemas 1/2
+remain readable. Selection, command forwarding, version compatibility, and strict
+telemetry regressions cover this change. CR-013 remains open and no deletion
+contract or conflict resolution policy changes.
+
+CI `34578669227` passed both Mac profiles and iOS, but the visionOS cancellation
+test threw a synthetic gate timeout (retained xcresult: 380 passed, one failed).
+The five-second fake replacement/arrival gates and worker polling are replaced
+with cancellable signals plus a one-minute overall test limit. Cancellation is
+still checked after gate release, the worker terminal is awaited, and server bytes,
+staged recovery bytes, no Trash mutation, and exactly one completion are required.
+New gate-ordering coverage checks release-before-arrival and cancellation/release
+races. The corrected suite finalized 475 passing Stability Mac tests and 384
+passing tests on each requested simulator, with zero failures/skips; the signed
+generic visionOS build also succeeded. Standard Mac UI validation remains pending.
+
+2026-09-11 continuation: working-set preparation now permits at most four
+independent materialized-folder reads at once using the existing
+InfomaniakConcurrency dependency. Results retain folder order and still commit in
+one transaction guarded by every container snapshot and the starting working-set
+anchor. Failure/cancellation discards the batch without advancing cursors or the
+successful watermark; throttling propagates its Retry-After metadata without a
+poll-local retry. A superseding journal stops new folder work; up to four reads
+may already be in flight. `WorkingSetSyncTests` adds gated overlap, ordered state,
+partial-batch nonpublication, cancellation, and throttling regressions. The retained
+large-crawl failure motivates this latency correction; live mitigation of CR-022
+remains pending until a complete original-suite rerun. No conflict policy or
+permanent-delete guarantee changes. The 381-test iOS and visionOS Simulator runs
+finalized successfully on this runtime (`potassium-finish-ios-01.xcresult` and
+`potassium-finish-vision-sim-01.xcresult`); CI `34573732948` also passes unit coverage
+on all platforms. The subsequent full standard Mac run finalized 415 passing tests
+including UI, and the Stability profile finalized 465 passing tests; both have zero
+failures/skips. Original live acceptance on the new runtime remains pending.
+
+Diagnostic subscriptions now capture their initial file state before returning the
+stream, preventing an immediately appended event from being absorbed into a later
+worker baseline. The cross-store regression appends without a startup sleep.
+The real SQLite WAL-contention regression uses dedicated opener/release queues so
+its synchronous busy wait cannot starve the cooperative task that releases its
+own test lock. Production SQLite timeout/transaction behavior is unchanged.
+The poll-scheduling regression now uses cancellable fake-I/O gates and a bounded
+overall test deadline after a CI-only timing failure; it still requires a subsequent
+poll for arrivals during I/O, no poll for cancelled waiters, and an empty final queue.
+Production scheduling policy is unchanged; CI `34576895561` passed the gated
+regressions on both Mac profiles and the requested simulators.
+
+2026-09-10 conflict-matrix continuation: the production plaintext and vault
+`modifyItem` sequences now run through shared injected executors. Regression
+coverage checks combined contents/name/parent changes, pending fields, contents
+plus Trash, invalid missing-content requests, cancellation, and the production
+error mapper. The vault callback previously took an early Trash branch and returned
+no pending fields, silently ignoring combined edits. It now commits supported
+edits first, then trashes using the returned revisions; a failed edit never invokes
+Trash. Unsupported fields, including currently unsupported standalone vault dates,
+remain pending. This is a callback correctness fix, not a new vault conflict policy.
+`ModificationCallbackTests` records the affected decision cells below.
+
+`CreationCallbackTests` and the catalog's `.mayAlreadyExist` cases exercise the
+production plaintext create router. Reconciliation hints preserve the existing
+conservative create policy: existing bytes are never overwritten by name; file
+replay depends on the modeled upload token, while directory replay can create a
+second usable directory. CR-009 remains mitigated with explicit unresolved gaps.
+Vault replay regressions now check the independent canonical metadata winner and
+preserved original content winner, minimize content-preservation failures as well
+as replay divergence, and verify duplicate journal rejection leaves SQLite state
+unchanged. No resolution policy changed for these cases.
+
+Permanent-delete preflight now uses the typed Trash metadata endpoint when supplied
+by the remote service, instead of treating active-item 404 as authoritative absence
+of a trashed identity. `ConflictDeletionTests` keeps active metadata unavailable
+while verifying deletion against the exact Trash identity and repeated authoritative
+absence. Existing version guards and the open CR-013 server race remain unchanged.
+
+The edit/move conflict profile now treats a stale local destination URL as pending
+within its existing deadline, using the same exact parent/name predicate as Restore.
+A completed server mutation alone cannot prove Finder has applied the returned
+parent. `FinderRestoreObservationTests.delayedMoveLocationCannotPassUntilParentAndNameBothMatch`
+rejects stale paths and wrong names and preserves cancellation. Run-specific
+reproduction and supporting successful backend spans are in the local audit.
+The bounded live rerun still timed out with a mismatching local parent; extending
+the observation did not resolve it. Successful plaintext modify terminals now
+include the existing run-salted metadata fingerprint to distinguish returned
+callback metadata from server state, without recording names or identifiers.
+`FileProviderOperationLifecycleTests.successfulCallbackRecordsReturnedMetadataOnce`
+checks that only the first successful terminal retains that fingerprint. The
+original preserve-both scenario shares the independent conflict path's cancellable
+gate scheduling and byte-checked reopening. No resolution policy changed.
+
+Instrumented live edit/move confirmed a returned-metadata mismatch, but an
+experimental post-upload metadata lookup did not resolve it and was removed.
+The typed move API returns a cancellable operation response. The harness now
+verifies the competing mutation's metadata and bytes before releasing the held
+local callback; request acceptance alone never establishes commit ordering.
+Attempt-scoped read-back evidence is required by conflict profile version 3 and
+newly sealed original preserve-both results. Barrier/profile regressions reject
+missing, unrelated, cancelled, late, and stale-attempt proof. Historical bundles
+remain readable but do not establish this stronger ordering requirement.
+
+Subsequent settled-cache evidence matched the returned callback fingerprint. The
+earlier harness comparison used metadata obtained before waiting for upload bytes;
+it now refetches metadata after byte verification. Destination observation first
+opens the bound parent in Finder, then verifies the actual parent URL resolves to
+the expected stable item and domain, together with the exact filename. A different
+parent/domain remains pending; URL spelling alone does not define provider identity.
+`FinderRestoreObservationTests.providerParentIdentityHandlesURLAliasesAndRejectsStaleParents`
+covers alias spelling, wrong parent/domain, and wrong name. These are harness fixes;
+no production mutation policy changed.
+
+Working-set delivery now publishes confirmed plaintext modify/direct-action results
+to the existing SQLite change journal before signaling. The merge requires that
+the journal's item still matches the caller's pre-mutation observation (or already
+equals the result); a competing writer is never overwritten. Publication neither
+advances remote cursors nor changes poll timestamps, and failure cannot replay an
+already committed remote mutation. Configured roots are not published as ordinary
+children. Poll commits compare their starting working-set anchor transactionally
+so an older in-flight crawl cannot overwrite the publication. Enumeration delivers
+available journal changes before starting another crawl. An in-flight poll checks
+for a superseding journal between folder/activity requests, discards its prepared
+container changes, and returns without advancing its successful watermark. Refresh
+remains within enumeration; no detached delivery worker survives that callback.
+Empty journals retain normal polling and expired anchors remain errors.
+`WorkingSetMutationDeliveryTests` and `WorkingSetSyncTests` cover persistence,
+competing writers, stale-poll rollback, watermark preservation, immediate delivery,
+supersession during remote reads, and expired anchors. This changes notification
+latency, not remote mutation or conflict policy. The complete fresh and already-running
+conflict profiles pass on `05e8e5f`; the original sixteen-scenario acceptance remains
+separate and in progress.
+
+Live evidence settling now uses a local one-second quiet interval after exactly
+one start and terminal per span, checked every 500 ms. Server Retry-After/backoff
+is unchanged. `StabilityDiagnosticSettlementTests` rejects pending work, missing
+starts, duplicate terminals, and premature quiet periods after new events.
+Version-2 launch evidence certifies extension **process** continuity using kernel
+birth, signing, and diagnostic identity. Apple's replicated-provider contract
+permits object invalidation/recreation inside one process; those events require
+complete successful spans and remain in the timeline. A process replacement,
+missing identity, mismatched build, failed lifecycle callback, or incomplete
+lifecycle telemetry still rejects acceptance. Version-1 proofs retain their original
+stricter object-lifetime interpretation and remain readable. The live warm false
+rejection has a failing old-code regression in `StabilityExtensionLaunchEvidenceTests`;
+corrected validation/reruns are recorded in the audit. Rejected candidates remain
+ineligible for acceptance; CR-013 stays open.
+
+Fresh live content races subsequently identified SQLite `BUSY` (primary code 5)
+while opening the snapshot store for `currentSyncAnchor`. Snapshot connections now
+install their existing five-second busy timeout before requesting WAL, so the
+initial database query can wait for transient cleanup/recovery contention.
+`SnapshotInitializationContentionTests` opens the production store under a real
+exclusive WAL lock, releases the lock, and checks retained data plus subsequent
+snapshot use. The regression failed on the previous order and passed after the
+correction on both macOS profiles and the iOS/visionOS simulators. All six fresh and
+six already-running conflict cases subsequently passed without a recurring SQLite
+failure. No transaction guard, schema, conflict policy, or remote
+mutation retry is changed. The precise lock owner in the live run is unobserved.
+
+The original Finder suite subsequently passed navigation and hydration but timed
+out before invoking eviction. Its domain-wide stabilization prerequisite completed
+no UI actions for that scenario. Hydration now closes its generated TextEdit
+document after byte verification; a failed close cannot complete the step.
+Eviction then uses fresh item/domain binding and the real Finder action without
+waiting for unrelated domain work. Non-hydrating download-state verification and
+final diagnostic settling are unchanged. `FinderHydrationSequenceTests` covers
+presenter release and failure gating. This is a macOS Stability harness correction,
+not a provider eviction or conflict-policy change; live rerun evidence is recorded
+in the audit and no new sixteen-scenario pass is inferred.
+The following live run verified eviction and a new download, then stopped at
+Restore navigation after successful exact trashed-item binding. Ten scenarios
+passed; no Restore action or permanent deletion occurred. Closed navigation-stage
+diagnostics support the next reproduction without weakening target assertions.
+The diagnostic reproduction located that timeout at Go to Folder destination
+verification. Bound trashed fixtures now use the existing-window native Finder
+target command, retaining exact parent verification and fresh item/domain binding
+after navigation. `FinderTrashedItemSequenceTests` rejects identity replacement
+during navigation and prevents actions after an unavailable parent. Restore still
+requires its provider callback and complete remote/local destination proof;
+permanent deletion retains its separate exact-item confirmation and subsequent
+revalidation. This UI route is pending live verification and changes no server policy.
+
+The deterministic matrix and independent live conflict profile are documented in
+`CONFLICT_TESTING.md`. Targeted runs explicitly skip unrelated scenarios and cannot
+certify the original sixteen-scenario suite. Current validation and preserved live
+failure/rerun evidence are in `STABILITY_LOOP_AUDIT.md`.
+
+An earlier live run passed ten scenarios through Trash, then failed Restore
+because the metadata callback queried the active-item endpoint for a trashed
+identity (HTTP 404 mapped to `.cannotSynchronize`). Metadata lookup now falls
+back to the typed Trash endpoint only after active-item HTTP 404, validating the
+same drive and item identity before returning a Trash parent. Only HTTP 404 from
+both identity endpoints becomes `.noSuchItem`; permissions, transport failures,
+cancellation, unavailable fallback, and mismatched identities fail closed.
+Content and mutation preflights continue to require active metadata. The
+`KDriveItemMetadataLookupTests` regressions cover these decisions. Live run
+`dc49fe60-e9e5-45fe-8261-85fd4339f5cc` verified successful Trash metadata
+fallback, exact Finder selection, and a completed Restore callback. Its independent
+active-item verification raced restoration and received 404. The harness now waits
+for the attested, item-specific Restore completion before checking active metadata;
+404 remains pending within the existing deadline, never success. Authentication,
+other operational failures, and wrong identity/drive/destination still fail.
+`FinderRestoreObservationTests` covers that boundary. A later run verified remote
+restoration but exposed a UI assertion gap: an identity-bound local URL could still
+point into Trash. Restore now also requires the expected local parent and filename
+before selecting the item. The regression rejects stale Trash locations and wrong
+names; its live rerun remains pending. The earlier eleven-scenario report cannot
+establish this stronger Restore acceptance. The preserved spans are recorded in
+`STABILITY_LOOP_AUDIT.md`; no change to the open CR-013 guarantee is claimed.
+
+Immediate materialization notifications, working-set enumeration, and the timer
+can request overlapping polls; `minimumInterval: 0` is not an in-flight lock.
+Polls are serialized per domain with the existing cancellation-aware
+`AsyncOperationLimiter`. The registry releases an idle domain after its last
+caller. Snapshot transactions and the bounded enumeration-race retries remain
+required. `WorkingSetSyncTests.immediateMaterializationPollsCannotOverlapEachOther`
+checks four simultaneous forced polls and a maximum of one active remote request.
+
+The live continuation also exposed redundant queued materialization polls taking
+up to 61 seconds. Pending materialization requests may reuse a successful poll
+only when that poll started after their materialized-set observations were
+persisted. A request arriving during remote I/O requires a subsequent poll;
+failed or throttled polls cannot satisfy queued observations. Other explicit
+polls retain their original behavior. `WorkingSetPollSchedulingTests` covers
+coalescing, failed-first-poll recovery, arrivals during I/O, and cancellation.
+No snapshot transaction guard or successful watermark is bypassed.
+
+An additional live failure identified materialization work continuing after its
+replicated instance was invalidated. The acknowledged callback launched an
+untracked task that retained the instance; `invalidate()` cancelled only its
+periodic poll. An instance-owned `FileProviderBackgroundWork` scope now rejects
+new work and synchronously requests cancellation of registered refresh tasks on
+invalidation. A short registration mutex bridges the synchronous system callback;
+task bodies and cancellation handlers execute outside it. A replacement instance
+owns a separate scope. The materialization acknowledgement still completes once
+before background refresh; it is never held for remote work or repeated on cancel.
+Cancellation is checked before loading refresh state and after reading materialized
+items, while existing poll checks and transaction guards remain in force. Cancelled
+working-set refreshes receive a cancellation terminal. `FileProviderBackgroundWorkTests`
+covers no subsequent request after cancellation, repeated invalidation, rejected
+late work, independent replacement instances, and immediate-completion registration.
+Live rerun is required; this does not establish why the prior process exited.
+
+`StabilityConflictBarrierTests` isolates the Stability-only conflict barrier from
+the live active-run pointer and covers exact item/correlation matching, wrong-run
+release rejection, successful release, cancellation, and deadline expiry. This
+does not change the server's conditional mutation contract or close CR-013.
+
+2026-09-10 live follow-up: real folder navigation reproduced a working-set
+snapshot compare-and-swap rejection after successful remote calls. The atomic
+rollback is retained. A claimed poll may repeat the full read/prepare/commit up
+to two times after `staleSnapshot`, with cancellable backoff and observable
+`concurrentSnapshot` checkpoints; it never retries a stale write or advances the
+watermark on a rejected transaction. Persistent contention still fails.
+`WorkingSetSyncTests.concurrentEnumerationIsRetriedWithoutAdvancingARejectedWatermark`
+covers both convergence and exhausted retries with actual concurrent SQLite saves.
+Current live validation is recorded in `STABILITY_LOOP_AUDIT.md`. CR-013 remains open.
+
+If concurrent enumeration already committed the exact prepared server result,
+the working-set transaction retains that newer container generation and commits
+its change batch without rewriting the container. This exception requires both
+snapshots to be fully enumerated advanced listings, the same non-nil server cursor,
+and equality of every item and metadata field independent of row order. Different
+contents, missing cursors, or different cursors still reject and roll back.
+`WorkingSetSyncTests.equivalentServerResultPreservesNewerGenerationButDifferentContentsStillReject`
+checks unchanged local generation/anchor and rejected deletions with equal cursors.
+
+The live comparison in `e02376a2-19db-45c5-b213-72165a517fe4` accepted a regular
+file's unchanged timestamp and rejected date updates for two positively
+correlated directory subjects with HTTP 400. Omitting the optional directory
+date did not stop the callbacks and that experiment was reverted. Directory
+timestamps now resolve to a freshly fetched authoritative server value without
+calling the file-only mutation route. Apple's SDK contract for `modifyItem`
+explicitly propagates a differing returned field to disk when it is not pending.
+This applies to automatic and explicitly touched plaintext directory dates:
+the server value wins. File dates and encrypted-vault metadata are unchanged.
+`KDriveMutationCoordinatorTests.directoryTimestampResolvesToServerValueWithoutFileOnlyMutation`
+covers both directory spellings; `fileTimestampMutationIsAppliedAndRefetched`
+protects file behavior. Repeated live passes of the first five scenarios,
+including `abb74c1a-43a8-4f04-a994-53fbf9042656`, verified this fix without
+recurrence of the directory date 400. Full 16-scenario acceptance remains open.
+
+2026-09-10 live diagnostic follow-up: partial-activity requests now use the
+upstream `with=file` expansion instead of `file,file.etag`, with exact request
+coverage in `KDriveAPIEvidenceTests.partialActivitiesRequestsOnlySupportedFileExpansion`.
+The first live navigation run recorded partial-activity 422 and standalone
+modification-date 400 failures. A failed response still leaves the working-set
+watermark unchanged; no error is suppressed and no destructive decision changes.
+The reproduced request defects have live rerun evidence above. CR-013 remains
+open; no underlying server permanent-delete guarantee has changed.
+
- Integration reviewed: 2026-08-10.
- Merge inputs: encrypted-vault head `a0e0839` and `origin/main` at `f042b7e`.
- macOS `build-for-testing` and the complete `potassiumProviderTests` target
@@ -32,6 +537,8 @@ below are independently normative for their respective domain type.
| Scenario | Required deterministic result | Destructive action permitted? | Regression evidence |
|---|---|---:|---|
+| File Provider callback combines supported edits and Trash | Commit supported edits first; trash only after success using the committed content and metadata revisions. Failed edits prevent Trash; unsupported fields remain pending. | Reversible Trash only after successful edit | `ModificationCallbackTests.vaultCombinedTrashUsesCommittedRevisionsAndPreservesUnsupportedFields`, `vaultMissingContentsCannotTrashOrModify` |
+| File Provider callback contains unsupported fields or standalone vault modification date | Return those fields pending; never acknowledge a mutation the vault service did not perform. | No | `ModificationCallbackTests.vaultUnsupportedFieldsAndDateAreNotFalselyAcknowledged` |
| Concurrent edits change the same file content from one base | Canonical winner retains the logical UUID; every loser becomes a stable conflict copy with independently authenticated metadata. Replay order cannot change the result. | No | `VaultJournalTests.concurrentContentEditsConvergeForEveryReplayOrder` |
| One concurrent edit changes content and another changes metadata | Merge both independent changes. | No | `VaultJournalTests.independentConcurrentContentAndMetadataEditsMerge` |
| Concurrent metadata edits disagree | Canonical transaction ordering selects the visible metadata and emits an opaque metadata conflict. | No | Existing randomized reducer coverage; dedicated expansion remains desirable. |
@@ -75,9 +582,10 @@ Local success does not close EV-014.
- macOS: `xcodebuild build-for-testing -destination 'platform=macOS'` succeeded
with signing and indexing disabled. Direct execution then passed 33 focused
tests: all journal, provisioning/maintenance, cryptography, and domain-format
- suites. The normal local macOS test host ran the activation-model assertions
- but hung while finalizing its Xcode result bundle, so this evidence does not
- claim a clean full-host exit.
+ suites. The 2026-09-08 profile-reliability run later produced a finalized
+ standard macOS unit result with 319 passed and zero failures, superseding the
+ earlier incomplete local-host observation. Its exact commands and result
+ bundle evidence are recorded in `STABILITY_LOOP_AUDIT.md`.
- iOS Simulator: the complete `potassiumProviderTests` target passed on
`platform=iOS Simulator,OS=26.5,name=iPhone 17` with signing and indexing
disabled.
@@ -116,8 +624,9 @@ audited truth table takes precedence and the inconsistency must be corrected.
## Legacy Plaintext Audit Status
-- Last source audit: 2026-08-13
-- Audited baseline: `codex/conflict-resolution-hardening` working tree
+- Last source audit: 2026-08-31
+- Audited baseline: `codex/file-provider-stability-loop` milestone 3 working
+ tree, retaining the 2026-08-13 mutation decisions
- Validation:
- `potassiumChannel`: `swift test` — 559 tests passed
- macOS: `KDriveMutationCoordinatorTests` — 25 tests passed (the selected
@@ -138,7 +647,7 @@ audited truth table takes precedence and the inconsistency must be corrected.
return `etag`; a matching `If-Match` replacement succeeds and changes the
ETag; a stale ETag is rejected with 409 or 412. The advanced listing routes
reject both `etag` and `files.etag` include resources with HTTP 422. The
- provider uses the desktop-compatible `files.capabilities` resource. A
+ provider uses the compatible `files.capabilities,files.is_favorite` resources. A
remaining 422 is surfaced as `.cannotSynchronize`, rather than mixing an
advanced change cursor with ordinary-listing pagination. Advanced-listing
snapshot ETags remain nullable and content mutations fail closed until
@@ -147,6 +656,96 @@ audited truth table takes precedence and the inconsistency must be corrected.
visionOS Simulator using the `potassiumProviderTests` target. It covers
initial and continued advanced listings, ETag exclusion, and propagation
of an unexpected 422 without changing listing protocols.
+ - 2026-08-31 Stability diagnostics validation passed thirteen focused macOS
+ tests, including a real subprocess writer, interrupted-tail recovery,
+ tombstone parity, unresolved-conflict preservation, paging/statistics,
+ cross-store observation, export/redaction, and completed-only retention.
+ In Stability only, activity and conflict evidence moves from SQLite to a
+ redacted append-only JSONL run bundle. Snapshot, anchor, enumerator, and
+ working-set state remains SQLite. Clear/domain removal use tombstones and
+ retain unresolved, blocked, and failed conflict events exactly as the
+ standard event-store contract requires. Uninstall cleanup removes only
+ local event/snapshot state through the selected store; no remote mutation,
+ conflict decision, staged-content cleanup, or hard purge was added.
+ - 2026-08-31 callback and typed-network instrumentation added actor-isolated
+ start/terminal spans and TaskLocal correlation. Diagnostic sink failures are
+ swallowed, File Provider completions remain exactly-once, and cancellation
+ races select only one terminal phase. `NSFileProviderError` recovery cases
+ are classified without retaining user info, expected share-link absence is
+ successful, and lazy transfers do not emit start-only evidence before
+ consumption. The instrumentation observes the
+ existing changed-field, conditional-mutation, preserve-both, and cleanup
+ decisions; it does not retry, replay, or alter any mutation. Focused span,
+ lifecycle, request-shape, and redaction tests are the regression evidence.
+ - 2026-09-01 Stability Lab validation passed 47 focused macOS tests across
+ diagnostics/run leasing, pure safety, and injected remote-lifecycle suites after the complete
+ Stability app/extension/test graph built successfully. Provision rejects
+ ordinary/existing lab domains and external or maintenance drives before a
+ mutation. Drive discovery establishes internal membership, while exact
+ locally persisted and remote root/marker evidence establishes lab-root
+ ownership. Provisioned roots are direct drive-root children; marker upload
+ uses conflict-as-error. Reset requires exact confirmation, complete bounded
+ pagination, matching local/remote marker evidence, and fresh root/marker/
+ child-parent plus system-registration validation before each reversible
+ trash request. A cross-process lifecycle lease excludes active/new runs
+ for the full reset. Root, marker,
+ and permanent-delete endpoints are not representable in the reset plan.
+ No live account or remote mutation was used for this validation. The final
+ signed hosted command exited 0 with `TEST EXECUTE SUCCEEDED` and a finalized
+ result bundle.
+ - 2026-09-01 Finder-runner implementation added a 16-step validated report,
+ immutable closed-schema assertions/API observations, read-only permission
+ preflight, and an explicit `--yes-live` command gate. Automated validation
+ uses only model, evidence-writer, parser, and checkpoint tests; it reads no
+ credential and performs no Finder or remote action. A live run was not
+ performed. Saved/system domain plus root/marker safety is rechecked before
+ every scenario. The runner never calls permanent deletion directly: restore
+ and permanent-delete scenarios retain the verified lab-root selection and
+ checkpoint without opening the user-global Trash or directing a destructive
+ action. Cancellation leaves the exact evicted item selected in Finder and
+ checkpoints without starting a transfer that could outlive the report;
+ contextual actions likewise checkpoint rather than invoking the remote API.
+ Existing-item mutation URLs (including the move destination) must resolve
+ to their expected File Provider item and configured domain immediately
+ before mutation; eviction reuses that single validated identifier. The
+ cached lab-root URL is rebound as the configured domain's root container
+ after baseline pagination and immediately before scenario execution.
+ Enumeration requires both item-listing and anchor/change terminal evidence,
+ and checkpoint reasons are scenario-bound.
+ Final sealing verifies all assertion/failure/checkpoint totals against the
+ immutable report. The historical 2026-09-01 focused Finder evidence run
+ reported all 33 cases passed before its Xcode result-log finalization issue.
+ It is superseded for macOS by the 2026-09-08 finalized full Stability unit
+ result: 327 passed with zero failures. The iPhone 17 iOS 26.5 Simulator and
+ generic visionOS Stability app/extension graphs also built successfully.
+ No live credential, Finder action, or remote mutation was used.
+ `CR-013` stays open for the product callback.
+ - 2026-09-01 API-evidence validation pinned the complete provider/context
+ operation matrix to potassiumChannel 0.3.0, the captured public API
+ revision, and the official iOS/Android/desktop commits recorded in
+ `STABILITY_LOOP_AUDIT.md`. Adapter fixtures cover inherited and unknown
+ share access, explicit-null expiration clearing, explicit duplicate names,
+ the one-billion-byte direct-upload boundary, and retry-safe 408/429
+ classification. A signed Stability test graph built successfully; the
+ historical selected API slices reported 34/34 and then 16/16 passes. The
+ 2026-09-08 finalized full Stability macOS unit result (327 passed, zero
+ failures) supersedes the earlier incomplete macOS result-log evidence. No
+ live credential, network request, or remote mutation was used. `CR-017`
+ through `CR-020` record the corrected/mitigated discrepancies; `CR-021`
+ remains open because share mutations expose no documented conditional
+ version token.
+ - 2026-09-01 completion validation built the signed Stability test graph and
+ ran the full unit-test target on macOS, iPhone 17 iOS 26.5 Simulator, and
+ Apple Vision Pro visionOS 26.5 Simulator; it also built the generic visionOS
+ graph with signing disabled. That historical cross-platform run emitted only
+ passing terminal cases, but its incomplete result logs are not current
+ macOS acceptance evidence. The 2026-09-08 finalized macOS profile results
+ report 319 standard and 327 Stability passes, each with zero failures; exact
+ commands and the current local-host limitation are in
+ `STABILITY_LOOP_AUDIT.md`. No live or remote mutation check ran. A final
+ added-line privacy scan also replaced the last credential-shaped test
+ literal with a runtime-only UUID canary; both affected suites then reported
+ 46/46 passes and the reviewer returned PASS.
- Finding state vocabulary: **Open**, **Mitigated**, or **Resolved**
Unit tests validate isolated coordinator operations, including a remote change
@@ -173,8 +772,11 @@ The table is derived from these implementation boundaries:
maps provider and API failures to File Provider errors.
- [`FileProviderEnumerator`](../potassiumProviderFileProvider/FileProviderEnumerator.swift)
validates listing, cursor, and snapshot state.
-- [`ProviderEventStore`](../PotassiumProviderCore/ProviderEventStore.swift) and
- the Activities UI record conflict state but do not replay failed mutations.
+- [`ProviderEventStore`](../PotassiumProviderCore/ProviderEventStore.swift),
+ [`StabilityDiagnostics`](../PotassiumProviderCore/StabilityDiagnostics.swift),
+ and the Activities UI record conflict state but do not replay failed
+ mutations. Stability serialization removes private item/account context while
+ retaining the decision state needed for this register.
Apple's replicated File Provider contract is also normative:
@@ -214,29 +816,69 @@ Infomaniak's public API contract documents the primitives used here:
`revisedAt` and size are diagnostic. Legacy timestamp versions and missing
ETags fail closed into preserve-both or `.failOnConflict` behavior.
+### 2026-09-10 live-run implementation evidence
+
+The PR-based live runner now uses Finder/Accessibility and TextEdit, item aliases,
+parent spans, process/build identity, strict v2 report validation, and the cancellable
+Stability-only post-preflight conflict barrier. New lab roots are created under
+verified `Private` using the authorized existing OAuth Keychain account. Initial
+live provisioning exposed fractional-date ownership-marker mismatch; canonical
+seconds plus registration resume corrected it, with codec/parent safety regression
+coverage. Real authentication, marker readback, and domain registration succeeded.
+The first permission-blocked run records zero passed scenarios. Live 16/16 cold/warm
+acceptance remains outstanding until finalized bundles demonstrate it. See the
+implementation audit for updated run/test results. `CR-013` is still **Open**.
+
## Legacy Plaintext Core Mutation Truth Table
+### Stability Lab Provisioning And Cleanup
+
+| Request or conflict | Predicate | Current action | Server mutation | Data-loss assessment | User recovery |
+| --- | --- | --- | --- | --- | --- |
+| Provision lab root | No saved or registered domain; selected drive has one internal non-maintenance discovery record; explicit drive root resolves as a directory | Verify the server-created `Private` directory and its drive-root parent, create one unique child below it, upload the fixed random marker with `conflict=error`, re-read both, persist exact root/marker evidence, then register File Provider | Creates a directory and marker file | Low. Internal discovery proves membership, while created-and-matched root/marker evidence proves lab ownership. Partial provisioning is never auto-cleaned, so a failed local save/registration can leave an orphaned development folder but cannot delete unrelated data. | Inspect the dedicated development drive and remove an abandoned folder manually only after verifying its marker. |
+| Marker collision or incomplete provisioning | Marker upload/verification fails | Stop, retain any created root, and do not register it or issue compensating deletion | No additional mutation after failure | Low data-loss risk; possible empty/orphaned lab root. | Verify the marker and remove the orphan manually from the dedicated account. |
+| Reset lab contents | Exact confirmation; root is a non-root child of its verified `Private` parent (or a historical top-level lab) and has matching created-and-persisted ownership evidence; marker and registered lab domain match; complete bounded listing | Exclude root and marker; before each action re-fetch root, marker, and target parent; call reversible trash only for a still-immediate child | Trashes verified immediate children | Low. There remains an unavoidable request-time race after the final metadata fetch, but trash is reversible and the target stable ID was inside the verified lab root at preflight. | Restore an item from kDrive trash if the reset intent was wrong. |
+| Missing/ordinary/unknown domain, wrong-profile runtime, encrypted domain, stale marker/root, partial/cyclic listing, moved target, or active run | Any safety predicate fails; registration is re-queried and the run lifecycle lock is held through reset | Reject before the affected mutation; never hard purge or permanently delete | No | Safe fail-closed behavior. | Use the documented dry-run plus safe uninstall path, repair registration/marker state, then retry. |
+| Finder Stability existing-item mutation | Explicit `--yes-live`; exact lab preflight is fresh; the user-visible URL resolves once immediately before the mutation to the expected stable item ID and configured domain, and that validated identifier is reused for eviction. A move also resolves its destination directory to the expected stable ID and domain. | Perform the scenario mutation only while both bindings match; otherwise fail the step before changing local or remote state. | Edit, rename, move, and trash use the normal File Provider callback path. Eviction is local only. Preserve-both setup deliberately combines a direct conditional remote replacement with a bound local write. | Low. Stable-ID/domain binding closes same-path replacement drift; a narrow request-time race remains after the final lookup, while trash remains reversible and content/version conflicts retain the existing preserve-both policy. | Correct the lab/Finder state and retry. Restore trash or compare preserved versions if a later callback fails. |
+| Finder Stability root-targeted create | Explicit `--yes-live`; immediately before each scenario the cached visible root URL still resolves as the File Provider root-container identifier in the configured lab domain, in addition to fresh saved/registered/remote lab evidence | Create the scenario file or directory only below that bound root URL; otherwise fail before the local write | Normal File Provider create callback path | Low. The repeated root-container/domain binding prevents a stale cached mount path from redirecting a write outside the lab; a narrow request-time race remains after resolution. | Repair File Provider registration/consent or the lab mount, then retry. |
+| Finder Stability restore/permanent-delete scenarios | Explicit live opt-in; generated run ownership and ancestry; trashed item resolves to the exact stable ID and provider domain; exact selected fixture confirmation before irreversible deletion and fresh binding afterward | Invoke Restore or selected-item Delete Immediately through Finder. Restore requires the completed item-specific callback, matching remote identity/parent/bytes, and exact local destination parent/name before UI selection. A stale Trash URL cannot pass. Native deletion dialogs must match the complete quoted display name read from the bound URL, including Finder's hidden-extension behavior, and exactly one scoped dialog with Cancel/Delete controls. URL/domain/item binding remains independent of display text (`FinderDeletionDialogTests`). Retain a blocked/failed result when exact identity or control is unavailable. Never Empty Trash. | Provider restore action or `deleteItem` callback; no direct API substitute | `CR-013` remains open: a disposable-file success does not create a server-side conditional-delete guarantee. | Retain evidence and run fixtures on failure; inspect the exact item without broadening Trash selection. |
+| Stability conflict ordering barrier | macOS Stability lab only; active run/case/step, attempt, scheduling point, and salted item alias match | Hold content before/after preflight or metadata after preflight until the competing remote operation completes; release on error/cancellation and enforce a deadline | Original real conditional request after release; normal preserve-both policy handles the response | No production policy change. Missing barrier arrival or missing two-version evidence cannot pass the live test. | Stop the run, retain staged/generated data, and inspect correlated spans. |
+| Ownership marker codec compatibility | Remote numeric dates and local ISO-8601 dates identify the same marker but differ below one second | Normalize only creation-time precision to the persisted seconds; continue exact UUID, root, drive, and parent matching | None for an existing marker | Fixes self-rejection without changing the mutation target or rewriting remote evidence. | Retry registration using the saved ownership record. |
+| Snapshot save or working-set write overlaps another SQLite writer | Save, poll claim, poll commit, or known-mutation publication needs to read then write shared state | Reserve the writer with `BEGIN IMMEDIATE`, bounded by the existing five-second busy timeout, then read and validate the current predicates. Preserve all snapshot/item/anchor guards and atomic rollback. | None; network work runs outside these transactions | Low. Avoids an immediate deferred read-to-write upgrade failure without forcing stale state or advancing a rejected poll's successful watermark. Persistent contention still fails. | Allow the system to retry after contention clears. `SnapshotWriteContentionTests`, `SnapshotGenerationPagingTests`, and working-set delivery/sync tests cover contention and retained guard behavior. |
+
| Request or conflict | Predicate | Current action | Server mutation | Data-loss assessment | User recovery |
| --- | --- | --- | --- | --- | --- |
| New file with no collision known locally | Always | Stage first, then upload by parent/name with `conflict=rename`, SHA-256, and deterministic `client_token`; request `with=etag`. Remove the stage only after success. | Creates an item | Low. The returned server item is authoritative and replay uses the same token. | None if successful; a failed create retains an unindexed staged copy. |
| New file collides with an existing name or type | Server applies rename policy | Create a visible uniquely named item; never request server-side overwrite/versioning. | Creates a second item | Low byte-loss risk; a safe duplicate is possible for `.mayAlreadyExist`. | Compare/delete the duplicate if it represents the same file. |
+| Direct create or replacement exceeds `1_000_000_000` bytes | Callback URL file size or loaded payload byte count is above the documented direct-upload maximum | Reject before mapping an oversized callback file into `Data`, retain the post-read count check for size/read races, and return `.cannotSynchronize` until a file-backed session adapter exists | No | No unsupported request is sent. The callback source remains File Provider-owned, but this early rejection does not create a separate provider conflict-stage copy; availability is blocked for large files. | Keep the File Provider source available and use a session-capable official client, or retry after session uploads are implemented. |
| New directory collides by name or type | Recognized HTTP 409, or named 422 collision | Retry once with a conflict filename. | Creates a second directory | Low byte-loss risk, but response-shape coverage is not live-validated. | Rename/merge folders if the response was not recognized. |
| Local content edit; remote unchanged | `C`, conditional upload succeeds | Stage first, then replace by `file_id` with `If-Match`, SHA-256, and deterministic token; remove stage only after success. | Conditional content replace | Low. A remote race cannot silently pass the checked ETag. | None. |
-| Advanced folder enumeration | API rejects `etag` or `files.etag` in advanced-listing `with` | Request `files.capabilities`; if the advanced route still returns HTTP 422, surface `.cannotSynchronize` and retain the prior snapshot/anchor. Do not substitute ordinary-directory pagination because it omits advanced actions and has incompatible cursor semantics. Snapshot items have no authoritative content ETag, so later content mutations preserve both or fail on conflict until direct ETag metadata is refreshed. | No mutation | Low byte-loss risk; synchronization pauses rather than committing an actionless response against an advanced anchor. | Retry after the service or provider is corrected; conflict copies retain local bytes when direct ETag metadata is still unavailable. |
+| Advanced folder enumeration | API rejects `etag` or `files.etag` in advanced-listing `with` | Request `files.capabilities,files.is_favorite`; if the advanced route still returns HTTP 422, surface `.cannotSynchronize` and retain the prior snapshot/anchor. Do not substitute ordinary-directory pagination because it omits advanced actions and has incompatible cursor semantics. Snapshot items have no authoritative content ETag, so later content mutations preserve both or fail on conflict until direct ETag metadata is refreshed. | No mutation | Low byte-loss risk; synchronization pauses rather than committing an actionless response against an advanced anchor. | Retry after the service or provider is corrected; conflict copies retain local bytes when direct ETag metadata is still unavailable. |
| Remote changes after preflight | `C`, conditional upload rejects with 409/412 | Refetch and upload a renamed conflict copy from the same staged bytes. | Creates a second item | Low. Both versions are preserved. | Compare or merge the visible files. |
| Local content edit vs already-changed remote content | `!C && U` | Upload a renamed conflict copy and leave the original unchanged. | Creates a second item | Low. Both versions are preserved. | Compare or merge the visible files. |
| `.failOnConflict` content conflict | `!C`, or conditional 409/412 | Do not mutate kDrive; return `.localVersionConflictingWithServer`; keep staged bytes and record recovery path. | No | Low immediate loss risk. This is intentional user-intervention behavior. | Reveal/export recovery copy, compare versions, then retry the desired change. |
| Staging fails | Stage write fails before any server mutation | Propagate local storage failure. | No | High: provider could not obtain its own durable copy, though File Provider still owns the callback URL. | Free local space and retry; no provider copy exists. |
| Preflight lookup fails after staging | `item(...)` fails | Return mapped retryable error and retain deterministic staged bytes. | No | Medium: bytes survive, but an event cannot always be indexed without authoritative parent metadata. | Let File Provider retry; unindexed copies require support/developer recovery. |
| Replace/conflict upload fails after staging | `!U` | Return mapped retryable error; retain stage; indexed conflict failures appear in Activities. | No confirmed success | Low immediate loss risk; provider-owned scheduling is still absent. | File Provider retries; Activities can reveal/export indexed recovery bytes. |
+| Replacement committed but response lost | Restart still has the old base ETag; current remote bytes may already equal the attempted edit | Conservatively take stale-content preserve-both path. Do not infer remote success solely from equal bytes | May create a redundant conflict copy | Bytes are preserved, but no-duplicate-effect acceptance is not met. `ConflictMatrixTests` records this CR-009 limitation; the model does not establish a server retry guarantee. | Compare the two preserved versions before removing an unwanted duplicate. |
+| Directory create committed but response lost | Restart retries parent/name without a persisted server-assigned identity | Existing collision policy creates a second conflict-named directory | May create a second directory | No byte loss, but child placement/reconciliation remains a CR-009 limitation. Deterministic tests verify both original and retry directory remain usable. | Merge or rename after inspecting both directories. |
| Rename vs remote rename/move | Stable file ID exists | Local name intent wins. Retry a recognized collision with a unique conflict name, then refetch. | Renames item | Low byte-loss risk. The remote same-field name loses by explicit policy. | Inspect final unique name; no byte merge required. |
| Retried rename already reflected remotely | `D` | Return latest item without another mutation. | No | Safe idempotent success. | None. |
| Move-only vs remote rename | Destination differs; local name unchanged | Move stable ID with `name=nil`, preserving the remote rename; kDrive uses `conflict=rename`. | Moves item | Low. Independent fields merge automatically. | None. |
| Combined move+rename vs remote metadata | Stable file ID exists | Local destination and name win; move uses `conflict=rename`; refetch authoritative item. | Moves/renames item | Low byte-loss risk. Same-field metadata follows explicit local intent. | Inspect server-selected unique name if collision occurs. |
-| Trash vs remote content/metadata | Stable file ID exists | Apply local trash intent after other requested fields. Remote bytes remain recoverable in trash. | Trashes item | Low immediate risk; trash is reversible. | Restore from trash if the intent was wrong. |
+| Trash vs remote content/metadata | Stable file ID exists | Apply local trash intent after other requested fields. Return the resulting managed item with Trash parent and `isTrashed` user info, rather than nil (which requests deletion of the local replica). If a combined edit preserved a local conflict copy, trash both versions and return that local copy's identity/version. Exclude the returned trashed item from active working-set publication. | Trashes item, or both preserved versions after a content conflict | Low immediate byte-loss risk; remote bytes remain recoverable in trash. `ModificationCallbackTests` covers simple/combined fields and competing content; live Restore eligibility still needs verification. | Restore from trash if the intent was wrong. |
| Permanent delete; remote matches base | `C && B` | Delete trashed item by stable ID. | Destructive delete | Residual high-impact race: Infomaniak documents no conditional delete token. | None after accepted deletion. |
| Permanent delete vs remote change | `!(C && B)` | Do not delete; return `.deletionRejected` containing latest trashed item. | No | Low. File Provider can recreate the item locally. | Review the recreated item and retry deletion if still desired. |
-| Permanent delete already completed | Latest lookup returns 404 | Return idempotent success. | No | Safe; prevents ghost/stuck deletion. | None. |
+| Permanent delete already completed | Authoritative Trash identity lookup returns 404 | Return idempotent success. | No | Safe; prevents ghost/stuck deletion. | None. |
+| Favorite or unfavorite | Stable item ID exists; no conditional favorite version is documented | Apply the explicit local favorite intent, refetch authoritative metadata, and invalidate both the old and returned parent containers | Changes favorite state only | Low. A same-field remote race is last-writer-wins, but no file bytes or hierarchy are changed. | Toggle the favorite state again if the final value is not desired. |
+| Duplicate contextual action | Source metadata refetch succeeds | Derive an explicit extension-preserving `copy` name, send it in the duplicate body, then refetch the returned stable ID; never rely on `{}` or server-selected naming | Creates one new item | Low. The source is unchanged. A destination-name collision may reject the operation without a confirmed mutation. | Choose another name or remove the colliding copy, then retry. |
+| Restore from trash | Trashed metadata is fresh; original parent still exists, otherwise configured drive root is used | Restore stable ID to the explicit verified destination and invalidate trash plus destination | Moves one item out of trash | Low and reversible. The original item bytes are not replaced; destination choice may fall back to root. | Move the restored item to the desired folder or trash it again. |
+| UI action selection identity | macOS may supply a system document identifier rather than the provider ID | Resolve the user-visible URL with the expected domain manager, reverse-resolve the provider ID/domain, require the expected domain and engine-valid ID before loading action data. Files on iOS/visionOS use validated canonical IDs without URL access. | None during resolution | Fail closed for wrong domain, unsupported engine, unresolved/virtual IDs, timeout or cancellation. Never infer identity from names or internal document-ID syntax. | Close the panel, reselect the exact item and retry. `ProviderActionItemResolverTests` covers isolation, timeout and late callback cancellation; fresh `8af5313` confirmed canonical resolution and the loaded Share form. Full contextual-action acceptance remains pending. |
+| Create share link | No current link; configuration has a documented `public`, `inherit`, or valid password access mode | Create the link with explicit capabilities and known access; reject invalid password configuration. The Actions view shows returned settings and reports success only when all reported settings match the request. | Creates link metadata | Medium privacy impact if the chosen access is too broad. Unknown access is rejected; a known but different response now produces a visible error. No file bytes change. `ShareLinkSettingsVerificationTests` covers each reported setting, omitted passwords and API date precision. | Review the displayed server settings or disable the link immediately and create a corrected one. |
+| Update share link | Current link exists; selected access is documented | Read current settings and send changed capabilities/access/date, always retaining explicit comment intent because omission inherits editing. Encode null only to clear an existing expiration; omit unchanged/absent dates. Keep requested password rotation explicit. No-op saves return the read configuration without PUT. A failed preflight prevents the update. Refetch; reject unknown access. The Actions view compares every reported setting and shows an error with the authoritative form on mismatch. | Patches link metadata | Medium. No conditional link version exists, so settings can still race. `KDriveAPIEvidenceTests` covers set/clear/absent dates, minimal patches, no-op saves, password rotation and failed preflight. The corrected live request returned HTTP 200, but comments still reverted; `ShareLinkSettingsVerificationTests` prevents false success. A manual comments-only patch also returned comments disabled; the cause remains unconfirmed. | Review authoritative settings or disable the link; a mismatch does not roll back a partially applied write. |
+| Delete share link | Stable item ID exists; no conditional link version is documented | Delete link metadata unconditionally and record only closed diagnostics | Disables the current share URL | Medium availability impact. A concurrent editor has no ETag protection and the old URL cannot be restored by the provider. | Create a new link and redistribute its URL. |
+| Restore historical version as copy | Selected immutable version ID, explicit current parent, and explicit destination name | Restore the historical revision to a new item and refetch its stable ID; never replace the current item | Creates one new item | Low. Current bytes and version chain remain unchanged; a name collision may reject without overwriting. | Rename the copy or delete it after comparison. |
+| HTTP 408 or 429 | Typed API rejection, with optional Retry-After metadata | Map to `.serverUnreachable` so File Provider can retry. Retain only a parsed nonnegative delta-seconds integer; discard invalid/HTTP-date values and never persist the raw header/body | No confirmed mutation; idempotent tokens and conditional writes govern retries | Low immediate loss risk. A request might have reached the server, so creates reuse deterministic tokens and replacements remain conditional. There is no provider-owned retry scheduler. | Let File Provider retry; use Activities recovery/export if staged content remains blocked. |
### Advanced-Listing Compatibility Regression Evidence
@@ -260,6 +902,7 @@ pending and are never falsely acknowledged.
| Fields in one callback | Branch executed | Applied remotely | Silently unhandled | Assessment |
| --- | --- | --- | --- | --- |
+| Standalone directory content-modification date | Refetch authoritative directory metadata | No timestamp write; resolve the local date to the server's date | None; explicit server-wins policy | Return that date with no pending date field; the SDK propagates it to disk. No directory bytes or child identity change. |
| Contents + filename | Rename, then contents | Both; content replaces the same stable file ID under the requested name | None | Automatic. Conditional content race still preserves both. |
| Contents + parent | Move, then contents | Both; move-only preserves an independent remote rename | None | Automatic. |
| Contents + filename + parent | Combined move/rename, then contents | All three | None | Automatic. |
@@ -272,7 +915,7 @@ pending and are never falsely acknowledged.
| ID | Severity | Finding | Consequence | State |
| --- | --- | --- | --- | --- |
-| `CR-001` | Critical | Combined `changedFields` were mutually exclusive and falsely reported complete. | The implementation now applies move/rename, content/date, and trash in order and returns unsupported fields pending. End-to-end extension callback coverage is still required. | **Mitigated** |
+| `CR-001` | Critical | Combined `changedFields` were mutually exclusive and falsely reported complete. | The production callback executor now has deterministic combined-field regression coverage for both engines, including the corrected vault early-Trash defect. OS-driven combined callback delivery and full live coverage remain required. | **Mitigated** |
| `CR-002` | High | Existing-item mutations had a fetch-then-mutate race. | Content now uses ETag/`If-Match`; permanent delete still lacks a documented conditional server primitive. | **Mitigated** |
| `CR-003` | High | Content replacement addressed latest parent/name instead of stable ID. | Replacement now uses `file_id`, authoritative ETag, and `If-Match`; conditional races preserve both. | **Resolved** |
| `CR-004` | High | Content versions used only `modifiedAt`, and the first ETag implementation did not persist ETags through SQLite snapshot round trips. | Versions now contain stable item ID plus ETag; snapshot schemas and in-place migrations retain ETag/revision metadata; legacy/missing ETags fail closed. | **Resolved** |
@@ -280,7 +923,7 @@ pending and are never falsely acknowledged.
| `CR-006` | High | Contents+trash ignored the new bytes. | Content is replaced/preserved before trash; conflict item and original are both trashed when required. | **Resolved** |
| `CR-007` | Medium | Failed uploads stranded private staged bytes. | Indexed failures have Activities reveal/export and deterministic replay; provider-owned scheduling and Retry Now remain absent. | **Mitigated** |
| `CR-008` | Medium | Stale delete/collision errors caused avoidable soft locks. | Stale permanent delete returns `.deletionRejected`; recognized collisions auto-rename. No `filenameCollision` bounce is needed for handled cases. | **Resolved** |
-| `CR-009` | Medium | Mutation replay was not idempotent. | Direct file create/replace/conflict copy use deterministic client tokens and hashes; directory create and `.mayAlreadyExist` identity reconciliation remain gaps. | **Mitigated** |
+| `CR-009` | Medium | Mutation replay was not idempotent. | Direct file create/replace/conflict copy use deterministic client tokens and hashes; directory create, ambiguous replacement success, and `.mayAlreadyExist` identity reconciliation remain gaps. | **Mitigated** |
| `CR-010` | Medium | Recoverable errors had no resolution signal. | Successful metadata/content/mutation operations signal authentication, quota, reachability, and synchronization errors resolved. | **Resolved** |
| `CR-011` | Low | Listing cursor, action, and snapshot anomalies fail closed. | Folder availability can be temporarily blocked, but ambiguous snapshots are not committed and remote data is not mutated. | **Mitigated** |
| `CR-012` | Low | Stale content edits use staged renamed preserve-both. | Both byte streams are preserved, including a 409/412 race after preflight. | **Resolved** |
@@ -288,6 +931,18 @@ pending and are never falsely acknowledged.
| `CR-014` | Medium | Unsupported File Provider metadata remains pending without an implementation. | The change is not lost, but File Provider can repeatedly resubmit it and soft-lock the item. | **Open** |
| `CR-015` | Medium | Retry cadence is delegated to File Provider; there is no provider-owned indefinite scheduler or Retry Now action. | Staged bytes survive, but recovery can depend on system resubmission or manual export. | **Open** |
| `CR-016` | Medium | New-file create bytes were not provider-staged before the initial upload. | Creates now stage deterministically before request construction and remove the copy only after confirmed success; regression coverage verifies failed creates retain bytes. | **Resolved** |
+| `CR-017` | Medium | Direct uploads had no documented one-billion-byte guard and the provider has no session/chunk adapter. | File Provider create and replace now reject an oversized callback URL before buffering and recheck the loaded count before request construction. The callback source remains File Provider-owned, but this early path creates no separate conflict stage. A file-backed session adapter is still required for availability. | **Mitigated** |
+| `CR-018` | High | Unknown share access widened to public, `inherit` was unavailable, and clearing expiration omitted nullable `valid_until`. | Documented rights are modeled and unknown values fail closed. Update sends null to clear an observed existing expiration, a timestamp to set one, and omits an already absent plan-gated setting. Request fixtures cover all three states and failed preflight. | **Resolved** |
+| `CR-019` | Medium | Duplicate-in-place sent an empty options body and depended on undocumented server-selected naming. | The coordinator refetches the source, derives an explicit extension-preserving copy name, sends it, and refetches the result. | **Resolved** |
+| `CR-020` | Medium | HTTP 408/429 were treated as nonretryable synchronization failures and Retry-After recovery metadata was dropped. | They now map to `.serverUnreachable`; only parsed delta seconds survive. Provider-owned retry cadence remains absent. | **Mitigated** |
+| `CR-021` | Medium | Share update/delete have no documented ETag or conditional version and can race another editor. | Request bodies and response access now fail closed, but accepted share mutations remain last-writer-wins until the API exposes a conditional primitive. | **Open** |
+| `CR-022` | Medium | Working-set change delivery waited behind long materialized-folder crawls despite confirmed local mutation results. | Live callbacks exceeded 90 seconds. Confirmed results now enter the journal with per-item comparison; poll-anchor comparison prevents an older crawl from overwriting them. Available deltas are delivered before polling. Deterministic regressions and complete fresh/already-running conflict profiles pass within the existing deadlines. A later cold original-suite run exposed 359–360-directory serial crawls and a preparation deadline; large materialized-set latency remains unresolved. An earlier original-suite continuation on `d4b5323` sealed failed during deep-seed preparation: the first working-set callback spent 61,235 ms on 423 advanced folder-list requests; all spans eventually completed. Exact visible-URL blocking dependencies remain unconfirmed (see audit). A later harness correction uses Apple’s required working-set signal for remote fixtures and separates targeted conflicts from unused deep-seed preparation; the updated build passed all six fresh and six already-running conflict cases. A subsequent pool of at most four independent folder reads preserved one guarded commit and cleared preparation on `716f907`; the fresh run passed eleven scenarios before a separate deletion-dialog automation failure. Complete fresh/warm acceptance remains pending. The native refresh target must not widen item-specific scenario evidence; all other spans remain required by global settling. | **Mitigated** |
+| `CR-023` | Medium | Live current-sync-anchor callbacks failed immediately on SQLite `BUSY` while opening the snapshot store. | Bounded cause inspection identified primary code 5 in two fresh content races. A real-lock production-store regression failed with the old initialization order and passed after installing the existing timeout before WAL setup. Both macOS profiles and iOS/visionOS targets pass, followed by all six fresh and six already-running conflict cases without recurrence. The precise live lock owner is unobserved. Failed bundles remain evidence. | **Mitigated** |
+| `CR-024` | Medium | Acknowledged materialization callbacks launched untracked work that retained invalidated provider instances. | Instance-scoped cancellation and registration regressions pass on both macOS profiles and iOS/visionOS simulators; generic visionOS builds. A live materialization child now records exactly one cancellation during invalidation, with all 30 prior child requests terminal and none afterward. The failed preparation bundle remains failed. The original process-exit cause is unconfirmed. | **Mitigated** |
+| `CR-025` | Medium | Deferred snapshot/working-set transactions could fail immediately with SQLite `BUSY` when upgrading a read under another WAL writer, despite the configured timeout. | Four synthetic production-store cases reproduced code 5 before the fix. These paths now acquire an immediate write reservation before reading current guards; focused iOS/visionOS persistence tests pass, including stale-state rejection and watermark preservation. The earlier live one-millisecond refresh failure is consistent with this defect but its exact statement and lock owner were not captured. Persistent contention beyond the existing timeout remains possible; live rerun is required. | **Mitigated** |
+| `CR-026` | High | Plaintext Trash completion returned nil, requesting local replica deletion instead of retaining managed Trash metadata. | Return explicit trashed metadata; combined edits retain the local conflict copy identity/version and exclude trashed items from active working-set publication. Combined-field regressions pass on Mac, iOS and visionOS. After Finder relaunch, a sealed fresh run passed Trash and Restore, including exact callback, bytes and destination. Earlier missing menus despite a true daemon activation rule remain a separate UI issue; repeat fresh/warm acceptance is still pending. | **Mitigated** |
+| `CR-027` | Medium | Share create/update could claim success after kDrive returned different capability settings. | Reproduced manually on the generated plain-text fixture: checked comments reverted after Save. The view now displays server settings and compares every reported field before reporting success; password secrecy and whole-second dates are respected. Field regressions pass and a sealed live run verifies the warning. The reason kDrive declines comments for this configuration remains unconfirmed. | **Open** |
+| `CR-028` | Medium | Warm working-set evidence could be delivered by a callback attributed to the previous scenario. | Confirmed exact new metadata arrived within 17 seconds but retained the earlier callback correlation. The runner now settles previous callbacks before creating this scenario’s fixture; no evidence is relabelled. Parent-terminal settlement regression passes only after the older callback closes. Current warm run `07253623-3e2b-4e3b-8362-14eaa4f471be` passes with all correlation and metadata gates preserved. | **Mitigated** |
## Legacy Plaintext User-Recovery Matrix
diff --git a/doc/CONFLICT_TESTING.md b/doc/CONFLICT_TESTING.md
new file mode 100644
index 0000000..10237a3
--- /dev/null
+++ b/doc/CONFLICT_TESTING.md
@@ -0,0 +1,226 @@
+# Conflict testing
+
+The conflict suite exercises production resolution code with two isolated
+persistent clients, production callback executors, and separately opted-in Finder
+races. `CONFLICT_RESOLUTION_TRUTH_TABLE.md` remains normative. Passing a model test
+is not proof of a live server guarantee or a complete Finder acceptance run.
+
+Original-suite runs now defer scenario 12 (permanent deletion) by default and
+continue the independent conflict and remaining scenarios. Add
+`--include-permanent-deletion` to `--run` to select its exact-item confirmation.
+Deferral is untested coverage and cannot satisfy sixteen-scenario acceptance.
+Independent `--conflicts` selection is unchanged.
+
+## Deterministic coverage
+
+`ConflictCase` assigns stable IDs, engine, ordering, expected resolution, and
+recovery assertions. `ConflictMatrixTests` uses a stateful typed service with
+persistent metadata/bytes, conditional versions, parent validation, collision
+allocation, and idempotency tokens. Each client retains its own cached version and
+staged bytes across reconstruction. The service double's idempotency is a specified
+model assumption; it is not new evidence of a kDrive guarantee. The catalog records
+competing operations and unresolved finding IDs as well as expected policy. Lost
+replacement responses and lost directory-create responses deliberately assert the
+current conservative duplicate behavior and keep CR-009 unresolved; these are
+policy regressions, not proof of exactly-once effects.
+
+The plaintext matrix covers stale and missing versions, a competing write after
+preflight, fail-on-conflict, rename/move combinations, normalized collisions,
+edit versus Trash, stale deletion, lost create/conflict-copy responses, same-name identity replacement, repeated
+metadata delivery, and parent
+movement/removal. Failure injection covers offline, authentication, permissions,
+throttling, quota, and cancellation. Assertions inspect identities, parentage,
+versions, bytes, staged recovery contents, and subsequent reads.
+
+
+The production create router is also exercised with `.mayAlreadyExist`. Tests
+verify that existing bytes survive, file replay reuses the created identity under
+the service-token model, and repeated directory delivery produces two usable
+directories under current policy. This records CR-009 rather than asserting that
+name matching safely reconciles identity. The router and both modification
+executors are called by the real extension. Parameterized tests
+cover combined contents/name/parent changes, contents plus Trash, and unsupported
+fields. The vault executor now applies supported changes before Trash, uses the
+committed revisions, and leaves unsupported fields pending. Missing content URLs
+fail before mutation. Lifecycle tests run the plaintext executor under actual
+`FileProviderOperationLifecycle` cancellation and check retained bytes, absence of
+server mutation, and no late success. Error tests call the production mapper.
+
+Vault matrix entries reuse the normative journal/service regressions. Additional
+coverage runs 32 deterministic seeds, six competing writers, reverse delivery and
+12 shuffled orders per seed. It checks preserved content revisions, canonical
+results, conflict copies, and two encrypted SQLite stores reopened after persistence.
+Metadata conflicts, stale purge, duplicate journal records, and causally valid
+reproducer reduction have dedicated tests. Replay divergence or content/graph preservation failures write a synthetic
+seed and minimized journal under the local temporary `potassium-conflict-failures`
+directory; no live data or credential is included.
+
+Run the unit target through Xcode, for example:
+
+```sh
+xcodebuild test -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability -destination 'platform=macOS' \
+ -only-testing:potassiumProviderTests
+```
+
+Use the standard scheme and the documented iPhone 17/iOS 26.5 and Apple Vision
+Pro/visionOS 26.5 destinations for shared-runtime regression validation. Do not run
+hosted tests or simulators during live Finder execution: they can register another
+extension build or steal UI focus.
+
+`SnapshotInitializationContentionTests` covers an environment exposed by the live
+matrix: opening the production snapshot store during a temporary exclusive WAL
+lock must wait, preserve existing data, and permit subsequent snapshot reads and
+writes. The audit retains the failing old-order result and corrected platform runs.
+
+## Independent live cases
+
+```sh
+# One focused reproduction, reusing the ordinary signed app:
+scripts/run-finder-stability.sh --conflicts --case content-after-preflight --yes-live
+
+# All six cases, serially; each gets a new run and generated subtree:
+scripts/run-finder-stability.sh --conflicts --yes-live
+
+# Acceptance profiles require explicit process lifecycle evidence:
+scripts/run-finder-stability.sh --conflicts --extension-state fresh --yes-live
+scripts/run-finder-stability.sh --conflicts --extension-state running --yes-live
+```
+
+The same `--extension-state` option is available with the original `--run` mode;
+its sixteen scenarios and exact-item permanent-delete confirmation stay required.
+
+The fresh profile verifies the lab first, waits for recorded work to settle, then
+terminates only the exact signed embedded provider process. Domain access launches
+the replacement while the recorder is active. It never terminates Finder, TextEdit,
+or the system File Provider daemon. The running profile requires the existing
+process to survive from before preflight until sealing; it does not start a process
+and call that a warm run. Failed lifecycle evidence cannot certify a case.
+
+Warm preparation explicitly signals the verified domain’s working-set enumerator.
+Cached root resolution may produce no extension callback, so passive waiting cannot
+establish readiness. Signal acknowledgement alone is insufficient: the same signed
+process must produce completed diagnostic evidence within the existing 90-second
+budget. Preflight failures retain a non-accepting, versioned
+`launch-preparation-failed.json` with closed stage/reason and numeric error data.
+
+Launch proof version 2 distinguishes a process from the replicated-provider objects
+it hosts. Apple permits discarding and recreating those objects within one process.
+Their initialization/invalidation spans must be complete and successful; they remain
+visible in the timeline. Kernel birth, signing, and diagnostic process identity must
+still match throughout. Missing/duplicated lifecycle telemetry and actual process
+replacement reject acceptance. Historical version-1 proofs keep their original
+stricter interpretation; failed old candidates are not upgraded into passes.
+
+Use `--build` to explicitly rebuild/install. Consent handling and `--watch` are
+shared with the Finder runner. Credentials remain in Keychain; the lab is the
+existing authorized plaintext root under `Private`. No encrypted vault is provisioned.
+
+| Case | Controlled ordering and required result |
+| --- | --- |
+| `content-before-preflight` | Hold after staging but before metadata lookup; competing remote edit commits; both byte streams must survive under distinct identities. |
+| `content-after-preflight` | Hold after matching version preflight; competing replacement commits; the real conditional write conflicts and both versions survive. |
+| `rename-rename` | Remote rename commits while the Finder rename is held after preflight; local rename intent wins on the same identity. |
+| `move-move` | Remote move commits while the Finder move is held after preflight; local destination wins and bytes stay unchanged. |
+| `edit-rename` | Remote rename commits while the Finder edit is held; edited bytes and the remote name both survive. |
+| `edit-move` | Remote move commits while the Finder edit is held; edited bytes and the remote destination both survive. |
+
+Targeted preparation creates a fresh run root and sibling destinations, then binds
+and opens the root. It does not depend on the original suite’s deep-folder seed.
+Each selected race still materializes its own file through Finder, binds every
+mutation target, and verifies its destinations. The original suite retains the
+full nested hierarchy, seed, and navigation assertions. Completed remote fixture
+batches request one `.workingSet` refresh; individual-folder native signals are
+ignored for replicated providers. Signal acknowledgement never satisfies UI,
+remote-state, or callback assertions. The native working-set refresh target does
+not expand the selected case’s subject set to all domain callbacks; complete global
+settling remains a separate requirement before sealing.
+
+Every result is selected in Finder, captured locally, reopened through Finder in
+TextEdit, and byte-checked. Gate files are scoped to run, case, salted item alias,
+correlation, unique attempt, and scheduling point. An old release cannot satisfy a
+later case. Cancellation/expiry ends the held attempt; overlapping claims fail
+instead of bypassing the gate. Direct API calls perform the competing mutation and
+verification only. The competing metadata and bytes must be read back while
+the local attempt is held; an accepted asynchronous move response does not prove
+that ordering. The attempt-scoped competitor verification record is mandatory for
+new version 3 profile results and the original preserve-both scenario.
+
+`conflict-profile.json` declares the narrower selection. The ordinary immutable
+Finder report still has all 16 entries; unrelated scenarios are explicitly
+`notSelectedForConflictProfile`. Sealing validates the selected case's exact gate
+and callback evidence. One targeted success never becomes a 16-scenario pass.
+Failed fixtures, screenshots, and diagnostics remain available. A sealing rejection retains a non-accepting `finder-evidence-rejected.json` candidate with a closed reason; it never replaces the required final report and summary. Complete correlated spans include their terminals during monitored settling. Finder and TextEdit
+cleanup addresses only owned windows/documents; dirty generated documents are saved,
+never silently discarded. An unavailable cleanup control leaves incomplete evidence.
+
+## Acceptance and remaining limitations
+
+### Current OS-specific evidence
+
+The September 12 continuation passed all six live conflict cases in both fresh and
+already-running extension profiles on the same arm64 host running **macOS 26.6.2
+(25G83)**, using **Xcode 26.5 (17F42)**. The selected Finder/Actions suite recorded
+14 passes, one unapplied-comments failure accepted by the operator as non-blocking,
+and permanent deletion unselected. It is not complete sixteen-scenario acceptance.
+The full Mac Stability unit target passed 558 tests on that macOS version. Focused
+shared tests passed 24 each on **iOS Simulator 26.5 (23F73)** and **visionOS Simulator
+26.5 (23O470)**. These simulator tests do not validate native Mac Finder integration.
+
+Retained XCResult device records confirm the tested OS versions. The live JSON
+reports do not embed OS metadata, so their version attribution is the shared host
+context rather than independent per-run capture. The audit's current conflict
+matrix and OS-version section record exact run IDs, result bundles, and provenance.
+
+Retain this environment output with every future live run's log, alongside its run
+IDs and results:
+
+```sh
+sw_vers
+uname -m
+xcodebuild -version
+```
+
+Read actual simulator versions/builds from XCResult device records. An SDK version,
+deployment target, or moving CI runner label is not the OS version of a passing run.
+After a macOS upgrade, rerun the fresh and already-running Finder/Actions and conflict
+profiles before claiming compatibility; inspect panel/menu binding, real cancellation,
+Trash restoration, historical-copy recovery, and working-set delivery. No next-macOS
+compatibility is claimed from the current passes.
+
+### Earlier validation snapshots
+
+The complete six-case fresh and already-running profiles passed on implementation
+`d4b5323`, with twelve sealed evidence bundles. Independent verification confirmed
+one signed build, six fresh processes, a continuous warm process, actual competing
+commits, reopened bytes, screenshots, complete diagnostic spans, and healthy final
+reports. The earlier accepted baseline `05e8e5f` and intervening failed bundles
+remain preserved. The audit records exact run IDs and the focused corrections.
+
+Latest finalized validation on the bounded working-set runtime: 470 macOS Stability
+tests, 415 standard macOS tests (including UI), 381 iOS Simulator tests, and 381
+visionOS Simulator tests passed, with zero failed or skipped. The signed generic
+visionOS build succeeded. Bundles are `potassium-finish-stability-mac-03.xcresult`,
+`potassium-finish-mac-03.xcresult`, `potassium-finish-ios-01.xcresult`, and
+`potassium-finish-vision-sim-01.xcresult`. The host-app package embedding follow-up
+also passed iOS Simulator and signed generic visionOS builds.
+
+The historical six fresh and six warm conflict passes above cover `d4b5323`;
+they do not certify the newer working-set runtime. `FileProviderBackgroundWorkTests`
+verifies cancellation, replacement-instance independence, and late-registration
+rejection. The retained failed-preparation trace establishes actual child-work
+cancellation during invalidation, mitigating CR-024 without converting a failed
+run to a pass. New gated folder-read tests verify bounded overlap, a single complete
+commit, cancellation, and Retry-After-preserving throttling.
+
+These conflict profiles do not certify the separate sixteen-scenario suite. Its
+complete fresh and already-running acceptance remains open. The latest sealed
+fresh run verified eleven scenarios through the strengthened Restore path, then
+failed while matching Finder's hidden-extension deletion confirmation. The focused
+correction has unit coverage and is undergoing a complete live rerun. Large
+materialized-set latency remains an open limitation. Live results are in `STABILITY_LOOP_AUDIT.md`.
+
+Keep permanent-delete CR-013 open. The six conflict cases do not permanently delete
+fixtures. Permanent deletion in the original suite still requires exact generated
+item confirmation. Directory-create reconciliation, ambiguous replacement success,
+and server-dependent guarantees must not be inferred from the controlled service.
diff --git a/doc/CONTEXTUAL_ACTIONS.md b/doc/CONTEXTUAL_ACTIONS.md
index b1da139..a106e92 100644
--- a/doc/CONTEXTUAL_ACTIONS.md
+++ b/doc/CONTEXTUAL_ACTIONS.md
@@ -1,7 +1,7 @@
# Contextual Actions
-Version 0.3 adds actionable kDrive commands to Finder and Files while remaining
-on potassiumChannel 0.2.0. Every action is single-selection.
+Version 0.3 adds actionable kDrive commands to Finder and Files using the
+version-pinned potassiumChannel 0.3.0 adapter. Every action is single-selection.
## Direct Provider Actions
@@ -20,8 +20,10 @@ contextual action is offered for the provider root.
`KDriveContextActionCoordinator` performs the remote sequence and returns the
affected parent IDs. Favorite mutations refetch authoritative metadata.
-Duplicate uses kDrive's server-side operation and refetches the created item,
-without downloading content. Restore checks whether the original parent still
+Duplicate refetches the source, derives an explicit extension-preserving copy
+name, sends it to kDrive's server-side operation, and refetches the created
+item without downloading content. It does not rely on undocumented empty-body
+server naming. Restore checks whether the original parent still
exists and falls back to the drive root when it does not. The extension then
invalidates affected snapshots and signals each parent plus the working set.
@@ -38,7 +40,7 @@ owns Download Now and Remove Download presentation. Trash items have the trash
container as their parent, expose trash state, and allow reading and permanent
deletion without rename, move, write, or retrash capabilities.
-The provider does not set `favoriteRank`: potassiumChannel 0.2.0 exposes
+The provider does not set `favoriteRank`: potassiumChannel 0.3.0 exposes
favorite state but no portable favorite ordering.
## UI Actions
@@ -62,9 +64,20 @@ failures propagate.
New links default to public read-only access, downloads enabled, file
information visible, and comments, editing, access requests, statistics, and
-expiry disabled. The user can choose password access, expiry, downloads, and
-comments. Existing links can be copied, sent through the system share sheet,
-updated, or disabled after destructive confirmation.
+expiry disabled. The user can choose public, inherited, or password access,
+expiry, downloads, and comments. Clearing an existing expiry explicitly sends
+nullable `valid_until`; an already absent expiry is omitted so an unrelated edit
+does not request a plan-gated setting. Updates omit unchanged settings except
+comments, whose API omission inherits editing; comment intent remains explicit.
+An unknown returned access value fails closed instead of
+defaulting to public. Existing links can be copied, sent through the system
+share sheet, updated, or disabled after destructive confirmation.
+
+After create or update, the form shows the settings returned by kDrive. If any
+reported setting differs from the request, the panel reports that the requested
+settings were not all applied, rather than claiming success. Review those values
+before sharing the link. This does not roll back a partially applied update;
+passwords cannot be verified from the response because kDrive does not return them.
Passwords and returned URLs remain in view-model memory only. They are never
logged, persisted, placed in activity summaries, or exported in diagnostics.
@@ -87,11 +100,18 @@ After success, it signals the destination parent and working set.
`KDriveContextActionProviding` contains only action-specific methods:
favorite, duplicate, trash restore, share-link CRUD, version pagination, and
-version restore. `PotassiumKDriveService` implements it exclusively with typed
-PotassiumKDrive 0.2.0 service calls. Existing `KDriveFileProviding` mutation
-semantics remain unchanged.
+version restore. `PotassiumKDriveService` implements it with typed
+PotassiumKDrive 0.3.0 service calls plus the documented nullable share-update
+body correction. Existing `KDriveFileProviding` mutation
+semantics remain unchanged except for the version-pinned corrections recorded
+in the conflict truth table.
For encrypted items, favorite, duplicate, trash restore, and logical version
restore call `EncryptedVaultProviding`. Thumbnails and versions are local
authenticated vault operations. Share-link panels stop before any kDrive
sharing call and explain that recipient-key sharing is not supported in v2.
+
+The plaintext version history uses a Form container so the macOS hosted view
+exposes Restore as an independent accessible button with its stable identifier.
+A List flattened that control even with explicit child containment. Confirmation
+still restores a new copy, preserving current file bytes.
diff --git a/doc/FILE_PROVIDER_CLEANUP.md b/doc/FILE_PROVIDER_CLEANUP.md
index 7414255..46dadde 100644
--- a/doc/FILE_PROVIDER_CLEANUP.md
+++ b/doc/FILE_PROVIDER_CLEANUP.md
@@ -118,3 +118,26 @@ The cleanup script also does not directly delete Finder storage,
Provider system state is corrupt beyond the supported APIs and the stale archive
repair, diagnose with `fileproviderctl dump` or `fileproviderctl check` first and
document any new cleanup path before automating it.
+
+## Stability Actions Registration
+
+Finder discovers Actions extensions separately from the active replicated provider.
+An older installed or DerivedData copy with the same Actions identifier can be
+launched even when the provider process is correctly attested. Stability preflight
+therefore requires exactly one matching Actions registration at the selected app's
+embedded extension path. This is a registration repair, not a domain or data reset.
+
+Inspect copies before changing registration:
+
+```sh
+pluginkit -m -A -D -v -i net.weavee.potassiumProvider.Actions
+```
+
+After a live run has finished and its owned windows are closed, use `pluginkit -r`
+with the exact inspected `.appex` paths for stale copies, then `pluginkit -a` with
+the selected ordinary app's embedded Actions extension. Do not remove app bundles,
+domains, CloudStorage files or credentials to repair this mismatch. Reopening an
+older app or building another profile may register another copy again; rerun
+preflight. User election by identifier applies to all copies, so `pluginkit -e use`
+does not select one particular physical copy. Never accept a loading panel without
+the expected Actions code hash and the run's exact item binding.
diff --git a/doc/FILE_PROVIDER_LIFECYCLE.md b/doc/FILE_PROVIDER_LIFECYCLE.md
index 9f1fc48..61fed27 100644
--- a/doc/FILE_PROVIDER_LIFECYCLE.md
+++ b/doc/FILE_PROVIDER_LIFECYCLE.md
@@ -122,7 +122,12 @@ Purpose: update contents, parent, name, or metadata for an existing item.
Behavior:
-- If the item is moved to `.trashContainer`, call `trashItem(...)` and return.
+- If the plaintext item is moved to `.trashContainer`, apply other requested fields,
+ call `trashItem(...)`, and return the managed item's metadata with the Trash parent
+ and `userInfo.isTrashed=true`. A nil callback result asks the system to delete the
+ local replica. If the edit preserved a conflict copy, trash both versions and return
+ the local copy's identity/version. Do not republish trashed metadata into the active
+ working set. `ModificationCallbackTests` covers the simple and combined-field paths.
- If `.contents` changed, expose upload byte progress, read the local contents
URL with mapped storage where available, and call `replaceFileOperation(...)`
under the shared transfer permit.
diff --git a/doc/KDRIVE_API_MAPPING.md b/doc/KDRIVE_API_MAPPING.md
index bda926a..9ee36e2 100644
--- a/doc/KDRIVE_API_MAPPING.md
+++ b/doc/KDRIVE_API_MAPPING.md
@@ -7,6 +7,21 @@ potassiumChannel's typed `KDriveService` and request builders.
Action-only operations are separated behind `KDriveContextActionProviding` so
the existing File Provider mutation protocol remains unchanged.
+The version-pinned comparison sources, live-result status, adapter decisions,
+tests, and truth-table impact are maintained in
+[`STABILITY_LOOP_AUDIT.md`](STABILITY_LOOP_AUDIT.md). The current dependency is
+potassiumChannel 0.3.0 at
+`db829f1f2bd8c2113a529c9c521bd5cdfb5ef4dc`; GPL client implementations are
+behavioral evidence only and are not copied.
+
+In the Stability profile, every operation below also emits a closed-enum
+diagnostic span with its route template and option shape. Callback TaskLocal
+correlation is inherited by nested requests; no raw URL, identifier, query
+value, header, body, name, path, or file bytes enter the diagnostic record.
+Lazy transfer spans begin only when their operation is consumed or cancelled.
+Expected share-link absence (`404`) completes successfully, while cancellation
+uses the closed cancelled phase instead of a failure record.
+
## Operation Map
| Provider operation | Local method | potassiumChannel call | Visible endpoint |
@@ -31,13 +46,13 @@ the existing File Provider mutation protocol remains unchanged.
| Trash | `trashItem(...)` | `trashFileV2` | `DELETE /2/drive/{driveId}/files/{fileId}` |
| Permanently delete trashed item | `deleteTrashedItem(...)` | `removeTrashedFile` | `DELETE /2/drive/{driveId}/trash/{fileId}` |
| Favorite/unfavorite | `setFavorite(...)` | `favoriteFile` / `unfavoriteFile` | typed kDrive favorite endpoints |
-| Duplicate in place | `duplicateItem(...)` | `duplicateFile` | `POST /3/drive/{driveId}/files/{fileId}/duplicate` |
+| Duplicate in place | `duplicateItem(..., name:)` | `duplicateFile` with explicit options | `POST /3/drive/{driveId}/files/{fileId}/duplicate` |
| Read trashed metadata | `trashedItem(...)` | `getTrashedFile` | typed kDrive trash metadata endpoint |
| Check restore parent | `existingFileIDs(...)` | `checkFilesExistence` | typed kDrive existence endpoint |
| Restore from trash | `restoreTrashedItem(...)` | `restoreTrashedFile` | typed kDrive trash restore endpoint |
| Read share link | `shareLink(...)` | `getFileShareLink` | `GET /2/drive/{driveId}/files/{fileId}/link` |
| Create share link | `createShareLink(...)` | `createFileShareLink` | `POST /2/drive/{driveId}/files/{fileId}/link` |
-| Update share link | `updateShareLink(...)` | `updateFileShareLink` | `PUT /2/drive/{driveId}/files/{fileId}/link` |
+| Update share link | `updateShareLink(...)` | pinned `updateFileShareLink` route plus corrected nullable-body adapter | `PUT /2/drive/{driveId}/files/{fileId}/link` |
| Disable share link | `deleteShareLink(...)` | `deleteFileShareLink` | `DELETE /2/drive/{driveId}/files/{fileId}/link` |
| List versions | `fileVersions(...)` | nondeprecated `listFileVersions` | `GET /3/drive/{driveId}/files/{fileId}/versions` |
| Restore version as copy | `restoreFileVersion(...)` | `restoreFileVersionToDirectory` | `POST /3/drive/{driveId}/files/{fileId}/versions/{versionId}/restore/{destinationDirectoryId}` |
@@ -60,6 +75,28 @@ claim to prove product ownership. Stored File Provider domains remain visible
for recovery when a later discovery response no longer includes an eligible
drive.
+The Stability Lab applies a stricter access gate before any remote mutation:
+exactly one discovery record must match the selected drive, its role must be
+internal, and it must not be in maintenance. This is internal membership, not
+an account-ownership claim. Provisioning then verifies
+the explicit drive-root metadata, creates one direct child, uploads a marker
+with `conflict=error`, and re-reads the root and marker. Reset fully paginates
+ordinary listing and uses only `trashItem`; it re-reads ownership evidence and
+each target's parent immediately before every trash request. Lab reset never
+calls `deleteTrashedItem` and never removes its root or marker.
+The locally persisted random marker plus exact remote root/marker identity is
+the Stability Lab ownership proof.
+
+The separately invoked Finder Stability run never calls `deleteTrashedItem`
+directly. After `--yes-live` and the same exact lab/marker preflight, its restore
+and permanent-delete scenarios retain the verified lab-root selection and emit
+typed operator checkpoints without opening the user-global Trash or directing
+a destructive action. This avoids both an irreversible, unconditional remote
+request and presenting unrelated Trash contents as lab-scoped UI. `CR-013` remains
+open for the product's existing permanent-delete callback; the runner does not
+claim to automate that unresolved risk, and CI/preflight never runs the live
+sequence.
+
Binary operations are exposed to File Provider as `KDriveTransferOperation`.
It preserves potassiumChannel's live Foundation progress, shared async result,
and cancellation of the underlying URL session task. Async convenience methods
@@ -72,15 +109,16 @@ Legacy directory listing uses:
- cursor from Apple page data
- limit `200`
- order by `name` ascending
-- retries without an included resource if the ETag-enabled request returns HTTP
- 422
+- requests `etag,is_favorite`; on HTTP 422, retries with `is_favorite` only so
+ losing the optional ETag cannot also hide favorite actions
Advanced directory listing uses:
- limit `200`
- order by `type`, then `name`
- per-field ascending order for `type` and `name`
-- `with=files.capabilities`, matching the open-source desktop kDrive client
+- `with=files.capabilities,files.is_favorite`, a narrow subset of Potassium's
+ advanced-listing preset that includes the state needed by Finder actions
- HTTP 422 is surfaced to File Provider as `.cannotSynchronize`; it does not
fall back to ordinary directory listing because that route has neither
advanced change actions nor compatible cursor semantics
@@ -95,16 +133,31 @@ Trash listing uses:
File create uses `UploadKDriveFileOptions` with:
-- `conflict: "version"`
+- `conflict: "rename"` in the production mutation coordinator, preserving the
+ server-created item when a name collision exists
- `directoryId: parentID`
- `fileName`
- optional `lastModifiedAt`
+- deterministic `clientToken`, SHA-256 `totalChunkHash`, and `with=etag`
File replace uses `UploadKDriveFileOptions` with:
-- `conflict: "version"`
-- `fileId`
+- stable `fileId`
+- required `If-Match` ETag
+- deterministic `clientToken`, SHA-256 `totalChunkHash`, and `with=etag`
- optional `lastModifiedAt`
+- no create-conflict option, directory ID, or filename
+
+The direct-upload endpoint has a documented maximum `total_size` of
+`1_000_000_000` bytes. File Provider create and replacement callbacks preflight
+the callback URL's file size before mapping it into `Data`; the loaded byte
+count is checked again before constructing the potassiumChannel upload
+operation to close a file-size/read race. Larger files fail closed with
+`KDriveDirectUploadError.requiresUploadSession` and map to File Provider
+`.cannotSynchronize`. The callback URL remains File Provider-owned, but no
+provider conflict-stage copy is created for this pre-buffer rejection. The app
+does not yet implement the file-backed upload-session/chunk path, so it never
+attempts a live direct upload above the limit.
Move uses `MoveKDriveFileOptions` with:
@@ -113,6 +166,65 @@ Move uses `MoveKDriveFileOptions` with:
Directory create does not currently pass an explicit conflict policy.
+## Contextual Mutation Adapters
+
+Duplicate-in-place refetches the source metadata and sends an explicit,
+extension-preserving destination name such as `Document copy.txt`. The pinned
+potassium request model makes the name optional, but official iOS and desktop
+clients always choose a name and the public evidence does not define an
+empty-body/server-selected naming contract. A collision is allowed to fail
+without mutating the source; the adapter never guesses that an empty body is
+safe.
+
+Share access supports the three documented values: `public`, `inherit`, and
+`password`. Any unknown response value throws
+`unsupportedShareLinkAccess`; it never widens access by defaulting to public.
+Share updates retain potassiumChannel's typed method, path, response envelope,
+and API client. The app replaces only the encoded body because the 0.3.0
+synthesized encoder omits a nil optional, while the endpoint defines
+`valid_until` as nullable. Read the current link first and send only changed
+capabilities, access policy, and expiration, except that comment intent always
+remains explicit: the [official update contract](https://developer.infomaniak.com/docs/api/put/2/drive/%7Bdrive_id%7D/files/%7Bfile_id%7D/link)
+defaults omitted/null `can_comment` to `can_edit`, rather than preserving its
+previous value. Encode JSON null only to clear an
+existing expiration, and omit the field when it is already absent or unchanged.
+An explicit changed date is encoded directly. Password rotation remains explicit
+because the current password cannot be read; an otherwise unchanged configuration
+returns the authoritative read without issuing an empty PUT. This avoids
+invoking a plan-gated expiration setting during an unrelated edit; Infomaniak's
+official client omits the field for free drives. A failed preflight does not update
+the link. This is not a conditional-write guarantee: concurrent changes can still
+race with the request. Request bodies, passwords, and returned share URLs never
+enter diagnostics. `KDriveAPIEvidenceTests` covers set, clear, unchanged absence,
+failed preflight, minimal comments updates, no-op saves, access changes and password
+rotation, including preservation of comments during an independent download restriction.
+
+After create/update, the Actions view compares all reported settings with the
+request before claiming success. It displays the authoritative response and an
+unapplied-settings error if any reported capability, access mode, or expiration
+differs. The check excludes passwords, which the API does not return, and normalizes
+expiration to whole seconds. It neither retries with broader access nor rolls back
+a partially applied write. `ShareLinkSettingsVerificationTests` covers every
+reported field and those representation boundaries. A generated-file live probe
+confirmed that HTTP 200 can accompany an unchanged comments setting; the server's
+reason remains unconfirmed.
+
+Favorite, trash restore, share update/delete, and permanent trash deletion do
+not have a documented conditional version token. Their exact last-writer and
+recovery behavior is recorded in the conflict truth table; no ETag condition is
+invented from client behavior alone.
+
+## Retry And Error Evidence
+
+Infomaniak's global API rule is 60 requests per minute, with stricter limits
+possible on individual routes. HTTP 408 and 429 are classified as retryable
+`.serverUnreachable` File Provider failures, alongside 5xx responses. When
+potassiumChannel supplies Retry-After metadata, the adapter retains only a
+parsed nonnegative delta-seconds integer. HTTP-date or invalid values are
+discarded, and raw headers and response bodies are never logged or persisted.
+The provider still delegates retry scheduling to File Provider; it does not
+sleep or run an independent retry loop.
+
## Advanced Listing Response Mapping
`listAdvancedDirectory(...)` maps potassiumChannel's
@@ -132,6 +244,20 @@ both "invalid" and "cursor".
The partial-activity request is batched at 200 identifiers and uses the last
durable successful-poll watermark. It includes create, delete, trash, restore,
update, rename, move, favorite, and share actions relevant to working-set state.
+Its expansion is `with=file`, matching the upstream iOS endpoint. The former
+`file,file.etag` expansion is unsupported by the known route contract. A live
+422 motivated this correction; the live rerun must establish whether it is the
+only cause. Failed partial responses still cannot advance a durable watermark.
+
+Live rerun `0965c244-7ea6-457e-bc34-56cba76a1033` recorded ten successful
+partial-activity requests and no repeated 422 after the expansion correction.
+Directory `last-modified` calls independently returned 400, while a generated
+regular file accepted its unchanged timestamp. Plaintext directory timestamp
+mutations therefore refetch and return the authoritative server date without
+issuing a file-only timestamp write. The local directory date resolves to the
+server value under Apple's returned-field propagation contract. An earlier
+experiment omitting the optional date did not stop callbacks and was reverted.
+Regular-file timestamp requests retain their behavior.
## Opaque vault mapping
@@ -142,3 +268,11 @@ ID, byte count, and `application/octet-stream`. Logical names, paths, MIME
types, dates, hashes, device names, favorites, shares, and versions are never
sent through this boundary. Latest/favorite/shared/activity/preview/thumbnail
endpoints are not called for encrypted items.
+
+Direct metadata, ordinary/Trash/working-set listings, and upload responses request
+`etag,is_favorite`. Advanced listings request `files.capabilities,files.is_favorite`;
+they still never request unsupported ETags. Missing favorite state remains nil,
+so neither favorite action is enabled from a guessed value. The explicit field is
+documented by the [metadata](https://developer.infomaniak.com/docs/api/get/3/drive/%7Bdrive_id%7D/files/%7Bfile_id%7D)
+and [upload](https://developer.infomaniak.com/docs/api/post/3/drive/%7Bdrive_id%7D/upload)
+contracts, and `files.is_favorite` is part of Potassium's advanced-listing preset.
diff --git a/doc/LISTING_AND_VERSIONING.md b/doc/LISTING_AND_VERSIONING.md
index c740731..e469766 100644
--- a/doc/LISTING_AND_VERSIONING.md
+++ b/doc/LISTING_AND_VERSIONING.md
@@ -75,7 +75,9 @@ file while a stale ETag is rejected.
The advanced `/listing` and `/listing/continue` routes are different: the live
API rejects both `etag` and `files.etag` in their `with` parameter with HTTP
422. potassiumChannel's compatible default excludes those resources, and the
-provider explicitly uses the desktop-compatible `files.capabilities` subset.
+provider uses `files.capabilities,files.is_favorite` from that compatible preset.
+The favorite include is necessary for Finder action predicates; missing favorite
+state remains unknown, rather than being coerced to false.
An advanced-listing 422 is surfaced as a retryable synchronization failure;
ordinary directory listings do not return the action feed and their pagination
cursors must never replace an advanced-listing sync cursor. Advanced-listing
diff --git a/doc/LOGGING.md b/doc/LOGGING.md
index ea40386..2457192 100644
--- a/doc/LOGGING.md
+++ b/doc/LOGGING.md
@@ -1,17 +1,200 @@
# Logging
-`potassiumProvider` uses three complementary diagnostic layers:
+`potassiumProvider` uses complementary diagnostic layers selected by build
+profile:
- Unified logging (`OSLog`) for developer diagnostics in the app and File
Provider extension.
- `Snapshots.sqlite3` activity/conflict rows for the user-visible Activities
timeline and retained support context.
- A redacted JSON support-log export created from the Activities tab.
+- In the opt-in `Stability` profile only, one versioned JSONL run bundle in the
+ app-group container for activity, conflict, callback, and API-shape evidence.
These layers are deliberately separate. Unified logging can be more granular
-for local development, while the SQLite trail and exported document only carry
+for local development, while durable trails and exported documents carry only
the small set of sanitized fields that are useful to users and support.
+## Stability Run Bundles
+
+Live edit, rename, and move evidence requires the corresponding `contents`,
+`filename`, or `parent` field on a successful `modifyItem` callback. Incidental
+last-used or timestamp callbacks cannot satisfy those scenarios. Finder Trash
+requires both `parent` and `trash`; permanent deletion requires `deleteItem`.
+
+`Stability` defines the `STABILITY` compilation condition on the app, shared
+core, File Provider, actions, unit-test, and UI-test targets. It preserves
+bundle identifiers, app groups, and the Keychain credential flow. The macOS
+Stability host alone runs outside App Sandbox to use assistive Accessibility;
+both extensions and the standard host remain sandboxed.
+The standard profile continues to store activity and conflict history in
+`Snapshots.sqlite3`.
+
+All existing unified `Logger` instances resolve to `OSLog.disabled` in
+Stability. The older debug log call sites include raw identifiers and localized
+error descriptions, so enabling them would violate the Stability privacy
+contract. Debug and Release keep unified logging; Stability uses only the
+closed-schema recorder. The environment-driven Debug UI fixture is also
+compiled out of Stability, even though the profile otherwise inherits Debug
+settings.
+
+After the operator explicitly starts a Stability run, every production event
+store construction site selects `KDriveProviderEventJSONLStore`. Snapshot,
+sync-anchor, enumerator, and working-set state still use SQLite; their table
+creation is intentionally independent from event-table creation. With no
+active run, the Stability factory returns no durable event recorder rather than
+falling back to SQLite or inventing a run.
+
+Each run is a complete directory under the app-group `StabilityRuns/runs`
+folder. `run.json` is exclusive-created and immutable; `events.jsonl` is
+append-only; `summary.json` marks completion. The Finder runner owns a newly
+created run through a private random-token and process-ID coordination file, atomically
+replaces the reserved `api-observations.jsonl` and `assertions.jsonl` files, and
+exclusive-creates immutable `finder-report.json` as their commit marker before
+the run is sealed. Final sealing decodes that report and rejects assertion,
+failure, or checkpoint totals that do not match it. A failed assembly never
+creates `summary.json` and retains
+ownership. Explicit stale-run recovery first proves the recorded owner process
+has exited, creates immutable `finder-abandoned.json`, removes any stale step
+pointer, and only then releases the active-run lease. An abandoned bundle stays
+incomplete and cannot be finalized by the ordinary app lifecycle.
+The `current-run.json` pointer contains only a random run UUID. Retention keeps the
+newest 20 completed bundles within 250 MiB and never prunes the active or an
+incomplete bundle. The active event file also rejects an append before it would
+exceed 250 MiB, preserving a replayable run that the operator can finish.
+
+JSONL writers use an advisory exclusive lock, one complete encoded record per
+write transaction, and `fsync`; readers take a shared lock. A second lifecycle
+lock serializes active-run selection, finish, append/read, and retention across
+the app, File Provider, and actions processes. Run paths reject symlinks and
+non-regular files, use owner-only permissions, and synchronize file and
+directory transitions. Replay ignores only an unterminated final record, which
+represents an interrupted append; the next writer truncates that tail before
+appending. A corrupt complete record is surfaced. Activity paging, statistics,
+observation, clear, domain removal, and support export use replay through the
+existing protocols; clear/removal are append-only tombstones.
+
+The Stability serializer removes names, paths, item and request identifiers,
+drive identifiers, recovery strings, staged paths, and arbitrary error domains
+before bytes reach the log. Domain identifiers become run-salted SHA-256
+pseudonyms, which remain stable only within that run. Callback/API diagnostics
+use closed enums for operation, phase,
+field/option shape, route template, and error/status class. They may include a
+random correlation UUID, bounded duration/progress values, booleans describing
+cursor/anchor state, and numeric status/error codes. They never include raw
+URLs, headers, request or response bodies, account identifiers, share links, or
+file data.
+
+The Stability Lab stores its remote root and ownership-marker identifiers only
+in the local domain configuration. The remote marker contains a random marker
+UUID plus drive/root identity, but no account identifier, display name, path,
+URL, or credential. None of these operational identifiers or marker bytes are
+copied into diagnostic events. Lab provisioning and reset use the same closed
+typed-network spans as other requests, so only route and option shapes are
+durable.
+
+Finder report version 2 adds run-local item aliases, observed UI action counts,
+expected extension code hashes, cancellation/conflict/working-set observations, and
+closed failure categories/reasons with supporting span IDs. Diagnostics version 3
+adds subject aliases, parent spans, process-instance UUIDs, code hashes, and safe
+numeric error codes. An optional `validationFields` array contains only known
+request-field classes from a 400 or 422 response; messages, values, and unknown keys
+are discarded. Version 1 Finder reports and older diagnostic records remain
+readable. New live reports require the newer evidence fields.
+
+Finder report version 3 adds `skipped(permanentDeletionNotSelected)`, valid only for
+scenario 12. Its UI/server assertions are not evaluated, and it contributes zero
+passes. All other live evidence requirements remain those of version 2. Historical
+versions 1/2 still decode; they cannot encode this new selection reason. A sealed
+15-pass/one-deferred report produces exit 4 and explicitly says full acceptance is
+incomplete. Missing telemetry and failed later steps still produce failure, not
+that completed-selection status.
+
+`concurrentSnapshot` identifies a rejected snapshot compare-and-swap without
+exporting the error's domain/container identifiers. Bounded working-set retries
+emit this class on nonterminal checkpoints; exhaustion emits a failed terminal.
+It never hides a remote failure or changes a successful watermark. Finder's
+scoped eviction alert maps to the closed `resourceBusy` failure reason; native
+Apple Event failures print numeric codes without private command text.
+
+Each passing step requires a baseline and postcondition, Finder-visible and remote
+assertions, and successful item-specific callback evidence from the expected build.
+Transfer sampling uses Foundation `Progress.fractionCompleted` so weighted child
+progress is visible before the parent's integer units advance. Only actual
+observations are bucketed; terminal completion is not intermediate progress.
+Cancellation accepts the expected cancelled fetch and requires real progress plus a
+later successful fetch for that same item. Trash uses `modifyItem`; permanent
+selected-item deletion uses `deleteItem`. Root enumeration cannot substitute for
+an actual working-set member event. Missing starts/terminals or contradictory
+terminals reject certification. Checkpoints are incomplete coverage.
+
+The timeline retains active-item HTTP 404 during Trash-aware metadata lookup.
+It is a handled intermediate failure only when the same subject, process/build,
+and enclosing metadata span have a subsequent successful `trashedItem` request
+and successful metadata callback. Missing or mismatched recovery, another status,
+or a failed enclosing callback remains a failure. This exception cannot satisfy
+the required Restore mutation evidence.
+
+`live-status.json` is a replaceable closed status snapshot for the read-only watch
+command. Watch shows scenario transitions, errors, cancellations, retry checkpoints,
+and extension lifecycle events; routine request successes stay in the timeline.
+The run recorder starts before context preflight can emit callbacks. A context
+preflight failure retains the incomplete unsealed bundle for explicit stale-owner
+recovery. This ordering alone does not attest a fresh extension launch, because
+registration may have launched it before the command starts.
+Selection diagnostics retain the last live window/parent binding flags, list-view
+state, and exact-name match counts. They contain no paths, titles, or row contents;
+an expired post-failure query cannot overwrite those observations.
+`diagnostic-timeline.json` is ordered by timestamp and event ID, and becomes
+immutable with the final report. `diagnostic-health.failed` latches a failed append;
+subsequent successful writes cannot erase that gap or certify the bundle. The runner
+retains monitoring through operator pauses and waits for outstanding spans to settle.
+Local screenshots live separately under `visual-evidence`; they are cropped to
+positively identified generated content and are excluded from ordinary exports.
+
+Successful plaintext modify callbacks also attach their returned item's
+`itemMetadataAlias` to the terminal event, allowing comparison with independently
+verified remote state. This reuses the optional version 3 field; historical
+events without it remain readable. The value contains no raw item metadata.
+
+Version 3 diagnostics optionally include `itemMetadataAlias`, a run-salted
+commitment to item identity, name, parent, and size. No raw metadata values are
+exported. The live report's optional `expectedWorkingSetMetadataAlias` becomes
+mandatory to certify scenario 15: its matching member terminal must be a child
+of a completed working-set enumeration. A newly introduced remote name change
+prevents stale membership from passing. Historical bundles remain decodable.
+
+The shared `ProviderDiagnosticSpan` emits one best-effort start and at most one
+terminal event even when completion, failure, and cancellation race. Every
+span has its own stable random span UUID; a separate Task-local random UUID
+correlates nested callback and request spans. During each Finder scenario, a
+private owner-only step pointer makes the same random correlation UUID visible
+to the app, File Provider extension, and action-extension processes; callback
+spans fall back to that value when there is no inherited task-local context.
+The pointer contains no domain, item, name, or path value and is removed before
+evidence finalization. Recorder
+failure never changes an API result. A terminal append is attempted before the
+system callback so finishing a run cannot silently turn a completed callback
+into start-only evidence.
+Task-local UUID propagation correlates a callback with its runtime-load and
+typed network spans without carrying domain, item, name, or path context.
+Instrumented surfaces include extension initialization/invalidation, runtime
+load, metadata, fetch, create/modify/delete, item/change/anchor enumeration,
+materialized and working-set refresh, thumbnails, known-folder resolution, and
+contextual actions. Long-lived app/extension/enumerator objects resolve the
+current Stability writer when each callback, app activity, view access, or
+service begins, so starting or rotating a run does not retain a missing or
+sealed writer. Transfer diagnostics
+start only when the lazy transfer is consumed or cancelled and use the same
+span for deduplicated progress, cancellation, and completion. An expected
+missing share link is recorded as a successful optional result.
+For an HTTP rejection, durable diagnostics keep only the numeric status and
+closed recovery class. The API adapter may retain a parsed nonnegative
+Retry-After delta-seconds integer for retry decisions, but never copies the raw
+header value or response body into JSONL, unified logs, activities, or exports.
+Share adapter diagnostics likewise never retain the selected access value,
+expiration, password, or returned URL.
+
## Categories And Correlation
`ProviderLog` is the shared logging namespace. Its categories are `app`,
@@ -21,13 +204,15 @@ the small set of sanitized fields that are useful to users and support.
`ProviderLogContext` creates a correlation ID, operation name, optional domain,
drive, and item context, plus a start time. File Provider activity rows receive
a correlation ID and measured duration. The `PotassiumKDriveService` records
-sanitized unified-log spans for every kDrive request with an operation name,
-correlation ID, duration, outcome, status code when available, and error
-domain/code.
+sanitized unified-log spans in standard builds and closed-schema JSONL spans in
+Stability for every typed kDrive request. Durable spans contain an enum
+operation/route/option shape, correlation UUID, bounded duration, phase, and
+status/error class; they never retain an error domain or description.
Network spans never include request URLs, query parameters, filenames, request
-or response bodies, bearer tokens, refresh tokens, remote account identifiers,
-or file bytes. The service does not currently expose a kDrive request ID, so the
+or response bodies, raw Retry-After values, bearer tokens, refresh tokens,
+remote account identifiers, or file bytes. The service does not currently
+expose a kDrive request ID, so the
optional durable `remoteRequestID` field remains empty unless a future typed API
surface provides one safely.
@@ -39,11 +224,11 @@ optional `correlationID`, `durationMilliseconds`, `networkOperation`,
`httpStatusCode`, and `remoteRequestID` fields. Existing databases migrate these
columns as nullable values.
-`KDriveProviderEventSQLiteStore` retains the newest 5,000 activity rows by
-default. This applies only to activity rows: unresolved, blocked, and failed
-conflict rows remain until a user action or domain cleanup removes them. The
-existing Clear action removes all activity rows and automatically resolved
-conflicts, preserving unresolved conflict state.
+In the standard profile, `KDriveProviderEventSQLiteStore` retains the newest
+5,000 activity rows by default. In Stability, whole completed run bundles are
+retained instead. In both profiles unresolved, blocked, and failed conflict
+events remain visible, and Clear removes activity plus automatically resolved
+conflicts while preserving unresolved conflict state.
The Activities screen pages over this retained history in batches of 50. This
only limits UI decoding and rendering; it does not reduce retention or support
@@ -83,3 +268,172 @@ log and cannot be used to recover omitted secrets or private URLs.
remote account information, or customer data to either logging layer.
- Add a migration and redaction test whenever a new durable diagnostic field is
introduced.
+
+### Targeted conflict profile evidence
+
+`conflict-profile.json` schema 3 declares the run ID, selected closed conflict
+case, and optional required extension launch mode. Historical schemas 1/2 are readable. `conflict-request.json` carries only run/case/correlation identifiers, a salted
+subject alias, scheduling point, and unique attempt UUID. Attempt-specific arrival,
+release, and cancellation files prevent an earlier release from satisfying a later
+case. Report sealing verifies the selected case and gate against its actual
+callback evidence. Other entries are `notSelectedForConflictProfile`; the full
+sixteen-scenario certificate is unchanged. Failures and local screenshots retain
+the existing privacy and immutability boundaries.
+
+Both `--run` and `--conflicts` may require `--extension-state fresh|running`.
+The original runner declares this in `extension-launch-request.json` schema 1;
+conflict runs declare it in their profile. Missing requested evidence cannot pass. The immutable
+`extension-launch.json` (schema 2) records microsecond integer timestamps (preserving kernel birth ordering), the signed
+build hash, diagnostic process UUID, and preparation fence. Fresh evidence requires
+an initialization terminal before the tested mutation in a newly born process.
+Running evidence requires an earlier callback and an unchanged kernel process.
+Complete successful initialization/invalidation spans may describe replicated
+objects being recreated inside that process, as Apple's contract permits. Their
+events remain in the correlated timeline. Missing, duplicated, failed, cancelled,
+or misordered lifecycle spans prevent certification, as do missing or mixed process
+identities/build hashes. Version-1 proofs retain their original object-lifetime
+restrictions; rejected candidates are not recertified in place. A historical profile without this requirement remains a
+targeted result without cold/warm certification. No PID, path, account, or raw
+item identifier is exported by this record.
+
+Conflict profile version 3 requires a version 1
+`conflict--competitor-verified.json` record. It carries the exact ticket
+and a salted metadata fingerprint, written only after independent metadata/byte
+verification while that attempt is held. Cancellation invalidates it. The original
+preserve-both scenario requires the same record at sealing. Historical profile
+versions 1/2 remain readable; their results do not certify this stronger ordering.
+
+Confirmed plaintext mutation results can enter the existing working-set SQLite
+journal without waiting for a remote crawl. This is not itself working-set
+membership telemetry: item-specific `workingSetRefresh` events still arise only
+when a real enumeration delivers the item. Journal publication preserves poll
+watermarks; poll commits validate their original working-set anchor. No database
+schema migration or diagnostic schema change is needed for this delivery path.
+When a newer journal supersedes an in-flight poll, the poll stops between
+folder/activity requests, discards its prepared container changes, and preserves
+its previous successful watermark. Enumeration awaits that work before delivering
+the newer journal; it creates no detached refresh worker. Only actual emitted
+working-set members carry membership metadata.
+
+Live step validation selects callbacks by run-local subject, step correlation, and start time, then retains their complete spans through monitored settling. Missing starts or terminals and contradictory late terminals still reject acceptance. A sealing rejection retains `finder-evidence-rejected.json` (schema 1, `eligibleForAcceptance: false`) with the candidate report, observations, and a closed error reason; this file never substitutes for the immutable final report and summary. External error descriptions are excluded.
+
+Local recorder settling uses a one-second quiet interval after every span has exactly one start and terminal, sampled every 500 ms within the existing deadline. It does not use server Retry-After/backoff. New events reset the interval; missing or duplicate span records cannot settle. Extension lifecycle validation remains strict through sealing.
+
+The fallback XPC-reply-invalid wrapper is inspected through at most four underlying errors for diagnostic classification. Known SQLite errors retain only their numeric result code and storage category; their message and statement are discarded. This uses existing version-3 fields, and historical opaque wrapper records remain readable. It changes diagnostics, not the error returned to File Provider.
+
+Go to Folder failure traces contain a closed UI stage, sheet count, and a Boolean
+indicating whether the owned window still has its previous destination. They never
+print entered paths, sheet text, or external error descriptions. These local traces
+support diagnosis; they do not replace required item binding or final report proof.
+
+Finder destination observation prints only a closed lookup phase (`resolveItemURL`
+or `bindDestinationParent`), diagnostic category, and numeric error code when a
+lookup fails. Local paths, error descriptions, and user-info are excluded. A logged
+lookup failure is not evidence of a remote API failure or a successful move.
+
+
+Provider-instance invalidation cancels its owned materialization refresh tasks;
+the already-completed acknowledgement is not completed a second time. A cancelled
+working-set refresh records one `cancelled` terminal, separately from the successful
+instance-invalidation span. Object invalidation still does not imply process exit.
+A missing background terminal after observed process disappearance prevents sealing;
+never synthesize a terminal from process absence.
+
+
+Warm-launch preparation requests a working-set enumeration after verifying the lab
+and existing process. Its acknowledgement is not callback evidence. Failures before
+scenario execution retain `launch-preparation-failed.json` version 1, with a closed
+stage/reason, safe error class and numeric code, and `eligibleForAcceptance: false`.
+This diagnostic does not replace a Finder report or summary. Missing initial process,
+process replacement, and callback deadline failures remain distinct; payloads, error
+domains, URLs, and localized descriptions are excluded.
+
+Fixture preparation traces only the fixed roles `root`, `nested`, `deep`, `sibling`,
+and `seed`, with resolving/resolved/failed states. They exclude paths, file names,
+and raw item identifiers. These traces locate a failed placeholder binding without
+changing its 90-second deadline or treating native signal acknowledgement as proof.
+
+The native working-set signal target is distinct from a scenario’s fixture subjects.
+Signaling records only the intended changed containers; the explicit working-set
+scenario selects `.workingSet` itself. All other callbacks stay in the global
+timeline and settlement checks. Diagnostic waits print closed reason transitions
+(such as `pendingOperations`), without raw errors or private subjects.
+
+
+JSONL change subscriptions establish their initial file fingerprint before returning
+the stream. An append between subscription return and the polling task's first turn
+must therefore emit a change notification. This does not add payloads or change the
+diagnostic/report schema; the cross-store observation regression has no startup sleep.
+
+The Stability-only transfer cursor reads new complete JSONL records, up to 1 MiB
+per local observation, rather than decoding historical events at every tick.
+It opens a regular file without following symlinks, retains a bounded boundary
+anchor, and rejects replacement, truncation, boundary rewriting, malformed records,
+and the writer-health latch. Shared-lock contention defers that observation without
+blocking the UI. An interrupted final record is retained until complete. This is
+only a scheduling aid; final certification still validates the entire immutable
+bundle with the existing strict parser and callback requirements.
+
+Failure screenshot observation has its own bounded 15-second budget. The failed
+operation remains failed and its original deadline is not extended. The driver
+can dismiss its known transient menu, then revalidate the exact selected row before
+capturing it; unrelated windows and broader screenshot regions remain excluded.
+Closed numeric menu observations identify anchor lookup, event posting, popup count
+and exact enabled-command count without exporting file names or URLs.
+
+Native pointer dispatch records the boolean result of the read-only event-posting
+permission check before emitting input. Denial fails immediately without presenting
+a consent dialog; the trace does not conflate a denied event with a missing command.
+
+Session pointer delivery also records whether the observed cursor reached its
+requested target after movement. This is a boolean diagnostic, not a substitute
+for an observed menu, invoked action or real provider callback. Coordinates remain
+local to the driver and are not exported.
+
+
+Actions panels on macOS can receive an opaque system selection identifier and
+expose their Accessibility tree in the Actions process rather than Finder. The
+production UI resolves the canonical provider identifier and verifies its domain
+before loading action data. Stability binds the panel's run-local alias to that
+canonical identifier and requires the installed Actions executable path/code hash.
+Do not use filenames, raw document IDs, a panel title, or the first window as a
+fallback. Resolution uses one bounded 90-second callback budget and retains no URL
+or raw system error in its messages. Negative resolver/panel-target tests cover
+wrong domains/engines, old code, ambiguous panels, timeout and cancellation.
+The sealed `212e54e` run passed 13 scenarios; cancellation and contextual actions
+failed and deletion was deferred. This is not completed Mac acceptance.
+
+Hosted Actions sheets may have an empty `AXWindows` array and a valid `AXMainWindow`.
+Stability includes that main window only from the attested Actions process and
+still requires the resolved run-local alias, explicitly published on the native
+AppKit root. Discovery deduplicates the same listed/main window. A visible form
+without this identity cannot produce a passing result or authorize cleanup.
+
+A containing-app permission preflight does not establish the Actions extension's
+first-use data-access consent. A live retry encountered that macOS prompt while
+opening its shared diagnostic coordinator file; the operator accepted it. Alias
+discovery now runs off the UI actor with bounded waiting and publishes a cached
+value; SwiftUI layout and AX binding perform no shared-store I/O. Done can dismiss
+a loading panel, while mutation-in-progress still disables it. Consent remains a
+system requirement; a blocked lookup or unbound panel cannot count as a pass.
+
+
+Stability preflight qualifies shared-container provisioning for the installed app,
+replicated extension, and Actions extension. A valid signature is insufficient:
+each embedded profile must authorize the signed explicit application identity and
+existing App Group, and be unexpired. Rejection reports only the affected target
+role; profile contents, developer identifiers and certificates are never exported.
+Both extension targets enable `REGISTER_APP_GROUPS = YES`. `--build` permits
+Xcode's normal automatic provisioning refresh using its saved developer account.
+See [Apple's container authorization guidance](https://developer.apple.com/documentation/xcode/accessing-app-group-containers).
+Repeated data-access prompts require inspecting provisioning before requesting
+another permission grant; do not reset TCC or migrate the app/Keychain group.
+
+
+A frontmost Finder process and valid event-posting permission do not prove that
+Finder receives a mouse click. Native pointer actions now require a system-wide
+AX hit belonging to the exact selected Finder row or popup item. Verification runs
+before moving and again after hover. An obstructed target records environment /
+uiUnavailable with local reason `pointerTargetObstructed`, without reading the
+other app's UI. Its failure screenshot is omitted to exclude unrelated content.
+The failed result remains unaccepted; clear the obstruction before rerunning.
diff --git a/doc/PERSISTENCE.md b/doc/PERSISTENCE.md
index d93cca2..c7aeb60 100644
--- a/doc/PERSISTENCE.md
+++ b/doc/PERSISTENCE.md
@@ -73,6 +73,21 @@ On initialization, the store configures SQLite with:
- a 5 second busy timeout, so short-lived concurrent writers can wait instead of
failing immediately
+Snapshot saves, poll claims, working-set commits, and known-mutation publication
+use `BEGIN IMMEDIATE` before reading their conditions. SQLite's default deferred
+transaction can read under another WAL writer but fail immediately when upgrading
+that read to a write; the busy timeout cannot resolve that upgrade. Reserving the
+writer first lets the existing timeout cover contention, then evaluates the current
+snapshot, anchor, item, and throttle predicates under that reservation. All writes
+remain atomic; network work stays outside the transaction. A writer that exceeds
+the five-second timeout still fails without committing partial state.
+
+`SnapshotWriteContentionTests` reproduces the four paths with an independent
+connection holding a temporary WAL write lock. Snapshot-generation and working-set
+regressions separately verify stale-state rejection, rollback, and watermark rules.
+See SQLite's [transaction modes](https://www.sqlite.org/lang_transaction.html)
+and [WAL isolation](https://www.sqlite.org/isolation.html).
+
Listing snapshots use three active tables:
`snapshot_heads` identifies the active generation for each domain/container.
diff --git a/doc/SHARE_SETTINGS_REPRODUCTION.md b/doc/SHARE_SETTINGS_REPRODUCTION.md
new file mode 100644
index 0000000..0ce6ecc
--- /dev/null
+++ b/doc/SHARE_SETTINGS_REPRODUCTION.md
@@ -0,0 +1,49 @@
+# Unapplied kDrive comment setting
+
+Observed 2026-09-12 on the operator-authorized Stability Lab. This is an open
+integration failure, not a confirmed account limitation or a proven vendor defect.
+No credential, account/file identifier, private URL, or live body is needed here.
+
+On 2026-09-13, the operator accepted this as a non-blocking limitation for the
+stability PR because file comments are outside the critical path. Investigation
+remains a follow-up under CR-027; the warning and failing scenario are preserved.
+
+## Reproduction
+
+1. Use a generated, disposable plain-text file inside the verified lab subtree.
+2. Open Finder → Share kDrive Link, select Inherit Access, and create the link.
+3. Enable Allow comments and verify that the checkbox is checked before Save.
+4. Save. The update is acknowledged, but the subsequent GET reports comments
+ disabled. A comments-only update also reproduced this behavior.
+5. The corrected app displays the returned settings and an explicit warning that
+ not all settings were applied. It does not claim success or widen access.
+
+A separate temporary public link to the same synthetic-only fixture also returned
+comments disabled after an explicitly checked Save. That link was disabled and
+a fresh panel reload confirmed no link remained. Inherited access alone therefore
+does not explain this probe. The automated scenario continues to use inherited access.
+
+Disabling the generated link works. Independently restricting downloads also
+persisted in a manual check. These observations do not establish why comments
+were declined or prove that every plan/access/file-type combination behaves alike.
+
+## Public contract and expected behavior
+
+The [official update contract](https://developer.infomaniak.com/docs/api/put/2/drive/%7Bdrive_id%7D/files/%7Bfile_id%7D/link)
+documents `can_comment` independently; omission/null inherits `can_edit`.
+The app therefore sends explicit comment intent on every actual update and omits
+unchanged access settings. It omits an already-absent expiration; JSON null is
+reserved for clearing an existing expiration. The typed route, JSON boolean key,
+and returned capability mapping have synthetic request/response regression coverage.
+
+The expected result of explicit `can_comment: true` is a subsequent reported
+comment capability of true, or an explicit rejection. A successful acknowledgement
+with false remaining in the returned capabilities is treated as an unapplied write.
+
+## Remaining evidence needed
+
+A vendor explanation or a comparison through the official kDrive sharing UI on
+the same generated file would establish whether this is a service constraint,
+a file/access-mode rule, or another integration detail. No message has been sent
+to Infomaniak. The strict comments assertion remains in scenario 16; subsequent
+link-disable and version-copy checks cannot convert that failure into a pass.
diff --git a/doc/STABILITY_LOOP_AUDIT.md b/doc/STABILITY_LOOP_AUDIT.md
new file mode 100644
index 0000000..7f9aaea
--- /dev/null
+++ b/doc/STABILITY_LOOP_AUDIT.md
@@ -0,0 +1,2730 @@
+# Stability Loop Implementation And Evidence Ledger
+
+This file is the auditable implementation ledger for
+[`STABILITY_LOOP_PLAN.md`](STABILITY_LOOP_PLAN.md). It contains no live account
+data, private identifiers, URLs containing identifiers, request/response
+bodies, or credentials. Historical milestone tables below describe their original
+validation. Current live execution and acceptance are recorded separately here.
+
+## 2026-09-10 — Conflict matrix continuation
+
+The same PR now contains a stable two-engine conflict catalog, two persistent
+synthetic plaintext clients, controlled preflight scheduling and failure injection,
+production callback executors/error mapping, and vault replay permutations with
+synthetic minimized reproducers. `doc/CONFLICT_TESTING.md` describes exact entry
+points, case ordering, assertions, and the distinction between model assumptions
+and real server guarantees.
+
+- Finalized deterministic validation: Stability macOS `potassium-conflict-mac-07.xcresult`
+ **414 passed**, standard macOS `potassium-conflict-standard-mac-01.xcresult`
+ **378 passed**, iPhone 17/iOS 26.5 `potassium-conflict-ios-02.xcresult`
+ **362 passed**, Apple Vision Pro/visionOS 26.5
+ `potassium-conflict-vision-sim-01.xcresult` **362 passed**; each has zero failed,
+ skipped, or expected failures. The signed generic visionOS build also succeeded
+ (`potassium-conflict-vision-build-01.log`). After strengthening returned-name
+ assertions, targeted Stability macOS `potassium-conflict-mac-08.xcresult`
+ finalized **19 passed**, zero failed/skipped.
+- The prior Mac06 and iOS01 bundles each retain one cancellation regression
+ failure: the synthetic scheduling gate could release between its last loop
+ cancellation check and return. Earliest divergence: the worker continued to the
+ remote mutation after release. Classification: harness, high confidence. The
+ gate now checks cancellation after release and the test waits for the actual
+ worker terminal before asserting no effects. Mac07/iOS02 are the passing reruns;
+ prior failing bundles remain available. No production cancellation guarantee
+ was inferred from that faulty test gate.
+- Callback inspection and the new regression exposed an encrypted contents+Trash
+ bug: the prior early branch acknowledged contents without applying them.
+ `VaultModificationExecutor` commits supported edits before Trash, uses the
+ committed revisions, preserves pending unsupported fields, and prevents Trash
+ on edit failure. Classification: provider; confidence high from source and
+ deterministic executor tests. The normative register was updated in this change.
+- `595b2e19-4d55-4813-a2bf-92af51b520cf`: targeted live content-after-preflight
+ case failed at the first generated Finder row before the gate. Report: zero
+ passed, one failed, fifteen deliberately unselected. The window closed and the
+ failed bundle was sealed. Explicitly signaling each generated container did not
+ resolve the intermittent empty listing. Root cause remains uncertain.
+- `f5124f0a-d40a-44cb-a362-1c4e9919c33d`: the same targeted case exercised the
+ real held conditional upload, competing remote edit, two preserved byte streams,
+ Finder selection/screenshots, and reopening both versions through TextEdit.
+ The report finalized with one passed and fifteen deliberately unselected;
+ `summary.json` sealed after pending enumeration settled. The read-only AX probe
+ observed the generated rows on this attempt. This is a targeted pass, not a
+ sixteen-scenario acceptance result. No initialization events were present, so
+ it does not establish a cold launch. Remaining live cases and cold/warm
+ acceptance remain open. CR-013 is unchanged.
+
+Latest checkpoint validation, after launch-evidence validation and additional replay
+cases: Stability macOS `potassium-conflict-mac-11.xcresult` **418 passed**;
+standard macOS `potassium-conflict-standard-mac-02.xcresult` **378 passed**;
+iOS `potassium-conflict-ios-03.xcresult` **362 passed**; visionOS Simulator
+`potassium-conflict-vision-sim-02.xcresult` **362 passed**. All are finalized with
+zero failed/skipped/expected failures. Mac09/Mac10 retain initial build failures
+from unavailable imported C/AX constants; Mac11 includes the corrected code.
+The Go to Folder and fresh/running live paths compile but still need live evidence.
+
+### Six-case live profile and remaining navigation failure
+
+The first serial six-case profile produced six immutable bundles; each selected
+case retains one result and fifteen deliberately unselected entries:
+
+| Run | Case | Result |
+| --- | --- | --- |
+| `1fa6de2b-357f-427c-9642-d24e5fb40e62` | content-before-preflight | Passed |
+| `62ba9b99-f444-4f0c-bf66-1178983435f2` | content-after-preflight | Passed |
+| `da404627-f436-4530-a18c-efb9dea2912f` | rename-rename | Passed |
+| `5482f043-e944-4bd9-9f6d-09ec45b7f978` | move-move | Failed before the scheduling gate; generated row absent |
+| `582be3ac-92b1-4293-8556-ef1a3a339f24` | edit-rename | Failed before the scheduling gate; generated row absent |
+| `8f4840b4-e63e-439d-b8d3-c83986bae5ac` | edit-move | Failed before the scheduling gate; generated row absent |
+
+The failures expected the exact generated file in the bound run folder. Observed:
+matching parent/window/list view, zero matching labels, and two AX busy indicators.
+Read-only snapshot inspection found the expected Nested/Sibling/conflict children.
+The run-folder enumeration spans `B61AAE52-078B-48AC-AE8A-8BD3E19AB715`
+(move-move) and `24C583DF-F7E1-4C1C-91B9-E25F4697DDEB` (edit-rename)
+completed in 363 ms and 159 ms respectively. These are supporting observations,
+not proof that Finder received or rendered the observer result. Classification:
+UI automation/environment remains unresolved; root-cause confidence low. Reproduce
+with `--conflicts --case move-move --yes-live`. No competing mutation was triggered
+in the failed cases. A native Go to Folder navigation path is being validated;
+no fix or complete live acceptance is claimed yet.
+
+All run-owned windows closed. Earlier bundles remain available. The invoking shell
+also reported a parse error after the app exited because the script was edited
+while that invocation was still reading it. The current script passes `bash -n`;
+this separate harness-development error does not alter the six sealed app reports.
+Do not edit the command script during a live invocation.
+
+Targeted navigation rerun `63316860-231f-40c9-b575-276d186f4fe9`
+(`--conflicts --case move-move --yes-live`) finalized one passed and fifteen
+unselected entries. Go to Folder navigation exposed the exact generated row;
+the actual Finder move reached its scheduling gate, the competing remote move
+completed, the local destination won on the same identity, and the file reopened
+with matching bytes. Owned windows closed and the bundle sealed. This is a passing
+reproduction of the previously failing case, not proof that the intermittent
+Finder issue is eliminated or that all live cases pass.
+
+### Fresh-extension profile: five passed, delayed local move observation failed
+
+The fresh profile used verified kernel births, signed code and matching initialization
+terminals. Its first five cases sealed as passed:
+`c7b545ba-27aa-4687-bf2f-2980b4ee8097` (content-before-preflight),
+`19f413dd-04cf-484c-affc-573d8dbeb3a1` (content-after-preflight),
+`7132ed32-3ebb-4fb8-83ad-acc227616db6` (rename-rename),
+`fa96b9ab-acd1-4628-b8a5-2d7d160db4f0` (move-move), and
+`75d63673-f6a5-444c-9619-d104ffe6bf4b` (edit-rename).
+Each includes `extension-launch.json`; these are case results, not a complete
+profile certificate. Launch timestamps retain microseconds because ISO-8601
+whole-second encoding otherwise loses the process/recorder ordering. The targeted
+`potassium-conflict-mac-12.xcresult` finalized 20 passed with zero failures/skips.
+
+`2a5fcd7a-b29e-4318-824d-b85510399864` (edit-move) reached the actual gate
+and failed at the immediate expected-local-destination assertion after server
+metadata and byte verification. The UI trace had not yet begun reopening the result;
+that identifies the destination guard as the earliest assertion divergence.
+Competing remote move span `1E274C51-8960-4449-A203-6FEAFF3FEE8D`,
+conditional replacement `5E212013-E5CB-442F-821C-3CEC094D91F5`, and its
+parent modify callback `C75AE248-F637-4F39-8781-97C48D37CBA5` all completed.
+Expected: the same item under the remote destination, with edited bytes and the
+unchanged filename, visible and reopenable in Finder. Observed: backend state
+passed, but the current provider URL did not yet satisfy the parent/name guard.
+Classification: harness timing, high confidence for the immediate assertion gap;
+the precise macOS propagation delay remains to be measured.
+
+The runner now reuses a bounded destination observation for Restore and conflict
+results, retaining exact parent/name and domain/item binding. A stale location
+remains pending rather than success. `FinderRestoreObservationTests` adds a
+stale-move/wrong-name/valid-location sequence and cancellation regression.
+Reproduce with `--conflicts --case edit-move --extension-state fresh --yes-live`.
+The failed bundle and fixtures are retained; its live rerun is pending.
+
+Latest finalized validation after create-callback coverage, stronger vault replay
+oracles and delayed-location handling: Stability macOS
+`potassium-conflict-mac-13.xcresult` **423 passed**; standard macOS
+`potassium-conflict-standard-mac-03.xcresult` **381 passed**; iOS
+`potassium-conflict-ios-04.xcresult` **365 passed**; visionOS Simulator
+`potassium-conflict-vision-sim-03.xcresult` **365 passed**. The signed generic
+visionOS build succeeded (`potassium-conflict-vision-build-02.log`). Subsequent
+original-run launch-option and required-evidence tests finalized
+`potassium-conflict-mac-14.xcresult` **28 passed**. All have zero failed/skipped/
+expected failures. Navigation now clears its previous selection before opening Go
+to Folder because the provider may already have moved that selected identity.
+The ordinary signed live build is being rerun with these changes.
+
+### Fresh profile rerun: persistent edit/move divergence
+
+The next complete fresh-extension profile again sealed five passes:
+`c278d5dc-206b-4515-ad0e-8434778ed15a` (content-before-preflight),
+`73c3dabc-abb5-469e-a273-5618f029c2c2` (content-after-preflight),
+`2ab03433-09f9-4167-bdb6-d5612a60cf0d` (rename-rename),
+`e11a5315-9f56-47e4-8b73-a92eca6a0a36` (move-move), and
+`45d8484d-0b46-4f89-95ce-134096f5bd03` (edit-rename).
+
+`704bbba7-b50f-4cc0-bc60-c2489a851859` (edit-move) failed and sealed.
+Expected: edited bytes on the same server identity under the remote destination,
+then the matching local destination and a successful Finder/TextEdit reopen.
+Observed: server metadata and bytes passed; `parentMatches=false nameMatches=true`
+persisted until the original 90-second deadline. Competing move
+`4DC78AA7-F823-47A0-AA61-EC3F80FC48B5`, replacement
+`7BA5059F-B37C-4C3A-961A-50363DCF6B71`, and modify callback
+`BD3B8BAF-3060-4EFB-A171-41252F6E0027` completed. The prior immediate
+assertion was too early, but waiting alone did not fix this failure. Classification
+remains provider/environment pending returned-metadata evidence; confidence low
+in the underlying cause. The report's harness/deadline classification describes
+the failing observation, not a proven root cause. All owned windows closed and
+earlier bundles remain intact. Reproduce with
+`--conflicts --case edit-move --extension-state fresh --yes-live`.
+
+Successful plaintext modify terminals now retain the existing sanitized metadata
+fingerprint so a targeted reproduction can compare returned callback metadata to
+the verified remote result. No mutation policy changed. Regression and live
+reproduction results follow below. The original sixteen-scenario preserve-both
+path now shares the independently selectable race's cancellable scheduling and
+reopening assertions instead of retaining the older sequential gate flow.
+
+Targeted diagnostic run `737ec849-789f-44b6-a58c-193a7c4ce023` sealed the
+same local-parent deadline failure. The held contents callback
+`FE5F48BF-2472-45E7-AD65-7C77DC10BBF8` returned metadata fingerprint
+`2996D4BC-A3F4-4127-89E9-4276E6A8C85D`, which differed from the independently
+verified remote result. The remote move `E4427365-6DBC-4EA6-B19C-E30A31DA609A`
+and upload `A162DA88-179A-458E-860A-C604660B3AAC` completed before that
+callback terminal. Classification: provider/API response reconciliation; high
+confidence in the returned-metadata mismatch, pending confirmation of which
+receipt field is stale. The upload adapter previously returned its receipt without
+a metadata refresh. An experimental correction performed one lookup after a confirmed
+upload and adopts metadata only when identity, drive, ETag, and size match that
+receipt. Read failures/newer content preserve the receipt and never replay the
+upload. `committedReplacementRefreshesOnlyMatchingContent` covers all three cases.
+The diagnostic-only Mac15 bundle finalized **23 passed**, zero failed/skipped.
+Finalized validation of the guarded metadata refresh: Stability macOS Mac17
+**426 passed**, standard macOS04 **383 passed**, iOS06 **367 passed**,
+visionOS Simulator05 **367 passed**, all with zero failures/skips/expected failures.
+The signed generic visionOS03 build succeeded. Mac16/iOS05/visionOS Simulator04
+each preserve one failure in the older recording-mock test, whose expected call
+sequence omitted the new post-upload item lookup; the expectation now includes
+that read. The new stateful post-upload regressions passed in those runs.
+Experimental rerun `580ca004-b95e-4f0b-8f00-d6286aff8aa4` again sealed the
+edit/move deadline failure with mismatching callback metadata and a stale local
+parent. The extra read did not resolve the issue and was removed, together with
+its experiment-specific tests. Its finalized validation is retained above as
+historical evidence, not the final implementation.
+
+Source inspection found that the typed move API returns `KDriveCancelResource`;
+the runner previously released the local callback after that accepted response
+without independently confirming the move. That did not prove the intended
+ordering. The harness now waits for the competing metadata and bytes, then records
+an immutable attempt-scoped verification while the gate is held, before releasing
+it. Profile version 3 and original preserve-both sealing require that proof.
+Barrier/profile tests reject proof before arrival, after release/cancellation,
+for another fixture or attempt, and missing verification. The live reproduction
+with this stronger ordering follows below. Earlier version 1/2 results remain
+readable but cannot establish the new ordering evidence requirement.
+
+`7ae44326-7d5b-4dc3-8942-51a77d76a898` verified competing server metadata
+and bytes before gate release, then again failed the local-parent deadline.
+Read-only cache inspection after settling found the returned fingerprint from
+contents callback `739EFC0D-2CBF-46A2-8DEE-920A700C1932` matched the final
+metadata under the expected destination. The generated file also existed under
+Sibling and was absent from its old folder after settling. This corrects the prior
+receipt diagnosis: the runner's comparison used a metadata snapshot obtained
+before waiting for edited bytes, so its size could still describe the base file.
+It now refetches metadata after byte verification. Confidence high in this harness
+comparison defect; no stale upload-receipt guarantee is inferred.
+
+The destination wait also preceded navigation into the destination, which could
+leave that folder unenumerated. The runner now opens the already bound parent
+first, then verifies the actual parent through stable item/domain identity and the
+exact filename before reopening. It records both identity and path comparisons;
+a mismatching identity never passes. Mac18 finalized **426 passed** for the ordering
+change; targeted Mac19 finalized **14 passed** for destination identity and gate
+proof. Both have zero failures/skips. The next ordinary signed live run includes
+destination-first navigation; its outcome follows below.
+
+`f5a8daed-2657-4ae4-b617-ad8726d92728` verified ordering and matching
+callback metadata, then timed out with both actual parent identity and path still
+mismatching. Its window cleanup completed and the failed bundle sealed. The
+provider's working-set callbacks took 90,946 and 106,390 ms (spans
+`173E1CF3-09AA-4DC2-BA8A-4D3C9FEE4A48` and
+`31397A22-3C3E-4469-88B7-C1685EBF3647`); change enumeration
+`E08A0CB2-D3DE-4BAC-872C-0A00ADEF4008` took 101,691 ms. Over 1,400
+directory-list starts occurred while 249 materialized items were retained. The
+change-enumeration path waited for a serialized full poll before consulting its
+journal. Classification: provider notification latency; high confidence in the
+measured blocking path, pending end-to-end confirmation of the correction.
+
+Confirmed mutation results now publish into that journal with a per-item comparison;
+poll commits compare their starting anchor before writing any container cursor.
+Already available changes bypass another crawl, while normal empty-journal polling
+continues. No additional remote concurrency, fixture removal, or longer scenario
+deadline was introduced. Mac20 finalized **430 passed**, zero failures/skips;
+`WorkingSetMutationDeliveryTests` covers durable delivery, stale writers/polls,
+unchanged poll watermarks, and expired anchors. The configured-root exclusion also
+built in the subsequent ordinary signed app. Targeted run
+`45502949-1161-4ef0-ae31-26bf59dd6c21` still failed and sealed. Its item
+was published in a one-item journal batch at the same time as contents callback
+`A7F219F1-1BF2-4A2A-96D4-22ABA21CF645` completed (17:27:54 UTC), but an
+enumeration that entered earlier was already awaiting the long poll. An entry-only
+journal check therefore did not fix the in-flight case.
+
+Delivery now rechecks the local journal during that wait. The refresh has a strong
+owner and a diagnostic span created before launch; returning a newly published
+change leaves the refresh running to its real terminal. The runner continues to
+monitor it and cannot seal while it is pending. This adds no remote polling
+concurrency or API retries. A regression holds refresh open, publishes an item,
+verifies delivery completes before refresh release, then releases and settles the
+worker. Mac21 finalized **431 passed**, standard macOS05 **386 passed**, iOS07
+**370 passed**, and visionOS Simulator06 **370 passed**; each has zero failures,
+skips, or expected failures. The signed generic visionOS04 build succeeded.
+Targeted run `2ac425d8-7de5-4a00-89b0-2982e3e60f93` completed the UI/server edit-move checks, matched the actual destination identity, and reopened the edited bytes. Final sealing rejected the bundle, so it is **not a pass**. Its exited owner was safely recovered; fixtures and evidence remain. The prior validator truncated callbacks at the UI step finish, excluding later terminals during monitored settling. Span selection now requires an in-step start and retains the complete correlated subject span. Regressions reject missing, unrelated, earlier, later, failed, and contradictory terminals. Rejected candidates now retain a closed reason and can never serve as acceptance markers. The next sealed live rerun remains required. Working-set latency is registered as CR-022; the existing share-access CR-018 is unchanged.
+
+After the sealing regressions, Mac22 finalized **433 passed**, standard macOS06 **388 passed**, iOS08 **372 passed**, and visionOS Simulator07 **372 passed**, all with zero failed/skipped/expected failures. Signed generic visionOS05 succeeded. These are finalized Xcode results; parameterized argument executions are additional to the function counts. The ordinary signed app was rerun with the same code.
+
+Run `b85d63d0-ef4c-4dd0-9671-00c5d1c1c079` failed during fixture preparation before the race: server creates/upload completed in six seconds, but resolving unopened ancestors waited behind 44,168 ms and 41,345 ms working-set enumerations. Preparation now navigates each bound parent before resolving its children; regression coverage rejects unbound descent.
+The extension later exited with background materialization-refresh span `5FA2F8D9-4DC8-4130-8EF4-B692E5421582` and request `E3014F5E-B790-41A6-B67B-99E9EA6CED19` unfinished. This is incomplete lifecycle evidence, not proof of a crash cause. The SDK recommends promptly acknowledging materialization changes and doing subsequent work as a timed task, so that acknowledgement remains unchanged. The experimental detached change-delivery refresh was also removed: polls now check for a superseding journal between remote requests and discard uncommitted work before returning within enumeration. New tests publish during relevant-item/folder requests and verify prompt delivery without later requests, cursor writes, or watermark advancement. Failed cleanup/settling now also retains a non-accepting candidate. Mac23/iOS09/visionOS08 stopped at a throwing Swift Testing macro expression in the new test closure; no test execution was accepted from those builds. After separating the awaited value from the assertion, Mac24 finalized **435 passed**, standard macOS07 **388 passed**, iOS10 **372 passed**, and visionOS Simulator09 **372 passed**, with zero failed/skipped/expected failures. Generic visionOS06 built successfully. Live14 remained unsealed after its bounded settling deadline and its exited owner was recovered without changing fixtures. Targeted run `7a0f29bf-92be-4570-a4f1-b943ded73668` completed preparation and all edit/move UI/server/reopen checks, but its rejected candidate records `wrongExtensionBuild`. Every callback actually used the expected code hash and one process identity. The final provider callbacks completed at 18:19:13 UTC; `runtimeInvalidate` started at 18:19:18, and sealing occurred at 18:19:20. The runner reused server polling's ten-second backoff for local recorder settling, missing the quiet interval before instance teardown. A dedicated local settlement fence now requires exactly one start/terminal per span plus one second of unchanged telemetry, checked every 500 ms within the existing ten-minute settling deadline. Lifecycle validation still rejects invalidation, restart, or code mismatch during the monitored run. Missing starts, duplicate terminals, pending work, and newly arriving events have regressions. The failed candidate and fixtures remain preserved; its exited owner was recovered. The next live run remains required. Mac25/iOS11/visionOS10 stopped at Swift Testing macro expansion of a mutating observation; the tests now evaluate that observation before asserting it. No execution from those failed builds is accepted. Generic visionOS07 built successfully; Mac26 finalized **437 passed** and standard macOS08 **390 passed**. The new settlement regressions finalized **2 passed** on iOS12 and **2 passed** on visionOS11, supplementing the prior complete 372-test results on each simulator. All have zero failed/skipped/expected failures. Generic visionOS07 succeeded.
+
+
+Targeted run `26985b87-5beb-44ef-b9ca-e1c263384316` completed the controlled move, matching callback metadata, destination identity, and reopening edited bytes. It correctly failed the diagnostic assertion: `currentSyncAnchor` span `AE0750A7-2FAC-4F47-B3BE-F373CE5F6471` failed after 4 ms at 18:30:47 UTC. The bundle sealed as failed; no active owner remains. The displayed Cocoa 4101 is our fallback XPC-reply-invalid wrapper, not evidence of an actual XPC failure. Its original activity error code was 0 and category unknown, so the underlying cause is unconfirmed (CR-023 remains open). The live SQLite store uses WAL and has no legacy snapshot rows; neither a schema-migration fault nor lock contention is proven.
+
+Diagnostic classification now inspects only the bounded cause chain of that fallback wrapper. Known SQLite failures retain their numeric primary/extended result code and storage category; configuration storage and decoding failures get closed categories. SQL statements, error messages, private paths, and user-info remain excluded. A regression passes a wrapped SQLite error containing a fresh canary and verifies only category/code survive. Callback error mapping and conflict policy remain unchanged. The next live reproduction can identify a storage result without inferring it from the wrapper. Earlier failed bundles remain untouched. Focused diagnostic/evidence validation finalized **22 passed** on Mac27 and **22 passed** on standard macOS09; diagnostic tests finalized **8 passed** on iOS13 and **8 passed** on visionOS12. Every result has zero failed/skipped/expected failures. Generic visionOS08 succeeded. These supplement the complete matrix runs recorded above.
+
+Fresh conflict profile Live17 reproduced CR-023 with the new sanitized diagnostics.
+The before-preflight content case (`17805594-eb35-441b-9213-0668838a7189`)
+recorded SQLite primary code **5 (`BUSY`)** in `currentSyncAnchor` span
+`F123ED16-B575-4AB8-9215-D5DC4A0DDC89`, after 4 ms. The after-preflight
+case (`af0412de-1c3d-40a7-b56d-b2fab11be0df`) recorded the same code in
+span `12C82AA9-79E2-4A15-AD76-1EB9B9F3E85D`, after 2 ms. Both content
+races produced and reopened the expected preserved versions, but correctly sealed
+as failed because of the unexpected provider failure. Their expected conditional
+HTTP 412 is a separate conflict event, not the SQLite defect. The complete fresh
+profile finished with **three selected cases passed and three failed**. Every case
+sealed its own report, with the other fifteen scenarios explicitly unselected.
+Rename/rename, move/move, and edit/rename passed. Edit/move
+(`cc9c048e-d389-4b7b-b06c-60f75acf0543`) verified the server bytes and matching
+callback metadata, then failed with a not-found category during local destination
+observation. There were no failed provider spans in that case. Its first observed
+parent was still the old parent; a later lookup threw. The exact lookup boundary
+and numeric code were not retained, so the next build records closed lookup phases
+and numeric errors without descriptions. Regressions require remote lookup errors
+to remain failures at either observation boundary. All owned windows were closed;
+no fixture cleanup was performed and no active recorder owner remains.
+
+Earliest relevant divergence: a fresh snapshot connection requests WAL mode before
+installing its five-second busy timeout. SQLite documents transient exclusive WAL
+locks during cleanup and recovery. High confidence: these live failures are storage
+contention. The exact lock owner and failing initialization statement remain an
+inference until the controlled regression and correction are validated. Reproduce
+with the fresh conflict profile; retain the two failed bundles. No remote policy,
+schema migration, or CR-013 guarantee has changed. An isolated synthetic SQLite
+experiment reproduced immediate code 5 with the existing order and successful
+opening after a 200 ms lock release when the timeout was installed first, preserving
+the existing row. This is preparatory evidence; the production Swift regression
+and live correction are still pending at this checkpoint. References:
+[SQLite WAL contention](https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode),
+[busy timeout](https://www.sqlite.org/c3ref/busy_timeout.html).
+
+The production-store Swift regression finalized as **one failed test** in Mac28
+with `database is locked (code: 5)` against the old order. After installing the
+existing timeout before WAL setup, the same regression passed and retained the
+synthetic row. Full unit-target results finalized: Mac29 **440 passed**, standard
+macOS10 **392 passed**, iOS14 **376 passed**, and visionOS Simulator13 **376 passed**.
+Every corrected bundle has zero failed/skipped/expected failures; generic visionOS09
+built successfully. The earlier red result is preserved as regression evidence.
+CR-023 is mitigated by the tested initialization correction; live acceptance is
+still pending. The original transaction guards and remote policies remain unchanged.
+
+Focused fresh edit/move Live18 (`7b199e30-30d0-4b26-a2f5-9e621c8cf0d2`)
+sealed **passed** on `27e008d`: verified competing commit, matching returned metadata,
+correct destination identity, and reopened edited bytes. Its cropped screenshot
+contains only the generated selected row. No failed provider spans were recorded.
+This is a single selected-case pass, not a full six-case profile or sixteen-scenario
+acceptance. The preceding transient local lookup error did not reproduce; its exact
+cause remains unconfirmed. Complete fresh and already-running conflict profiles
+are now being rerun serially on this same ordinary signed build.
+
+Complete fresh profile Live19 finalized **six of six selected cases passed** on
+`27e008d`. Every case has an immutable Finder report, summary, version-3 conflict
+profile, attempt-specific competing-commit proof, launch attestation, correlated
+timeline, and local screenshots. The existing sixteen-entry report explicitly
+marks the fifteen unrelated scenarios unselected for each case; this is conflict
+profile acceptance, not sixteen-scenario acceptance.
+
+| Fresh case | Preserved run |
+| --- | --- |
+| Content before preflight | `611e4e0a-863d-4691-aa20-bb075b57fc19` |
+| Content after preflight | `6df7a894-00a4-423b-beb9-3e860d70c97f` |
+| Rename/rename | `96a7939d-52d3-4941-9ecb-c0e927477941` |
+| Move/move | `721f60fb-d3ab-4f42-9412-0c72e057312c` |
+| Edit/rename | `419dcd83-40c3-43bc-a151-38ddab987733` |
+| Edit/move | `ae77c1fe-3477-4d95-86ec-f0ab9626d191` |
+
+The content-after-preflight case recorded the expected conditional HTTP 412 before
+preserving and reopening both versions. No SQLite failures recurred. Edit/move
+initially observed the old local parent, then verified the correct destination and
+reopened the edited bytes within its unchanged deadline. The earlier not-found
+lookup failure did not recur and remains unconfirmed. The standalone read-only
+watch command also reported the active scenario, initialization, and settling.
+Live20 started the complete already-running profile immediately after Live19, with
+no rebuild, test execution, or extension replacement between them; it is still
+running at this checkpoint. No generated fixtures were removed.
+
+Live20's first already-running case (`aba975fb-0e39-4444-95f0-0d0ce9bce447`)
+completed its UI/server/reopen checks but retained a non-accepting candidate with
+`wrongExtensionBuild`. No callback failed, and every diagnostic span had exactly
+one start and terminal. The signed code hash and process identity matched the final
+fresh case throughout; the process controller also verified unchanged kernel birth
+time. The observed events were replicated-object invalidation at 19:35:40 UTC,
+initialization at 19:35:43, and final invalidation at 19:39:50, all in the same process.
+The exited owner was recovered without altering the retained candidate or fixtures.
+
+Classification: harness/assertion; high confidence. The launch validator treated
+any object initialization/invalidation as process replacement. Apple's installed
+`NSFileProviderReplicatedExtension.h` (lines 256–264) explicitly permits multiple
+replicated instances in one process, including discarding and recreating an instance.
+The forthcoming version-2 proof distinguishes those lifecycle events from process
+restart, requires their complete successful spans, and retains signed process/birth
+continuity checks. Version-1 bundles retain their original validation semantics;
+the rejected warm candidate will not be retroactively certified. Mac30's narrow
+Xcode selector executed zero tests and is not accepted as regression evidence; the
+whole lifecycle group is being run to demonstrate the old false rejection.
+
+Mac31 finalized **five passed and one failed**: the new balanced-object-lifecycle
+regression reproduced `wrongExtensionBuild` on the old validator. With version 2,
+Mac32 finalized **445 passed**, standard macOS11 **392 passed**, iOS15 **376 passed**,
+and visionOS Simulator14 **376 passed**. Every corrected result has zero failed,
+skipped, or expected failures; generic visionOS10 built successfully. New negative
+coverage rejects process identity changes before/after preparation, failed or
+cancelled instance callbacks, absent/duplicate terminals or starts, and misordered
+or mixed lifecycle operations. Historical version-1 decoding and its original
+stricter validation remain covered. New complete fresh/warm runs are required on
+the ordinary signed build with the corrected proof; Live19 remains the preserved
+passing milestone for its earlier build and version-1 evidence.
+
+Complete profiles Live21 and Live22 finalized **six fresh plus six already-running
+cases passed** on implementation `05e8e5f`. Both commands exited successfully.
+Independent inspection verified all twelve immutable reports and summaries, the
+complete case sets, conflict profile version 3, launch proof version 2, competing
+commit records, screenshots, correlated timelines, and exactly one start/terminal
+per diagnostic span. Every selected case passed; the other fifteen entries in each
+report remain explicitly unselected. No failed assertions, checkpoints, unhealthy
+writer markers, or SQLite errors were present.
+
+| Case | Fresh run | Already-running run |
+| --- | --- | --- |
+| Content before preflight | `d4bce3b3-7157-4ad6-ade6-b4074b1dd547` | `d9cef2b7-fdf4-473b-9c0d-02d2b289435d` |
+| Content after preflight | `a3f16534-e021-461d-92f6-8bcb79489468` | `eeca8ece-8258-4aa5-821b-21baea6a38b6` |
+| Rename/rename | `3493a91e-3791-438a-b264-78ce3474aaae` | `7447ac50-a5a0-40fd-b22c-a35214a8cfc2` |
+| Move/move | `b05c3362-9881-4c3b-8f38-e474ac38f85d` | `b14719f2-e4b1-4c6a-94dd-5a43c9a8c2a3` |
+| Edit/rename | `dfbd93dd-68c5-4866-b49c-f6e25f0e61ad` | `65284fb1-138e-40ab-a113-9bc2f651b70a` |
+| Edit/move | `b94b1c58-090e-41f6-b91c-bb9a9fa39934` | `1d516fca-5420-4696-901e-068e7c5e57e1` |
+
+All twelve proofs identify one signed build. The six fresh cases used six distinct
+processes; all already-running cases reused the final fresh process and attested
+birth before recording. The first warm case included one completed object
+invalidation and one completed initialization in that same process, directly
+verifying the lifecycle correction that the old proof rejected. Earlier failed
+and rejected bundles remain unchanged. The isolated not-found local lookup from
+Live17 did not recur; its exact cause remains unconfirmed. All owned Finder and
+TextEdit windows were closed and all generated fixtures remain preserved.
+
+**Conflict profile acceptance is complete for this build.** The original
+sixteen-scenario contract remains independent and is now being rerun from the same
+ordinary signed app. No sixteen-scenario pass or permanent-delete guarantee is
+inferred from the conflict profiles; CR-013 remains open.
+
+### Original suite: eviction prerequisite timeout
+
+Original full run `30e2b9a1-dc02-49da-950b-cc52ed482338` on `05e8e5f`
+sealed **two passed, one failed, thirteen skipped**. Navigation and fresh hydration
+passed. Eviction expected a real Remove Download action and non-hydrating state
+verification; observed zero UI actions and a harness deadline while awaiting
+`NSFileProviderManager.waitForStabilization`. Its scenario correlation was
+`EC29A776-0F48-4A04-BEC5-9A47B8AE6140`, with no supporting mutation spans.
+No provider diagnostic span failed. The first divergence is therefore the
+pre-action harness barrier, not an observed Finder eviction rejection. Confidence
+is high for that location; the particular pending domain work is unobserved.
+The failed report, screenshots, diagnostics, and fixtures remain preserved; owned
+Finder/TextEdit windows closed and the owner lease released. The conditional warm
+invocation did not run after the fresh failure.
+
+Apple's `NSFileProviderManager.h` describes stabilization as waiting for filesystem
+and provider changes up to the call, across the domain. That is broader than this
+scenario's already-verified item prerequisite. Hydration now closes its generated
+TextEdit presenter after byte verification, addressing the previously observed
+resource-busy hazard without closing unrelated documents. Eviction proceeds with
+fresh exact-item/domain binding, the real Finder action, and its existing
+non-hydrating state assertion. No deadline or evidence requirement was relaxed;
+end-of-run diagnostic settling remains required. `FinderHydrationSequenceTests`
+checks open/verify/close ordering and that a failed presenter close prevents
+completion and leaves eviction blocked. Reproduce using the original
+`--run --extension-state fresh --yes-live` command. Live rerun is pending.
+
+Mac33 finalized **447 passed**, zero failed/skipped/expected failures, including
+both presenter-sequence regressions. This follow-up changes only the macOS
+Stability UI harness; standard macOS11, iOS15, visionOS Simulator14, and the generic
+visionOS10 build above remain the finalized validation for unchanged shared code.
+
+Rerun `60cd782a-c361-4580-a8dd-3153ee26d5fc` on `b1eeb79` sealed **ten
+passed, one failed, five skipped**. Remove Download now executed, its non-hydrating
+state checks passed, and Download Now produced a new successful content fetch with
+matching bytes. The installed provider CDHash remained identical to the twelve
+accepted conflict runs. This is live regression evidence for the presenter-release
+harness correction; it does not identify the earlier domain stabilization blocker.
+
+Restore failed before its context action. The exact trashed fixture was resolved
+and bound (two run-salted subject aliases), then Finder navigation timed out with
+zero completed UI actions. Correlation `B757FF17-F259-40CA-939D-69A9EB10591D`
+retains successful metadata fallback: active lookup `98142803-231F-49FD-AEFB-B7388197205F`
+returned expected 404, Trash lookup `DBF38023-F6E2-4407-94C8-0C487D01505B`
+and enclosing item lookup `EBCFA176-F8BA-47BF-8021-51AA0874B798` completed.
+No Restore callback occurred. Expected: navigate to the exact bound Trash parent,
+select the generated fixture, and invoke Restore. Observed: navigation did not
+finish before selection. Classification: UI automation; confidence high for the
+navigation boundary, low for the specific native transition. A closed stage trace
+now distinguishes activation, navigation-sheet availability, path assignment,
+submission, and destination verification without retaining paths or sheet text.
+The failure and fixtures remain preserved; owned windows closed and the owner lease
+released. No permanent deletion or warm original-suite run occurred. Reproduce with
+`--run --extension-state fresh --yes-live`; further diagnosis is pending.
+
+## 2026-09-10 — Live Finder implementation in progress
+
+PR #22 now targets `main`; all stability work continues on
+`codex/file-provider-stability-loop`. The original plan and implementation through
+`49ac27a`, plus the live suite at `d8718c2`, are included in that single PR.
+The operator authorized the saved lab account and Keychain login. Authentication,
+remote ownership, registration, and domain binding have passed real preflight.
+The lab is a new child of the verified server-created `Private` folder. Neither
+`Private`, the lab root, its marker, nor previous contents are disposable fixtures.
+
+**Acceptance remains open:** there are no complete passing cold/warm Finder runs.
+Permission checkpoints and skipped scenarios are not passes. Code implementing a
+scenario does not establish that its selectors or provider behavior work live.
+
+Implemented runner paths include all 16 scenarios, injectable fresh-window UI
+driving, monotonic deadlines, monitored permission/confirmation panels, a scoped
+post-preflight conflict barrier, exact target ancestry and provider binding,
+item-specific diagnostic aliases, process/build identity, local row screenshots,
+read-only watch, and immutable version 2 reports with correlated timelines.
+Ordinary callbacks require successful terminals; the cancellation scenario
+requires actual progress, cancellation, and a subsequent successful fetch of the
+same item. The legacy global active-step pointer is not sufficient evidence.
+
+### Findings and reproduction evidence
+
+Current continuation evidence:
+
+- **Current live failure:** `5711656b-7079-446a-a21a-6716126d63a5`
+ finalized with zero passed, one failed, and fifteen skipped scenarios. The last
+ live observation confirmed the owned window, expected parent, and list view,
+ but no matching text row or other named element. The failure was reported as
+ `windowMismatch`; the driver now preserves its last live observation instead of
+ querying after the deadline and producing misleading binding flags. Classification:
+ UI automation; the underlying cause remains uncertain. The preceding run
+ `2bc01352-eedb-4bac-9292-ad266e98a179` failed at the same navigation stage.
+ Both windows closed and their immutable failed reports remain available.
+
+- **Restore assertion gap and re-trash failure:**
+ `c43e7fd0-23eb-4121-b06f-3a4ab0d0f0d2` reported eleven passed, one failed,
+ and four skipped scenarios. The completed Restore callback, remote identity,
+ parent, and bytes were verified, but the UI assertion did not require the
+ returned local URL to be under the restored parent. The following re-trash
+ produced no matching mutation callback and remote Trash lookup remained 404.
+ A stale local Trash URL is a hypothesis, not a confirmed provider defect.
+ Classification: harness assertion gap, high confidence from source inspection;
+ root cause of the missing re-trash remains uncertain. Restore now also waits
+ for the exact local parent and filename before Finder selection. Regression
+ coverage rejects a stale Trash location or wrong name. The historical eleven
+ passes do not establish current Restore acceptance. No permanent-delete
+ confirmation or deletion occurred. The strengthened live assertion remains
+ unexercised because subsequent navigation failed.
+
+- **Intermittent Finder listing:** `4f870547-58ff-44d3-9d85-d71f079cb3ac`
+ exhausted the full navigation budget waiting for the first generated row. A
+ read-only AX observer confirmed the exact run window, Finder foreground, and
+ zero matching `Nested` text fields. Provider enumeration span
+ `6148A63A-8459-4D48-ADC6-5670B82EC1AA` completed successfully; read-only
+ inspection of the provider-owned snapshot database found both generated children
+ in the run-root generations. This does not support an empty provider listing.
+ Finder navigation now explicitly resolves the directory as an Apple Events alias
+ before assigning its target. Later runs reproduced the UI failure, so alias
+ coercion has not resolved it. List-view drift is also unsupported by the latest
+ live observation. Reproduce with the documented opt-in command and inspect the
+ first selection's closed observations; do not weaken the row assertion.
+
+- **Recorder ordering:** the owned recorder now starts before context preflight,
+ which can launch the extension. An injected regression emits an initialization
+ event during preflight, then fails preflight; the event must survive and no final
+ report may certify that incomplete run. Real initialization telemetry was still
+ absent in the next live bundle. Plugin registration can precede command startup;
+ its role is unproven. Full cold/warm lifecycle attestation remains open.
+
+- **Current Mac follow-up validation:** `potassium-live-stability-mac-15.xcresult`
+ finalized with 393 passed and zero failures/skips, including local Restore
+ destination, recorder-before-preflight regressions, and the latest row-observation
+ logging. The signed live build also succeeded. The previous shared-runtime results
+ below remain applicable; subsequent edits are guarded by macOS/STABILITY.
+
+- **Completed navigation and Restore race:** `dc49fe60-e9e5-45fe-8261-85fd4339f5cc` again passed
+ ten scenarios, now including all six added navigation captures and the remote
+ change proof. It verified the Trash metadata fallback and exact Finder selection,
+ then invoked Restore. The provider callback
+ `787D06E3-4F3F-4075-B44E-C48F6B144A31` and its remote mutation
+ `20B899BE-5186-4934-B886-8B4E33D2AD22` completed successfully. The runner's
+ active-item check `573C63B4-9FA9-4934-ABC9-755E89EB0CA1` raced the callback
+ and returned 404, prematurely failing verification. Classification: harness;
+ high confidence from callback/API ordering and source inspection. The focused
+ fix gates verification on the exact attested Restore callback, keeps 404 pending,
+ and still requires matching identity, parent, bytes, UI, and all diagnostic proof.
+ New regression tests cover absent callbacks, pending 404, operational errors,
+ and wrong identities/destinations. Complete Restore verification awaits rerun.
+ The report finalized with 10 passed, 1 failed, 5 skipped; its window closed.
+
+- **Navigation timing:** `983e7d51-38ba-4905-bdf2-7c9e19426d30` still
+ exhausted the driver's early 10-second row wait. After removing that cutoff,
+ `15a8d165-afae-446a-a337-0b2cc76dd68f` completed root, nested, Back,
+ Forward, and parent captures (`200.png` through `205.png`) and verified history
+ navigation. A read-only AX observer confirmed the generated folder's exact row
+ appearing and becoming selected. The remote-change portion then exhausted the
+ aggregate 90-second deadline, which had also included fixture preparation.
+ Preparation now has a separate bounded 90-second phase; it must complete before
+ the scenario budget starts. Its time and diagnostics remain in the report.
+ This change requires a rerun. No complete navigation or Restore pass is claimed.
+ The next run `947a40d9-6a97-45fb-9700-6a71fc6f3ca9` instead hit Apple Events
+ -10006 before selection, so closed command labels were added for diagnosis.
+ `0cf21c51-e88f-403c-9169-ad116c62c0b9` again completed all six navigation
+ captures but exhausted its budget before the remote-change proof. The five
+ read-only placeholder resolutions/domain bindings still preceded navigation
+ inside its budget; those now finish in the bounded preparation phase.
+
+- **Current finalized unit validation:** macOS Stability
+ `potassium-live-stability-mac-11.xcresult`: 387 passed; standard macOS
+ `potassium-live-standard-mac-03.xcresult`: 361 passed; iPhone 17/iOS 26.5
+ `potassium-live-ios-07.xcresult`: 345 passed; Apple Vision Pro/visionOS 26.5
+ `potassium-live-vision-sim-05.xcresult`: 345 passed. All have zero failures or
+ skips. The signed generic visionOS build `potassium-live-vision-build-05.log`
+ exited successfully. Simulator tests are kept separate from live Finder runs
+ to avoid competing UI activation. A signed live rerun of the new row wait is
+ in progress; full cold/warm acceptance remains open.
+
+- **Navigation follow-up:** runs `730c6781-b416-4783-9a5d-4212762a80f5`
+ and `4b2c1ec5-a8d9-45bf-8db5-c366295dd265` stopped at the first generated
+ folder selection before any navigation capture. Both retained two completed UI
+ actions (lab/root navigation), zero selected-row captures, and a selection timeout;
+ their dedicated Finder windows closed and reports finalized. During the second
+ run, a read-only Finder snapshot confirmed the expected generated root and zero
+ selected items. An isolated selection of the first run's already rendered folder
+ succeeded. Probable cause: assigning selection before Finder renders the new
+ folder's rows. The driver now waits for the exact displayed row and revalidates
+ its own front-window identity before assignment; regression tests reject missing
+ rows and ignored selections. Live rerun is pending. These runs did not reach Restore.
+
+- **Metadata-fix validation:** `potassium-live-stability-mac-10.xcresult`
+ finalized with 385 passed and no failed/skipped tests. The iPhone 17/iOS 26.5
+ result `potassium-live-ios-07.xcresult` finalized with 345 passed. The signed
+ generic visionOS build `potassium-live-vision-build-05.log` exited successfully.
+ Navigation selector changes made afterward require their own Mac/live rerun.
+
+- **Restore metadata fix, live validation pending:** the metadata callback now uses
+ `KDriveItemMetadataLookup` to consult typed Trash metadata only after active-item
+ HTTP 404. It verifies drive/item identity, preserves operational errors, and
+ reports `.noSuchItem` only after both endpoints return 404. It does not broaden
+ mutation/content preflight. New regression tests cover successful fallback,
+ authoritative absence, identity mismatch, operational failure, and cancellation.
+ Evidence validation accepts a handled nested 404 only with matching successful
+ Trash and enclosing metadata spans; this never replaces the Restore callback.
+
+- **Commit-time validation:** `/private/tmp/potassium-live-stability-mac-09.xcresult`
+ finalized with 375 passed, zero failed/skipped, including the added navigation
+ milestone failure test. This validates the source being committed; it does
+ not replace the outstanding live rerun of those additional captures.
+
+- **Farthest live result:** `0c8fea91-5123-4c8f-87d7-54b7e451a9fc` passed the
+ first ten scenarios, including verified TextEdit editing/upload, rename, move,
+ and the provider's `modifyItem` Trash transition. Restore failed before its
+ UI action: active-item metadata returned HTTP 404 in span
+ `90C81DC4-C76A-4D87-8DE1-230929B3EFA5`, then the provider returned
+ `.cannotSynchronize` (-2005) in parent span
+ `3DAFD9B8-B676-43F3-A495-52BBFE75B915`. Source inspection confirms that
+ `item(for:request:)` consults only active-item metadata, without a Trash lookup
+ fallback in that build. The focused correction above is awaiting validation.
+ The run finalized with 10 passed, 1 failed,
+ 5 skipped, and its Finder window closed. Its fixtures and evidence remain.
+ Scenarios 11–16 and both complete cold/warm acceptance runs remain open.
+- Navigation now captures generated rows at the root, nested levels, Back,
+ Forward, and parent milestones; the sibling capture follows its remote
+ change. A missing milestone capture stops further navigation. These added
+ captures postdate the latest live bundle and require a live rerun.
+
+- `/private/tmp/potassium-live-stability-mac-08.xcresult`: 374 passed, zero
+ failed/skipped, including native TextEdit sequencing failure guards, fresh
+ working-set metadata evidence, and pending-poll coalescing. The requested
+ iPhone 17/iOS 26.5 and Apple Vision Pro/visionOS 26.5 runs finalized with
+ 335 passed each in `potassium-live-ios-06.xcresult` and
+ `potassium-live-vision-sim-04.xcresult`. The signed generic visionOS build
+ `potassium-live-vision-build-04.log` exited successfully. Current standard
+ macOS validation and complete cold/warm live runs remain outstanding.
+- `5d9ec73b-175e-436f-b558-5c9294dc818c`,
+ `d3fffe35-dda6-4bed-9a87-146de8c07997`, and
+ `2d333d69-822d-4cc0-afd8-5963b4ce18b6` each passed six scenarios, then
+ failed editing. The French keyboard layout explains why a hard-coded US
+ Command-A could quit TextEdit. Replacing shortcuts with an opened menu still
+ left native menu tracking stuck, which the operator reported and force quit.
+ The subsequent `1f8e7574-9fba-4e1e-bd3e-cf7bf4b669d3` stopped at hydration
+ while the earlier menu was still blocked. All dedicated Finder windows closed.
+ A fresh local generated-file probe now passes direct AXMenuItem Select All,
+ Paste, Save, matching disk bytes, and exact-document closure. It also confirms
+ that AXEdited belongs to the close button, not the TextEdit window. This local
+ probe is diagnosis, not a live Finder scenario pass. The later ten-scenario
+ live result above verifies the resulting TextEdit correction.
+
+- `0901be6f-ba75-4dde-a1d7-70d5d7e7d80f` verified the first six scenarios,
+ including entering the Finder-created directory with verified remote parentage.
+ TextEdit did not contain the requested replacement after process-addressed
+ synthetic keys; no content-change callback followed. The runner now verifies
+ the editor text and Save acknowledgement before closing the exact document.
+ An attempted WindowServer-key workaround was subsequently replaced by the
+ verified native menu-item actions described above.
+- `a431f636-8889-4a08-8e32-833547c1673b` verified navigation and hydration,
+ then exhausted the eviction deadline after waiting for system stabilization.
+ The contextual eviction command was invoked; no resource-busy alert was
+ recorded. Global working-set polls took 19–61 seconds, including queue time,
+ and did not report errors. Each materialization notification queued another
+ full refresh. Pending notifications now share only a successful poll begun
+ after their observations, with regression coverage for ordering and failure.
+ The Finder window closed and the failed bundle was finalized. Rerun pending.
+
+- `/private/tmp/potassium-live-stability-mac-05.xcresult` finalized with 363
+ passed and zero failed/skipped, including the per-domain poll scheduling test.
+- `67f1dda7-7d1b-4c77-a90f-eb5fff93e1e7` subsequently recorded 33 completed
+ working-set refreshes, no failures, and no contention retries. This is live
+ evidence for serializing immediate materialization polls with other polls.
+ It still failed directory naming; all five earlier scenarios passed and the
+ dedicated Finder window closed. The name editor is contained in the expected
+ window bounds but absent from its child tree, selected-row tree, and ordinary
+ AX parent/focused-window identity. An alternative checks the exact new Finder
+ selection, editor value/process, verified front window ID, and fresh bounds.
+ Its live result is pending; weak or ambiguous matches cannot pass.
+
+- `/private/tmp/potassium-live-vision-build-03.log`: signed generic visionOS
+ build completed successfully with `-allowProvisioningUpdates`. Xcode refreshed
+ provisioning; the earlier missing-App-Groups profile blocker is superseded.
+- `fef2a881-ccea-4e2e-8770-15f3f6a588b7` still recorded six exhausted snapshot
+ refreshes alongside 33 successful refreshes. Bounded retries and equivalent
+ result handling are not a complete resolution of working-set contention.
+ Keep this finding open; do not describe a single recovered poll as a complete fix.
+
+- `/private/tmp/potassium-live-ios-04.xcresult` and
+ `/private/tmp/potassium-live-vision-sim-03.xcresult` each finalized with 330
+ passed, zero failed/skipped on the requested iPhone 17/iOS 26.5 and Apple
+ Vision Pro/visionOS 26.5 simulators. They include the shared snapshot retry,
+ equivalent-result transaction checks, and strict contextual evidence tests.
+
+- `/private/tmp/potassium-live-stability-mac-04.xcresult` finalized with 359
+ passed, zero failed/skipped. This includes SQLite snapshot retry and Finder
+ window-ownership regressions; later UI refinements need the final rerun.
+- `47dae10c-d9ba-4c6f-8d1b-4f0b7db6635b` recorded 11 completed working-set
+ refreshes and one recovered `concurrentSnapshot` checkpoint, with no failed
+ refresh. This verifies the focused retry on a real concurrent enumeration.
+- The operator observed and dismissed Finder's "Unable to Remove Download" /
+ "Resource busy" alert. This establishes that the command was invoked; it was
+ not simply a missing menu selector. The runner had sent TextEdit's close key
+ without observing document closure. Waiting for the exact document window to
+ disappear and the manager's documented testing stabilization barrier precedes
+ eviction; a scoped detector records and dismisses this specific failure alert.
+- `abb74c1a-43a8-4f04-a994-53fbf9042656` certified the first five scenarios:
+ navigation, hydration, eviction, download, and Finder file creation. It closed
+ its Finder window after failing directory creation (5 passed, 1 failed, 10
+ skipped). The generated parent was correlated to its cached snapshot without
+ exporting identifiers: Finder created the default folder name, with no rename
+ callback. Replace the fixed entry delay with a fresh editable-name-field check.
+ No full 16-scenario or cold/warm acceptance is established by this run.
+
+2026-09-10 continuation: runs `6bd4ce12-06fa-4392-ac35-37afe3f09a11`,
+`14e6edaa-05c8-4889-abb6-b8cea7e5bf73`, and
+`75af7047-2894-4bb7-9c01-a3be17d9602e` each certified navigation and hydration,
+then stopped at Finder eviction (2 passed, 1 failed, 13 skipped). The directory
+date 400 and partial-activity 422 did not recur. This closes those reproduced
+request defects, not broader working-set correctness or 16-scenario acceptance.
+
+The next cleanup run, `5e82a9fb-1240-49ef-8363-6e4e52243658`, verified closure
+of the run-owned Finder window after failure. Cleanup checks the created window
+ID and kernel process start time (Finder's LaunchServices launch date can be nil).
+`FinderNavigationTests` cover unrelated/reused process identities and observable
+cleanup errors. Finder popup tracking can reject Apple Events with
+`errFinderIsBusy` (-15260), documented in Apple's SDK `FinderRegistry.h`.
+Await menu dismissal and bounded read-only readiness before another operation;
+cleanup must remain monitored and cannot silently certify a failed close.
+
+That run also classified both former unknown working-set failures as
+`concurrentSnapshot`: all remote requests completed, then the atomic snapshot
+commit rejected a container changed by concurrent enumeration. The focused fix
+repeats the entire snapshot read and remote preparation at most twice, retaining
+the throttle claim and successful watermark until an atomic commit succeeds.
+Retries emit closed `concurrentSnapshot` checkpoints. Persistent contention still
+fails; no stale write is forced. Regression injects real SQLite concurrent saves
+and checks successful convergence plus bounded persistent failure and watermark
+preservation. Current test and live rerun results remain pending.
+
+| Finding | Expected / observed and earliest divergence | Confidence, fix, and regression |
+| --- | --- | --- |
+| Marker date codec mismatch | A freshly provisioned marker should verify after reloading configuration. Remote numeric dates retained fractions; local ISO8601 dates lost them, so equality rejected the same ownership marker before Finder started. | High. Canonicalize marker dates to seconds on creation and decoding without weakening UUID/drive/root/parent checks. `markerSurvivesRemoteAndDomainDateEncodings` covers the two codecs. Real registration resume subsequently verified the existing lab. |
+| Stability host sandbox blocks assistive AX | Runner should request usable Accessibility access. The sandboxed host could not use the assistive APIs. | High; Apple's [sandbox restrictions](https://developer.apple.com/documentation/security/protecting-user-data-with-app-sandbox) corroborate. Only the macOS Stability containing app is outside App Sandbox; both extensions and standard profiles remain sandboxed. Signed entitlement inspection verified this boundary. |
+| CLI sheet event loop and launcher connection | A permission pause should retain the run and respond to the panel. A bare RunLoop did not reliably dispatch the sheet; restarting the launcher also lost its stdout connection. | High for the harness lifecycle defect. Run NSApplication's event loop, ignore SIGPIPE, launch through LaunchServices with persistent private stdout/stderr. The paused run remained readable through the new panel. |
+| Automation consent classification | An app needing first-time Apple Events consent should pause and request it. `errAEEventWouldRequireUserConsent` was classified as Finder unavailable. | High. Treat both consent statuses as checkpoints. A subsequent live preflight passed Finder Automation and Accessibility. |
+| Ambiguous privacy grant / duplicate signed copies | Settings shows Screen Recording enabled, while the installed runner still receives denial. The ordinary `/Applications` app has a different designated signing requirement from the installed Stability app; current and rebuilt Stability requirements match. | Medium; duplicate registration is a supported hypothesis, not proven TCC internals. Reuse one installed bundle by default, with explicit `--build`, and ask the operator to bind the grant to that exact path. No privacy database modifications or broad resets. |
+| Working-set partial-activity HTTP 422 | Working-set refresh should validate the partial-activity response and advance its durable watermark. A `listPartialActivities` failure with numeric status 422 preceded a failed `workingSetRefresh` span. | Resolved for the reproduced request: use upstream's `with=file`; ten requests succeeded in `0965c244-7ea6-457e-bc34-56cba76a1033`, with no recurrence in subsequent Finder runs. Snapshot contention is a separate finding. |
+
+Preserved bundles live under the app group's `StabilityRuns/runs` directory:
+`5a365b82-a543-4d84-9dc1-06619259ee56` (permission checkpoint),
+`c235fa55-8253-4344-8443-425fc4ef5062` (launcher-interrupted, explicitly abandoned),
+`84942857-a39e-4bb4-8e6e-56aa27a98bdc` (Automation classification failure), and
+`ed3e55d5-3fb0-4897-a003-95cb3b543daa` (remaining Screen Recording checkpoint).
+These bundles have zero scenario passes and must remain available for comparison.
+Live completion no longer automatically prunes earlier bundles.
+
+### Finalized validation to date
+
+Subsequent live findings:
+
+- `0965c244-7ea6-457e-bc34-56cba76a1033`: ten partial-activity requests succeeded
+ after `with=file`; no 422 recurrence. Date-only directory mutations still
+ failed with 400. Hidden-extension handling passed; selection verification
+ remained blocked.
+- `73bb9a3a-d0d5-4e3b-a4cc-83724c20c5dc` and
+ `d9d0dab7-ca73-48dd-8d95-26979058ff56`: retained older Finder windows could
+ have exactly equal titles and frames. Bind the front Apple Events window ID
+ before using AX focus to disambiguate. A direct comparison also reproduced
+ Finder's `count selection` returning zero while a fetched selection list
+ contained one item. The runner now fetches and validates a single list snapshot.
+- `e02376a2-19db-45c5-b213-72165a517fe4`: all navigation UI checks and cropped
+ row screenshot capture succeeded; diagnostic certification correctly failed
+ on directory date updates. A separate-correlation API comparison accepted the
+ untouched generated file's existing timestamp. Both rejected subjects matched
+ directory types in the provider's own snapshot cache (no raw identifiers or
+ data exported). Omitting directory content dates did not prevent callbacks in
+ `672f3d5e-6497-4fa7-b9d7-084710480fa3`; that experiment was reverted. The
+ supported fix refetches and returns the server directory date without issuing
+ the file-only request. The SDK's `NSFileProviderReplicatedExtension.h` describes
+ propagation of differing non-pending returned fields to disk. Regression:
+ directory timestamp server-wins and regular-file mutation/refetch tests in
+ `KDriveMutationCoordinatorTests`; live rerun pending.
+- Installer correction: an absent obsolete plug-in registration is normal, but
+ `pluginkit -r` returned nonzero between backup and install. The verified staged
+ bundle was installed and its previous bundle retained. Cleanup is now best
+ effort after install; the install path has a rollback guard. No lab/domain or
+ credential was removed.
+
+The first real navigation bundle, `aed38bf2-8864-4d67-a3ae-f990c3c1596d`,
+finalized with one failed scenario and fifteen skipped. Fresh extension build
+attestation passed. Finder reached both nested levels, Back/Forward/parent, and
+Sibling, where the generated remote fixture was present. The UI exposed the
+display name without its extension, while the selector required the full filename;
+this was the immediate 90-second assertion timeout. The selector now obtains the
+display name for the exact bound URL and rejects ambiguous labels. Its regression
+covers hidden extensions and duplicate labels; live rerun is required. This does
+not hide the provider failures independently recorded in the same bundle:
+`listPartialActivities` 422 and `updateModificationDate` 400 on date-only
+`modifyItem` callbacks. Failure classification and API diagnosis continue.
+
+All commands use the root Xcode project and `-only-testing:potassiumProviderTests`.
+No simulator test invokes the live account. Result paths are local, untracked.
+
+| Destination / profile | Finalized result | Scope caveat |
+| --- | --- | --- |
+| macOS Stability | `/private/tmp/potassium-live-tests-mac-02.xcresult`: 339 passed, 0 failed/skipped | Predates the latest confinement, callback waiter, and validation-field additions; final rerun required. |
+| iPhone 17 / iOS 26.5 | `/private/tmp/potassium-live-ios-01.xcresult`: 315 passed, 0 failed/skipped | Updated shared suite is being rerun. |
+| Apple Vision Pro / visionOS 26.5 | `/private/tmp/potassium-live-vision-sim-02.xcresult`: 321 passed, 0 failed/skipped | Includes confinement and waiter tests; predates validation-field additions. The earlier cancellation test fixture had impossible timestamp ordering and was corrected before this run. |
+| generic visionOS | `/private/tmp/potassium-live-vision-build-02.log`: build succeeded with `CODE_SIGNING_ALLOWED=NO` | Signed device build is blocked by the locally selected actions-extension profile lacking App Groups. This is compile validation, not a signed-device pass. |
+
+Still required: finalized current macOS standard/Stability results and updated
+shared-platform checks, real Finder selector diagnosis, complete healthy evidence
+for all 16 scenarios twice, and focused regression/rerun evidence for each live
+failure. CR-013 remains open regardless of disposable permanent-deletion success.
+
+## Evidence Revisions
+
+| Source | Pinned revision | Role |
+| --- | --- | --- |
+| potassiumChannel | tag `0.3.0`, commit `db829f1f2bd8c2113a529c9c521bd5cdfb5ef4dc` | Typed request/service behavior used by this project |
+| Infomaniak public API documentation | accessed 2026-08-31; page-data version `f77cce2c8a7919ddbfd690c32bd0a019`; navigation snapshot SHA-256 `b831cb542fedc9f06f2b1e28f98c1ec86a6d9a98ef00d20b2c3de95dac0d178d` | Published route and field contract; no public source revision is exposed |
+| Infomaniak iOS | `90c2e2560630b075b77b9e87b46b44d385a05283` | Behavioral comparison only; GPL code is not copied |
+| Infomaniak Android | `25e07993e87e8ae50f7c73aeaf4c6802eef1d434`; Core submodule `7037dab1428b8eb23021db74f5881c4a39fdba83` | Behavioral comparison only; GPL code is not copied |
+| Infomaniak desktop | `f72372661e79744c243fd4963465aaae92a6eba7` | Behavioral comparison only; GPL code is not copied |
+
+## Ordered Milestones
+
+| Milestone | Implementation state | Focused validation | Adversarial review | Commit |
+| --- | --- | --- | --- | --- |
+| Stability build profile and JSONL event store | implemented; reviewer fixes applied | 13 macOS `StabilityDiagnosticsTests` passed 2026-08-31 | all actionable findings fixed | `2fd9fbb` |
+| Callback and network instrumentation | implemented; final reviewer repairs applied | signed `build-for-testing` passed; 27/27 focused executions passed | final bounded pass: no remaining finding | `c348d0c` |
+| Stability Lab and safe root lifecycle | implemented; reviewer fixes applied | Stability graph built; 47/47 focused executions passed | final pass: no remaining actionable defect | `871c04d` |
+| Finder Accessibility runner and checkpoints | implemented; reviewer repairs applied | current signed Stability test graph built with Finder-only entitlements; all 33 focused cases reported passed in both scheme executions; standard macOS, iOS Simulator, and generic visionOS app graphs built | all actionable findings fixed | `347b2a3` |
+| API evidence matrix and adapter corrections | implemented; reviewer repairs applied | signed Stability graph built; final eight-case API slice reported 16/16 passes; historical macOS finalization evidence is superseded by the 2026-09-08 finalized profile results below | final bounded pass: no remaining blocker | `c61cf6e` |
+| Cross-platform completion validation | complete; no live checks run | macOS, iPhone 17 iOS 26.5 Simulator, Apple Vision Pro visionOS 26.5 Simulator test graphs built; generic visionOS built; 2026-09-08 finalized macOS profile results replace the earlier incomplete macOS-host observation | final evidence pass: no remaining finding | `5acee67` |
+| macOS test-profile reliability | implemented; no production behavior changed | finalized standard unit result: 319 passed; finalized Stability unit result: 327 passed; both zero failed/skipped. Shared schemes now explicitly disable coverage. | profile-boundary and ordinary-domain fail-closed coverage reviewed; local full UI-host rerun remains an environment limitation | `test: isolate profile-specific macOS tests`; `chore: stabilize macOS test schemes` |
+
+## Architecture Integration Checklist
+
+- Event-store construction sites: app model, File Provider runtime (including
+ fallback load), contextual-action runtime, and app-group uninstall cleanup.
+- Snapshot SQLite table creation no longer creates activity/conflict tables.
+ Snapshots, anchors, enumerator state, and working-set state remain SQLite.
+- `Stability` exists on the project and all six targets with `STABILITY`,
+ testability, and unoptimized Swift. The two shared Stability schemes contain
+ no credential arguments or environment variables.
+- The unit-test target cannot import the File Provider extension executable.
+ Callback behavior must therefore be implemented in or extracted to the
+ shared core and invoked by the extension entry points.
+- Runtime callback coverage must include extension initialization/invalidation,
+ materialization checks, item lookup, content fetch, create/modify/delete,
+ enumerator creation/invalidation, item/anchor/change enumeration, working-set
+ refresh, known-folder work, thumbnails, and contextual actions.
+- Risks still open for later milestones: permission/checkpoint behavior, safe
+ remote-root ownership proof, and ensuring docs/truth-table evidence stays synchronized.
+ Callback exactly-once semantics and transfer cancellation now share the
+ actor-isolated diagnostic lifecycle and have focused race coverage.
+ Active-run, append/read/finish, and retention transitions now share a
+ cross-process lifecycle lock and crash-durable file transitions.
+
+## Milestone 1 Decision Records
+
+| Decision | Evidence | Live result | Chosen behavior | Tests | Truth-table impact |
+| --- | --- | --- | --- | --- | --- |
+| Build/profile isolation | Project target/scheme map at `efd5925`; plan requirements | not applicable | Reuse production IDs/groups/entitlements; define `STABILITY` in a distinct configuration and omit UI automation from its unit Test action | build-settings inspection; `StabilityDiagnosticsTests` | none; no mutation behavior changed |
+| Event-store selection | Four production construction sites and existing event protocols | not applicable | Standard uses SQLite; Stability uses only the active run's JSONL and never silently falls back | factory-selection test | cleanup implementation reviewed; mutation/conflict decisions unchanged |
+| SQLite boundary | `KDriveSnapshotSQLiteStore.createTables` previously called event-table creation | not applicable | Snapshot/anchor/working-set SQLite remains; activity/conflict tables are independent and JSONL-only in Stability | factory test asserts both JSONL and SQLite exist | none |
+| Private-data boundary | `AGENTS.md`, plan invariants, existing support-export model | not applicable | Redact before encoding; closed-enum diagnostic schema; no names, paths, item/request/account/drive identifiers, raw URLs/bodies/headers/data/share links | raw-byte private-value test; enum-only encoding test | conflict event fields are redacted, decision state preserved |
+| JSONL durability | Plan requirement for app/extension/actions concurrent writers | not applicable | lifecycle plus event-file locks, complete-record append, `fsync`; ignore and truncate only an unterminated tail; corruption otherwise surfaces; reject before the 250 MiB active-event limit | real subprocess and two-store writers; concurrent coordinators; interrupted-tail, corruption, capacity, post-finish, and symlink tests | unresolved conflict records replay unchanged except private fields |
+| Clear/removal and retention | Existing event-store semantics and safe cleanup rules | not applicable | append tombstones; preserve unresolved conflicts; prune only whole completed bundles, never active/incomplete | replay and completed-only retention tests | conflict cleanup semantics unchanged |
+
+## Milestone 2 Decision Records
+
+| Decision | Evidence | Live result | Chosen behavior | Tests | Truth-table impact |
+| --- | --- | --- | --- | --- | --- |
+| Callback lifecycle | Apple replicated File Provider callback/cancellation contract; existing `FileProviderOperationLifecycle` | not run | start once; first completion/failure/cancellation wins; attempt the terminal append before invoking File Provider completion so run finalization cannot lose it | span terminal-race and lifecycle failure/cancellation tests | observes existing mutation outcomes; no decision changes |
+| Correlation | plan privacy boundary and structured-concurrency task inheritance | not applicable | propagate only a random callback UUID through `TaskLocal`; give each nested span a separate stable UUID so concurrent child operations can be paired | nested-correlation and span-identity tests | no mutation/conflict change |
+| Typed request evidence | potassiumChannel `0.3.0` service calls and the route map | not run | record only enum operation, route template, option shape, phase, duration, and class; never raw request data | drive-discovery request/diagnostic test; schema/redaction tests | no request behavior change |
+| Transfer cancellation | potassiumChannel progress/cancel operation and File Provider progress contract | not run | start lazily when consumed or cancelled; forward progress by deduplicated buckets; race cancel and value completion through one terminal gate | lazy transfer, progress, forwarding, and terminal-race tests | retries and conflict policies unchanged |
+| HTTP and callback diagnostic class | API rejection classifier plus `NSFileProviderError.Code`, without retaining body/header/user-info metadata | not run | map closed HTTP and File Provider recovery classes; cancellation records `cancelled`; expected share-link 404 is a successful optional result | closed classifier tests including 429/507 and File Provider recovery cases; optional-404 adapter test | Milestone 5 maps 408/429 and oversized direct uploads to recoverable File Provider errors; diagnostic payloads remain closed |
+| Active-run binding | JSONL writers reject sealed runs and a Stability run may start after a long-lived app/extension object | not applicable | resolve the active recorder at callback or service-construction time; never cache a missing or sealed run writer for the object lifetime | factory/run lifecycle tests plus reviewer inspection | no mutation/conflict change |
+
+## Review And Validation Log
+
+### 2026-09-09 — macOS test-profile reliability repair
+
+- `FinderStabilityCommandTests` now compiles only under `os(macOS) &&
+ STABILITY`. Five ordinary-domain app-model tests and the concurrent ordinary
+ domain-add setup test compile only in the standard profile. A Stability-only
+ regression proves `addDomain` rejects an ordinary domain before either
+ registration or persistence. The related Stability Lab registration-race
+ test is also profile-gated because it exercises Stability provisioning.
+ This changes no File Provider mutation, version, retry, cleanup, or error
+ mapping decision.
+- Before the scheme edit, the following credential-free macOS commands created
+ finalized result bundles on macOS 26.6.2. The standard unit result at
+ `/tmp/potassium-provider-m1-standard-rerun/Logs/Test/Test-potassiumProvider-2026.09.08_23-43-42-+0200.xcresult`
+ reports `result: Passed`, 319 total/passed, zero failed, and zero skipped.
+ The Stability result at
+ `/tmp/potassium-provider-m1-stability/Logs/Test/Test-potassiumProvider-Stability-2026.09.08_23-40-14-+0200.xcresult`
+ reports `result: Passed`, 327 total/passed, zero failed, and zero skipped:
+
+ ```sh
+ env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild test -project potassiumProvider.xcodeproj -scheme potassiumProvider -configuration Debug -destination 'platform=macOS,arch=arm64' -enableCodeCoverage NO -only-testing:potassiumProviderTests -derivedDataPath /tmp/potassium-provider-m1-standard-rerun CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO COMPILER_INDEX_STORE_ENABLE=NO
+
+ env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild test -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=macOS,arch=arm64' -enableCodeCoverage NO -derivedDataPath /tmp/potassium-provider-m1-stability CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO COMPILER_INDEX_STORE_ENABLE=NO
+ ```
+
+- The two shared app Test actions now persist `codeCoverageEnabled="NO"`; no
+ coverage consumer exists in the repository. A follow-up unadorned full
+ standard-scheme run (including UI) and unadorned profile runs reached their
+ app-hosted test processes but this local Xcode host did not write a result
+ bundle `Info.plist`. They were interrupted and are not recorded as passes.
+ This is an environment limitation, not a test success or a File Provider
+ behavior result. No credential, File Provider registration, Finder command,
+ or remote mutation occurred in any attempt.
+
+### 2026-08-31 — Milestone 1 review and repair
+
+- `xcodebuild -list -project potassiumProvider.xcodeproj`: configuration and
+ scheme discovery succeeded.
+- `xcodebuild -showBuildSettings ... -configuration Stability`: confirmed
+ `STABILITY`, production identity, app group, and entitlements.
+- `xcodebuild test -quiet -project potassiumProvider.xcodeproj -scheme
+ potassiumProvider-Stability -destination 'platform=macOS,arch=arm64'
+ -only-testing:potassiumProviderTests/StabilityDiagnosticsTests`: thirteen
+ focused tests passed after the adversarial findings were repaired. An earlier
+ unsigned attempt could build but could not materialize the hosted macOS test
+ worker; signed local execution succeeded.
+- Adversarial findings fixed: compiled out the environment UI fixture, disabled
+ legacy unified logs, added lifecycle locking and post-finish refusal,
+ truncated interrupted tails, added owner-only/no-symlink/crash-sync file
+ handling, bounded the active event file, matched zero-limit/tombstone/paging/
+ statistics/observation/export behavior, and added a real child-process writer
+ test. Two bounded final-scan turns were interrupted after the reviewer agent
+ did not return; the earlier review's complete actionable list is resolved and
+ the focused suite/privacy scan were rerun afterward.
+- No live network or remote mutation was performed.
+
+### 2026-08-31 — Milestone 2 review and repair
+
+- Signed macOS `build-for-testing` passed for the Stability scheme, including
+ Core, app, both extensions, and the unit bundle.
+- The final credential-free `test-without-building` command exited normally
+ with 27/27 selected executions successful: all diagnostic-span and operation-
+ lifecycle tests across the Stability test-plan variants, plus exact lazy
+ transfer, optional share-link, and concurrent transfer-start/cancel adapter
+ tests. No live or default hosted suite was selected. Earlier focused runs
+ encountered a local `DTServiceHub` logarchive-finalization hang after their
+ result streams had completed; signing the refreshed hosted bundle allowed the
+ final run and result bundle to close normally.
+- Reviewer repairs rechecked lifecycle state after a suspended start append,
+ moved terminal append attempts ahead of system callbacks, added stable span
+ IDs, deduplicated progress buckets, instrumented transfer progress, classified
+ File Provider recovery errors, rebound long-lived objects per active run,
+ acknowledged materialization promptly after its callback span and kept the
+ background work in correlated child spans, covered every SDK changed-field
+ shape, separated enumerator lifecycle operations, and corrected cancellation,
+ lazy-transfer, and expected-404 outcomes. The final pass also found and fixed
+ the deferred-span reentrancy race, restored standard share-link unified spans,
+ and made app activity storage resolve the active run dynamically.
+- `git diff --check` and the added-line credential/private-URL scan passed.
+- No live network or remote mutation was performed.
+
+### 2026-09-01 — Milestone 3 Stability Lab
+
+- Added a Stability-only macOS lab UI backed by the existing manual-token
+ Keychain flow. Standard and Stability build identities register only their
+ matching ordinary/lab domain purpose.
+- Provisioning rejects any saved or registered domain and any external,
+ unavailable, duplicate, or maintenance drive before remote mutation. It
+ verifies the explicit drive root, creates one unique top-level directory,
+ uploads a fixed marker with conflict-as-error, verifies it, persists root and
+ marker IDs, and only then registers File Provider. Partial provisioning is
+ retained for manual recovery; there is no automatic remote rollback.
+- Reset requires an exact typed phrase and a complete bounded listing. The
+ reset plan cannot represent root/marker/permanent deletion. Before every
+ `trashItem`, the coordinator re-reads internal drive access, system domain
+ registration, root, marker, and target parent and aborts on drift. A
+ cross-process lifecycle lease blocks an active or newly starting diagnostics
+ run for the entire reset. File Provider and action runtimes reject a domain
+ purpose that does not match their compiled profile.
+- `xcodebuild build-for-testing -quiet -project potassiumProvider.xcodeproj
+ -scheme potassiumProvider-Stability -configuration Stability -destination
+ 'platform=macOS,arch=arm64' -derivedDataPath
+ /tmp/potassium-provider-stability-lab-dd
+ COMPILER_INDEX_STORE_ENABLE=NO` exited 0.
+- The final signed `test-without-building`, restricted to
+ `StabilityDiagnosticsTests`, `StabilityLabSafetyTests`, and
+ `StabilityLabRemoteCoordinatorTests`, exited 0 with `TEST EXECUTE SUCCEEDED`:
+ 47 Swift Testing cases passed in three suites and the result bundle finalized
+ at `/tmp/potassium-lab-final-v4-20260901.xcresult`. An earlier two-suite run had
+ passed before local Xcode stalled during result-bundle finalization; the
+ successful final run supersedes that incomplete result artifact.
+- No live credential was read, no File Provider domain was registered, and no
+ remote mutation was performed.
+
+## Milestone 3 Decision Records
+
+| Decision | Evidence | Live result | Chosen behavior | Tests | Truth-table impact |
+| --- | --- | --- | --- | --- | --- |
+| Domain/build isolation | shared production identity; File Provider registration map; plan invariant | not run | legacy records decode ordinary; app, File Provider, and action runtimes accept only the current profile's consistent purpose; live registration evidence is re-queried before provision/reset mutations | configuration profile tests; ordinary-domain no-call and domain-change race tests | adds fail-closed lab registration/cleanup row |
+| Lab-root ownership | pinned client drive eligibility plus server-authoritative discovery/root/marker metadata | not run | treat discovery as internal membership only; prove lab ownership by this build's random marker plus exact persisted and remote root/marker identity under the explicit drive root | external-drive no-mutation, provision-shape, and marker mismatch tests | adds provisioning predicate without claiming product ownership |
+| Ownership marker | plan requirement; remote upload/download and local configuration contracts | not run | marker payload has no name/path/account data; persist marker file ID separately; require local/remote marker equality and preserve the file | marker round-trip, missing/mismatch/parent/duplicate evidence tests | adds marker collision and preservation rows |
+| Reset mutation | safe cleanup policy and truth-table trash/permanent-delete distinction | not run | exact confirmation, complete bounded pagination, preserve root/marker, fresh TOCTOU checks, trash only, live domain isolation, and a cross-process inactive-run lease | pagination/cursor, reset preservation, target/root/marker/domain drift, and run-lease tests | documents reversible trash and keeps unconditional permanent delete risk separate |
+
+## Milestone 4 Decision Records
+
+| Decision | Evidence | Live result | Chosen behavior | Tests | Truth-table impact |
+| --- | --- | --- | --- | --- | --- |
+| Command and credential boundary | plan invariant; existing manual-token Keychain flow | not run | command accepts preflight/run plus local stale-run recovery, permission prompting only where applicable, explicit `--yes-live`, and explicit `--yes-recover`; context loads the saved manual-token credential from Keychain and never accepts credential/account/root inputs; recovery reads no credential and performs no remote action | parser rejection, recovery dispatch, and closed console-status tests | no mutation semantic change |
+| macOS permission preflight | Apple Accessibility trust and Apple Events target-permission APIs; File Provider registered/visible-domain APIs | not run | Accessibility, Finder Automation, File Provider registration, consent, and lab safety are typed preflights; the macOS Stability configuration alone carries Automation plus Finder-scoped sandbox Apple Events entitlements; OS consent and variable contextual UI return checkpoint exit 3, not product failure | pure permission/consent/checkpoint evaluator and build-setting/entitlements isolation tests | no conflict decision change |
+| Scenario and assertion contract | plan's fixed scenario list; File Provider-visible root; typed remote adapter | not run | execute 16 steps in fixed order; a failure skips later steps while a typed UI checkpoint permits independent later scenarios; checkpoint reasons are restricted to their scenario; every passing step requires Finder-visible plus fresh server-authoritative assertions, correlated baseline/postcondition observations, and scenario-appropriate successful callback/network terminals; enumeration requires both item enumeration and anchor/change evidence; working-set requires a known same-run item and preserve-both requires two distinct Finder-visible candidates; restore/delete/cancel/contextual UI stop at checkpoints instead of substituting direct APIs, starting an unobserved transfer, or using global progress | report order, checkpoint classification, terminal-state, correlation, conjunctive enumeration/anchor evidence, missing/wrong-source diagnostic, round-trip, and immutable-evidence tests | observes mutation/conflict cells without redefining them |
+| Finder evidence privacy and durability | Stability JSONL lifecycle and privacy boundary | not applicable | create an exclusive runner-owned run, publish a private per-step cross-process correlation pointer, replace closed assertion/API JSONL files, exclusive-create one immutable validated report as commit marker, verify caller summary counts against that report, then seal summary; on failure retain owner PID/token and the active unsealed bundle; explicit recovery requires a dead owner, writes immutable abandonment evidence, clears a stale step, and prevents ordinary finalization; store no identifiers, names, paths, URLs, bodies, headers, shares, or bytes in report evidence | prohibited-key/canary, duplicate/missing observation and diagnostic, summary mismatch, ownership exclusion, live/dead owner, stale-step abandonment, partial-write/retry, and one-write tests | diagnostics only |
+| Live mutation scope | exact lab root/marker preflight and explicit operator confirmation | not run | live sequence is outside CI and operates only through the verified non-root lab; recheck sole saved/system domain, remote root/marker, and cached root URL's root-container/configured-domain binding before every scenario; immediately bind every existing-item mutation URL (and move destination) to its expected File Provider item and configured domain, reusing the single validated identifier for eviction; reset stays trash-only; restore and permanent deletion leave the lab root selected and never open global Trash or direct a destructive action; cancellation leaves an exact evicted item selected without starting a download; contextual actions never call the remote API directly | structural model, single-resolution item/domain drift, root/domain drift, and command safety tests; live validation remains operator-only | runner adds no permanent-delete mutation; `CR-013` remains open for the product callback |
+
+## 2026-09-01 — Milestone 4 Finder runner
+
+- Added the Stability-only macOS command and wrapper, Apple permission
+ preflight, File Provider-visible scenario runner, closed 16-step report, and
+ immutable assertion/API evidence assembly. No credential or private value is
+ accepted on the command line or written to the evidence schema.
+- The command exclusively owns its active run, uses a private per-step pointer
+ for cross-process diagnostic correlation, and cannot seal a partial evidence
+ assembly. Restore, permanent-delete, cancellation, and localized contextual
+ UI terminate at explicit Finder/operator checkpoints rather than being
+ represented by direct remote calls, an unobserved transfer, or domain-global
+ progress.
+- Failed/crashed commands retain ownership; explicit local recovery verifies
+ the owner process is gone, writes an abandonment marker, removes a stale
+ correlation pointer, and prevents ordinary summary finalization. Passing
+ steps now require scenario-specific correlated terminal diagnostics;
+ enumeration requires both listing and anchor/change terminals. Checkpoint
+ reasons are scenario-bound, existing mutation URLs and the move destination
+ are rebound to expected File Provider item/domain identities, eviction reuses
+ its single validated identifier, the cached lab root is rebound after
+ baseline pagination and immediately before execution, and summary sealing
+ rejects aggregate counts that differ from the immutable report.
+- The then-current signed Stability graph completed `build-for-testing`, including
+ the Stability-only Automation and Finder-scoped sandbox Apple Events
+ entitlements. All 33 selected Swift Testing cases reported passed in both
+ scheme executions across three suites. That historical invocation did not
+ finalize its result bundle. The finalized 2026-09-08 macOS Stability result
+ above supersedes it with 327 passed and zero failures. The Stability
+ app/extension graph also built successfully for a
+ standard macOS Debug build, the iPhone 17 iOS 26.5 Simulator Stability build,
+ and the generic visionOS Stability build after verifying all Finder-only
+ sources are platform-gated. The final read-only adversarial review found no
+ remaining code defect; its stale-evidence finding was closed by this current
+ rebuild/run. No live credential was read, no Finder operation ran, and no
+ remote mutation was performed during automated validation.
+
+## 2026-09-01 — Milestone 5 API evidence and adapters
+
+- Completed the operation-by-operation matrix against the potassiumChannel
+ `0.3.0` pin, the captured official documentation revision, and pinned iOS,
+ Android, and desktop sources. Reference client code was used only as behavior
+ evidence; no GPL implementation was copied. Every live-result cell remains
+ `not run` except the already-sanitized advanced-listing observation because
+ no development credential or remote-mutation authority was provided.
+- Corrected five discrepancies: modeled inherited share access and rejected
+ unknown access values; explicitly encoded a cleared share expiration as JSON
+ null while retaining the pinned typed route; rejected direct uploads above
+ `1_000_000_000` bytes before callback buffering or request construction;
+ mapped 408/429 to retryable
+ server-unreachable recovery while retaining only safe parsed delta seconds;
+ and sent an explicit extension-preserving duplicate name instead of `{}`.
+- A fresh signed Stability `build-for-testing` completed successfully. The
+ focused `test-without-building` selected `KDriveAPIEvidenceTests` and
+ `KDriveContextActionTests`; all 17 unique cases passed in both scheme
+ executions (34/34 reported successes). After the HTTP 429 precedence and
+ fixture-literal repairs, the then-current seven-case API slice reported 14/14
+ passes. After the pre-buffer repair, the final eight-case API slice reported
+ all 16 passes across both scheme executions. That historical invocation did
+ not finalize its result bundle; the finalized 2026-09-08 full Stability
+ macOS result above supersedes it with 327 passed and zero failures.
+- `git diff --check` and the credential/private-value scan were rerun after the
+ final repairs. No live API request, File Provider mutation, or remote cleanup
+ was performed.
+
+## 2026-09-01 — Cross-platform completion validation
+
+All commands removed the inherited Infomaniak and App Store Connect credential
+variables. The four build commands exited successfully:
+
+```sh
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild build-for-testing -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=macOS,arch=arm64' -derivedDataPath /tmp/potassium-final-macos COMPILER_INDEX_STORE_ENABLE=NO
+
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild build-for-testing -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=iOS Simulator,OS=26.5,name=iPhone 17' -derivedDataPath /tmp/potassium-final-ios COMPILER_INDEX_STORE_ENABLE=NO
+
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild build -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'generic/platform=visionOS' -derivedDataPath /tmp/potassium-final-vision-device COMPILER_INDEX_STORE_ENABLE=NO CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
+
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild build-for-testing -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=visionOS Simulator,OS=26.5,name=Apple Vision Pro' -derivedDataPath /tmp/potassium-final-vision COMPILER_INDEX_STORE_ENABLE=NO
+```
+
+The full Swift unit-test target was then run without rebuilding on each hosted
+destination. Output was filtered only for terminal test/error lines:
+
+```sh
+set -o pipefail
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild test-without-building -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=macOS,arch=arm64' -derivedDataPath /tmp/potassium-final-macos COMPILER_INDEX_STORE_ENABLE=NO -only-testing:potassiumProviderTests 2>&1 | rg --line-buffered "(Test case .* (passed|failed)|Test Suite|Testing started|error:|TEST EXECUTE|BUILD INTERRUPTED)"
+
+set -o pipefail
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild test-without-building -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=iOS Simulator,OS=26.5,name=iPhone 17' -derivedDataPath /tmp/potassium-final-ios COMPILER_INDEX_STORE_ENABLE=NO -only-testing:potassiumProviderTests 2>&1 | rg --line-buffered "(Test case .* (passed|failed)|Test Suite|Testing started|error:|TEST EXECUTE|BUILD INTERRUPTED)"
+
+set -o pipefail
+env -u INFOMANIAK_TOKEN -u ASC_ISSUER_ID -u ASC_KEY_ID -u ASC_KEY_NAME -u ASC_KEY_PATH -u ASC_TEAM_ID xcodebuild test-without-building -quiet -project potassiumProvider.xcodeproj -scheme potassiumProvider-Stability -configuration Stability -destination 'platform=visionOS Simulator,OS=26.5,name=Apple Vision Pro' -derivedDataPath /tmp/potassium-final-vision COMPILER_INDEX_STORE_ENABLE=NO -only-testing:potassiumProviderTests 2>&1 | rg --line-buffered "(Test case .* (passed|failed)|Test Suite|Testing started|error:|TEST EXECUTE|BUILD INTERRUPTED)"
+```
+
+At the time, the macOS, iOS Simulator, and visionOS Simulator unit-test process
+emitted only passing terminal cases and no failure/error line, but their result
+bundles did not finalize. The 2026-09-08 finalized macOS profile results above
+supersede that incomplete macOS observation; iOS and visionOS have not been
+rerun as part of this macOS reliability repair. Existing non-fatal Swift
+Testing/Sendable warnings remain outside this stability-loop change. No live
+credential was consumed, no Finder scenario ran, and no local or remote File
+Provider mutation was performed.
+
+A final scan of every added line in `codex/stability-loop-plan...HEAD` found one
+credential-shaped literal in a newly added in-memory test fixture. It was
+replaced with a per-execution UUID canary; the related Finder-command and Lab
+remote-coordinator fixtures now contain no committed credential value, and the
+injected remotes discard the canary without logging or persistence. The signed
+macOS test graph rebuilt successfully and all 23 affected cases reported passed
+in both scheme executions (46/46) before that historical result-finalization
+issue. The finalized 2026-09-08 macOS profile results above are the current
+macOS evidence. The reviewer returned PASS, and the repeated branch-range
+added-literal scan found no nonempty credential or authorization-header fixture.
+
+## API Decision Ledger
+
+Evidence keys used by the per-operation matrix:
+
+- **P** — the project-pinned OpenCow42 potassiumChannel `0.3.0`, peeled commit
+ [`db829f1f`](https://github.com/OpenCow42/potassiumChannel/tree/db829f1f2bd8c2113a529c9c521bd5cdfb5ef4dc).
+- **D** — [Infomaniak API reference](https://developer.infomaniak.com/docs/api)
+ snapshot identified in Evidence Revisions. The public site exposes no source
+ commit; each route was re-read from that captured page-data revision.
+- **I** — official [iOS kDrive source](https://github.com/Infomaniak/ios-kDrive/tree/90c2e2560630b075b77b9e87b46b44d385a05283)
+ at `90c2e256`; API fetchers and File Provider behavior were compared.
+- **A** — official [Android kDrive source](https://github.com/Infomaniak/android-kDrive/tree/25e07993e87e8ae50f7c73aeaf4c6802eef1d434)
+ at `25e07993`, including Core submodule `7037dab1`.
+- **K** — official [desktop kDrive source](https://github.com/Infomaniak/desktop-kDrive/tree/f72372661e79744c243fd4963465aaae92a6eba7)
+ at `f7237266`; network-job request shapes were compared.
+
+Reference-client code is GPL behavioral evidence only. No implementation was
+copied. `not run` means no development credential was supplied and therefore
+no live request was sent. Dependency request tests at **P** cover unmodified
+typed builders; app fixture names identify local adapter or policy coverage.
+
+| Protocol operation | Version-pinned evidence and discrepancy | Live result | Chosen behavior | Affected tests | Truth-table impact |
+| --- | --- | --- | --- | --- | --- |
+| `listDrives()` | P typed core has no public discovery helper; I/A load eligible drive roles; public D does not expose `/2/drive/init` | not run | retain the small typed app request, accept one internal non-maintenance membership record, and never claim account ownership | `kdriveServiceLoadsDriveRolesFromDriveInitOnly`; Stability Lab external/duplicate/maintenance rejection | lab ownership rows remain fail closed |
+| `item(driveID:fileID:)` | P/D/I/A/K agree on stable file ID metadata; ETag is an included resource | not run | request direct metadata with `with=etag`; treat it as authoritative before versioned mutations | mutation coordinator matching/stale-version suites | `C` remains stable ID plus ETag |
+| `listDirectory(...)` | P/D/I/A expose per-folder cursor listing; ordinary listing accepts ETag | not run | request ETag first; retry without `with` only for the known 422 compatibility response | `kdriveServiceFallsBackToDirectoryListingWithoutETagAfter422` | no cursor protocol substitution |
+| `listAdvancedDirectory(..., cursor:nil, ...)` | P supplies `/listing`; K uses drive-wide advanced listing; D currently has no public route page | prior sanitized observation only: `etag` and `files.etag` rejected; `files.capabilities` accepted | use `files.capabilities`; surface 422 and retain the prior snapshot/anchor | initial advanced-listing and 422 tests | advanced-listing row retained |
+| `listAdvancedDirectory(..., cursor:value, ...)` | P supplies `/listing/continue`; K corroborates advanced cursors; D currently has no page | same prior sanitized observation | preserve advanced cursor/action semantics and never fall back to ordinary listing | continued advanced-listing and 422 tests | advanced-listing row retained |
+| `listTrash(...)` | P/D/I/A expose cursor-paginated trash listing | not run | order and page through typed trash results; validate pagination before committing state | listing validator; trash enumeration coverage | no mutation change |
+| `downloadFile(...)` | P/D/I/A/K agree on stable-ID download | not run | async convenience consumes the same lazy operation | lazy download fixture; transfer diagnostics tests | no conflict change |
+| `downloadFileOperation(...)` | P exposes Foundation progress/cancel; I/K corroborate cancellable transfer work | not run | lazy start, one underlying cancellation, deduplicated progress, one terminal diagnostic | lazy transfer/progress/start-cancel tests | retry semantics unchanged |
+| `thumbnail(...)` | P/D/I/A expose typed thumbnail size options | not run | use typed request; record only option shape | `kdriveServiceFetchesThumbnailThroughPotassiumRoute` | no mutation change |
+| `uploadFile(...)` | P/D/I/A/K direct-upload contract; D says files over 1 GB require a session | not run | async convenience uses the guarded operation | upload request and direct-size boundary tests | adds large-upload fail-closed row |
+| `uploadFileOperation(...)` | P encodes total size/conflict/token/hash; D caps direct upload at `1_000_000_000` bytes and documents sessions above it | not run | preflight callback-file size before loading, validate the loaded count again before request construction, and return a mapped synchronization error until a file-backed session adapter exists | upload shape; pure byte boundary; sparse-file pre-buffer rejection | `CR-017` mitigated |
+| `replaceFile(...)` | P/D agree `file_id` plus `If-Match`; I/K corroborate conditional replacement | not run | async convenience performs the same guarded stable-ID replacement | conditional replace and race tests | conditional preserve-both rows unchanged |
+| `replaceFileOperation(...)` | P/D support ETag condition and direct-upload limit | not run | validate size first; send stable ID, ETag, deterministic token/hash; preserve both on 409/412 | exact replace request; conditional-race tests; size boundary | `C`/409/412 and `CR-017` |
+| `createDirectory(...)` | P/D/I/A/K expose parent-ID plus name creation | not run | typed create; recognized collisions receive one explicit conflict name | directory create/collision coordinator tests | directory-collision row unchanged |
+| `renameItem(...)` | P/D/I/A/K use stable ID plus explicit name | not run | local same-field intent wins; recognized collision gets one conflict name | rename/refetch/idempotence/drift tests | rename rows unchanged |
+| `moveItem(...)` | P/D/I/A/K use stable ID and destination; P supports `conflict=rename` and optional name | not run | merge move-only with remote rename; combined move/rename applies local name | move/rename concurrency suites | move rows unchanged |
+| `updateModificationDate(...)` | P/D/I expose integer `last_modified_at` | not run | update only when it is the remaining requested field, then refetch | combined-field coordinator tests | combined-fields date row unchanged |
+| `trashItem(...)` | P/D/I/A/K use stable ID and reversible trash | not run | local trash intent wins after requested content/metadata work | trash drift/concurrent-edit tests | reversible trash row unchanged |
+| `deleteTrashedItem(...)` | P/D expose stable-ID delete; no source documents ETag/If-Match | not run | retain preflight/refetch rejection, but accepted request remains unconditional | permanent-delete matching/stale tests | `CR-013` remains open |
+| `setFavorite(...)` | P/D/I/A expose separate favorite/unfavorite mutations; no conditional token | not run | mutate stable ID, then refetch authoritative metadata and invalidate both possible parents | `favoriteRefetchesMetadataAndInvalidatesBothParents` | adds favorite row |
+| `duplicateItem(...)` | P allows an optional name and previously emitted `{}`; I/K always provide an explicit name; A uses copy-to-directory | not run | refetch source, derive an explicit extension-preserving `copy` name, send it, then refetch the created stable ID; never depend on server-selected naming | duplicate coordinator/name-policy and exact request-body tests | `CR-019` resolved |
+| `trashedItem(...)` | P/D/I/A expose trashed metadata | not run | re-read the stable trashed item before choosing a restore parent | restore coordinator tests | adds restore row |
+| `existingFileIDs(...)` | P/I/A expose batch existence checks | not run | verify the original restore parent, otherwise choose configured drive root | restore original/root fallback tests | adds restore row |
+| `restoreTrashedItem(...)` | P/D/I/A use stable ID plus explicit destination | not run | restore to verified original parent or root fallback; never permanently delete | restore original/root failure tests | adds restore row |
+| `shareLink(...)` | P/D/I/A expose `public`, `inherit`, and `password`; prior app decoder treated unknown rights as public | not run | decode all three known rights; expected 404 is optional success; unknown rights fail closed | optional-404, inherit decode, unknown-right tests | `CR-018` resolved |
+| `createShareLink(...)` | P/D/I/A define known rights and capability fields | not run | validate password mode and encode the selected known right; diagnostics never retain URL/password | share defaults plus exact body tests | adds share-create row |
+| `updateShareLink(...)` | D makes `valid_until` nullable; I/A explicitly clear with null; P 0.3.0 synthesized encoding omitted nil | not run | retain P's typed route/response but replace its body with an app adapter that explicitly encodes JSON null; unknown response rights fail closed | exact null/inherit body and response tests | `CR-018` resolved; `CR-021` open for no ETag |
+| `deleteShareLink(...)` | P/D/I/A expose unconditional delete and no link-version condition | not run | disable by stable item ID; do not log/share the URL; document stale-editor race | contextual-action and diagnostic tests | adds share-delete row; `CR-021` open |
+| `fileVersions(...)` | P/D/I expose page/per-page, descending creation order, and immutable version IDs | not run | use nondeprecated typed route and explicit paging | version-page model/action tests | adds version-list/restore evidence |
+| `restoreFileVersion(...)` | P/D/I restore a selected version to an explicit destination/name | not run | always restore as a new copy and refetch its stable ID; current file is unchanged | version action/coordinator tests | adds restore-as-copy row |
+| working-set listings | P typed latest/favorite/shared routes; I/A/K corroborate working-set categories | not run | bounded deduplicated union; never infer success from root existence alone | `WorkingSetSyncTests`; Finder working-set assertion tests | no mutation change |
+| `listPartialActivities(...)` | P typed `/listing/partial`; D currently has no public page; I/K corroborate change feeds | not run | batch 200 stable IDs; advance watermark only after a validated complete response | partial-activity and failed-poll tests | cursor/anchor fail-closed rows unchanged |
+| HTTP rejection mapping | D documents a global 60 requests/minute limit; P retains raw Retry-After metadata | not run | map 408/429 to retryable `.serverUnreachable`; retain only a parsed nonnegative delta-seconds integer, never the raw header/body; other 4xx remain deliberate | table-driven 408/429/507/5xx classifier tests | adds retry mapping row; `CR-020` mitigated |
+
+### Accepted discrepancy corrections
+
+1. Added `inherit` and changed unknown share access from a widening `.public`
+ fallback to a typed failure.
+2. Added an app-owned update body that explicitly sends `valid_until: null`
+ while retaining potassiumChannel's pinned method, path, response, and client.
+3. Rejected direct create and replacement callback files above one billion
+ bytes before buffering, then rechecked the loaded count before constructing
+ an upload operation. Session uploads remain a documented implementation gap;
+ the callback source stays File Provider-owned but this early path creates no
+ separate conflict-stage copy.
+4. Changed HTTP 408 and 429 recovery to `.serverUnreachable` and retained only
+ parsed delta seconds from Retry-After.
+5. Changed duplicate-in-place to send an explicit derived name instead of an
+ empty options body.
+
+The advanced-listing and `/2/drive/init` routes remain client/live-evidence
+adapters because the public documentation snapshot does not contain them.
+Permanent trash deletion and share update/delete remain marked unconditional;
+no conditional primitive is invented without authoritative evidence.
+
+
+### Navigation diagnostic rerun
+
+Mac34 finalized **447 passed**, zero failed/skipped/expected failures, after the
+closed navigation-stage trace. Ordinary signed build `a057b29` also succeeded.
+Cold reproduction `5cffad00-31d8-44d6-9add-409792a703c8` did not reach Restore:
+fixture preparation exceeded its unchanged 90-second budget. Navigation transitions
+that were initially pending subsequently verified, but the full preparation chain
+did not finish. Provider working-set spans `07AB759D-088D-43DE-8E05-52517EF21D3D`
+and `33EB5ED4-9330-4844-88B5-CB700F5AD53F` completed in 84,513 ms and 72,760 ms;
+enumerate-changes span `6D3DD105-385F-4F52-B65D-02ABB2BB2EE5` took 58,998 ms.
+These overlapping durations locate expensive background work; they do not by
+themselves prove which work delayed placeholder preparation. Classification:
+harness deadline with provider/environment latency unresolved. No assertion budget
+is extended, no scenario passes are inferred, and fixtures/evidence remain retained.
+An already-running original-suite reproduction is next to isolate Restore navigation
+without another cold preparation cycle. The accepted conflict profiles remain intact.
+
+The cold diagnostic run sealed **zero passed, one failed, fifteen skipped**, with
+all spans terminal and its owner lease released. The first long refresh above
+contained 359 advanced-directory requests; their completed child spans plus the
+other API children totaled 52,268 ms. The 72,760 ms refresh had no child request
+spans, consistent with waiting for the existing per-domain permit. A later refresh
+`2474E774-CEEB-4041-AA8D-5354536B7C03` took 119,600 ms and contained 360
+advanced-directory requests. The current coordinator polls every materialized
+container serially; no second concurrency mechanism is proposed without a focused
+regression and rate-limit analysis. Local-mutation journal delivery remains fixed,
+but cold preparation/settlement with the enlarged retained lab is unresolved.
+
+The warm-only follow-up refused during launch preparation, before fixture setup.
+Its generic message does not establish the precise failed initial-state check.
+Run `3d6d382c-1692-4c0b-b0e7-3ecd3fa4c7b4` retained a preflight-only bundle
+and an exited owner lease; a subsequent diagnostic launch also refused until
+`--recover-stale-run --yes-recover` preserved the abandoned bundle and released
+that lease. No remote cleanup occurred. A diagnostic original-suite command without
+`--extension-state` then started to inspect Restore; it cannot satisfy the fresh
+or already-running acceptance certificates.
+
+
+### Restore navigation transition isolated
+
+Diagnostic run `905e0eae-bf1d-4f0d-b277-f399ab575a7c` sealed **ten passed,
+one failed, five skipped**, with all owned windows closed and the owner released.
+Restore correlation `277F476B-58F7-4B90-BA39-C1D591FBE030` failed specifically
+at `verifyNavigationDestination`: the Go to Folder path had been assigned and
+submitted, but the expected parent did not verify within the scenario deadline.
+No Restore action occurred. This isolates the UI transition with high confidence;
+it does not identify the exact alternative target presented by macOS. The local
+trace and immutable report are retained. This diagnostic run has no launch-state
+certificate and is not counted toward fresh/running acceptance.
+
+The focused candidate restores Finder's existing-window Apple Events target command
+only for an already-bound trashed fixture, the navigation route used by the earlier
+run that reached the real Restore callback. It verifies the exact parent and current
+Finder process/window ownership, then rebinds the exact item/domain before the
+contextual action. It never opens or selects an arbitrary Trash item. Ordinary
+navigation keeps Go to Folder; no generic fallback skips failed destination checks.
+`FinderTrashedItemSequenceTests` covers ordering, identity replacement during
+navigation, and unavailable-parent failure without a later action. Permanent-delete
+confirmation/rebinding remains mandatory. The candidate still requires a live rerun.
+
+Mac35 finalized **450 passed**, zero failed/skipped/expected failures, including
+the three new Trash navigation/rebinding regressions. The candidate changes only
+the macOS Stability harness; prior finalized standard macOS and simulator results
+remain the shared-runtime validation. A fresh-extension full-suite rerun is next.
+
+
+Fresh attempt `7d06262e-93fb-4700-b480-bb37e43f1138` on `e80999e` again
+exceeded the unchanged fixture-preparation budget before any scenario completed;
+it did not exercise the new Trash route. The owned Finder window closed. Completed
+working-set spans `50A055EF-28AF-4D7A-AC05-40CE00556C0C` and
+`0456AC1E-E595-4045-ACDB-00A46C77E6B9` took 76,902 ms and 68,736 ms.
+During settling, the installed provider process subsequently became absent while
+a working-set refresh and a child directory listing remained without terminals.
+The recorder's last write was 21:26:16 UTC; no writer-failure marker was present.
+This is observed process loss with incomplete telemetry, not proof of a crash cause.
+The runner retains its bounded settling wait; no terminal is fabricated and no
+acceptance is inferred. The Trash candidate is unit-tested and ordinarily built,
+but remains unverified live because this attempt never reached it. The twelve
+accepted conflict bundles and unchanged provider/shared runtime remain separate.
+
+The final candidate for `7d06262e-93fb-4700-b480-bb37e43f1138` was rejected
+with `incompleteRun` and `eligibleForAcceptance: false`. After its owner exited,
+explicit stale-run recovery preserved the candidate and released only the local
+lease. The conditional warm invocation did not run. No fixture was deleted.
+
+### Invalidated-instance materialization work
+
+Object invalidation span `21E2DE11-B3FA-4033-8FC9-E9F209D36766` completed
+at 21:26:01 UTC. Materialization acknowledgement `51C14976-49EE-491C-96EF-68EA012A4F1B`
+had already completed at 21:24:59; its child refresh
+`18EF5FFF-C7FE-4234-A24B-B8F8F21D6731` continued and started directory request
+`0BC42058-387A-4617-91F4-676A67AE645F` at 21:26:16, fifteen seconds after
+invalidation. Both child spans remained open when the process became absent.
+
+Source inspection found the acknowledged materialization task was untracked and
+retained its provider instance, while `invalidate()` cancelled only the periodic
+poll. Apple's installed `NSFileProviderReplicatedExtension.h` lines 256–264 require
+invalidation to release references so the discarded instance can deallocate;
+[Apple's invalidate documentation](https://developer.apple.com/documentation/fileprovider/nsfileproviderreplicatedextension/invalidate())
+is the corresponding contract. Classification: provider lifecycle; confidence high
+for unowned work continuing after invalidation, unconfirmed for the process-exit cause.
+Reproduce the retained cold original-suite workload and inspect child requests after
+instance invalidation; ordinary object replacement within a live process remains valid.
+
+The correction adds a per-instance cancellable task scope. Registration/invalidation
+uses a short `Synchronization.Mutex` because the system callback is synchronous;
+operations and cancellation handlers run outside the mutex. Acknowledgement remains
+prompt and exactly once, even if invalidation rejects later work. New instances use
+independent scopes. Existing remote poll/cursor/transaction policy is unchanged.
+`FileProviderBackgroundWorkTests` checks cancellation before another request,
+replacement-instance independence, repeated invalidation, late-start rejection,
+and 100 immediately finishing registrations. Mac36 finalized **453 passed**, zero
+failed/skipped/expected failures. Other platform validation and live reruns are
+pending; CR-024 remains open. The previously accepted twelve conflict bundles are
+preserved as baseline evidence for their unchanged historical build.
+
+Finalized validation for the lifecycle correction: Mac36 **453 passed**, standard
+macOS12 **395 passed**, iOS16 **379 passed**, and visionOS Simulator15 **379 passed**;
+all have zero failed/skipped/expected failures. The signed generic visionOS11 build
+succeeded. Complete fresh and already-running conflict profiles will be rerun on
+the ordinary signed build because the provider runtime changed. Earlier accepted
+bundles remain baseline evidence; CR-024 awaits live verification.
+
+
+### Lifecycle correction: fresh conflict rerun and warm preflight gap
+
+All six fresh conflict cases sealed as passes on `3a5eb52`:
+
+| Case | Run |
+| --- | --- |
+| Content before preflight | `1ccd7913-4c94-426d-ab21-4b6b830141a1` |
+| Content after preflight | `12c43c08-9739-4dd0-a366-9fc1924e8c53` |
+| Rename/rename | `9e6d0be0-6a8d-4e52-ac01-73ccafcaf4f7` |
+| Move/move | `d5fbf055-58ac-4457-b706-f7a1c9522ec4` |
+| Edit/rename | `10172d8c-beea-420f-9ebd-8b4280b44cc7` |
+| Edit/move | `2105c8c9-f9db-4860-b2bb-8f02b44ec8d3` |
+
+The corresponding warm invocation `8bd78e81-4ff6-4394-94b5-6c17c76a1df2`
+stopped before any Finder scenario. Its 23 diagnostic events contain only completed
+runner API requests and progress, with no extension-source event. The initial
+process observation succeeded, but the installed provider was absent on subsequent
+inspection. The old catch did not distinguish a process-change guard from the
+callback deadline; the exact guard and process-exit cause remain unconfirmed. Its
+exited owner was explicitly recovered, preserving the unsealed bundle and fixtures.
+No warm pass is inferred. Fresh cancellations observed so far belong to periodic
+polls, not the formerly untracked materialization child; CR-024 stays open.
+
+Source inspection found warm preparation passively awaited an extension callback,
+although cached visible-root resolution need not invoke the extension. It now asks
+the verified domain's working-set enumerator for refresh, checks the same process
+before and after acknowledgement, then retains the existing completed-callback,
+signing, continuity, and 90-second assertions. Apple's installed
+`NSFileProviderManager.h` specifies working-set signaling for replicated providers,
+including when no UI enumerator is open; the corresponding
+[Apple method documentation](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/signalenumerator(for:completionhandler:))
+is the API contract. Classification: harness preflight; high confidence for the
+missing request, no claim that it explains the provider's exit. Reproduce by running
+the warm profile immediately after the final fresh case closes its owned windows.
+
+Preflight failures now retain a version-1, non-accepting closed diagnostic containing
+stage, reason, safe error class, and numeric code. Tests cover the signal's ordering,
+process replacement on either side, request failure, and exclusion of private error
+payloads. Existing launch-proof tests still reject missing callback evidence.
+Mac37 finalized **457 passed**, zero failed/skipped/expected failures. This correction
+is confined to the macOS Stability harness; the latest shared-runtime standard macOS,
+iOS/visionOS Simulator, and generic visionOS results above remain applicable.
+The corrected complete live profiles are next.
+
+
+### Replicated refresh signaling and independent conflict preparation
+
+On `e774d08`, content-before-preflight (`72ff51b6-9a27-4b1c-b2be-2ad9ad0e5ba0`)
+sealed as passed. Content-after-preflight (`139a83cb-68fa-450b-8cd1-efd3559d160f`)
+sealed as failed after its 90-second fixture-preparation deadline, before the gate
+or competing mutation. Its windows closed; no failed provider/API span was recorded.
+The exact unresolved fixture role was not in that build’s trace and is not inferred
+from the generic timeout. Independent later cases continued. The failure preserves
+evidence that a targeted conflict could be blocked by the original suite’s unused
+deep hierarchy before exercising its own race.
+
+The harness called native `signalEnumerator` for the root and each generated folder.
+Apple’s replicated-provider contract explicitly ignores those identifiers and
+requires `.workingSet`. Production mutation/action paths already include a
+working-set signal; only the Finder harness omitted it. The correction signals
+once per completed fixture batch, retains all intended subject aliases, and keeps
+actual enumeration/UI/server assertions. Root/folder/duplicate batches and signal
+errors have regression tests. This corrects a documented API misuse; it does not
+establish that ignored signals were the sole cause of the observed deadline.
+
+Conflict preparation now provisions its root and sibling destinations and opens
+only the bound root. Each race still creates, hydrates, edits/moves, verifies, and
+reopens its own file. The original 16-scenario preparation retains both nested
+levels and the seed. Regressions require root binding and prove a conflict does not
+request the unrelated deep/seed bindings. Closed fixture-role traces identify
+future resolution failures. No deadline, conflict gate, byte/identity assertion,
+or callback requirement is relaxed. Validation and a new complete profile remain
+required; the active old-build profile is allowed to finish with evidence preserved.
+
+The old-build profile finished with **five passed and one failed**, all six bundles
+sealed, and no active owner. The conditional warm invocation did not run. The
+remaining passes were rename/rename `8905e0f1-a179-4952-95eb-6246cc4b65ea`,
+move/move `819d702a-a2be-42cc-aa91-d54d1d4c855b`, edit/rename
+`e3710396-4435-4743-8897-431922003165`, and edit/move
+`f5ff88a2-1297-42c7-b923-b1703a973636`. Mac38 finalized **462 passed**, zero
+failed/skipped/expected failures, including the five new signaling/preparation
+regressions. Shared-runtime platform results above are unchanged.
+
+### Materialization lifetime correction exercised live
+
+The failed-preparation bundle `139a83cb-68fa-450b-8cd1-efd3559d160f` also
+exercised the corrected provider lifetime. Acknowledgement
+`7D27532C-80DB-487D-8F8A-182CB146CED1` completed exactly once at 22:22:46 UTC.
+Its child refresh `6ADBD1EC-3F68-40C6-BC24-0490CB88B716` was still active when
+instance invalidation `1FB5887A-BBD6-4E78-92F7-280B09885858` began at
+22:23:52. The child recorded one cancellation terminal (65,492 ms), between
+invalidation start and completion. All 30 child operations completed before that
+invalidation; no child request started afterward and no success followed cancellation.
+This is actual materialization-child evidence, distinct from a periodic-poll
+cancellation. The whole failed run sealed with every diagnostic span terminal.
+CR-024 is **Mitigated**: deterministic and live evidence verify the specific task
+ownership/cancellation correction. The original process-exit cause remains
+unconfirmed, and this supporting trace does not turn the failed conflict case into
+a pass.
+
+
+### Native refresh target versus intended evidence subjects
+
+On `14165a1`, first case `6eebdc61-53b1-48c8-be14-612e0e73b42c` completed
+its controlled remote replacement, preserved both byte streams, captured both
+results, and reopened them through Finder/TextEdit. Content callback
+`F3C09A11-EE50-4B74-A47B-3A2154E942DE` completed at 22:41:27 UTC; the final
+reopen fetched the original's remote bytes at 22:41:47. The case nevertheless
+failed at 22:41:58 while awaiting diagnostics. Working-set enumerate-changes span
+`E3C9B9C5-F6A9-4872-AEAF-C96ABA876168` started at 22:41:27 and completed at
+22:42:27, after the scenario deadline. The failed bundle sealed and remains failed.
+
+The preceding signaling change had mistakenly added the native `.workingSet`
+refresh target to every scenario's intended subjects. That broadened an item-specific
+check to unrelated domain work. Classification: harness correlation; confidence
+high from the selected subject set, late span, and source. The correction records
+only the changed fixture containers while still signaling the native working set.
+The explicit working-set scenario may select that subject. Global recorder settling
+continues to require every span's terminal before sealing, so no background evidence
+is discarded. A production-helper regression distinguishes the native refresh
+target from recorded scenario subjects. Diagnostic waits now trace their closed
+reason on transitions. Neither the 90-second budget nor any callback/byte/identity
+assertion is relaxed. Validation and a complete corrected profile are pending.
+
+The second case, `576fd66c-0e6e-49a8-bcf8-c8541583cbea`, also exercised the
+real conditional HTTP 412 and completed its UI checks before failing during the
+diagnostic wait. Both failed bundles sealed. To avoid repeating the now-identified
+harness regression, the app was stopped at the next case’s preflight boundary
+(`ecaf392d-8d77-4c56-8511-baaa62e64ac9`), after rechecking that no Finder
+scenario had started. Its preflight had no pending diagnostic span. Explicit
+stale-owner recovery preserved that abandoned bundle; no fixture mutation or UI
+action occurred in it. This was an interrupted profile with two failures, not a
+complete six-case run; the conditional warm command did not run.
+
+Mac39 finalized **463 passed**, zero failed/skipped/expected failures, including
+the production signaling-helper regression. Global settlement, callback mappings,
+expected conflict terminals, and negative unrelated-callback checks are unchanged.
+A new complete fresh/running profile is next.
+
+
+### Updated-build conflict acceptance completed
+
+Implementation `d4b5323` passed all six fresh-extension cases and all six
+already-running cases. Independent bundle verification confirmed the exact case
+sets, one signed build, six distinct fresh process identities, and all warm cases
+reusing the final fresh process. Every selected result has one pass and fifteen
+explicitly unselected scenarios, zero checkpoints/failures, schema-3 competing
+commit evidence, schema-2 launch proof, screenshots, timelines, final reports, and
+summaries. Every diagnostic span has exactly one start and terminal, with no
+writer-health or storage failure. Both commands completed and no owner lease remains.
+
+| Case | Fresh | Already running |
+| --- | --- | --- |
+| `content-before-preflight` | `bc8e0f08-f007-48f3-9b6a-449c334941c8` | `5f1c469d-5252-44c7-b241-ee939ee48365` |
+| `content-after-preflight` | `89a2eab6-23a5-4216-b546-fddc2575d40e` | `73008dac-19b1-4340-aa71-f83ff6da670b` |
+| `rename-rename` | `2a89e78c-d3b7-4471-8be0-f6fec7360397` | `35d92642-715d-475c-a950-da551d896f9f` |
+| `move-move` | `9e07afec-747a-4161-949b-456f5e32eb01` | `57cd7037-54a9-4600-8c68-43f5b5b6d36a` |
+| `edit-rename` | `cfb29081-cc2a-4a2c-955f-752da494edf0` | `ad8eecbe-f2aa-4bb4-89cb-376e794c4853` |
+| `edit-move` | `0cca0007-ad5c-4aeb-956b-86b3d67c5b5d` | `09b60947-427e-4cdc-ba84-503796034893` |
+
+This verifies the warm observation request, independent fixture preparation, and
+separation of native refresh targets from scenario subjects on the updated build.
+Global background work still settled before sealing; the 90-second operation
+budgets and all UI/API/callback assertions remained intact. Previous failures
+and abandoned preflights remain unchanged. No fixture was permanently deleted.
+
+Latest finalized platform validation remains Mac39 **463 passed**, standard macOS12
+**395 passed**, iOS16 **379 passed**, visionOS Simulator15 **379 passed**, and
+generic visionOS11 build succeeded, all accepted test bundles with zero
+failed/skipped/expected failures. The last corrections affect only macOS Stability.
+The separate original sixteen-scenario suite remains incomplete and is next.
+CR-013 remains open; CR-022 retains its large-materialized-set latency limitation.
+
+
+### Original-suite continuation: deep seed preparation remains blocked
+
+Fresh original-suite run `79693887-95c6-48da-9fb0-464e246aaf6c` on
+`d4b5323` sealed with **zero passed, one failed, and fifteen skipped**. The
+90-second preparation began at 23:58:03 UTC on September 10 and expired at
+23:59:33. Closed fixture traces verified the run root, nested directory, and deep
+directory, then identified seed resolution as the failing operation. No scenario
+completed, no permanent deletion occurred, and the conditional warm command did
+not run. Run-owned Finder windows closed. All 2,834 diagnostic spans have exactly
+one start and terminal, with no failed provider/API diagnostic; the final report,
+summary, and timeline are retained. No safe selected-item screenshot was available
+at this preparation failure, so none was captured. This is failed evidence, never
+an acceptance bundle.
+
+Expected: resolve and bind the generated hierarchy within the existing preparation
+budget, then begin verified navigation. Observed: preparation expired while
+resolving the seed. The earliest long callback was working-set change enumeration
+`C235C29A-BB19-418E-A7BD-30F17F04E669`: 61,235 ms, including 423 advanced
+folder-list requests and four partial-activity requests. Subsequent materialization
+refresh `9BE3ABEB-24FF-462C-BDA6-95D3332EB0FC` took 77,704 ms; another
+`D3D6BE0D-98E3-4099-AF9E-5B96DF6EA204` took 124,180 ms including queued
+work. Later callbacks settled successfully before sealing.
+
+Classification: harness deadline with observed provider refresh latency. Confidence
+is high in the measured crawl cost and seed-resolution timeout; the trace does not
+establish the precise dependency blocking the system's visible-URL callback.
+CR-022's large-materialized-set latency limitation remains unresolved. No focused
+runtime correction or successful rerun is claimed for this failure. Reproduce with
+`scripts/run-finder-stability.sh --run --extension-state fresh --yes-live` using the
+preserved lab and the ordinary signed build. Investigate refresh scheduling and
+visible-URL delivery before changing policy; keep the deadline, fixture confinement,
+retained evidence, and complete-span requirements intact. The independently
+verified six fresh and six already-running conflict passes remain valid.
+
+
+### CI failures and bounded refresh continuation
+
+The latest pre-continuation CI (`34545034527`) failed seven Mac setup UI tests,
+plus cross-store diagnostic observation on Mac/iOS and three scheduling/contention
+regressions on visionOS. Direct inspection of synthetic Mac fixtures found an
+Advanced disclosure identifier propagating over its token/save controls and drive
+actions exposed as List rows. Removing the parent identifier and using a grouped
+Form exposes the intended identifiers in native Accessibility. UI tests now use
+the disclosure role, a contained sign-in heading, and content-scoped empty-state
+queries that do not accidentally match the toolbar.
+
+Local `potassium-finish-mac-01.xcresult` finalized with **408 passed, five failed,
+zero skipped** (413 total). All failures are UI tests; the first changes fixed two
+of the seven original UI failures but do not yet establish a passing UI suite.
+Further UI failures retain the synthetic app's accessibility description. An earlier
+UI initialization attempt timed out while enabling automation and was interrupted;
+it is not a finalized passing test result. Later launch failures were initially
+attributed to debugger authorization, but a direct launch established a missing
+embedded InfomaniakConcurrency package framework and a library-validation failure.
+The host app now declares that shared package product so Xcode embeds and signs it.
+A focused run then launched successfully without changing Developer Mode and passed
+drive navigation. One later retry could not obtain a process ID while a previous
+synthetic test instance remained open. After that instance closed, the next retry launched and
+reached its UI assertion. Explicit per-test app teardown now closes owned test
+instances; no global security setting has been changed by the runner.
+
+CI `34573732948` on `3b15d54` finalized iOS and visionOS successfully; Mac retained
+five setup UI failures and no unit-test failures. Four failed after a drive-row
+centre click did not navigate: the label's spacer lacked a hit area. The Mac row
+now defines a rectangular content shape. Native inspection confirmed that the
+Advanced arrow toggled but its label did not. The label now toggles the native expansion binding; a normal centre-click
+regression finalized successfully in `potassium-finish-ui-07.xcresult`. UI tests
+explicitly target the sibling build product, and launch/performance fixtures use synthetic state.
+Failure tree attachments are now scoped to the synthetic app window.
+
+The JSONL subscription baseline was established inside a later worker task, allowing
+an intervening append to be missed. It is now captured before subscription return.
+The real WAL-contention test now releases its lock on a dedicated queue, independent
+of the Swift executor blocked by the opener. Existing SQLite production behavior
+is unchanged. The updated cross-store observation and contention regressions pass
+in the local finalized Mac run above and both finalized simulator runs. CI on
+`3b15d54` also passed these regressions.
+
+The retained 423-request working-set trace motivates a separately focused change:
+up to four independent folder reads may overlap, retaining ordered results and the
+single guarded commit. New gated regressions require actual overlap, no prefix
+publication, complete identities/parentage/cursors, and cancellation or HTTP 429
+without cursor/watermark advancement. Retry-After metadata remains intact. A newer
+journal still supersedes the whole prepared batch; already-started reads are bounded
+by four. The new runtime passes all 381 unit tests on each requested simulator, with
+finalized `potassium-finish-ios-01.xcresult` and
+`potassium-finish-vision-sim-01.xcresult` bundles. The generic visionOS build also
+succeeded. CI unit coverage passes on all three platforms. Both local Mac profile reruns now pass; live verification of the latency
+correction remains pending. CR-022's
+latency limitation and CR-013's deletion guarantee remain open.
+
+The host-app package embedding change also passed the signed generic visionOS
+build (`potassium-finish-vision-build-02.log`). The standard Mac run
+`potassium-finish-mac-02.xcresult` finalized with **415 passed, zero failed or
+skipped**, including all setup, activity, and launch UI tests. Explicit teardown
+closed test-owned app instances. The Stability profile then finalized
+`potassium-finish-stability-mac-02.xcresult` with **465 passed, zero failed or
+skipped**. Both Mac profiles are passing; complete live Finder acceptance remains
+pending. Test extension registrations were removed before rebuilding the ordinary
+signed Stability app.
+
+### Launch-metric CI follow-up
+
+CI `34575745738` on `716f907` fixed all five previous setup UI failures. Its Mac
+job failed only `testLaunchPerformance`: iteration 3 returned zero launch metrics
+while iteration 0 returned one. The app launched under distinct process IDs in all
+iterations; this trace does not establish an application startup defect. The local
+full run retained all launch metrics and passed. Classification: test harness / CI
+measurement, with moderate confidence in the measurement boundary as the cause.
+The regression now reuses one application controller, terminates outside the
+measured interval, waits for the responsive-launch metric and an app window, and
+retains the metric assertion. Local/CI reruns are pending; no missing measurement
+is accepted as a pass. Validation waits until the active Finder run settles.
+
+The same CI run finalized iOS successfully but failed one visionOS scheduling test:
+`materializationArrivingDuringIORequiresAnotherPollAndCancellationDoesNotPoll`
+(16.923 seconds). Its three-second observation and five-second fake-I/O deadlines
+could expire while the suite competed for simulator execution. The old workflow
+retained only console output, so the exact failed assertion was unavailable; this
+is a supported timing hypothesis, not a proven provider defect. Fake I/O now waits
+on explicit cancellable gates, with start notifications and the test's one-minute
+overall deadline. Queue-count observation retains cancellation and final empty-queue
+assertions. No production scheduling behavior changes. CI now retains result bundles
+for all destinations for 14 days and also runs the Mac Stability unit profile.
+These test-only follow-ups are awaiting validation while the live run is paused.
+
+### Original-suite continuation at exact deletion confirmation
+
+Ordinary signed build `716f907` passed preflight, including saved-Keychain login,
+verified lab/domain ownership, Accessibility, Finder automation, screen recording,
+and extension registration. Fresh run `9b684af4-ca17-4795-8c79-74195aeee299`
+cleared fixture preparation and verified scenarios 1–11, including Remove Download,
+TextEdit editing, Trash, and the strengthened Restore callback/bytes/destination
+checks. After the exact-fixture confirmation, Finder's Delete Immediately command
+was invoked, but the runner timed out waiting for its native confirmation dialog.
+The sealed result is **11 passed, one failed, four skipped**, classified as UI
+automation/deadline. No `deleteItem` callback was recorded. All 7,464 diagnostic
+spans have exactly one start and terminal; report, timeline, and selected-row
+screenshots remain preserved. Owned Finder windows closed. Neither permanent
+deletion nor complete fresh/warm acceptance is proven; CR-013 stays open.
+
+### Hidden-extension deletion dialog and finalized CI diagnosis
+
+The earliest deletion divergence is after the native contextual command press:
+`selectedDeletionDialog()` required the full URL filename in the message, while
+the retained step-12 screenshot shows Finder hiding its extension. An independent
+synthetic local fixture on the same Mac reproduced the native prompt using only
+its display name. That prompt was cancelled, the fixture retained, and its Finder
+window closed. Confidence is high in the display-name mismatch; the failed live
+run did not retain the native dialog itself, so the rerun remains necessary.
+
+The driver now reads Finder's display name for the exact bound URL before invoking
+Delete Immediately and matches its complete quoted prompt. It does not derive
+identity from that display string. New-window/owned-sheet scope, URL matching,
+exact controls, one distinct matching dialog, operator confirmation, and fresh
+provider/domain binding remain mandatory. Repeated references to the same AX
+object are deduplicated; two distinct dialogs remain ambiguous. Safe candidate
+counts and a match Boolean aid future failures without logging names or URLs.
+`FinderDeletionDialogTests`, Trash rebinding, and poll-scheduling coverage finalized
+10 selected tests with zero failed/skipped in
+`potassium-deletion-dialog-tests-01.xcresult`. Full Mac and live reruns are pending.
+
+CI `34576895561` on `fe763b6` finalized the Stability profile with 465 passing tests;
+iOS and visionOS also succeeded, including the gated poll-scheduling regression.
+The standard Mac result has 414 passed and one failure: its launch-signpost metric
+again omitted a sample despite successful launches and window assertions. Changing
+the responsive-launch boundary did not resolve the instrumentation failure. The
+retained bundles are available as that run's platform artifacts.
+
+The benchmark is now named `testLaunchToSetupReadinessPerformance`. `XCTClockMetric`
+measures the explicit launch, activation, Setup navigation, and enabled Add Account
+observation. It includes automation overhead and must not be compared with the
+old system first-frame metric. Every iteration still requires timing and successful
+UI assertions; there is no skip, retry, or fabricated missing sample. Validation is
+pending. This changes benchmark scope, not provider or conflict policy.
+
+The updated standard Mac suite finalized **415 passed, zero failed/skipped** in
+`potassium-finish-mac-03.xcresult`. All five measured readiness samples were present
+(mean 3.507 s, relative standard deviation 4.698%); this validates the new benchmark
+locally, not its comparability with the former launch signpost. The full Stability
+profile then finalized **470 passed, zero failed/skipped** in
+`potassium-finish-stability-mac-03.xcresult`, including the deletion-dialog regressions.
+The ordinary signed app and live rerun are next.
+
+### Optional permanent deletion and continuation (2026-09-11)
+
+The operator requested that the deletion checkpoint become optional. `--run` now
+defers scenario 12 before re-trashing the restored fixture and proceeds to 13–16.
+`--include-permanent-deletion` explicitly selects the existing exact-item prompt;
+it never serves as confirmation. Report schema 3 records
+`skipped(permanentDeletionNotSelected)` with unevaluated assertions. Fifteen
+verified scenarios return exit 4 and cannot establish full sixteen-scenario
+acceptance. Historical reports remain readable, and schema 3 uses the same strict
+live telemetry validation as schema 2. Finder/TextEdit ownership cleanup remains
+mandatory. CR-013 stays open.
+
+The previous fresh run `a5494939-1e92-4ddd-8d35-cd7de1d9300d` had already sealed
+before this selection change: 11 passed, one failed, four skipped. The corrected
+display-name matcher found the exact native confirmation, but permanent deletion
+still timed out with no `deleteItem` callback in its retained timeline. The earliest
+unresolved divergence is now after that matched dialog; the underlying cause is
+not established. This remains failed deletion evidence, independent of the newly
+requested default deferral. Its report, timeline, and screenshots are preserved.
+
+Focused selection/report/command tests finalized 40 passed in
+`potassium-optional-deletion-tests-01.xcresult`. Expanded coverage then finalized
+475 passed, zero failed/skipped in `potassium-optional-deletion-stability-mac-01.xcresult`.
+This includes schema-3 rejection of historical telemetry, skip continuation,
+explicit option forwarding, and cancellation-gate ordering. Other destinations
+and the ordinary signed live rerun are pending.
+
+CI `34578669227` passed both Mac profiles and iOS. The retained visionOS bundle
+contains 380 passed and one failure: the callback cancellation test threw
+`NSURLErrorDomain -1001` from its synthetic five-second gate. Explicit cancellable
+arrival/release/worker-terminal signals now replace these scheduling timeouts; a
+one-minute overall test deadline still prevents indefinite waiting. The real
+mutation executor, exactly-one completion, retained staging bytes, source bytes,
+and absence of server/Trash mutations remain asserted. Production behavior is
+unchanged; the simulator rerun is pending.
+
+The expanded iPhone 17/iOS 26.5 and Apple Vision Pro/visionOS 26.5 runs both
+finalized **384 passed, zero failed/skipped** in
+`potassium-optional-deletion-ios-01.xcresult` and
+`potassium-optional-deletion-vision-sim-01.xcresult`. The signed generic visionOS
+build also succeeded (`potassium-optional-deletion-vision-build-01.log`). The
+standard Mac UI suite is still running; no live suite is active during validation.
+
+Standard Mac validation finalized **418 passed, zero failed/skipped** in
+`potassium-optional-deletion-standard-mac-01.xcresult`, including UI and readiness
+measurement. Ordinary signed build `9b71d76` passed all preflight checks, including
+saved-Keychain authentication, ownership/domain binding, extension registration,
+Accessibility, Automation, and screen recording. The fresh live run now starts
+with deletion deferred; no local builds or tests overlap it.
+
+### Deletion deferral verified; transfer progress divergence
+
+Fresh ordinary build `9b71d76`, run `343fc032-7799-41a9-ae93-92270cc31d66`, sealed
+**12 passed, one failed, three skipped**. Scenarios 1–11 passed, scenario 12 was
+explicitly `skipped(permanentDeletionNotSelected)` with no re-trash/prompt, and
+scenario 13 passed its actual conditional-upload race with both versions reopened.
+Scenario 14 failed `cancellationNotExercised`; 15/16 were skipped after that failure.
+The 64 MiB and 256 MiB fixtures each completed before cancellation. Download spans
+`9E25BB88-F224-452D-B56E-504E16CC7286` (1,429 ms) and
+`CDBF9DB6-7298-4FF8-8B34-E5DDDA6AFA38` (3,854 ms) had zero-only intermediate
+progress. The report/timeline/screenshots and generated fixtures remain preserved;
+owned Finder windows closed and monitoring continued through settlement.
+
+`trackProgress` divided the parent's completed units by total units. Foundation
+`Progress` with a weighted child can retain zero parent units at 40% and 90% child
+completion (independent local Foundation reproduction). The corrected sampler uses
+`fractionCompleted`; a new test exercises the production sampler with this exact
+hierarchy and verifies intermediate buckets, one cancellation, and no late success.
+Confidence is high in this sampler defect; whether it explains all missing live
+progress awaits rerun. No transfer is slowed, fabricated, or counted as cancelled
+without Finder and correlated callback evidence. Reproduce with the ordinary
+`--run --extension-state fresh --yes-live` command, leaving deletion deferred.
+
+The progress correction finalized **25 focused tests on each of standard Mac,
+Stability Mac, iPhone 17/iOS 26.5, and Apple Vision Pro/visionOS 26.5**, with zero
+failed/skipped (`potassium-transfer-progress--01.xcresult`). The signed
+generic visionOS build passed. CI `34580616050` on the preceding optional-deletion
+commit `9b71d76` finalized green on all three jobs, including both Mac profiles.
+These results validate the sampler and cancellation regressions; the corrected
+ordinary app still needs a live cancellation result.
+
+### Native context-menu capability mismatch
+
+The progress-corrected fresh run `efa72a78-de91-4f37-8561-ad72348ed2bb` sealed
+**two passed, one failed, thirteen skipped**. Eviction timed out before any menu
+command was invoked. Computer use observed the exact generated name field with
+only Open Finder Item advertised and no eviction alert. An independent owned
+Finder window opened that same generated fixture's native context menu by secondary
+click and exposed Remove Download. The menu was dismissed without selecting any
+action, and the inspection window closed. Earlier failing evidence is preserved.
+
+Earliest divergence: `selectedMenuAnchor` required advertised `AXShowMenu`, although
+the driver actually uses WindowServer secondary-click routing. Finder can omit
+that action. The corrected target uses the unique exact display-name field of the
+independently bound selection and requires its fresh rectangle to be contained in
+the bound window. Duplicate names, absent geometry, wrong names, empty labels, or
+out-of-window rectangles yield no click. Three regression cases exercise these
+confinement boundaries. No File Provider callback, transfer, or conflict policy
+changes. Validation and the next live retry are pending.
+
+The corrected context target finalized **21 passed, zero failed/skipped** in
+`potassium-context-anchor-stability-mac-02.xcresult`, and the standard Mac build
+succeeded. The first selected build retained a missing CoreGraphics import error;
+that import was corrected before the passing rerun. The shared progress runtime
+retains its preceding four-destination focused validation; this follow-up changes
+only the macOS Stability UI driver.
+
+### Progress correction confirmed live; cancellation dispatch still late
+
+Fresh run `5cfc3989-748f-47c4-befa-6f427f98dae4` on `1aa4117` sealed
+**12 passed, one failed, three skipped**. The corrected menu route passed eviction
+and all earlier scenarios; deletion stayed deferred and preserve-both passed.
+The 256 MiB download span `2B6FCD63-C5EC-41A2-A546-8EE0EC09BC53` now reports actual
+10–90% intermediate progress and finishes in 3,959 ms. This independently confirms
+the weighted-progress sampler correction. Its fetch callback
+`E42510C3-3467-4E2C-B62F-6BEE205BEA3B` completed at 4,448 ms without cancellation.
+Both transfer attempts were already complete when the runner checked after its
+contextual dispatch/progress wait, so cancellation remained unexercised. The
+owned Finder window closed and all prior failed bundles remain preserved.
+
+Download Now now uses a native mouse click on the uniquely identified, enabled
+menu item and returns after menu dismissal. It does not wait on the command's AX
+completion or a subsequent Finder Apple Event. Transfer diagnostics still decide
+completion/cancellation. Other actions retain AX dispatch and their result
+observations. Sequence tests reject transfer-completion waits on the download
+route, retain result checks elsewhere, and reject failed dispatch. A safe elapsed
+dispatch duration aids the next trace. This is a harness scheduling correction;
+the exact blocking stage in the previous invocation remains uncertain pending
+the new timed live evidence.
+
+The download-dispatch correction finalized **24 passed, zero failed/skipped** in
+`potassium-download-dispatch-stability-mac-01.xcresult`; the standard Mac build
+succeeded. The preceding context-menu commit `1aa4117` also finalized green CI
+(`34582880304`) across all destinations. Shared-runtime validation is unchanged
+from the progress correction; this follow-up changes only Stability UI dispatch.
+
+### Timed dispatch and independent continuation (2026-09-11)
+
+Run `5ddafe6a-d0f7-4a7a-a088-d1b516da5a10` on `1fd42e7` sealed **12 passed,
+one failed, three skipped**. Scenario 12 was deliberately deferred. Download Now
+returned in 0.568 and 0.574 seconds, while the 256 MiB download took 5,431 ms and
+its fetch 5,894 ms. Actual intermediate progress was present, but neither attempt
+was cancelled. Owned Finder windows closed; all failing bundles and fixtures remain.
+
+The generic progress wait inherited the API poller's 2/4/8/10-second backoff and
+redecoded the entire event file on each observation. Confidence is high that this
+is unsuitable for short transfers; its exact share of the live delay is unmeasured.
+A Stability-only cursor now registers before UI dispatch, decodes appended records
+at 50 ms intervals, and rejects file identity/boundary/health/format failures.
+Only the exact item, correlation, code hash, process and callback parent qualify.
+The cancel-control wait ends when that transfer completes. This does not fabricate
+cancellation, throttle real transfers, or weaken final evidence validation.
+
+Working-set and contextual actions now prepare independent small fixtures and
+repeat safety preflight after earlier failures. This allows those checks to retain
+real evidence while the transfer failure remains failed. No dependent mutation
+is resumed and no skipped step becomes a pass. Reproduce using the ordinary
+`--run --extension-state fresh --yes-live` command; deletion stays deferred.
+
+Initial focused Stability validation finalized **53 tests passed**, zero failures
+or skips, in `potassium-transfer-tail-stability-mac-01.xcresult`. Parameterized
+instances total 67. The final health-path guard also finalized 53 passed in the `-02` bundle.
+The standard Mac decoder/evidence selection passed 27 tests; each simulator passed
+26. The generic signed visionOS build passed. CI on `1fd42e7` passed both Mac profiles and visionOS but failed iOS
+in `kdriveServiceExposesLazyObservableDownloadOperation`; that artifact is retained
+for diagnosis separately from the live automation result.
+
+The retained CI iOS bundle confirms the lazy-download failure was the test's exact
+phase-array assertion (at most one progress event), with 384 other tests passing.
+The test now checks one start, one completion, only monotonic progress between them,
+and one shared span. It retains lazy-start, request, and exact byte assertions.
+This permits legitimate multiple progress samples without permitting extra terminals
+or events after completion. The containing suite finalized **62 Stability Mac tests**
+and **67 standard Mac tests**, zero failed/skipped. Initial single-method selections
+ran zero tests and are explicitly excluded from validation; class selection verified
+the affected test actually executed. Simulator lifecycle reruns are pending.
+
+### Independent cases reached; favorite metadata omitted (2026-09-11)
+
+`fdaa23ad-7080-4463-9cf0-456982831b03` on `a525f87` sealed **13 passed, two
+failed, one deferred**. All prior successful cases passed again; deletion stayed
+deferred without a prompt. Working-set refresh passed with its fresh metadata
+proof. Cancellation observed intermediate progress while both transfers were still
+active, confirming the new local cursor's scheduling improvement. Neither transfer
+was cancelled; the larger transfer ran approximately 15 seconds, so control
+identification/selection needs further diagnosis. No cancellation success is claimed.
+
+Contextual actions timed out at the first favorite menu command, after the generated
+file was edited and uploaded. Computer use observed the exact fixture's Share,
+Version History, and Duplicate provider actions, but neither Favorite action. The
+activation rules require known boolean favorite state; the adapter's metadata and
+list requests omitted the optional `is_favorite` include. This is a concrete mapping
+gap with high-confidence connection to the missing menu, although the old live
+response field was not exported. API documentation and Potassium's advanced-listing
+preset support the additional include. Missing state still remains nil. No menu
+predicate is broadened and no unknown state is treated as false.
+
+The focused fix adds favorite state to direct metadata, ordinary/Trash/working-set
+listings, uploads, and advanced initial/continuation listing resources. The ordinary
+ETag fallback retains favorite state. Regressions verify tri-state mapping, stable
+identity/parent/name/size, exact include queries, and retained rejection of unsupported
+advanced ETags. The first test build hit nested `#require` macro recursion; that
+expression was split before rerunning. All failed bundles and run fixtures remain;
+owned Finder windows closed and monitoring settled normally.
+
+The preceding lifecycle correction finalized **67 tests on each simulator**, zero
+failed/skipped; both runs explicitly executed the corrected lazy-transfer test.
+Those results supplement 62 Stability Mac and 67 standard Mac lifecycle tests.
+
+Favorite-state validation finalized **73 Stability Mac tests** and **78 standard
+Mac tests**, zero failed/skipped; the generic visionOS build passed. CI for the
+preceding `a525f87` cursor/continuation change finalized green on all destinations
+(run `34586472123`). The next signed favorite-state build still needs live evidence.
+
+### Native transfer ring and wrong Actions installation (2026-09-11)
+
+Run `67e2885c-8102-4b41-91eb-32bdb39fa750` on `765a651` sealed **13 passed,
+two failed, one deferred**. The favorite metadata correction is now supported live:
+two successful favorite root/child span pairs and duplicate root/child spans were
+recorded, with each typed remote check passing. The full contextual scenario still
+failed, so none of its partial success is promoted to a scenario pass. The final
+share panel was observed at Loading kDrive; the runner timed out before finding
+its Stability item identifier. All owned Finder windows closed and evidence settled.
+
+Process inspection identified the Actions process from a separate older installed
+app, while the replicated instance ran the selected Stability build. PlugInKit listed
+three physical Actions copies with the same identifier: an older DerivedData copy,
+the separate installation, and the selected Stability app. No Actions diagnostics
+came from the expected build. This is high-confidence environment/registration
+mismatch; the loading behavior cannot establish a defect in the current Actions
+implementation. Two positively inspected duplicate registrations were removed,
+leaving bundles, domains, credentials and fixtures intact. The stale Actions process
+had already exited when its run-bounded identity was checked. Preflight now uses
+bounded read-only discovery and rejects missing, duplicate, wrong-path and malformed
+Actions registration. Exact source-code-hash checks at certification are unchanged.
+
+During the larger transfer, computer use observed the generated selected row's
+AXProgressIndicator at fraction 0.950928, with no labeled Cancel control. The runner
+had observed real progress before either transfer completed but never pressed a
+cancel control. A narrowly confined native click on that active indicator is now an
+attempted fallback, with finite 0 < progress < 1, one indicator, and row/window
+geometry checks. A click is not cancellation evidence; the same strict callback
+terminal/recovery assertions decide the result. Tests reject missing/terminal values,
+multiple indicators, and absent or out-of-row/window geometry.
+
+Favorite-state tests finalized **78 passed on each simulator** in addition to 73
+Stability Mac and 78 standard Mac; the generic visionOS build passed. The new
+registration/native-control selection finalized **29 Stability tests**, zero failed
+or skipped, in `potassium-native-cancel-stability-mac-01.xcresult`. No shared runtime
+or mutation semantics changed in this follow-up. Its signed live rerun is pending.
+
+### Retained pre-command timeout (2026-09-11)
+
+Run `40740dba-43e4-450f-9bf6-5845a9a5014d` on `5ae7040` passed the stricter
+Actions registration preflight, but sealed **three passed, two failed, eleven
+skipped**. Enumeration, hydration and independent working-set refresh passed.
+Eviction and contextual actions timed out before the contextual command was
+recorded as invoked. No current Actions callback was observed. The native transfer
+fallback was not exercised because its earlier prerequisite failure caused a skip.
+Owned Finder windows closed and the bundle/fixtures remain retained.
+
+The trace does not yet distinguish missing anchor geometry from ambiguous popup
+menus or a missing command. Closed numeric menu observations now distinguish
+anchor lookup, event posting, popup count and exact enabled-command count without
+exporting item names or URLs. Preserve-both and cancellation also create independent
+fixtures, so selection now allows all four advanced cases after earlier failures;
+remaining dependent operations still skip. Tests preserve failure outcomes and
+conflict-profile exclusion. No unexercised step or native click is a pass.
+
+The failed step's zero remaining budget also prevented its screenshot helper from
+performing fresh AX/Apple Event checks. Failure-only observation now receives a
+separate bounded 15 seconds to dismiss the known popup and revalidate/capture the
+same generated row. The failed operation's deadline and result stay unchanged.
+
+Popup discovery also incorrectly counted expanded submenus as independent visible
+menus. The driver now searches transient roots, excludes the application menu bar,
+and matches only direct commands of the single root popup. Swift Testing covers
+expanded Open With submenus, two genuinely distinct popups, hidden menus and
+traversal exhaustion. This is a supported selector defect; the retained timeout
+trace alone does not prove it caused that particular run. Live confirmation remains
+pending. CI `34589769700` passed macOS, iOS Simulator and visionOS on `5ae7040`.
+
+The popup/continuation regressions finalized **36 passing Stability Mac tests**,
+zero failures/skips, in `potassium-popup-root-stability-mac-01.xcresult`; the standard
+Mac build passed (`potassium-popup-root-standard-mac-01.log`). This follow-up changes
+only the macOS Stability harness; shared-runtime simulator validation remains the
+previous finalized evidence.
+
+### Native input dispatch continuation (2026-09-11)
+
+Fresh run `2c4581d7-db8c-45d6-ac3d-b4e0a7eb4116` reached eviction and posted
+the confined secondary-click events, but observed zero popup menus. Read-only
+computer use independently found no popup. The trace narrows the first divergence
+to native input dispatch, before a provider mutation. It does not establish whether
+event timing, inherited input state or another WindowServer condition suppressed
+the menu. The separately budgeted failure screenshot was retained for the exact
+generated row. The run sealed **four passed, three failed, nine skipped**. Navigation,
+hydration, independent preserve-both and working-set refresh passed. Eviction,
+cancellation and contextual actions failed before command dispatch; dependent
+steps including deletion skipped. All four advanced cases were attempted, the owned
+windows closed, and the failed evidence/fixtures remain preserved.
+
+Native clicks now explicitly move the pointer, clear inherited modifiers, request
+a single click and allow a short event-delivery interval. The target is revalidated
+after movement, and a pressed button is released even on cancellation. Transfer
+completion during hover prevents cancellation input. Root-menu and exact-row
+confinement remain mandatory; actual callbacks still prove the action. This is a
+focused input-dispatch correction requiring live verification, not a declared
+provider fix. Ordinary UI observations are capped at 90 seconds even inside the
+600-second transfer scenario. Regression coverage checks post-hover revalidation,
+modifier/click semantics, cancellation release and deadline bounds.
+
+The native sequence additionally checks `CGPreflightPostEventAccess` before input
+and emits only its boolean outcome. A denial fails before movement or target
+lookup, without a permission prompt. This provides evidence to distinguish denied
+input from an absent menu in the next signed run.
+
+Native pointer/ordinary-deadline regressions finalized in
+`potassium-pointer-sequence-stability-mac-02.xcresult` with zero failures/skips; the
+standard Mac build passed (`potassium-pointer-sequence-standard-mac-01.log`). The
+first test build required an explicit core-module import in the new test file; its
+failed bundle remains retained. The preceding commit's CI `34591774207` passed all
+three platform jobs. Live verification of the new pointer sequence is next.
+
+### Finder Actions toolbar path (2026-09-11)
+
+The subsequent `686eb94` run reports native event-posting permission granted but
+still observes no popup after the revised secondary click. The pointer changes do
+not resolve that live failure; its cause below input dispatch is still unconfirmed.
+Read-only computer use exposes Finder's `Action` menu button with the description
+“Perform tasks with the selected items.” The driver now prefers pressing that
+named control inside the one toolbar of its bound window, after revalidating the
+exact selection. Only one enabled, geometrically confined control is accepted.
+Its AX request has a bounded timeout and the actual popup must still be observed.
+The existing exact direct-command lookup and remote/callback assertions remain.
+Missing toolbar controls retain the confined secondary-click fallback. Regression
+coverage rejects missing, duplicate, disabled and unconfined toolbar controls;
+live confirmation is pending.
+
+The pointer rerun `9964d0bb-1186-40fd-b77d-1a24f9c273fb` sealed with **four
+passed, three failed, nine skipped**, the same scenario outcomes as the preceding
+run. Native posting permission was true at each failed menu request. Preserve-both
+and working-set refresh passed independently; no failed menu invoked its provider
+command. Owned windows closed and the evidence/fixtures remain retained. The
+Actions toolbar path is the next isolated harness change to validate.
+
+Toolbar selection regressions finalized successfully in
+`potassium-toolbar-menu-stability-mac-01.xcresult` with zero failures/skips. The
+standard Mac build passed (`potassium-toolbar-menu-standard-mac-01.log`). The
+preceding transfer scenario's duration fell to 106 seconds including fixture
+preparation and failure capture; its menu lookup used the 90-second UI cap.
+
+### Limit the toolbar route to observed commands (2026-09-11)
+
+Run `f8f56eaf-4755-4f02-aa8b-462cbf4eff63` on `7209e06` passed scenarios
+1–10. Remove Download and Download Now worked through the toolbar; the download
+dispatch returned in 0.72 seconds. Restore then exposed an important UI distinction:
+the toolbar menu omits `Restore from kDrive Trash`. Computer use observed only
+Finder's built-in commands, including Empty Trash, which was never invoked. The
+contextual-actions menu likewise omitted the provider's favorite command. Toolbar
+routing is now limited to the two verified built-in download commands. Provider
+actions and selected-item permanent deletion retain their item-context route, with
+parameterized regression coverage for that distinction.
+
+Native pointer delivery moves from the HID entry point to the logged-in session's
+public event tap, retaining permission, target revalidation, modifier, click and
+release checks. A boolean cursor-position observation after movement distinguishes
+a posted request from actual pointer movement; no coordinates are exported. This
+is a candidate routing correction requiring live confirmation. A missed native
+click still cannot count as a completed operation or cancellation.
+
+The toolbar run eventually sealed **12 passed, three failed, one skipped** after
+its outstanding callbacks settled. No evidence was recovered or forcibly sealed.
+All owned windows closed. The command-routing/session-pointer regressions finalized
+in `potassium-session-pointer-stability-mac-01.xcresult` with zero failures/skips;
+the standard Mac build passed (`potassium-session-pointer-standard-mac-01.log`).
+
+
+## 2026-09-11: current Actions selection identifier failure
+
+Fresh run `8a4d7d89-48f1-4a54-81c5-f9266e0934cd` on `212e54e` sealed with
+13 passed, two failed and permanent deletion deferred. Session-level native input
+successfully exercised provider Restore, favorite/unfavorite and duplicate. Both
+transfer attempts exposed intermediate progress and received a confined indicator
+click, but completed successfully without a cancellation terminal. Cancellation
+remains failed; clicking an indicator is not sufficient evidence.
+
+For contextual actions, the expected outcome was a loaded share panel for the bound
+fixture. Computer use and the operator screenshot instead observed “Action
+Unavailable” from the current installed Actions executable. The earliest divergence
+is `ProviderActionViewModel.load`: FileProviderUI supplied an opaque macOS document
+identifier, which was passed directly to the numeric kDrive parser. Root-cause
+confidence is high for this availability defect. The source file remained intact;
+no share mutation was dispatched. This is a provider UI boundary defect even though
+the retained report classifies the eventual missing-panel timeout as automation.
+Reproduce by selecting a generated plaintext item and choosing Share kDrive Link.
+
+The focused correction resolves the incoming selection with the domain's
+`getUserVisibleURL`, then `getIdentifierForUserVisibleFile`, checks the returned
+domain, and validates the identifier against the configured plaintext/vault engine.
+No filename lookup, document-ID parsing, content read, or server mutation is used
+for resolution. One 90-second deadline covers both callback stages; cancellation
+and late replies fail closed. iOS/visionOS retain validated canonical IDs because
+iOS replicated extensions cannot obtain user-visible URLs. System error details
+are not displayed by the resolver. The model publishes its canonical identifier
+before loading action data, also correcting its run-local panel alias.
+
+The Actions sheet belongs to a separate Accessibility process even though Finder
+hosts it visually. The driver now searches the selected app's Actions executable
+only after physical-path and code-hash verification, and requires one exact
+run-local fixture alias. A title, an unbound panel, an older executable, and duplicate
+matching panels cannot authorize an action. Live verification of this correction
+and fresh/already-running acceptance remain pending. Earlier bundles and fixtures
+are retained; CR-013 stays open.
+
+Sources: Apple's [URL resolution](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/getuservisibleurl(for:completionhandler:))
+and [reverse identifier/domain resolution](https://developer.apple.com/documentation/fileprovider/nsfileprovidermanager/getidentifierforuservisiblefile(at:completionhandler:)),
+plus the installed macOS SDK's `NSFileProviderManager.h` security-scope and iOS
+availability contract. Regression coverage: `ProviderActionItemResolverTests`,
+`FinderActionPanelTargetTests`, and the existing callback-deadline tests.
+
+The observed hosted error sheet also lacked its toolbar-only Done button. macOS
+now renders Done inside the hosted view; other platforms retain their toolbar.
+Runner cleanup closes only a panel with the exact expected alias and attested
+process before closing its owned editor and Finder windows. It waits for work to
+finish and never dismisses an unrelated or unbound panel.
+
+Finalized focused Mac results for the identity correction: 25 Stability tests and
+23 standard-profile tests passed with zero failures/skips. Bundles are
+`/private/tmp/potassium-action-identity-stability-mac-03.xcresult` and
+`/private/tmp/potassium-action-identity-standard-mac-02.xcresult`. A same-domain
+replacement regression also rejects a canonical selection whose URL resolves to a
+different stable ID. The generic visionOS build passed (`potassium-action-identity-vision-build-02.log`).
+
+The final iPhone 17/iOS 26.5 and Apple Vision Pro/visionOS 26.5 Simulator
+results each passed 23 tests with zero failures/skips, retained at
+`/private/tmp/potassium-action-identity-ios-02.xcresult` and
+`/private/tmp/potassium-action-identity-vision-sim-02.xcresult`.
+
+
+### Live identity correction and hosted panel discovery
+
+Fresh `8af5313` run `7200afb4-464d-480d-acdf-390d77e88ccc` sealed with
+13 passed, two failed and deletion deferred. The current Actions process resolved
+the selection, completed item lookup and share-settings lookup, and visibly rendered
+the Share form and Done. The original invalid-identifier failure is fixed. The
+contextual scenario still timed out before entering the form: native read-only AX
+inspection after settling showed an empty Actions `AXWindows` list, a valid
+`AXMainWindow` containing its 11-node form, and no exposed Stability identifier.
+No share mutation occurred, and the generated source remains preserved. The report
+retains automation failure; this is not a contextual-action pass.
+
+Discovery now includes the attested process's main window, deduplicated against its
+listed windows. It still requires one exact fixture alias. The Stability-only macOS
+controller publishes the resolved alias on its native AppKit root, because the
+hosted SwiftUI NavigationStack's identifier did not cross the accessibility bridge.
+Regression coverage rejects an unrelated main window and duplicate discovery;
+actual form interaction and owned-panel cleanup still require the next live run.
+The earlier Actions process-tree observation was incomplete: computer use could
+find the main window, whereas AXWindows alone could not. Cancellation remains failed:
+both real transfers completed without a cancelled callback. CR-013 stays open.
+
+The hosted-panel follow-up passed 26 focused Stability Mac tests with no failures
+or skips (`/private/tmp/potassium-action-panel-stability-mac-01.xcresult`) and the
+standard Mac build. Its additional runtime branches are macOS/Stability-only;
+the identity resolver retains the finalized cross-platform results above.
+
+
+### Permission-gated diagnostic lookup during view layout
+
+Fresh `2ebe661` run `8b4d51f5-f35d-4f06-bb62-ea174945f8ab` sealed with
+13 passed, two failed and deletion deferred. Contextual actions timed out before
+any Actions API diagnostic. The retained process sample
+`/private/tmp/potassium-action-panel-loading-sample.txt` shows the main thread in
+`ProviderActionRootView.body`, through `activeAlias`/`activeRun`, blocked in `open`
+for the shared coordinator lock during initial hosted-view layout. The operator
+confirmed a visible macOS data-access prompt and accepted it. This supports an
+environment permission gate, not an inferred layout-engine or server deadlock.
+
+Run discovery is now dispatched off the UI actor with one bounded callback waiter.
+The model publishes the result; SwiftUI rendering and the native AX identifier
+subscriber use only that cached alias. They never open the shared store during
+layout. Lookup cancellation, timeout, absent-run behavior and off-main execution
+have dedicated regressions. Done remains available during read-only loading and
+cancels the load task; mutation-in-progress still disables dismissal. This does not
+bypass macOS consent. The Actions extension may need its own data-access consent
+even when containing-app preflight has passed. Native panel discovery still needs
+live verification after the granted permission. CR-013 stays open.
+
+The off-main lookup and loading dismissal follow-up finalized with 30 Stability Mac
+tests and 23 tests on each of standard Mac, iPhone 17/iOS 26.5 Simulator, and
+Apple Vision Pro/visionOS 26.5 Simulator, all with zero failures/skips. Bundles:
+`/private/tmp/potassium-panel-lookup-{stability-mac,standard-mac,ios,vision-sim}-01.xcresult`.
+The generic visionOS build passed (`potassium-panel-lookup-vision-build-01.log`).
+Computer use confirmed no Actions window remained after the operator accepted the
+prompt and the previous run finished. A new signed live retry is still required.
+
+
+### Actions shared-container provisioning correction (2026-09-11)
+
+Fresh `a961bf5` run `2653b37a-79e5-431d-9a5d-537967bc5e53` sealed
+**13 passed, two failed, one deferred**. The original opaque-identifier error did
+not recur. The next earliest divergence was an Actions worker blocked in `open`
+while loading `DomainConfigurationFileStore`, before an Actions API span or share
+mutation. The earlier `2ebe661` prompt was explicitly accepted by the operator;
+that grant did not establish access for this newer build. Failed evidence and
+fixtures remain retained. Cancellation was again unexercised.
+
+Read-only inspection found the installed Actions profile was a wildcard profile
+without any App Groups grant. Its signature claimed the existing shared group.
+The app and replicated extension profiles did authorize that group. All three
+were developer-signed with embedded, unexpired profiles and expanded application
+identifiers. Both extension targets omitted `REGISTER_APP_GROUPS`, which only the
+containing app enabled. Classification: environment/build configuration; high
+confidence in the provisioning mismatch, pending live proof that correcting it
+resolves the prompt. Apple documents this exact shared-container authorization
+requirement in [Accessing app group containers](https://developer.apple.com/documentation/xcode/accessing-app-group-containers).
+
+Both extensions now register their existing group for all configurations. The
+ordinary Stability build allows Xcode to refresh provisioning through its saved
+developer account. No group/Keychain migration or privacy-permission reset occurs.
+Preflight checks the installed app and both extension signatures, explicit matching
+application/team claims, expiration, and exact shared-group grants before lab
+access. It emits only a target role and remediation text. This qualification does
+not replace macOS CMS trust, device eligibility, or runtime entitlement validation.
+Negative tests reject missing/different/wildcard grants, mismatched or unexpanded
+identities, absent signature claims, malformed and expired profiles.
+
+CI `34600310726` passed iOS and visionOS but exposed a timeout-test scheduling
+race on Mac: a 100 ms simulated file read could win against a delayed 10 ms timer
+under full parallel load. The regression now holds that synchronous read behind
+a test-only gate until the waiter actually expires. This changes test scheduling,
+not production deadlines. Local build attempt `potassium-group-provisioning-stability-mac-01`
+retains an initial compile error (optional CMS byte pointer), corrected before rerun.
+Finalized validation: `potassium-group-provisioning-stability-mac-02.xcresult`
+10 focused tests and `potassium-group-provisioning-stability-full-01.xcresult`
+**529 tests** passed. Standard Mac (`potassium-group-provisioning-standard-mac-02`),
+iPhone 17/iOS 26.5 (`potassium-group-provisioning-ios-01`) and Apple Vision Pro/
+visionOS 26.5 (`potassium-group-provisioning-vision-sim-01`) each passed nine
+resolver tests, zero failures/skips; the signed generic visionOS build passed.
+Standard Mac01 remains incomplete: its tests executed, but Xcode's coverage
+collector blocked opening a container. The exact test process was stopped and
+Mac02 finalized with `-enableCodeCoverage NO`; no coverage claim is made for it.
+Inspection of the new signed products confirms explicit profiles authorizing the
+existing shared group for all three executables. Live verification remains pending.
+CR-013 stays open.
+
+
+### Native pointer obstruction and retained provisioning retry (2026-09-11)
+
+CI for `27e5dc9` finalized green for macOS, iOS Simulator and visionOS. The first
+ordinary install attempt rejected a refreshed profile with a stale cached resource
+signature before replacing the installed app. Cleaning only that ordinary build's
+DerivedData through `xcodebuild clean`, then rebuilding, produced valid signatures
+and authorized shared-group profiles for the app and both extensions. Installed
+preflight passed; no group migration, credential extraction or privacy reset occurred.
+
+Fresh `27e5dc9` run `b8c89033-2685-4812-ac9b-f8e9a1280a7c` sealed
+**12 passed, three failed, one deferred** and closed its owned Finder/TextEdit
+windows. Restore and contextual actions timed out before any context popup appeared;
+cancellation observed real progress but no cancelled fetch. The run never launched
+the Actions panel, so it does not yet prove the provisioning correction's live
+panel behavior. A later settling `workingSetRefresh` also recorded storage code 5
+(span `18787735-BEE5-4026-BD61-ED69EC4484F1`); its cause remains unconfirmed.
+Earlier failing bundles and all generated remote fixtures remain retained.
+
+A separate local generated-file probe reproduced the earlier divergence: Finder
+was frontmost and Accessibility/event-posting permission checks were true, yet the
+system-wide AX hit at its exact filename field belonged to a different process.
+An independent on-screen window-owner check agreed. The probe stopped before
+posting clicks and closed only its own window. It did not inspect another app's
+UI or identify the obstructing window. Classification: environment; high confidence
+in the observed obstruction, not proof of the owner or every earlier click failure.
+The operator was asked whether a remaining permission/error dialog was visible;
+Computer Use cannot inspect the macOS notification-alert app.
+
+Native clicks now require the hit's process and bounded AX ancestry to reach the
+exact selected row or popup item, both before moving and after hover. Another app,
+another Finder window, missing hit, crossed process or cyclic ancestry fails closed.
+The harness records the existing environment/uiUnavailable evidence classification
+and a local `pointerTargetObstructed` reason; no report schema/field changes occur.
+It omits a failure screenshot for obstruction so other apps' content cannot be
+captured. Missing visual evidence cannot turn this failure into acceptance.
+Regressions prove that another app is never traversed and no mouse-down is posted
+when obstruction appears before or after hover. `potassium-pointer-ownership-stability-mac-01.xcresult`
+finalized **19 passed**, zero failures/skips. The standard Mac build also passed
+(`potassium-pointer-ownership-standard-mac-01.log`). The signed ordinary `27e5dc9` install
+is retained while awaiting resolution; the pointer guard still needs a live rerun.
+CR-013 remains open. No full fresh/warm acceptance is claimed.
+
+### 2026-09-12 — Desktop retry and reproduced WAL write contention
+
+The operator confirmed moving desktop windows during run
+`13dbc241-f71d-423e-a46c-bfccbc3a8b14`. Its sealed zero-pass, five-failure report
+remains failed; that interrupted observation cannot establish a provider regression.
+No run lease remained when execution resumed.
+
+The next fresh run on installed `98e93d6`,
+`4e42af18-dd4c-4956-a037-bf32c9930528`, sealed **13 passed, two failed, one
+unselected**. Restore passed through its actual menu/callback, authoritative bytes,
+and restored destination. All first eleven scenarios, preserve-both and working-set
+refresh passed. Cancellation stopped with `selectionMismatch` before native click
+dispatch. Favorite/unfavorite/duplicate completed and the current Actions extension
+loaded the Share form without a permission prompt, but interaction with its Access
+control timed out before a share mutation. The runner closed its owned windows.
+Closed cancellation stages and panel candidate/control counts were added to locate
+the remaining automation failures; no selectors or acceptance guards were relaxed.
+
+The earlier one-millisecond storage-code-5 refresh motivated
+`SnapshotWriteContentionTests`: four production-store operations execute while an
+independent connection holds a temporary WAL write reservation. On the unchanged
+deferred implementation all four cases failed with `database is locked (code: 5)`
+in `/private/tmp/potassium-resume-contention-before.xcresult`. SQLite.swift 0.16.0
+defaults its transaction to deferred; SQLite documents that upgrading a WAL read
+under another writer can fail immediately despite a busy timeout.
+
+Snapshot saves, poll claims, working-set commits and mutation publication now use
+`BEGIN IMMEDIATE`, reserving the writer before reading their current predicates.
+The existing five-second timeout, stale-state comparisons, transactional rollback,
+and successful-watermark rules are unchanged; no remote work holds the lock. This
+reproduces and corrects a storage defect consistent with the old live trace, but
+does not identify that trace's exact statement or lock owner. CR-025 tracks that
+distinction; CR-023 remains the separate initialization finding.
+
+Finalized validation so far: **27 passed** on iPhone 17/iOS 26.5 and **27 passed**
+on Apple Vision Pro/visionOS 26.5, in `potassium-resume-contention-after.xcresult`
+and `potassium-resume-contention-vision.xcresult`. Both include all four new
+contention cases plus snapshot-generation, working-set, and initialization
+regressions. Full Stability macOS `potassium-resume-stability-mac-01.xcresult`
+finalized **535 passed**, zero failed/skipped/expected failures. Standard macOS
+`potassium-resume-standard-mac-01.xcresult` finalized **412 unit tests passed**
+with coverage disabled; the signed generic visionOS build
+`potassium-resume-vision-build-01.log` succeeded. The updated signed live rerun
+remains pending.
+
+The diagnostic build's fresh run `8393e4a2-89d4-41ee-bd13-f766d1b63d8e` then
+sealed **12 passed, three failed, one skipped**. Restore opened a native popup
+containing only standard Finder commands and timed out; the registered provider
+and Actions paths were unique and current. The 256 MiB transfer completed in about
+4.7 seconds despite a native progress-ring click, without a cancelled callback.
+Share reported two matching host windows and refused to bind. Working-set refresh
+and preserve-both passed. These observations do not establish full acceptance.
+
+The next driver revision identifies the native alias-bearing panel roots and
+deduplicates only equal AX objects across Finder and the attested Actions process;
+different panels bearing the same alias remain rejected. It avoids redundant
+selection/activation after Download Now and rechecks pending telemetry after the
+expensive pointer ancestry validation. Missing provider menu commands can trigger
+at most three same-selection reopens, two seconds apart inside the original
+deadline. System commands, disabled commands and ambiguous matches do not qualify.
+Focused macOS Stability regressions finalized **20 passed**, zero failures or
+skips (`potassium-resume-finder-driver-02.xcresult`). The signed build passed
+preflight and its fresh live run is in progress.
+
+Sources: [SQLite transaction modes](https://www.sqlite.org/lang_transaction.html),
+[WAL isolation](https://www.sqlite.org/isolation.html), and the pinned
+SQLite.swift `Connection.transaction` default. Apple's replicated File Provider
+documentation was rechecked on 2026-09-12. No conflict or permanent-delete policy
+changed; CR-013 remains open and deletion remains optional.
+
+
+The next fresh run, `42dabdc7-e3c3-4e31-9bc9-0721f93b2d2f`, verified that the
+Finder host and Actions main window contain the **same AX panel root**. Share
+therefore bound successfully, then failed at the Access picker (`roleCount=1`,
+`matchCount=0`). A stable picker identifier now replaces its label selector.
+Done dismissed the real sheet, but the detached Actions main window retained the
+alias; this caused cleanup/evidence sealing to fail. `finder-evidence-rejected.json`
+retains the report, and the exited owner was safely abandoned through the documented
+recovery command. This is not an accepted or sealed successful run.
+
+Panel binding now also requires the alias in the owned Finder host, rejecting a
+stale detached main window. A regression covers that dismissal boundary. Restore
+remained unavailable through all three same-item menu reopens; that ineffective
+retry was removed. Cancellation still completed after the native ring click despite
+removing redundant selection and adding a final pending check. The next diagnostic
+build adds a confined generated-row capture during the 256 MiB transfer and closed
+AX action-availability observations. These observations must not count as cancellation.
+
+
+Apple's local macOS 26.5 `NSFileProviderReplicatedExtension.h` (modify callback
+contract, lines 629–634) defines a nil result as a request to delete the local replica.
+The plaintext executor returned nil after every successful Trash request. It now
+returns managed Trash metadata, and the callback sets the Trash parent/user info.
+A preserved local conflict copy keeps its own identity/version when both versions
+are trashed. Active working-set publication excludes those items. Existing remote
+Trash order, preservation, and permanent-deletion policy are unchanged. CR-026 tracks
+this callback defect; simple and combined-field regressions were expanded. Validation
+and causal confirmation of the missing Restore menu are pending. The current live
+run uses the earlier installed runtime and cannot validate this source change.
+
+
+Diagnostic run `29c73070-350d-42bc-83d8-c8737c5f703b` sealed **four passed,
+three failed, nine skipped**. The hydration case passed but Finder offered Download
+Now instead of Remove Download; eviction failed and dependent cases were skipped.
+iOS unit tests ran alongside this diagnostic run, so desktop interaction overlap
+cannot be excluded. Subsequent live acceptance runs will run alone.
+
+Share binding, picker selection, Create Link and Save Changes all dispatched
+successfully. The actual update failed with **HTTP 403 / permission** in the current
+Actions extension; the scenario then reported a harness deadline waiting for an
+unchanged server property. Cleanup now closed the sheet and run-owned Finder windows.
+The transfer capture contains only the generated row: its pie indicator exposed
+neither AXPress nor AXCancel, and a confined native click still produced no cancelled
+fetch. The temporary capture was removed after inspection. A generic `.dat` fixture
+replaces the MacBinary archive type to avoid an archive preview consumer, and local
+pointer timestamps will distinguish dispatch from transfer start/finish.
+
+Infomaniak's official iOS `ShareLinkSettings.encode` omits `validUntil` for free
+drives. Our adapter sent null unconditionally even while merely changing comments.
+It now preflights an absent selected expiration, sends null only to clear an existing
+date, omits already absent expiration, and sends explicit dates unchanged. This is a
+payload correction consistent with the 403, not proof of the lab plan or server's
+rejection reason. Tests cover all three states and failed preflight. Existing
+last-writer-wins risk remains; no account permissions are altered.
+
+Public reference: [Infomaniak ShareLink model](https://github.com/Infomaniak/ios-kDrive/blob/master/kDriveCore/Data/Models/ShareLink.swift),
+read 2026-09-12. Full macOS Stability validation before this latest share-body change
+finalized **538 passed**, zero failures/skips (`potassium-resume-stability-mac-02.xcresult`).
+The Trash selection passed **17 tests** on iOS (`potassium-resume-trash-ios-01.xcresult`).
+
+
+The corrected shared code finalized **415 standard macOS unit tests passed**
+(`potassium-resume-standard-mac-02.xcresult`) and **19 passed each** on iOS and
+visionOS Simulator (`potassium-resume-share-trash-ios-02.xcresult` and
+`potassium-resume-share-trash-vision-02.xcresult`), with zero failures/skips.
+These include the set/clear/absent-expiration request cases and the combined Trash
+regressions. Fresh live confirmation remains pending.
+
+The isolated fresh retry `ec30a74b-1c10-4d36-a2f2-67c72475dc57` produced a
+candidate with **11 passed, four failed, one skipped**. The first ten scenarios
+and preserve-both passed. Restore again omitted its custom command; cancellation
+again completed without a cancelled fetch. Working-set refresh timed out with
+cancelled refresh operations (about 31–37 seconds), without a recorded SQLite
+code-5 failure. Share update now returned **HTTP 200**, but the following reads
+still reported comments disabled. This resolves the observed HTTP rejection only;
+it does not establish a successful settings change. Evidence assembly rejected the
+candidate, so the retained `finder-evidence-rejected.json` is ineligible for
+acceptance. The exited owner was recovered with the documented preserve-evidence
+command. Its diagnostics contain two replicated-provider process instance IDs,
+with invalidation followed by a new process initialization between scenarios.
+The launch controller requires one process throughout the selected run; this
+explains the unverified-build assembly boundary without weakening that requirement.
+
+A read-only `fileproviderctl evaluate` check matched the exact generated trashed
+item using its existing run-salted diagnostic alias. The daemon reported
+`isTrashed=true` and evaluated the Restore activation rule as true. Only these
+closed results were emitted; no raw identifiers, paths or command output were
+retained. Finder's missing menu therefore disagrees with the daemon's state at
+inspection time. Finder was relaunched through its native UI before the next retry;
+neither the predicate nor acceptance requirements changed.
+
+Native pointer timestamps place the 256 MiB cancellation click about 3.4 seconds
+into a 5.1-second fetch. The exact active indicator still exposed neither AXPress
+nor AXCancel, and Show Progress Window was disabled during the transfer. This
+rules out a completed fetch at dispatch as the sole explanation for that attempt.
+The `.dat` fixture did not resolve cancellation.
+
+Share automation now waits for the enabled Save Changes form after creation and
+verifies the checkbox changes from false to true before submitting the update.
+This separates panel response-application timing from server persistence. The
+focused macOS Stability selection finalized **27 passed**, zero failures/skips
+(`potassium-resume-finder-driver-04.xcresult`). The signed generic visionOS build
+also passed (`potassium-resume-vision-build-02.log`). The next isolated fresh retry
+is pending; no full fresh/warm acceptance or CR-013 closure is claimed.
+
+Fresh run `99d0e147-c40d-41e3-82ed-8d076b64a82d` after Finder relaunch sealed
+**13 passed, two failed, one unselected**. Restore completed its real custom
+callback, exact destination and byte checks; working-set refresh also passed.
+Cancellation failed before starting a transfer because Download Now was absent.
+Contextual actions failed before favorite dispatch because the command was absent.
+Both popups remained open without gaining their missing provider commands. One
+provider process served the whole run, so this report sealed normally.
+
+After the runner exited, manual CUA navigation to the same retained generated
+actions file in a new Finder window exposed Download Now and all four applicable
+custom commands. A new driver correction permits one replacement of its owned
+window when a recognized provider command is absent after two seconds. It preserves
+the existing deadline, exact parent/selection checks and process/window ownership;
+disabled or ambiguous commands, arbitrary commands and permanent deletion cannot
+trigger this path. `FinderMenuWindowRefreshTests` covers these boundaries. This
+differs from the previously ineffective same-window popup reopening.
+
+Manual Share interaction on that file verified Inherit Access creation, an actual
+false-to-true comments checkbox change, and a false value returned after Save while
+the old UI claimed success. A separate download restriction remained selected after
+Save. The Actions view now compares every reported configuration field with the
+request, shows authoritative values, and reports an unapplied-settings error on
+mismatch. It does not guess the server's reason, retry with broader access, or claim
+to roll back a partial write. Passwords cannot be compared because they are not
+returned, and expiration comparison respects whole-second transport precision.
+The live harness recognizes that explicit error instead of waiting for unchanged
+server metadata until its deadline. The original comments assertion remains strict.
+
+The updated macOS Stability unit suite finalized **547 passed**, zero failed/skipped
+(`potassium-resume-stability-mac-03.xcresult`); the iOS selection finalized
+**21 passed**, zero failed/skipped (`potassium-resume-sharing-verification-ios-01.xcresult`).
+The latest share-result UI wait and non-trapping date comparison were added after
+that Mac build and require subsequent build/live verification.
+
+A confined manual copy from a retained 256 MiB generated file to a new local
+temporary directory exposed Finder's genuine `stop progress` button in its Copy
+window. The transfer completed before the attempted stop, so no cancellation is
+claimed. This remains a diagnostic observation, outside the sealed run. Two local
+copy probes and the failed remote fixtures remain available; no sealed evidence was
+changed and no permanent deletion was selected.
+
+The cancellation scenario now adds one 256 MiB copy attempt after the existing
+64/256 MiB ring attempts. It copies a new generated item to a new empty local
+temporary directory and targets only Finder's newly created Copy progress window,
+with both exact quoted names and one enabled Stop button. Existing/ambiguous
+progress windows and missing/mismatched labels cannot be cancelled. Real telemetry
+must show intermediate progress and a cancelled fetch; Stop itself never passes
+the case. A separate Download Now must complete for the same item, with exact local
+and server bytes. Failure retains the probe; successful cancellation plus verified
+recovery permits removal of only the owned local temporary directory. No transfer
+throttle or direct provider/API cancellation is used. This path awaits live proof.
+
+The corrected shared code finalized **418 standard Mac unit tests passed**
+(`potassium-resume-standard-mac-03.xcresult`) and **21 visionOS Simulator tests
+passed** (`potassium-resume-sharing-verification-vision-01.xcresult`), zero failures
+or skips. The focused Finder copy/selection/transfer and share-verification suite
+also passed (`potassium-resume-finder-copy-mac-01.xcresult`). Signed installation
+and a new isolated live run remain necessary before acceptance.
+
+Fresh run `c6d8e57e-ce9b-4179-b9c8-444835c00023` then sealed **14 passed,
+one failed, one unselected**. Restore, preserve-both and working-set refresh passed.
+The new exact-target Copy Stop produced one cancelled download and one cancelled
+provider fetch after intermediate progress, followed by a new successful fetch for
+the same item and exact local/server bytes. Cancellation is now live-verified on
+that runtime; the original progress-ring attempts still did not cancel.
+
+Contextual actions reached the Share update. The checkbox was independently
+verified true before Save; the new unapplied-settings error stopped the case with
+API/remoteError evidence rather than a deadline. Cleanup dismissed the bound sheet
+and the report sealed. Thus only the contextual-actions scenario remains failed
+in this selected fresh run; permanent deletion remains deliberately unselected.
+
+The next share adapter revision uses the endpoint's optional update fields as a
+patch: read the current link, send only changed capabilities/access/date, explicitly
+clear an existing expiration when requested, and keep password rotation explicit.
+An unchanged configuration returns the authoritative read without an empty PUT.
+This tests whether resending unchanged editing/access policy resets independent
+comments; that causal hypothesis still requires live confirmation. Post-write
+comparison stays strict, and there is still no conditional version or rollback.
+
+### Comments-only follow-up and remaining action coverage
+
+A manual retained-fixture check with the installed minimal update again verified
+comments checked before Save and unchecked in the authoritative response, with
+the new explicit mismatch error. Redundant access fields were therefore not the
+cause in this probe. Disabling that generated link succeeded. No public access,
+editing permission or destructive deletion was added as a workaround.
+
+The official PUT contract (retrieved 2026-09-12) states that omitted/null
+`can_comment` inherits `can_edit`. Updates now always include comment intent;
+a regression preserves existing comments when downloads are restricted. Other
+unchanged fields and already-absent expiration remain omitted. The public source is
+https://developer.infomaniak.com/docs/api/put/2/drive/%7Bdrive_id%7D/files/%7Bfile_id%7D/link. No live body, URL or identifier was recorded.
+
+The native version list combined its row into one accessibility element, hiding
+the Restore button. The plaintext row now contains separate children. Case 16
+continues its disable and historical-copy checks after the specific unapplied-share
+warning, but retains and returns that failure even when the later checks succeed.
+All other errors still stop normally. Fresh current-build validation remains pending.
+
+Current regression validation: the full macOS Stability unit target passed 553
+tests (`potassium-resume-stability-mac-04.xcresult`). The shared API, returned-setting
+and mutation-callback selection passed 24 tests on iOS Simulator and 24 on visionOS
+Simulator (`potassium-resume-share-patch-ios-02.xcresult` and
+`potassium-resume-share-patch-vision-02.xcresult`). The current normal signed app
+passed installation/permission/ownership preflight before isolated Finder execution.
+A sanitized reproduction is in [SHARE_SETTINGS_REPRODUCTION.md](SHARE_SETTINGS_REPRODUCTION.md).
+
+Fresh `17d1e74b-8bb0-4672-89cf-fb84e3c7bf20` sealed 14 passed, one failed and
+deletion unselected. Cancellation produced real cancelled fetch/download terminals
+again, then independently recovered exact bytes. Trash restoration and working-set
+refresh passed. After the expected comments mismatch, Disable Link confirmation
+found two enabled matches across the form and dialog and timed out. The new driver
+selects one leaf confirmation sheet within the bound panel, requiring its exact
+prompt, safety message and Cancel/confirmation pair; duplicates and wrong actions
+fail closed. Regression tests cover both link disable and Restore as Copy.
+
+Manual version validation: child containment alone did not expose Restore from the
+hosted List. Changing the plaintext history container to Form exposed the independent
+button with its stable identifier. Native activation opened the exact Restore as
+Copy confirmation, and the retained synthetic version was restored as a new copy.
+The current-byte and historical-byte assertions remain required in the live runner.
+The confirmation/panel/share selection passed 12 Mac tests
+(`potassium-resume-confirmation-mac-01.xcresult`).
+
+Warm `db0ab199-ecfb-4f86-86eb-401067df178d` sealed 13 passed, two failed and
+deletion unselected. Cancellation passed again. The working-set target's exact new
+metadata arrived at 18:15:26Z, 17 seconds after scenario 15 began, through an
+enumeration that started at 18:14:59Z during cancellation. Its original correlation
+correctly did not satisfy scenario 15. This was an evidence-boundary race, not
+missing server metadata. Before creating the next working-set fixture, the runner
+now waits for prior diagnostics to settle within a separate 90-second preparation;
+the actual scenario keeps its 90-second budget and every existing correlation,
+parent-callback, metadata and process gate. No event is reassigned to another step.
+The settlement regression requires the older parent callback to finish even when
+its member delivery has completed. CR-028 tracks current live verification.
+
+The same run verified the link-disable confirmation and successful API deletion.
+Restore as Copy found its exact sheet; the API succeeded in 341 ms even though
+AXPress reported failure as the remote UI dismissed. Confirmation activation now
+happens exactly once, followed by a new explicit success result and the unchanged
+independent server/byte assertions. A pre-existing success is rejected; a failed
+AX return never causes a second restore. Result matching has positive and negative
+regressions. The comments mismatch remains a mandatory scenario failure.
+
+The full macOS Stability target passed 558 unit tests
+(`potassium-resume-stability-mac-06.xcresult`). An initial compile failure in the
+new settlement test's mutating expression was corrected by evaluating before the
+Swift Testing expectation macro; the subsequent full run passed. The current version
+form also builds for iOS Simulator and generic visionOS.
+
+A controlled temporary public link to the retained synthetic-only text fixture also
+returned comments disabled after Save, disproving inherited access as the sole cause
+in this probe. It was immediately disabled; a newly opened share panel performed
+a fresh read and showed Create Link with no existing link. The automated case still
+uses inherited access, and the comments assertion is unchanged. No credentials or
+private URL were recorded. See the sanitized reproduction document for follow-up.
+
+Current warm run `07253623-3e2b-4e3b-8362-14eaa4f471be` sealed 14 passed,
+one failed and permanent deletion unselected. The prior-callback preparation
+settled, and scenario 15 then passed its original correlation/metadata requirements.
+Cancellation again produced a real cancelled fetch and exact same-item recovery.
+Case 16 reproduced unapplied comments, then verified link deletion and historical
+restore independently. Restore AXPress returned -25205, but the newly appearing
+success result was verified without a second activation. The restored copy's bytes
+matched the historical fixture and the original retained its current bytes. Only
+then did the case return the preserved share-settings failure (`api / remoteError`).
+Thus comments are the sole failing selected scenario condition in this sealed run;
+no success gate was removed, and permanent deletion still prevents full acceptance.
+CR-028 is mitigated by the warm evidence. Targeted fresh/warm conflict validation
+is continuing against the same installed build.
+
+### Current conflict matrix and remaining input
+
+All six targeted cases passed with fresh and already-running extensions on the
+current installed build. Each independent report sealed one selected pass and
+15 unrelated scenarios skipped; these are conflict-profile passes, not full-suite
+acceptance. Both command groups exited 0.
+
+| Conflict case | Fresh extension run | Already-running extension run |
+| --- | --- | --- |
+| Content before preflight | `047bfac9-ac58-45e1-9bf5-7557ae0694f1` | `d09303e8-df9b-47d9-b968-ac2a719977e3` |
+| Content after preflight | `218356d2-3dc3-4563-ae5a-dddb94812e7f` | `c202ac3a-e19d-47c7-9c0f-e3b312288dc6` |
+| Rename / rename | `59ecc6bb-fd7b-4947-adf1-178088db8f07` | `4110acbc-5b2b-49cb-9e7b-df5e3286b6e9` |
+| Move / move | `8e76f00c-d808-493c-b0ee-d7c36efd3c44` | `3bb9d138-eaca-40d4-b58c-b2d3c5dbf0ce` |
+| Edit / rename | `ff8ea036-cbb9-4c81-bac1-b548290d8994` | `994fa794-1e08-47a8-83dd-ba1612d51969` |
+| Edit / move | `95185192-51ae-4316-b821-c6abac64032a` | `bae70b22-e0ed-42b8-9756-27622302f5ae` |
+
+The current full selected warm suite remains 14 passed, one failed and permanent
+deletion unselected. Its sole failure is `supportedContextualActions`: kDrive
+acknowledges the comment-setting update but reports comments disabled. The app now
+shows that discrepancy; link disable and historical-copy/current-byte checks passed.
+An official-client comparison on the same lab account or a vendor explanation is
+still needed. No browser login was assumed and no vendor message was sent. The
+sanitized reproduction is ready for review. Permanent deletion remains optional
+and requires an explicit operator request; CR-013 remains open.
+
+At the pre-commit validation snapshot, GitHub PR #22 CI was green at `98e93d6`;
+those remote jobs predate these fixes. Local evidence for this change is the
+558-pass macOS Stability
+unit suite, 24 focused iOS Simulator tests, 24 focused visionOS Simulator tests,
+final iOS/visionOS version-form builds, the selected live suite, and the 12 targeted
+conflict reports above. No XCTest UI suite was rerun in this continuation; the
+installed Finder/Actions flows were validated through native UI and server checks.
+
+### Accepted merge limitation (2026-09-13)
+
+The operator accepted the unapplied file-comments setting as non-blocking for this
+PR because comments are outside the critical path. Official-client comparison or
+vendor clarification is a follow-up, not required operator input before merging.
+CR-027 remains open; the UI warning and strict failed scenario result are preserved.
+The accepted limitation does not count as a passing test, establish full-suite
+acceptance, authorize permanent deletion, or close CR-013. Publish the current fixes
+and require CI for the updated PR head before assessing merge readiness.
+
+### OS versions covered by the current evidence (2026-09-13)
+
+The September 12 local validation used Xcode 26.5 (17F42) on an arm64 Mac running
+macOS 26.6.2 (25G83). The retained XCResult device records confirm these runtime
+versions; the host's `sw_vers` and `xcodebuild -version` were checked again on
+September 13.
+
+| Validation | OS runtime and build | Result |
+| --- | --- | --- |
+| Full macOS Stability unit target | macOS 26.6.2 (25G83), arm64 | 558 passed |
+| Focused shared-runtime tests, iPhone 17 Simulator | iOS 26.5 (23F73) | 24 passed |
+| Focused shared-runtime tests, Apple Vision Pro Simulator | visionOS 26.5 (23O470) | 24 passed |
+| Native Finder/Actions integration on the same Mac host | macOS 26.6.2 (25G83), arm64 | 14 passed, comments failed and accepted as non-blocking, permanent deletion unselected |
+| Six live conflict cases, fresh and already-running extension profiles, on the same Mac host | macOS 26.6.2 (25G83), arm64 | 12 independent passes |
+
+The unit-result bundles are `potassium-resume-stability-mac-06.xcresult`,
+`potassium-resume-share-patch-ios-02.xcresult`, and
+`potassium-resume-share-patch-vision-02.xcresult` under `/private/tmp` on this host.
+Inspect with `xcrun xcresulttool get test-results summary --path --format json`;
+the runtime comes from `devicesAndConfigurations[].device`, not a deployment target
+or SDK label. Use the top-level test count; parameterized execution counts differ.
+
+The sealed live JSON reports do not embed an OS version. Their OS attribution here
+is the shared test-host context, supported by the contemporaneous XCResult records;
+it is not an independently captured per-run OS attestation. Existing sealed reports
+remain unchanged. For future live runs, retain `sw_vers`, `uname -m`, and
+`xcodebuild -version` with the run log and record them alongside the run IDs.
+
+These passes cover the versions listed above. They do not establish compatibility
+with the next macOS release. After an OS upgrade, rerun fresh and already-running
+Finder/Actions integration and conflict profiles, inspecting hosted panels, menus,
+cancellation, Trash restoration, version recovery, and working-set delivery before
+claiming compatibility. Preserve failures and adjust behavior only from new evidence.
diff --git a/doc/STABILITY_LOOP_PLAN.md b/doc/STABILITY_LOOP_PLAN.md
new file mode 100644
index 0000000..de2c85e
--- /dev/null
+++ b/doc/STABILITY_LOOP_PLAN.md
@@ -0,0 +1,123 @@
+# File Provider Stability Loop
+
+Implementation progress and the required evidence/decision ledger live in
+[`STABILITY_LOOP_AUDIT.md`](STABILITY_LOOP_AUDIT.md). That ledger is part of
+this plan: a milestone is not complete until its implementation, validation,
+privacy scan, reviewer result, and affected truth-table cells are recorded
+there.
+
+## Summary
+
+Build an opt-in macOS Stability configuration for the legacy plaintext File
+Provider. It will use a real development bearer token stored through the
+existing manual-token Keychain flow, replace activity/conflict SQLite
+persistence with per-run JSONL audit files, drive Finder through Accessibility
+automation, and verify resulting server state against a lab-owned disposable
+kDrive root.
+
+The API audit will use version-pinned evidence from the official
+[iOS](https://github.com/Infomaniak/ios-kDrive),
+[Android](https://github.com/Infomaniak/android-kDrive), and
+[desktop](https://github.com/Infomaniak/desktop-kDrive) clients, with live
+server observations taking precedence.
+
+## Stability Runtime And Diagnostics
+
+- Add a `Stability` Xcode build configuration and app/File Provider extension
+ schemes, using a `STABILITY` compilation condition across all runtime-owning
+ targets.
+- Reuse the current app identity. Before a run, preflight must reject normal
+ registered domains and instruct the operator to use
+ `scripts/uninstall-file-provider.sh --dry-run` then the explicit safe `--yes`
+ reset; never invoke hard purge automatically.
+- Introduce a shared diagnostic interface (`ProviderDiagnosticRecording` plus
+ versioned `ProviderDiagnosticEvent`) for File Provider callbacks and typed
+ kDrive operations.
+- In Stability builds, construct a JSONL-backed event store everywhere the
+ current SQLite event store is created: app, File Provider extension, and
+ contextual-action runtime. Snapshot, anchor, and working-set SQLite storage
+ remains unchanged.
+- Write one app-group run bundle per run: immutable `run.json`, append-only
+ `events.jsonl`, API observations, assertion results, and a final summary.
+ Support Activity UI paging/export by replaying the JSONL events through the
+ existing event-store protocols.
+- Record sanitized callback starts/completions, changed-field shapes,
+ pagination/anchor state, request route templates and option shapes,
+ status/error classes, durations, progress/cancellation, and correlation IDs.
+ Never log tokens, authorization headers, URLs with identifiers, bodies, file
+ bytes, names, paths, or share links.
+- Serialize cross-process JSONL appends with an advisory file lock; tolerate
+ only a partial trailing line after interruption, and surface other corruption
+ in the Stability Lab. Retain the newest 20 completed runs up to 250 MB,
+ pruning only whole completed bundles.
+
+## Live Finder Lab
+
+- Add a Stability Lab to provision a unique top-level test root under a
+ dedicated non-customer account, store its stable ID in the domain
+ configuration, and register the File Provider against that root rather than
+ the drive root.
+- Reuse the existing manual access-token UI and shared Keychain; do not accept
+ credentials from launch arguments, environment variables, files, or scripts.
+- Require an explicit destructive confirmation to reset the lab root. Verify
+ that the configured root is non-root, directly owned by the lab, and matches
+ its stored ownership marker before deleting only its contents.
+- Add an app command mode and `scripts/run-finder-stability.sh` to start/finish
+ runs, capture API baselines and assertions, open Finder, and assemble the
+ evidence bundle without printing credentials or private URLs.
+- Add an Accessibility/Apple Events Finder runner with preflight checks for
+ Accessibility, Finder Automation, File Provider registration, and consent
+ state. It automates stable actions and reports a checkpoint—not a false
+ failure—for OS-owned consent or variable contextual UI.
+- Cover enumeration and change anchors; hydrate/evict/download; file and
+ directory creation; edits/uploads; rename; move; trash/restore/permanent
+ deletion; concurrent remote changes and preserve-both behavior;
+ cancellation/progress; working-set refresh; and supported contextual actions.
+ Each step asserts both Finder-visible state and server-authoritative state,
+ then attaches correlated diagnostics.
+
+## kDrive API Evidence Audit
+
+- Create a version-pinned API evidence matrix covering every operation in
+ `KDriveFileProviding` and `KDriveContextActionProviding`: route/options,
+ pagination/action semantics, ETags and conflict policies,
+ transfer/cancellation behavior, mutations, trash, sharing, versions, and
+ error mapping.
+- For each entry, capture the potassiumChannel 0.3.0 pin, official API
+ documentation, live observation, and exact source permalink/commit from the
+ three reference clients. Record discrepancies, the selected behavior, and its
+ rationale using: live server, then official docs, then client implementation.
+- Turn each accepted decision into typed request/response fixture tests. Treat
+ client code as behavioral evidence only; do not copy GPL implementation.
+- Correct stale documentation that still claims potassiumChannel 0.2.0, and
+ update `doc/KDRIVE_API_MAPPING.md`, `doc/LOGGING.md`, and
+ `doc/TESTING_AND_DEVELOPMENT.md`.
+- Update `doc/CONFLICT_RESOLUTION_TRUTH_TABLE.md` and its regression evidence
+ for every mutation, ETag, conflict, retry, or error-mapping decision affected
+ by the audit.
+
+## Validation
+
+- Unit-test JSONL encoding/replay, locking, interrupted writes, retention,
+ redaction, event-store compatibility, Stability-only factory selection, root
+ ownership guards, and no-token/no-private-value assertions.
+- Add adapter tests for every audited request shape and server rule, using
+ fixtures generated from sanitized live observations; keep all live checks out
+ of CI.
+- Test Stability Lab provisioning/reset and command preflight with injected
+ remote, Keychain, and domain fakes.
+- Validate the Accessibility runner's permission and checkpoint behavior
+ locally; run full live Finder scenarios only with the development account.
+- Run the relevant Xcode build/test matrix on macOS, iOS Simulator, and
+ visionOS after shared runtime changes.
+
+## Assumptions
+
+- Initial live coverage is macOS Finder and the legacy plaintext engine only;
+ opaque vault validation becomes a separate stability loop.
+- The operator provisions and manually enters a real development access token,
+ and uses an account/root that contains no customer data.
+- Finder automation may require one-time macOS permissions and explicit
+ checkpoints for non-deterministic system UI.
+- The Stability build shares today's identity, so its domain-isolation preflight
+ is mandatory.
diff --git a/doc/TESTING_AND_DEVELOPMENT.md b/doc/TESTING_AND_DEVELOPMENT.md
index 29c194d..a536218 100644
--- a/doc/TESTING_AND_DEVELOPMENT.md
+++ b/doc/TESTING_AND_DEVELOPMENT.md
@@ -7,6 +7,8 @@ source of truth.
- Project: `potassiumProvider.xcodeproj`
- Scheme: `potassiumProvider`
+- Stability schemes: `potassiumProvider-Stability` and
+ `potassiumProviderFileProvider-Stability`
- App target: `potassiumProvider`
- File Provider extension target: `potassiumProviderFileProvider`
- File Provider UI extension target: `potassiumProviderActions`
@@ -14,9 +16,23 @@ source of truth.
- Unit test target: `potassiumProviderTests`
- UI test target: `potassiumProviderUITests`
-The shared `potassiumProvider` scheme runs `potassiumProviderTests` in its Test
-action. UI automation remains a separate Xcode test-target workflow and is not
-part of the shared scheme's command-line test path.
+The existing `potassiumProvider` scheme includes both unit and UI test bundles.
+The `potassiumProvider-Stability` Test action intentionally includes only the
+unit test bundle so a diagnostic-store test never drives Finder or consumes a
+live credential. The File Provider Stability scheme has no Test action because
+the unit target imports the shared core, not the extension executable.
+
+Both shared app Test actions set `codeCoverageEnabled="NO"`. This repository
+has no coverage-upload or reporting consumer, and explicitly disabling coverage
+keeps coverage collection out of macOS Test actions. It does not disable any
+test bundle or live-safety gate.
+
+The unit suite is compile-time profile separated. `FinderStabilityCommandTests`
+is compiled only for macOS `STABILITY`; ordinary-domain registration, reload,
+error-path, and concurrent-add expectations are compiled only for the standard
+profile. The Stability profile instead verifies that the ordinary `addDomain`
+path fails closed without registration or persisted domain state. Neither
+profile's Test action runs a live Finder scenario.
Do not use Tuist or root-level SwiftPM commands for validation unless the
project is intentionally migrated.
@@ -35,10 +51,283 @@ Swift package dependencies are resolved by Xcode:
The app imports split potassiumChannel modules directly. It should not import an
old monolithic `potassiumChannel` module name.
-The project requires the published potassiumChannel 0.2 release line.
-`Package.resolved` must stay locked to the validated 0.2.0 release unless a
-later compatible package version is adopted and the full validation matrix is
-rerun.
+The project requires potassiumChannel 0.3.0. `Package.resolved` is locked to
+tag `0.3.0` at commit
+`db829f1f2bd8c2113a529c9c521bd5cdfb5ef4dc`. Changing that pin requires the
+adapter evidence matrix and full validation matrix to be rerun.
+
+## Stability Profile
+
+The `Stability` configuration is an opt-in debug-shaped build profile. It uses
+the production app identity, app group, entitlements, and manual-token Keychain
+flow, while adding the `STABILITY` Swift compilation condition. It never reads
+credentials from scheme arguments, environment variables, scripts, or test
+fixtures. The Debug-only UI fixture and all existing unified loggers are
+compiled out/disabled in this profile. Live checks stay outside CI.
+
+List and inspect it with:
+
+```sh
+xcodebuild -list -project potassiumProvider.xcodeproj
+xcodebuild -showBuildSettings \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -configuration Stability \
+ -destination 'platform=macOS'
+```
+
+Run only the profile/JSONL unit slice on macOS:
+
+```sh
+xcodebuild test \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -destination 'platform=macOS,arch=arm64' \
+ -only-testing:potassiumProviderTests/StabilityDiagnosticsTests
+```
+
+An active run must be created by the Stability Lab tab or command before production
+runtime processes append JSONL. The factory deliberately returns no event
+store when no run exists. Ordinary Debug and Release builds keep SQLite
+activity/conflict history. `Snapshots.sqlite3` remains the store for snapshots,
+anchors, and working-set state in every profile.
+
+`StabilityDiagnosticsTests` covers run-manifest/pointer selection, concurrent
+coordinators, a real subprocess writer, two in-process writers, interrupted
+tail recovery, complete-line corruption, capacity refusal, symlink rejection,
+post-finish append refusal, retention, redaction, paging/filtering, statistics,
+cross-store observation, tombstone clear/domain removal, export, and factory
+selection. Its subprocess check uses Xcode's bundled Python executable only to
+act as an independent POSIX-locking process; production code has no Python or
+script dependency.
+
+Run the callback/network and Lab safety slices without live credentials:
+
+```sh
+xcodebuild test \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -destination 'platform=macOS,arch=arm64' \
+ -only-testing:potassiumProviderTests/ProviderDiagnosticSpanTests \
+ -only-testing:potassiumProviderTests/FileProviderOperationLifecycleTests \
+ -only-testing:potassiumProviderTests/StabilityLabSafetyTests \
+ -only-testing:potassiumProviderTests/StabilityLabRemoteCoordinatorTests
+```
+
+The exact network-outcome checks use Swift Testing identifiers including their
+parentheses:
+
+```sh
+xcodebuild test-without-building \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -destination 'platform=macOS' \
+ '-only-testing:potassiumProviderTests/PotassiumProviderCoreTests/kdriveServiceExposesLazyObservableDownloadOperation()' \
+ '-only-testing:potassiumProviderTests/PotassiumProviderCoreTests/missingShareLinkIsRecordedAsSuccessfulOptionalResult()' \
+ '-only-testing:potassiumProviderTests/PotassiumProviderCoreTests/concurrentLazyTransferStartAndCancelShareOneDiagnosticSpan()'
+```
+
+These tests use only in-memory recorders, fake kDrive services, and pure root observations. They do
+not read Keychain credentials, register a File Provider domain, or mutate a
+remote account. The hosted macOS test bundle must be signed on machines where
+the unsigned XCTest worker cannot materialize.
+
+### Stability Lab safety workflow
+
+The macOS Stability build exposes a dedicated Stability Lab tab. Provisioning
+is enabled only when no saved or system-registered File Provider domain is
+present. Connect the dedicated non-customer development account through the
+existing OAuth or manual-token Keychain login, load an internal non-maintenance drive reached by
+that account, and let the lab create one unique folder inside the verified server-created
+`Private` directory, plus a fixed-name ownership marker. The
+stable root and marker file IDs are stored in the domain configuration; the
+domain is registered only after that evidence is durable locally. A failed
+registration leaves the ownership record in place and never auto-deletes the
+remote folder.
+
+Drive discovery proves internal membership, not product ownership. Lab-root
+ownership is instead bound procedurally: this build creates a child of the verified `Private` directory and a random versioned marker, persists the exact root/marker
+IDs locally, and requires those remote objects and marker bytes to match on
+every preflight.
+
+Before using a shared-identity Stability build, inspect ordinary domains with:
+
+```sh
+scripts/uninstall-file-provider.sh --dry-run
+```
+
+If the plan is correct, use the explicit safe cleanup path:
+
+```sh
+scripts/uninstall-file-provider.sh --yes
+```
+
+The app, File Provider extension, and contextual-action runtime all reject a
+saved domain whose purpose does not match the current build profile. The lab
+never invokes `--hard-purge`. Reset requires the exact phrase
+`DELETE STABILITY LAB CONTENTS`. It fully consumes the root listing, preserves
+the root and marker, and immediately re-fetches the root, marker, and each
+planned child's current parent before calling the reversible trash endpoint.
+It also re-queries system registration isolation immediately before each
+mutation. It never calls permanent deletion. A cross-process lifecycle lease
+prevents a diagnostics run from starting during reset and rejects reset while a
+run is active. Live provisioning/reset is opt-in and was not executed by the
+automated test suite.
+
+### Finder Stability runner
+
+The macOS Stability app runs an opt-in native Accessibility/Apple Events suite.
+The ordinary app bundle must be signed; test-host products with XCTest injection
+are not valid live builds. The macOS Stability containing app runs outside App
+Sandbox because Apple excludes assistive Accessibility APIs from sandboxed apps.
+Hardened runtime remains enabled. The File Provider and action extensions remain
+sandboxed, as do the standard app profiles. Accessibility, Automation, and screen
+recording still require normal macOS consent.
+
+```sh
+# Build/reinstall after source changes at the stable LaunchServices location.
+scripts/run-finder-stability.sh --build --preflight
+# One-time creation, or safe registration resume for an existing owned lab.
+scripts/run-finder-stability.sh --provision --yes-live
+# Read-only authentication, ownership, domain, and permission checks.
+scripts/run-finder-stability.sh --preflight --request-permissions
+# Reuse the installed signed Stability bundle without compiling again.
+scripts/run-finder-stability.sh --run --yes-live --request-permissions
+# Optional full acceptance: also exercise permanent deletion with exact-item confirmation.
+scripts/run-finder-stability.sh --run --yes-live --include-permanent-deletion
+scripts/run-finder-stability.sh --app "$HOME/Applications/Potassium Stability.app" --watch
+scripts/run-finder-stability.sh --recover-stale-run --yes-recover
+```
+
+The wrapper launches the app through LaunchServices with a private local console
+log, giving the standalone runner its own permission identity. Credentials remain
+in the app's Keychain flow; OAuth refresh also stays inside the app. No credential,
+account ID, remote URL, or root path is accepted as a runner argument. Exit 0 means
+ready or fully passed (according to the chosen mode), 3 means an unresolved
+checkpoint, 2 a safety rejection, and 1 failure or incomplete evidence. Exit 4 means
+the 15 selected scenarios passed and permanent deletion was deliberately deferred;
+it is a completed selection with a sealed report, not full sixteen-scenario acceptance.
+
+The default install is `~/Applications/Potassium Stability.app`; `--build` is
+explicit after the first install, refuses a running containing app, retains a
+local backup, verifies signing, and registers the installed extensions. `--app`
+selects an existing bundle without replacing it. A release copy in `/Applications`
+can have the same bundle identifier with a different designated signing
+requirement. Grant permissions to the installed Stability path, and compare
+signing requirements/registrations when Settings shows an enabled grant that the
+running app cannot use. Do not repeatedly compile or reset all privacy settings
+as a substitute for identifying the registered app.
+
+Every run creates a fresh owned subtree below the verified lab. The lab root,
+ownership marker, and previous contents are preserved. The runner binds each
+mutation target to its stable File Provider item and domain and re-fetches the
+ancestry of generated sources and destinations. It owns one Finder window, resolves
+fresh Accessibility state, and fails when a unique expected control is unavailable.
+UI observations use the remaining scenario deadline (90 seconds for ordinary
+scenarios), including delayed row rendering; they do not impose an earlier
+10-second observation cutoff. Failed row observations record only binding flags
+and matching-row counts, never window titles or other row contents.
+Initial fixture preparation has its own 90-second budget. It must finish within
+that budget before the first scenario receives its 90 seconds; setup failures
+cannot advance to navigation. The first report interval retains both phases and
+their diagnostics, so its total duration can exceed one scenario budget.
+File creation uses Finder copy/paste; editing opens Finder's selection in TextEdit
+and saves only the verified document.
+Hydration verifies the downloaded bytes, then closes the generated TextEdit
+document before eviction. Failure to release that presenter fails hydration.
+Eviction freshly binds the exact item and invokes Remove Download without a
+domain-wide stabilization prerequisite; it verifies the resulting menu/download
+state without reading the evicted file. Final diagnostic settling remains required.
+
+For independent two-client conflict testing, use `--conflicts --yes-live`, optionally
+with `--case content-after-preflight` (or another case listed in
+[Conflict testing](CONFLICT_TESTING.md)). Each selected case owns fresh fixtures and
+an immutable profile manifest. Unrelated scenarios are explicitly unselected; a
+conflict-profile pass never satisfies the original sixteen-scenario acceptance.
+
+The 16 scenarios cover navigation/change anchors, hydration, eviction, download,
+file/directory creation, edit/upload, rename, move, trash, restore, permanent
+selected-item deletion, concurrent preserve-both, cancellation/progress, actual
+working-set membership, and contextual actions. Trash expects `modifyItem`;
+permanent deletion expects `deleteItem`. Restore and deletion require an exactly
+identified provider-managed trashed fixture. Scenario 12 is deferred by default,
+before re-trashing the restored fixture or presenting a confirmation. Scenarios
+13–16 continue with their independent fixtures. `--include-permanent-deletion`
+selects scenario 12 and its exact-fixture confirmation, followed by rebinding.
+The flag is accepted only with `--run`; it does not grant confirmation itself.
+Empty Trash is never used.
+For a bound trashed fixture, the runner navigates its owned Finder window to the
+exact parent with Finder's native Apple Events target command, verifies that
+destination, and rebinds the item/domain before selecting its contextual action.
+Ordinary folder navigation continues to use Go to Folder. The native target path
+does not broaden Trash selection or bypass the destination/identity guards.
+Unavailable UI or inaccessible trash identity is incomplete coverage, never a pass.
+Metadata lookup checks the active endpoint first, then the typed Trash endpoint
+only after HTTP 404. The resolved drive/item identity must match exactly. Only
+absence from both endpoints becomes `noSuchItem`; the handled active 404 remains
+in diagnostics and needs successful correlated recovery evidence. This metadata
+fallback does not allow content or mutation preflight to operate on trashed items.
+Restore verification waits for the exact provider callback to complete, then polls
+active metadata within the existing deadline. A temporary 404 is pending; it cannot
+establish success. Identity, drive, parentage, contents, Finder visibility, and the
+final correlated diagnostic validation are still required. The resolved local URL
+must also have the expected restored parent and filename before Finder selection;
+an identity-bound URL that still points into Trash cannot pass.
+
+The Stability-only conflict barrier holds the exact local mutation after its real
+version preflight while the runner performs a competing typed remote replacement.
+Release sends the original real conditional request; no response is fabricated.
+The barrier is cancellable and bounded. Cancellation uses a 64 MiB generated
+transfer, retries with 256 MiB if necessary, and requires observed progress, actual
+Finder cancellation, one cancellation terminal, and a subsequent successful fetch
+for the same fixture. Contextual actions exercise favorite/unfavorite, duplicate,
+inherited-access share-link create/update/disable, and version restore as a copy.
+
+Ordinary scenario budgets are 90 seconds and transfer budgets 10 minutes. Operator
+pauses retain the same monitored run and extend the active deadline only on resume.
+The runner panel rechecks permissions and safety before continuing. Polling backs
+off and observes retry-after values. Callback waits reject late/double completion.
+After a scenario failure later scenarios are skipped and fixtures remain intact;
+the runner closes its dedicated Finder window after capturing evidence, on both
+success and failure. Cleanup addresses only the created window ID and verifies
+Finder's kernel process start time, so relaunches and reused IDs cannot close an
+unrelated window. Monitoring continues through closure and callback settlement.
+Cleanup failures remain visible and prevent certification. Started work must
+settle before final sealing.
+
+Version 2 and 3 reports require UI observations, fresh remote verification, item-specific
+spans, and the expected extension code hash/process identity. Missing starts,
+telemetry, conflicting terminals, cached hydration, root-only working-set evidence,
+and untriggered conflict/cancellation cannot certify a pass. Version 3 adds the
+scenario-12-only skip reason `permanentDeletionNotSelected`; its assertions remain
+not evaluated. Versions 1 and 2 remain readable as historical evidence. A successful
+full acceptance requires all 16 scenarios, including explicitly selected deletion,
+on both a fresh extension and an already running extension; unit tests and permission
+checkpoints do not establish live acceptance. See `STABILITY_LOOP_AUDIT.md` for the
+current completed evidence and outstanding live coverage.
+Recording begins before context preflight. A failed context load preserves an
+unsealed bundle requiring explicit stale-owner recovery after the runner exits.
+The launcher may register the extension before this point, so recording order
+alone is insufficient evidence of a fresh extension launch.
+
+Screenshots are cropped to generated selected Finder rows and retained locally in
+`visual-evidence`, outside ordinary diagnostic exports. Closed failure reasons,
+run-local aliases, a chronological diagnostic timeline, and immutable reports support
+diagnosis. A writer-health failure latch prevents sealing after a recorder gap.
+`--watch` reads active scenario/state, sanitized errors, cancellations, retries,
+and lifecycle events. The Test action and
+CI never invoke this live command. Stale-run recovery requires the recorded owner
+to have exited, preserves an immutable abandonment record, and only releases the
+local lease; it performs no remote mutation.
+
+TextEdit editing uses native Select All, Paste, and Save menu-item actions,
+without opening a menu-tracking loop or assuming a US physical keyboard layout.
+The runner verifies the replacement text before Save, observes the close button's
+edited flag clearing, then closes only the bound document. The local menu probe
+and the subsequent real provider edit/upload scenario both passed; the full
+16-scenario cold/warm acceptance remains open.
+Permanent deletion requires absence from active-file, existence, and Trash API
+checks; disappearance from Trash alone could mean restoration.
## Commands
@@ -92,6 +381,23 @@ xcodebuild test \
-destination 'platform=macOS'
```
+Run the isolated Stability unit suite on the same Mac destination:
+
+```sh
+xcodebuild test \
+ -project potassiumProvider.xcodeproj \
+ -scheme potassiumProvider-Stability \
+ -configuration Stability \
+ -destination 'platform=macOS,arch=arm64'
+```
+
+Do not add credentials to either command. Accept a run only when its
+`.xcresult` summary reports `result: Passed`, zero failed tests, and zero
+cancelled tests. The standard scheme includes its UI action; its completion is
+separate from the ordinary and Stability unit-profile acceptance checks. See
+[`STABILITY_LOOP_AUDIT.md`](STABILITY_LOOP_AUDIT.md) for the dated macOS result
+bundle evidence and any current local-host limitation.
+
Use `xcodebuild -showdestinations` to copy the exact Mac destination if local
Xcode requires a more specific macOS variant.
@@ -185,14 +491,16 @@ and visionOS Files:
and can still be permanently deleted.
3. Verify Download Now and Remove Download are system-provided for normal files
and folders.
-4. Create public and password-protected links, update options, copy/share the
- URL, and disable the link. Inspect activity export and unified logs to ensure
- the URL and password never appear.
+4. Create public, inherited-access, and password-protected links. Set and then
+ clear an expiration, copy/share the URL, and disable the link. Inspect
+ activity export and unified logs to ensure the URL, password, access value,
+ and expiration never appear. An unknown returned access value must stop the
+ action instead of being displayed as public.
5. Page a document's version history and restore a version as a collision-safe
copy in its current parent. Confirm the current file is unchanged.
6. Exercise Show in Finder/Files and Sync Now for every configured drive.
-## 0.2.0 Transfer Gates
+## 0.3.0 Transfer Gates
Run these checks on macOS with a development File Provider domain and a test
kDrive account. Do not use customer data.
@@ -207,6 +515,16 @@ kDrive account. Do not use customer data.
below 125% of the single-transfer baseline.
3. Repeat cancellation while the second transfer is waiting. Confirm it never
starts and the next transfer can acquire the released permit.
+4. Confirm a direct create or replacement at exactly `1_000_000_000` bytes is
+ admitted by the pure request preflight, while one byte above is rejected
+ before callback content loading or request construction with the
+ session-required error. Automated coverage uses a sparse oversized file to
+ exercise the pre-buffer boundary without allocating or sending a one-gigabyte
+ payload. Use an official session-capable client for larger files until this
+ provider has a file-backed session path.
+5. With a sanitized mock response, confirm HTTP 408 and 429 map to File Provider
+ `.serverUnreachable`, a numeric Retry-After value is parsed, and invalid or
+ HTTP-date values are discarded without entering diagnostics.
Automated `AsyncOperationLimiter` tests cover the concurrency cap, cancellation
while waiting, and permit release after errors. These manual checks cover the
@@ -223,3 +541,156 @@ known logical names, paths, types, dates, hashes, device names, or plaintext
bytes. Benchmark 100,000 items, 10,000 siblings, and multi-gigabyte files. Safe
migration/rekey design and independent review with all high-severity findings
resolved are required before default enablement.
+
+
+### Mac setup accessibility and concurrent regression tests
+
+The Mac drive management screen uses a grouped Form to expose action controls.
+The Advanced disclosure control is identified by its native disclosure role and
+label, leaving the token/save controls' own identifiers intact. Both the arrow and
+label toggle its expansion state. Drive navigation rows include their spacer in
+the hit area, so a normal centre click opens the destination. UI tests distinguish
+inline empty-state controls from toolbar refresh and accept the combined accessible
+sign-in heading. Synthetic fixtures remain isolated from saved accounts and tokens.
+
+Diagnostic change subscriptions cover appends immediately after registration.
+Their cross-store test waits for the observation under the test's overall deadline,
+without assuming a worker starts within a subsecond sleep. The SQLite initialization
+contention test runs its blocking opener and lock-release timer on dedicated queues;
+this preserves real WAL contention without depending on cooperative executor width.
+Working-set batch tests use explicit suspension gates to verify the four-folder
+limit, complete ordered results, and no cursor/watermark advancement after cancellation
+or HTTP 429. Live deadlines and original sixteen-scenario acceptance are unchanged.
+
+The Mac UI runner launches its sibling app product by URL to avoid another checkout's
+Launch Services registration. All launch and performance tests use synthetic setup
+state, and diagnostic trees are scoped to the app window. When sharing a SwiftPM
+product between the core framework and extensions, retain its host-app product
+dependency: Xcode must embed and sign the generated package framework. A build-only
+or unsigned CI pass cannot establish successful local library validation.
+
+`testLaunchToSetupReadinessPerformance` measures elapsed time from launching the
+synthetic app through opening Setup and observing its enabled Add Account control.
+Termination happens before measurement. It uses `XCTClockMetric`, so every sample
+covers the whole verified interaction, including XCTest automation overhead. It is
+not comparable to the former first-frame/responsive-launch signpost benchmark.
+Two CI runs lost built-in launch signpost samples despite successful app/window
+checks; both failures remain retained. Missing timing samples or failed readiness
+assertions still fail the new benchmark.
+
+Finder permanent-deletion confirmation uses the exact display name obtained from
+the verified selected URL before opening the contextual command. Hidden extensions
+must not be guessed or stripped from paths. Only one newly opened dialog or sheet
+of the owned window may match the complete quoted name with exactly Cancel/Delete
+controls. Different selections, partial names, or distinct matching dialogs fail
+closed. Exact-fixture operator confirmation and provider/domain rebinding remain
+required, and only a `deleteItem` callback plus authoritative absence proves deletion.
+
+Poll-scheduling tests hold fake I/O behind explicit cancellable gates. Start
+notifications establish ordering; queue-count observations are bounded by the
+overall one-minute test deadline rather than a short executor-scheduling assumption.
+CI runs both Mac profiles and retains `.xcresult` bundles for 14 days for every
+destination, including failures. Interrupted/incomplete bundles remain diagnostic
+evidence only and must never be treated as passing results.
+
+Finder context menus use a secondary click on the unique display-name field of the
+verified selection, with fresh geometry contained in the owned window. The driver
+does not require an advertised `AXShowMenu` action for this mouse route: Finder can
+omit that action while still exposing the native contextual menu. Ambiguous fields
+and missing/out-of-window geometry remain hard failures.
+
+Download Now is dispatched by a native click inside the uniquely identified popup
+menu and returns once that popup dismisses. Its transfer completion is observed
+through diagnostics, allowing cancellation while work is active; the command does
+not wait for AX action completion or a later Finder Apple Event. Other contextual
+actions keep their result checks.
+
+Preserve-both, cancellation/progress, working-set refresh and contextual actions
+are independent continuation cases:
+they each prepare a new run-owned fixture and repeat safety preflight even when
+an earlier scenario failed. The failed step stays failed, and the bundle cannot
+be certified as passing. Dependent steps still stop after a prerequisite failure.
+
+Transfer cancellation registers an incremental diagnostic cursor before Download
+Now and checks appended local records every 50 ms. This does not contact the API;
+remote verification retains bounded backoff and Retry-After. A callback must match
+the scenario, item alias, extension build, process, and parent span. Intermediate
+progress excludes 0% and 100%. A completed transfer ends the cancel-control search
+and triggers the larger-fixture retry; it never counts as cancellation. Recovery
+uses another pre-dispatch cursor and requires a new successful fetch.
+
+The cancellation driver also recognizes Finder's active AX progress indicator in
+the exact bound row. It can send one native click to its fresh, confined center
+when no labeled Cancel control exists. A click is only an attempted UI action;
+actual cancellation, exactly one terminal and a subsequent successful fetch remain
+mandatory. Empty/completed/nonfinite progress and ambiguous or unconfined geometry
+are rejected.
+
+Actions extension registration is checked separately during preflight. Finder can
+launch an older Actions copy while the replicated instance uses the correct build.
+Discovery must contain exactly the selected app's Actions extension; missing,
+duplicate or malformed discovery is rejected. See `FILE_PROVIDER_CLEANUP.md` for
+registration-only repair. This does not replace final Actions code-hash evidence.
+
+Finder contextual command selection is confined to the single visible transient
+root menu and its direct commands. Expanded submenus are subordinate to that root;
+application menu-bar commands and ambiguous independent popups are excluded.
+
+Native Finder clicks move before pressing and revalidate the bound target after
+that movement. Ordinary menu/control observations retain a 90-second limit inside
+a transfer scenario; only transfer progress/completion waits use the longer budget.
+
+For Remove Download and Download Now, the driver prefers Finder's named Actions
+toolbar control in its bound window after verifying the single selected item. The
+toolbar omits provider actions and selected-item deletion, so those retain the item
+context menu. An absent toolbar control also retains the confined secondary-click
+path. Both paths must expose one actual popup containing the exact enabled command
+before any action is invoked.
+
+
+Actions panels on macOS can receive an opaque system selection identifier and
+expose their Accessibility tree in the Actions process rather than Finder. The
+production UI resolves the canonical provider identifier and verifies its domain
+before loading action data. Stability binds the panel's run-local alias to that
+canonical identifier and requires the installed Actions executable path/code hash.
+Do not use filenames, raw document IDs, a panel title, or the first window as a
+fallback. Resolution uses one bounded 90-second callback budget and retains no URL
+or raw system error in its messages. Negative resolver/panel-target tests cover
+wrong domains/engines, old code, ambiguous panels, timeout and cancellation.
+The sealed `212e54e` run passed 13 scenarios; cancellation and contextual actions
+failed and deletion was deferred. This is not completed Mac acceptance.
+
+Hosted Actions sheets may have an empty `AXWindows` array and a valid `AXMainWindow`.
+Stability includes that main window only from the attested Actions process and
+still requires the resolved run-local alias, explicitly published on the native
+AppKit root. Discovery deduplicates the same listed/main window. A visible form
+without this identity cannot produce a passing result or authorize cleanup.
+
+A containing-app permission preflight does not establish the Actions extension's
+first-use data-access consent. A live retry encountered that macOS prompt while
+opening its shared diagnostic coordinator file; the operator accepted it. Alias
+discovery now runs off the UI actor with bounded waiting and publishes a cached
+value; SwiftUI layout and AX binding perform no shared-store I/O. Done can dismiss
+a loading panel, while mutation-in-progress still disables it. Consent remains a
+system requirement; a blocked lookup or unbound panel cannot count as a pass.
+
+
+Stability preflight qualifies shared-container provisioning for the installed app,
+replicated extension, and Actions extension. A valid signature is insufficient:
+each embedded profile must authorize the signed explicit application identity and
+existing App Group, and be unexpired. Rejection reports only the affected target
+role; profile contents, developer identifiers and certificates are never exported.
+Both extension targets enable `REGISTER_APP_GROUPS = YES`. `--build` permits
+Xcode's normal automatic provisioning refresh using its saved developer account.
+See [Apple's container authorization guidance](https://developer.apple.com/documentation/xcode/accessing-app-group-containers).
+Repeated data-access prompts require inspecting provisioning before requesting
+another permission grant; do not reset TCC or migrate the app/Keychain group.
+
+
+A frontmost Finder process and valid event-posting permission do not prove that
+Finder receives a mouse click. Native pointer actions now require a system-wide
+AX hit belonging to the exact selected Finder row or popup item. Verification runs
+before moving and again after hover. An obstructed target records environment /
+uiUnavailable with local reason `pointerTargetObstructed`, without reading the
+other app's UI. Its failure screenshot is omitted to exclude unrelated content.
+The failed result remains unaccepted; clear the obstruction before rerunning.
diff --git a/potassiumProvider.xcodeproj/project.pbxproj b/potassiumProvider.xcodeproj/project.pbxproj
index 5387804..30a22de 100644
--- a/potassiumProvider.xcodeproj/project.pbxproj
+++ b/potassiumProvider.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ D00001172FF9000000000017 /* InfomaniakConcurrency in Frameworks */ = {isa = PBXBuildFile; productRef = D00001162FF9000000000016 /* InfomaniakConcurrency */; };
+ D00001152FF9000000000015 /* InfomaniakConcurrency in Frameworks */ = {isa = PBXBuildFile; productRef = D00001142FF9000000000014 /* InfomaniakConcurrency */; };
C09842BF2FF8491700CB8B7E /* PotassiumChannelCore in Frameworks */ = {isa = PBXBuildFile; productRef = C09842BE2FF8491700CB8B7E /* PotassiumChannelCore */; };
C09842C12FF8491700CB8B7E /* PotassiumKDrive in Frameworks */ = {isa = PBXBuildFile; productRef = C09842C02FF8491700CB8B7E /* PotassiumKDrive */; };
C09842C32FF8491700CB8B7E /* PotassiumOAuth in Frameworks */ = {isa = PBXBuildFile; productRef = C09842C22FF8491700CB8B7E /* PotassiumOAuth */; };
@@ -157,6 +159,7 @@
buildActionMask = 2147483647;
files = (
D00000012FF9000000000001 /* PotassiumProviderCore.framework in Frameworks */,
+ D00001172FF9000000000017 /* InfomaniakConcurrency in Frameworks */,
C09842BF2FF8491700CB8B7E /* PotassiumChannelCore in Frameworks */,
C09842C32FF8491700CB8B7E /* PotassiumOAuth in Frameworks */,
C09842C12FF8491700CB8B7E /* PotassiumKDrive in Frameworks */,
@@ -182,6 +185,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ D00001152FF9000000000015 /* InfomaniakConcurrency in Frameworks */,
D00000072FF9000000000007 /* SQLite in Frameworks */,
D00000042FF9000000000004 /* PotassiumChannelCore in Frameworks */,
D00000052FF9000000000005 /* PotassiumKDrive in Frameworks */,
@@ -270,6 +274,7 @@
);
name = potassiumProvider;
packageProductDependencies = (
+ D00001162FF9000000000016 /* InfomaniakConcurrency */,
C09842BE2FF8491700CB8B7E /* PotassiumChannelCore */,
C09842C02FF8491700CB8B7E /* PotassiumKDrive */,
C09842C22FF8491700CB8B7E /* PotassiumOAuth */,
@@ -343,6 +348,7 @@
);
name = PotassiumProviderCore;
packageProductDependencies = (
+ D00001142FF9000000000014 /* InfomaniakConcurrency */,
D00001042FF9000000000004 /* SQLite */,
D00001012FF9000000000001 /* PotassiumChannelCore */,
D00001022FF9000000000002 /* PotassiumKDrive */,
@@ -1038,6 +1044,7 @@
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.FileProvider;
+ REGISTER_APP_GROUPS = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
@@ -1078,6 +1085,7 @@
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.FileProvider;
+ REGISTER_APP_GROUPS = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
@@ -1117,6 +1125,7 @@
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.Actions;
+ REGISTER_APP_GROUPS = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
@@ -1157,6 +1166,7 @@
MACOSX_DEPLOYMENT_TARGET = 26.5;
MARKETING_VERSION = 0.4.0;
PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.Actions;
+ REGISTER_APP_GROUPS = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
@@ -1174,6 +1184,296 @@
};
name = Release;
};
+ F30000012FFD000000000001 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "STABILITY=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG STABILITY $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Stability;
+ };
+ F30000022FFD000000000002 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = Config/potassiumProvider.entitlements;
+ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Config/potassiumProviderStability.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ ENABLE_APP_SANDBOX = YES;
+ "ENABLE_APP_SANDBOX[sdk=macosx*]" = NO;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ ENABLE_USER_SELECTED_FILES = readwrite;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = Config/potassiumProviderInfo.plist;
+ "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
+ "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
+ "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
+ "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
+ "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
+ "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
+ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ REGISTER_APP_GROUPS = YES;
+ SDKROOT = auto;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
+ F30000032FFD000000000003 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProviderTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = auto;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/potassiumProvider.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/potassiumProvider";
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
+ F30000042FFD000000000004 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProviderUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = auto;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ TEST_TARGET_NAME = potassiumProvider;
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
+ F30000052FFD000000000005 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES;
+ BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = (
+ "@executable_path/../Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.PotassiumProviderCore;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SDKROOT = auto;
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_INSTALL_MODULE = YES;
+ SWIFT_INSTALL_OBJC_HEADER = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ VERSIONING_SYSTEM = "apple-generic";
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
+ F30000062FFD000000000006 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CODE_SIGN_ENTITLEMENTS = Config/potassiumProviderFileProvider.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ ENABLE_APP_SANDBOX = YES;
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = Config/potassiumProviderFileProviderInfo.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.FileProvider;
+ REGISTER_APP_GROUPS = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = auto;
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
+ F30000072FFD000000000007 /* Stability */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CODE_SIGN_ENTITLEMENTS = Config/potassiumProviderActions.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 5;
+ DEVELOPMENT_TEAM = 2LST6WT4P6;
+ ENABLE_APP_SANDBOX = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ INFOPLIST_FILE = Config/potassiumProviderActionsInfo.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.5;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 26.5;
+ MARKETING_VERSION = 0.4.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.weavee.potassiumProvider.Actions;
+ REGISTER_APP_GROUPS = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = auto;
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ XROS_DEPLOYMENT_TARGET = 26.5;
+ };
+ name = Stability;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -1181,6 +1481,7 @@
isa = XCConfigurationList;
buildConfigurations = (
C09842A52FF847B000CB8B7E /* Debug */,
+ F30000012FFD000000000001 /* Stability */,
C09842A62FF847B000CB8B7E /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1190,6 +1491,7 @@
isa = XCConfigurationList;
buildConfigurations = (
C09842A82FF847B000CB8B7E /* Debug */,
+ F30000022FFD000000000002 /* Stability */,
C09842A92FF847B000CB8B7E /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1199,6 +1501,7 @@
isa = XCConfigurationList;
buildConfigurations = (
C09842AB2FF847B000CB8B7E /* Debug */,
+ F30000032FFD000000000003 /* Stability */,
C09842AC2FF847B000CB8B7E /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1208,6 +1511,7 @@
isa = XCConfigurationList;
buildConfigurations = (
C09842AE2FF847B000CB8B7E /* Debug */,
+ F30000042FFD000000000004 /* Stability */,
C09842AF2FF847B000CB8B7E /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1217,6 +1521,7 @@
isa = XCConfigurationList;
buildConfigurations = (
D00000812FF9000000000001 /* Debug */,
+ F30000052FFD000000000005 /* Stability */,
D00000822FF9000000000002 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1226,6 +1531,7 @@
isa = XCConfigurationList;
buildConfigurations = (
D10000812FFB000000000001 /* Debug */,
+ F30000062FFD000000000006 /* Stability */,
D10000822FFB000000000002 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1235,6 +1541,7 @@
isa = XCConfigurationList;
buildConfigurations = (
E20000812FFC000000000001 /* Debug */,
+ F30000072FFD000000000007 /* Stability */,
E20000822FFC000000000002 /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -1270,6 +1577,16 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ D00001162FF9000000000016 /* InfomaniakConcurrency */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = C0DCD8ED2FFA44AB00520215 /* XCRemoteSwiftPackageReference "swift-concurrency" */;
+ productName = InfomaniakConcurrency;
+ };
+ D00001142FF9000000000014 /* InfomaniakConcurrency */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = C0DCD8ED2FFA44AB00520215 /* XCRemoteSwiftPackageReference "swift-concurrency" */;
+ productName = InfomaniakConcurrency;
+ };
C09842BE2FF8491700CB8B7E /* PotassiumChannelCore */ = {
isa = XCSwiftPackageProductDependency;
package = C09842BD2FF8491700CB8B7E /* XCRemoteSwiftPackageReference "potassiumChannel" */;
diff --git a/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider-Stability.xcscheme b/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider-Stability.xcscheme
new file mode 100644
index 0000000..bec0733
--- /dev/null
+++ b/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider-Stability.xcscheme
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider.xcscheme b/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider.xcscheme
index b253dfb..e1b0ea3 100644
--- a/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider.xcscheme
+++ b/potassiumProvider.xcodeproj/xcshareddata/xcschemes/potassiumProvider.xcscheme
@@ -53,6 +53,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/potassiumProvider/ConflictLogView.swift b/potassiumProvider/ConflictLogView.swift
index 727ab82..39cfa61 100644
--- a/potassiumProvider/ConflictLogView.swift
+++ b/potassiumProvider/ConflictLogView.swift
@@ -17,7 +17,7 @@ struct ConflictLogView: View {
actionDependencies: ProviderActivityActionDependencies? = nil
) {
let viewModel = ConflictLogViewModel(eventStore: eventStore)
- #if DEBUG
+ #if DEBUG && !STABILITY
viewModel.actionErrorMessage = ProviderUITestFixture.initialActivityActionError()
self.actionDependencies = actionDependencies
?? ProviderUITestFixture.activityActionDependencies()
diff --git a/potassiumProvider/ContentView.swift b/potassiumProvider/ContentView.swift
index 703832c..dee0703 100644
--- a/potassiumProvider/ContentView.swift
+++ b/potassiumProvider/ContentView.swift
@@ -36,6 +36,15 @@ struct ContentView: View {
Label("Activities", systemImage: "clock.arrow.circlepath")
}
.tag(ProviderAppTab.activities)
+
+ #if os(macOS) && STABILITY
+ StabilityLabView(model: model)
+ .providerNavigationAnimation()
+ .tabItem {
+ Label("Stability Lab", systemImage: "testtube.2")
+ }
+ .tag(ProviderAppTab.stabilityLab)
+ #endif
}
.onAppear {
selectedTab = ProviderAppTabSelectionPolicy.defaultSelection(
@@ -49,6 +58,9 @@ enum ProviderAppTab: Hashable {
case status
case setup
case activities
+ #if os(macOS) && STABILITY
+ case stabilityLab
+ #endif
}
enum ProviderAppTabSelectionPolicy {
diff --git a/potassiumProvider/FileProviderDomainRegistrar.swift b/potassiumProvider/FileProviderDomainRegistrar.swift
index c6d2ff7..1b5bd24 100644
--- a/potassiumProvider/FileProviderDomainRegistrar.swift
+++ b/potassiumProvider/FileProviderDomainRegistrar.swift
@@ -7,6 +7,7 @@ import PotassiumProviderCore
protocol ProviderDomainRegistering {
func addDomain(for configuration: ProviderDomainConfiguration) async throws
func removeDomain(for configuration: ProviderDomainConfiguration) async throws
+ func registeredDomainIdentifiers() async throws -> Set
func knownFolderSyncStates() async throws -> [String: ProviderKnownFolderSyncState]
func claimKnownFolders(for configuration: ProviderDomainConfiguration, parentFileID: Int) async throws
func claimKnownFolders(
@@ -75,6 +76,10 @@ struct KnownFolderPreflight: Equatable, Sendable {
}
extension ProviderDomainRegistering {
+ func registeredDomainIdentifiers() async throws -> Set {
+ []
+ }
+
func knownFolderSyncStates() async throws -> [String: ProviderKnownFolderSyncState] {
[:]
}
@@ -145,6 +150,10 @@ struct FileProviderDomainRegistrar: ProviderDomainRegistering {
}
}
+ func registeredDomainIdentifiers() async throws -> Set {
+ Set(try await registeredDomains().map { $0.identifier.rawValue })
+ }
+
func knownFolderSyncStates() async throws -> [String: ProviderKnownFolderSyncState] {
#if os(macOS)
let domains = try await registeredDomains()
@@ -230,7 +239,13 @@ struct FileProviderDomainRegistrar: ProviderDomainRegistering {
func userVisibleRootURL(for configuration: ProviderDomainConfiguration) async throws -> URL {
let manager = try await manager(for: configuration)
- return try await manager.getUserVisibleURL(for: .rootContainer)
+ return try await StabilityCallbackWaiter().wait { completion in
+ manager.getUserVisibleURL(for: .rootContainer) { url, error in
+ if let error { completion(.failure(error)) }
+ else if let url { completion(.success(url)) }
+ else { completion(.failure(StabilityDeadlineError.expired)) }
+ }
+ }
}
func signalWorkingSet(for configuration: ProviderDomainConfiguration) async throws {
diff --git a/potassiumProvider/FileProviderUninstallCommand.swift b/potassiumProvider/FileProviderUninstallCommand.swift
index 2d03221..090afeb 100644
--- a/potassiumProvider/FileProviderUninstallCommand.swift
+++ b/potassiumProvider/FileProviderUninstallCommand.swift
@@ -345,15 +345,21 @@ private struct AppGroupFileProviderUninstallLocalState: FileProviderUninstallLoc
try FileManager.default.removeItem(at: configurationURL)
}
- guard FileManager.default.fileExists(atPath: snapshotsDatabaseURL.path) else {
- return
+ if FileManager.default.fileExists(atPath: snapshotsDatabaseURL.path) {
+ let snapshotStore = try KDriveSnapshotSQLiteStore(databaseURL: snapshotsDatabaseURL)
+ try await snapshotStore.removeSnapshots(domainIdentifier: domainIdentifier)
}
- let snapshotStore = try KDriveSnapshotSQLiteStore(databaseURL: snapshotsDatabaseURL)
- try await snapshotStore.removeSnapshots(domainIdentifier: domainIdentifier)
-
- let eventStore = try KDriveProviderEventSQLiteStore(databaseURL: snapshotsDatabaseURL)
- try await eventStore.removeEvents(domainIdentifier: domainIdentifier)
+ if ProviderRuntimeProfile.current == .stability
+ || FileManager.default.fileExists(atPath: snapshotsDatabaseURL.path)
+ {
+ let eventStore = try ProviderEventStoreFactory.make(
+ profile: .current,
+ standardDatabaseURL: snapshotsDatabaseURL,
+ stabilityRootDirectoryURL: containerURL.appendingPathComponent("StabilityRuns", isDirectory: true)
+ )
+ try await eventStore?.removeEvents(domainIdentifier: domainIdentifier)
+ }
}
func removeAccountRecords(accountIdentifiers: [String]) throws {
diff --git a/potassiumProvider/FinderActionConfirmation.swift b/potassiumProvider/FinderActionConfirmation.swift
new file mode 100644
index 0000000..dcd68a9
--- /dev/null
+++ b/potassiumProvider/FinderActionConfirmation.swift
@@ -0,0 +1,54 @@
+#if os(macOS) && STABILITY
+struct FinderActionConfirmationObservation {
+ let texts: [String]
+ let enabledButtonTitles: [String]
+}
+
+/// Matches only a leaf confirmation sheet inside the already bound action panel.
+enum FinderActionConfirmation: CaseIterable {
+ case disableShareLink, restoreVersion
+
+ var buttonTitle: String {
+ switch self {
+ case .disableShareLink: "Disable Link"
+ case .restoreVersion: "Restore as Copy"
+ }
+ }
+
+ var prompt: String {
+ switch self {
+ case .disableShareLink: "Disable this share link?"
+ case .restoreVersion: "Restore this version as a new copy?"
+ }
+ }
+
+ var message: String {
+ switch self {
+ case .disableShareLink: "Anyone using the current URL will lose access."
+ case .restoreVersion: "The current file will not be overwritten."
+ }
+ }
+
+ func uniqueMatchIndex(in dialogs: [FinderActionConfirmationObservation]) -> Int? {
+ let matches = dialogs.indices.filter { index in
+ let dialog = dialogs[index]
+ let text = dialog.texts.joined(separator: " ")
+ return dialog.enabledButtonTitles.sorted() == ["Cancel", buttonTitle].sorted() &&
+ text.contains(prompt) && text.contains(message)
+ }
+ return matches.count == 1 ? matches.first : nil
+ }
+
+ func hasSuccessfulResult(in texts: [String]) -> Bool {
+ switch self {
+ case .disableShareLink:
+ texts.contains("Disabled share link.")
+ case .restoreVersion:
+ texts.contains {
+ $0.hasPrefix("Restored ") && $0.hasSuffix(" as a new copy.") &&
+ $0.count > "Restored as a new copy.".count
+ }
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderActionPanelTarget.swift b/potassiumProvider/FinderActionPanelTarget.swift
new file mode 100644
index 0000000..9d5345f
--- /dev/null
+++ b/potassiumProvider/FinderActionPanelTarget.swift
@@ -0,0 +1,48 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+/// The Actions sheet can have its own AX process despite being hosted in Finder.
+/// Neither a matching bundle identifier nor a window title alone binds the panel.
+enum FinderActionPanelTarget {
+ static func isExpectedProcess(executableURL: URL?, expectedURL: URL, codeHash: String?, expectedCodeHash: String?) -> Bool {
+ guard let executableURL, let codeHash, let expectedCodeHash else { return false }
+ return executableURL.standardizedFileURL.resolvingSymlinksInPath() == expectedURL.standardizedFileURL.resolvingSymlinksInPath()
+ && codeHash == expectedCodeHash
+ }
+
+ /// FileProviderUI can omit a hosted sheet from AXWindows while exposing it
+ /// as AXMainWindow. Discovery does not authorize it; the exact alias still must match.
+ static func candidates(listed: [Element], main: Element?, equal: (Element, Element) -> Bool) -> [Element] {
+ var result = listed
+ if let main, !result.contains(where: { equal($0, main) }) { result.append(main) }
+ return result
+ }
+
+ static func index(alias: String, windowIdentifiers: [[String]]) -> Int? {
+ guard alias.hasPrefix("provider.stability.action."),
+ UUID(uuidString: String(alias.dropFirst("provider.stability.action.".count))) != nil else { return nil }
+ let matches = windowIdentifiers.indices.filter { windowIdentifiers[$0].contains(alias) }
+ return matches.count == 1 ? matches.first : nil
+ }
+
+ /// A remote view can be reached from both Finder's host window and the
+ /// extension's AXMainWindow. Deduplicate the actual panel, not its hosts.
+ /// Distinct panels bearing the same alias remain ambiguous. The first
+ /// candidate must be the owned Finder host; a detached AXMainWindow can
+ /// retain its alias after dismissal and cannot authorize a panel alone.
+ static func windowIndex(alias: String, panels: [[Element]],
+ identifier: (Element) -> String?, equal: (Element, Element) -> Bool) -> Int? {
+ guard index(alias: alias, windowIdentifiers: [[alias]]) != nil,
+ panels.first?.contains(where: { identifier($0) == alias }) == true else { return nil }
+ var unique: [Element] = []
+ var result: Int?
+ for (index, roots) in panels.enumerated() {
+ for root in roots where identifier(root) == alias {
+ if !unique.contains(where: { equal($0, root) }) { unique.append(root) }
+ result = index
+ }
+ }
+ return unique.count == 1 ? result : nil
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderContextMenuTarget.swift b/potassiumProvider/FinderContextMenuTarget.swift
new file mode 100644
index 0000000..61dfcbb
--- /dev/null
+++ b/potassiumProvider/FinderContextMenuTarget.swift
@@ -0,0 +1,21 @@
+#if os(macOS) && STABILITY
+import Foundation
+import CoreGraphics
+
+enum FinderContextMenuTarget {
+ struct Field {
+ let name: String?
+ let bounds: CGRect?
+ }
+
+ /// Fields must come only from the independently verified selected row.
+ /// Advertised AX actions do not govern WindowServer secondary-click routing.
+ static func point(displayedName: String, windowBounds: CGRect, fields: [Field]) -> CGPoint? {
+ guard !displayedName.isEmpty else { return nil }
+ let matches = fields.filter { $0.name == displayedName }
+ guard matches.count == 1, let bounds = matches.first?.bounds,
+ bounds.width > 1, bounds.height > 1, windowBounds.contains(bounds) else { return nil }
+ return CGPoint(x: bounds.midX, y: bounds.midY)
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderCopyCancellationTarget.swift b/potassiumProvider/FinderCopyCancellationTarget.swift
new file mode 100644
index 0000000..646dd5e
--- /dev/null
+++ b/potassiumProvider/FinderCopyCancellationTarget.swift
@@ -0,0 +1,14 @@
+#if os(macOS) && STABILITY
+enum FinderCopyCancellationState { case waiting, cancellable, finished }
+
+enum FinderCopyCancellationTarget {
+ /// Finder's copy preparation shows both quoted names while it hydrates the
+ /// source. A generic progress window or a shared name alone is insufficient.
+ static func matches(sourceName: String, destinationName: String, labels: [String]) -> Bool {
+ guard !sourceName.isEmpty, !destinationName.isEmpty,
+ sourceName != destinationName else { return false }
+ return labels.contains { $0.contains("“" + sourceName + "”") } &&
+ labels.contains { $0.contains("“" + destinationName + "”") }
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderDeletionDialog.swift b/potassiumProvider/FinderDeletionDialog.swift
new file mode 100644
index 0000000..6ed55f4
--- /dev/null
+++ b/potassiumProvider/FinderDeletionDialog.swift
@@ -0,0 +1,31 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+
+struct FinderDeletionDialogObservation {
+ let texts: [String]
+ let buttonTitles: [String]
+}
+
+/// Display text identifies a dialog only after the caller has bound the exact
+/// provider item and selected URL. Never derive display names by stripping an
+/// extension: Finder can hide it or give a trashed item a different local name.
+struct FinderDeletionDialogExpectation {
+ let selectedURL: URL
+ let displayName: String
+
+ func uniqueMatchIndex(selection: URL, dialogs: [FinderDeletionDialogObservation]) -> Int? {
+ guard !displayName.isEmpty, FinderUIURLIdentity.matches(selection, selectedURL) else { return nil }
+ let prompt = "Are you sure you want to delete “\(displayName)”?"
+ let matches = dialogs.indices.filter { index in
+ let dialog = dialogs[index]
+ guard dialog.buttonTitles.sorted() == ["Cancel", "Delete"] else { return false }
+ // Require the entire quoted name, not a substring of another item.
+ return dialog.texts.contains {
+ $0 == prompt || $0.hasPrefix(prompt + "\n") || $0.hasPrefix(prompt + " ")
+ }
+ }
+ return matches.count == 1 ? matches.first : nil
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderHydrationSequence.swift b/potassiumProvider/FinderHydrationSequence.swift
new file mode 100644
index 0000000..a0b0e48
--- /dev/null
+++ b/potassiumProvider/FinderHydrationSequence.swift
@@ -0,0 +1,15 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+@MainActor
+enum FinderHydrationSequence {
+ /// Finish inspecting the generated document before releasing its presenter.
+ /// The following Finder eviction must not inherit an open TextEdit document.
+ static func execute(using ui: any FinderDocumentUIDriving, url: URL,
+ verifyDownloadedBytes: () async throws -> Void) async throws {
+ try await ui.edit(url, contents: nil)
+ try await verifyDownloadedBytes()
+ try await ui.closeOwnedEditorDocuments()
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderLiveAdvancedScenarios.swift b/potassiumProvider/FinderLiveAdvancedScenarios.swift
new file mode 100644
index 0000000..30eb0d6
--- /dev/null
+++ b/potassiumProvider/FinderLiveAdvancedScenarios.swift
@@ -0,0 +1,272 @@
+#if os(macOS) && STABILITY
+import AppKit
+import FileProvider
+import PotassiumProviderCore
+
+extension FinderLiveRunSession {
+ func waitRestoredLocation(_ item: KDriveRemoteItem) async throws -> URL {
+ try await waitVisibleDestination(item)
+ }
+
+ func waitVisibleDestination(_ item: KDriveRemoteItem) async throws -> URL {
+ let parent = try require(owned[item.parentID])
+ let parentURL = try await visible(parent)
+ // Open the verified destination before waiting for its child. Waiting
+ // in the old folder can leave the destination unenumerated in Finder.
+ try await ui.navigate(to: parentURL)
+ var result: URL?
+ var lastObservation: String?
+ try await poll {
+ result = try await FinderRestoreObservation.observeVisibleDestination(name: item.name) {
+ let candidate = try await self.visible(item)
+ return candidate
+ } matchesParent: { candidateParent in
+ let binding = try await StabilityCallbackWaiter<(String, String)>().wait(timeout: self.deadline.remaining()) { completion in
+ NSFileProviderManager.getIdentifierForUserVisibleFile(at: candidateParent) { identifier, domain, error in
+ if let error { completion(.failure(error)) }
+ else if let identifier, let domain { completion(.success((identifier.rawValue, domain.rawValue))) }
+ else { completion(.failure(FinderLiveError.unsafeTarget)) }
+ }
+ }
+ let parentMatches = FinderStabilityTargetBinding.matches(expectedFileID: parent.id,
+ expectedDomainIdentifier: self.context.domain.domainIdentifier,
+ actualItemIdentifier: binding.0, actualDomainIdentifier: binding.1)
+ let pathMatches = FinderUIURLIdentity.matches(candidateParent, parentURL)
+ let observation = "parentIdentityMatches=\(parentMatches) parentPathMatches=\(pathMatches)"
+ if lastObservation != observation {
+ print("finder stability: local destination observation; " + observation)
+ lastObservation = observation
+ }
+ return parentMatches
+ }
+ return result != nil
+ }
+ return try require(result)
+ }
+
+ func waitRestored(_ item: KDriveRemoteItem) async throws -> KDriveRemoteItem {
+ let alias = StabilityDiagnosticIdentity.alias(for: String(item.id), runID: run.runID)
+ var restored: KDriveRemoteItem?
+ try await poll {
+ let callbacks = try self.diagnostics().filter {
+ $0.source == .fileProviderExtension && $0.operation == .restoreTrashedItem &&
+ $0.parentSpanID == nil && $0.subjectAlias == alias && $0.correlationID == self.correlationID
+ }
+ guard callbacks.allSatisfy({ $0.processCodeHash == self.expectedExtensionCodeHash && $0.processInstanceID != nil }) else {
+ throw StabilityLiveEvidenceError.wrongExtensionBuild
+ }
+ guard !callbacks.contains(where: { [.failed, .cancelled].contains($0.phase) }) else {
+ throw StabilityLiveEvidenceError.unexpectedFailure
+ }
+ restored = try await FinderRestoreObservation.observe(
+ callbackCompleted: callbacks.contains { $0.phase == .completed }, expected: item) {
+ try await self.context.remote.item(driveID: self.context.domain.driveID, fileID: item.id)
+ }
+ return restored != nil
+ }
+ let result = try require(restored)
+ remember(result)
+ return result
+ }
+
+ func waitTrashed(_ item: KDriveRemoteItem, exists: Bool) async throws {
+ guard let actions = context.remote as? any KDriveContextActionProviding else { throw FinderLiveError.missingCapability }
+ try await poll {
+ do {
+ let trashed = try await actions.trashedItem(driveID: self.context.domain.driveID, fileID: item.id)
+ return exists && trashed.id == item.id
+ } catch where KDriveRemoteErrorClassifier.isNotFound(error) { return !exists }
+ }
+ }
+
+ func waitDeleted(_ item: KDriveRemoteItem) async throws {
+ guard let actions = context.remote as? any KDriveContextActionProviding else { throw FinderLiveError.missingCapability }
+ try await poll {
+ // Absence from Trash alone can also mean that an item was restored.
+ // Require independent absence from the active-item and existence APIs.
+ guard try await actions.existingFileIDs(driveID: self.context.domain.driveID, fileIDs: [item.id]).isEmpty else { return false }
+ do {
+ _ = try await self.context.remote.item(driveID: self.context.domain.driveID, fileID: item.id)
+ return false
+ } catch where KDriveRemoteErrorClassifier.isNotFound(error) {}
+ do {
+ _ = try await actions.trashedItem(driveID: self.context.domain.driveID, fileID: item.id)
+ return false
+ } catch where KDriveRemoteErrorClassifier.isNotFound(error) { return true }
+ }
+ }
+
+ func preserveBoth() async throws {
+ // Share the independently selectable race's cancellable scheduling,
+ // exact version/identity checks, and reopening of both byte streams.
+ try await executeConflict(.contentAfterPreflight)
+ }
+
+ func cancelTransfer() async throws {
+ for mebibytes in [64, 256] {
+ let data = Data(repeating: 0x5A, count: mebibytes * 1_024 * 1_024)
+ // Use generic data rather than a MacBinary archive type, so an
+ // archive preview is not an additional consumer of the transfer.
+ let item = try await upload(name: "transfer-\(mebibytes).dat", parent: require(root), data: data)
+ transfer = item
+ try await signal(NSFileProviderItemIdentifier(String(item.parentID)))
+ let url = try await visible(item)
+ let subject = StabilityDiagnosticIdentity.alias(for: String(item.id), runID: run.runID)
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ let attempt = FinderTransferObservation(subject: subject, correlation: correlationID, codeHash: expectedExtensionCodeHash)
+ try await ui.contextAction("Download Now", on: url)
+ try await waitForLocalObservation {
+ try attempt.ingest(tail.readAvailable())
+ return attempt.hasIntermediateProgress || attempt.downloadFinished
+ }
+ print("finder stability transfer: intermediate progress \(attempt.hasIntermediateProgress); already completed \(attempt.downloadFinished)")
+ guard !attempt.downloadFinished else { continue }
+ let invoked = try await ui.cancelDownload(url) {
+ try attempt.ingest(tail.readAvailable())
+ return attempt.canCancel
+ }
+ guard invoked else { continue }
+ try await waitForLocalObservation {
+ try attempt.ingest(tail.readAvailable())
+ return attempt.fetchCancelled || attempt.fetchCompleted
+ }
+ guard attempt.fetchCancelled else { continue }
+ cancellationObserved = true
+ // A new cursor prevents the uncancelled first-size attempt or the
+ // cancelled callback from satisfying this recovery download.
+ let recoveryTail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ let recovery = FinderTransferObservation(subject: subject, correlation: correlationID, codeHash: expectedExtensionCodeHash)
+ try await ui.contextAction("Download Now", on: url)
+ try await waitForLocalObservation {
+ try recovery.ingest(recoveryTail.readAvailable())
+ return recovery.fetchCompleted
+ }
+ guard try Data(contentsOf: url) == data else { throw FinderLiveError.assertionFailed }
+ try await waitBytes(item, expected: data)
+ return
+ }
+ // Some Finder versions expose the download pie only as status. Copying
+ // an evicted item to a new local folder exposes Finder's actual Stop
+ // control while the system hydrates it. This still must cancel the real
+ // provider fetch and permit a byte-exact recovery download.
+ try await cancelCopyTransfer()
+ }
+
+ private func cancelCopyTransfer() async throws {
+ let data = Data(repeating: 0x5A, count: 256 * 1_024 * 1_024)
+ let item = try await upload(name: "transfer-copy-256.dat", parent: require(root), data: data)
+ transfer = item
+ try await signal(NSFileProviderItemIdentifier(String(item.parentID)))
+ let url = try await visible(item)
+ let probe = FileManager.default.temporaryDirectory
+ .appendingPathComponent("finder-copy-cancellation-" + UUID().uuidString, isDirectory: true)
+ try FileManager.default.createDirectory(at: probe, withIntermediateDirectories: false)
+ let subject = StabilityDiagnosticIdentity.alias(for: String(item.id), runID: run.runID)
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ let attempt = FinderTransferObservation(subject: subject, correlation: correlationID, codeHash: expectedExtensionCodeHash)
+ let invoked = try await ui.copyAndCancelDownload(url, to: probe) {
+ try attempt.ingest(tail.readAvailable())
+ if attempt.fetchCancelled || attempt.fetchCompleted { return .finished }
+ return attempt.canCancel ? .cancellable : .waiting
+ }
+ guard invoked else { throw FinderLiveError.cancellationNotExercised }
+ try await waitForLocalObservation {
+ try attempt.ingest(tail.readAvailable())
+ return attempt.fetchCancelled || attempt.fetchCompleted
+ }
+ guard attempt.fetchCancelled else { throw FinderLiveError.cancellationNotExercised }
+ cancellationObserved = true
+ let recoveryTail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ let recovery = FinderTransferObservation(subject: subject, correlation: correlationID, codeHash: expectedExtensionCodeHash)
+ try await ui.contextAction("Download Now", on: url)
+ try await waitForLocalObservation {
+ try recovery.ingest(recoveryTail.readAvailable())
+ return recovery.fetchCompleted
+ }
+ guard try Data(contentsOf: url) == data else { throw FinderLiveError.assertionFailed }
+ try await waitBytes(item, expected: data)
+ // Failure retains the probe. Remove only our own temporary copy after
+ // Stop dismissed its window and the independent recovery was verified.
+ try FileManager.default.removeItem(at: probe)
+ }
+
+ func contextActions() async throws {
+ guard let actions = context.remote as? any KDriveContextActionProviding else { throw FinderLiveError.missingCapability }
+ let old = Data("actions original version\n".utf8), current = Data("actions current version\n".utf8)
+ let parent = try require(root)
+ let item = try await upload(name: "actions.txt", parent: parent, data: old)
+ try await signal(NSFileProviderItemIdentifier(String(parent.id)))
+ let url = try await visible(item)
+ try await ui.edit(url, contents: String(decoding: current, as: UTF8.self))
+ try await waitBytes(item, expected: current)
+ try await bind(url, item: item)
+ try await ui.contextAction("Add to kDrive Favorites", on: url)
+ _ = try await waitMetadata(item) { $0.isFavorite == true }
+ try await bind(url, item: item)
+ try await ui.contextAction("Remove from kDrive Favorites", on: url)
+ _ = try await waitMetadata(item) { $0.isFavorite == false }
+ let beforeCopy = Set(try await list(parent).map(\.id))
+ try await bind(url, item: item)
+ try await ui.contextAction("Duplicate on kDrive", on: url)
+ var duplicate: KDriveRemoteItem?
+ try await poll { duplicate = try await self.list(parent).first { !beforeCopy.contains($0.id) && !$0.isDirectory }; return duplicate != nil }
+ try await waitBytes(require(duplicate), expected: current)
+ remember(try require(duplicate))
+ try await bind(url, item: item)
+ try await ui.contextAction("Share kDrive Link…", on: url)
+ try await ui.panelAction(.inheritAccess)
+ try await bind(url, item: item)
+ try await ui.panelAction(.createLink)
+ try await poll { try await actions.shareLink(driveID: self.context.domain.driveID, fileID: item.id)?.configuration.access == .inherit }
+ try await ui.panelAction(.toggleComments)
+ try await bind(url, item: item)
+ var unappliedShareSettings = false
+ do {
+ try await ui.panelAction(.saveLink)
+ try await poll { try await actions.shareLink(driveID: self.context.domain.driveID, fileID: item.id)?.configuration.allowsComments == true }
+ } catch KDriveContextActionError.shareLinkSettingsNotApplied {
+ // Keep this case failed, but verify the independent disable/recovery
+ // operations on this generated fixture before returning the failure.
+ unappliedShareSettings = true
+ print("finder stability UI: share mismatch retained; checking remaining contextual actions")
+ }
+ try await ui.panelAction(.disableLink)
+ try await bind(url, item: item)
+ try await ui.panelAction(.confirmDisableLink)
+ try await poll { try await actions.shareLink(driveID: self.context.domain.driveID, fileID: item.id) == nil }
+ try await ui.panelAction(.done)
+ let versions = try await actions.fileVersions(driveID: context.domain.driveID, fileID: item.id, page: 1, pageSize: 50)
+ let historical = versions.versions.filter { $0.size == old.count }
+ guard !versions.hasMore, historical.count == 1, let version = historical.first else { throw FinderLiveError.missingCapability }
+ let beforeRestore = Set(try await list(parent).map(\.id))
+ try await bind(url, item: item)
+ try await ui.contextAction("Version History…", on: url)
+ try await ui.panelAction(.restoreVersion(version.id))
+ try await bind(url, item: item)
+ try await ui.panelAction(.confirmRestore)
+ var restored: KDriveRemoteItem?
+ try await poll { restored = try await self.list(parent).first { !beforeRestore.contains($0.id) && !$0.isDirectory }; return restored != nil }
+ let restoredItem = try require(restored)
+ try await waitBytes(restoredItem, expected: old)
+ try await waitBytes(item, expected: current)
+ remember(restoredItem)
+ try await ui.panelAction(.done)
+ try await ui.select(visible(restoredItem))
+ print("finder stability UI: link disabled and historical copy verified; current bytes preserved")
+ if unappliedShareSettings { throw KDriveContextActionError.shareLinkSettingsNotApplied }
+ }
+
+ /// Allow every run-related started callback to reach a terminal before sealing.
+ /// An interrupted transfer never becomes an apparently complete evidence bundle.
+ func drain() async throws {
+ var settlement = StabilityDiagnosticSettlement()
+ while deadline.remaining() > .zero {
+ if settlement.observe(try diagnostics()) { return }
+ // This reads local telemetry only. The server backoff can reach ten
+ // seconds, longer than the idle extension's teardown interval.
+ try await Task.sleep(for: min(.milliseconds(500), deadline.remaining()))
+ }
+ throw StabilityDeadlineError.expired
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderLiveConflictScenarios.swift b/potassiumProvider/FinderLiveConflictScenarios.swift
new file mode 100644
index 0000000..71cdf3e
--- /dev/null
+++ b/potassiumProvider/FinderLiveConflictScenarios.swift
@@ -0,0 +1,126 @@
+#if os(macOS) && STABILITY
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+
+extension FinderLiveRunSession {
+ func executeConflict(_ conflict: StabilityLiveConflictCase) async throws {
+ let parent = try require(root)
+ let baseBytes = Data("conflict base\n".utf8)
+ let localBytes = Data("conflict local\n".utf8)
+ let remoteBytes = Data("conflict remote\n".utf8)
+ let original = try await upload(name: "conflict.txt", parent: parent, data: baseBytes)
+ try await signal(NSFileProviderItemIdentifier(String(parent.id)))
+ let url = try await visible(original)
+ // Materialize the base through Finder before arming an upload gate.
+ try await ui.edit(url, contents: nil)
+ let base = try await context.remote.item(driveID: context.domain.driveID, fileID: original.id)
+ guard let etag = base.etag else { throw FinderLiveError.assertionFailed }
+ let localDestination = try require(nested), remoteDestination = try require(sibling)
+ let destinationURL = conflict == .moveMove ? try await visible(localDestination) : nil
+ try await bind(url, item: original)
+ let ticket = try StabilityConflictBarrier.arm(run: run, itemIdentifier: String(original.id),
+ correlationID: correlationID, caseID: conflict)
+ defer { try? StabilityConflictBarrier.release(ticket, run: run) }
+ var localFailure: Error?
+ let localTask = Task { @MainActor in
+ do {
+ switch conflict {
+ case .renameRename: try await self.ui.rename(url, to: "Local.txt")
+ case .moveMove: try await self.ui.move(url, to: self.require(destinationURL))
+ default: try await self.ui.edit(url, contents: String(decoding: localBytes, as: UTF8.self))
+ }
+ } catch { localFailure = error; throw error }
+ }
+ defer { localTask.cancel() }
+ do {
+ try await poll {
+ if let localFailure { throw localFailure }
+ return StabilityConflictBarrier.reached(ticket, run: self.run)
+ }
+ conflictReached = true
+ try await verifyOwned(original)
+ let competing: KDriveRemoteItem
+ switch conflict {
+ case .contentBeforePreflight, .contentAfterPreflight:
+ _ = try await context.remote.replaceFile(driveID: context.domain.driveID, fileID: original.id,
+ expectedETag: etag, clientToken: KDriveMutationIdentity.clientToken([run.runID.uuidString, "competitor"]),
+ contentHash: KDriveMutationIdentity.contentHash(remoteBytes), contents: remoteBytes, lastModifiedAt: Date())
+ competing = try await waitMetadata(original) { $0.id == original.id && $0.parentID == parent.id &&
+ $0.name == original.name && $0.contentVersion != base.contentVersion }
+ try await waitBytes(competing, expected: remoteBytes)
+ case .renameRename, .editRename:
+ try await context.remote.renameItem(driveID: context.domain.driveID, fileID: original.id, name: "Remote.txt")
+ competing = try await waitMetadata(original) { $0.id == original.id && $0.parentID == parent.id && $0.name == "Remote.txt" }
+ try await waitBytes(competing, expected: baseBytes)
+ case .moveMove, .editMove:
+ try await verifyOwned(remoteDestination)
+ try await context.remote.moveItem(driveID: context.domain.driveID, fileID: original.id,
+ destinationParentID: remoteDestination.id, name: nil)
+ competing = try await waitMetadata(original) { $0.id == original.id &&
+ $0.parentID == remoteDestination.id && $0.name == original.name }
+ try await waitBytes(competing, expected: baseBytes)
+ }
+ try StabilityConflictBarrier.recordVerifiedCompetingMutation(ticket, run: run,
+ itemIdentifier: String(competing.id), metadataAlias: StabilityDiagnosticIdentity.metadataAlias(for: competing, runID: run.runID))
+ print("finder conflict: competing server state verified before gate release")
+ try StabilityConflictBarrier.release(ticket, run: run)
+ try await localTask.value
+ if conflict == .contentBeforePreflight || conflict == .contentAfterPreflight {
+ var localItem: KDriveRemoteItem?
+ try await poll {
+ for candidate in try await self.list(parent) where !candidate.isDirectory && candidate.id != original.id {
+ if try await self.context.remote.downloadFile(driveID: self.context.domain.driveID, fileID: candidate.id) == localBytes {
+ localItem = candidate
+ }
+ }
+ return localItem != nil
+ }
+ let copy = try require(localItem)
+ let remote = try await context.remote.item(driveID: context.domain.driveID, fileID: original.id)
+ guard remote.parentID == parent.id, remote.name == original.name, copy.parentID == parent.id,
+ copy.id != remote.id, copy.contentVersion != base.contentVersion,
+ remote.contentVersion != base.contentVersion else { throw FinderLiveError.assertionFailed }
+ try await waitBytes(remote, expected: remoteBytes)
+ try await showAndReopen(copy, expected: localBytes, capture: 301)
+ try await showAndReopen(remote, expected: remoteBytes, capture: 302)
+ } else {
+ let expectedParent = conflict == .moveMove ? localDestination.id : conflict == .editMove ? remoteDestination.id : parent.id
+ let expectedName = conflict == .renameRename ? "Local.txt" : conflict == .editRename ? "Remote.txt" : original.name
+ var current = try await waitMetadata(original) { $0.id == original.id && $0.parentID == expectedParent && $0.name == expectedName }
+ let expectedBytes = conflict == .renameRename || conflict == .moveMove ? baseBytes : localBytes
+ try await waitBytes(current, expected: expectedBytes)
+ // TextEdit save can return before upload completion. Refresh the
+ // metadata after byte verification so its size/version is current.
+ current = try await waitMetadata(original) { $0.id == original.id && $0.parentID == expectedParent &&
+ $0.name == expectedName && $0.size == expectedBytes.count }
+ let subject = StabilityDiagnosticIdentity.alias(for: String(original.id), runID: run.runID)
+ let returned = try diagnostics().last { $0.operation == .modifyItem && $0.phase == .completed &&
+ $0.subjectAlias == subject && $0.correlationID == correlationID && $0.itemMetadataAlias != nil }
+ if let returned {
+ let matches = returned.itemMetadataAlias == StabilityDiagnosticIdentity.metadataAlias(for: current, runID: run.runID)
+ print("finder conflict: callback metadata matches verified remote result=\(matches)")
+ }
+ try await showAndReopen(current, expected: expectedBytes, capture: 303)
+ }
+ } catch {
+ localTask.cancel()
+ try? StabilityConflictBarrier.release(ticket, run: run)
+ _ = try? await localTask.value
+ throw error
+ }
+ }
+
+ private func showAndReopen(_ item: KDriveRemoteItem, expected: Data, capture: Int) async throws {
+ remember(item)
+ let url = try await waitVisibleDestination(item)
+ try await bind(url, item: item)
+ try await ui.select(url)
+ try await ui.capture(in: run.directoryURL.appendingPathComponent("visual-evidence"), sequence: capture)
+ try await ui.edit(url, contents: nil)
+ let bytes = try Data(contentsOf: url)
+ guard bytes == expected else { throw FinderLiveError.assertionFailed }
+ lastVisibleURL = url
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderLiveRunSession.swift b/potassiumProvider/FinderLiveRunSession.swift
new file mode 100644
index 0000000..5f382a0
--- /dev/null
+++ b/potassiumProvider/FinderLiveRunSession.swift
@@ -0,0 +1,318 @@
+#if os(macOS) && STABILITY
+import AppKit
+import FileProvider
+import PotassiumProviderCore
+
+@MainActor
+final class FinderLiveRunSession {
+ let context: FinderStabilityLiveContext
+ let run: StabilityRunHandle
+ let ui: any FinderUIDriving
+ let expectedExtensionCodeHash: String
+ let expectedActionsCodeHash: String
+ var root: KDriveRemoteItem?
+ var nested: KDriveRemoteItem?
+ var deep: KDriveRemoteItem?
+ var sibling: KDriveRemoteItem?
+ var seed: KDriveRemoteItem?
+ var file: KDriveRemoteItem?
+ var directory: KDriveRemoteItem?
+ var transfer: KDriveRemoteItem?
+ var bytes = Data("stability file version one\n".utf8)
+ var owned: [Int: KDriveRemoteItem] = [:]
+ var subjects: Set = []
+ var correlationID = UUID()
+ var conflictReached = false
+ var cancellationObserved = false
+ var workingSetMember = false
+ var expectedWorkingSetMetadataAlias: UUID?
+ var lastVisibleURL: URL?
+ var navigationURLs: (root: URL, nested: URL, deep: URL, sibling: URL, seed: URL)?
+ var deadline = StabilityDeadline(budget: .seconds(90))
+
+ init(context: FinderStabilityLiveContext, ui: any FinderUIDriving) throws {
+ self.context = context
+ self.ui = ui
+ guard let run = try StabilityDiagnosticIdentity.activeRun() else { throw FinderLiveError.missingRun }
+ self.run = run
+ let extensionURL = Bundle.main.bundleURL.appendingPathComponent("Contents/PlugIns/potassiumProviderFileProvider.appex")
+ guard let hash = StabilityDiagnosticIdentity.codeHash(at: extensionURL) else { throw FinderLiveError.unverifiedBuild }
+ self.expectedExtensionCodeHash = hash
+ guard let actionsHash = StabilityDiagnosticIdentity.codeHash(at: Bundle.main.bundleURL.appendingPathComponent("Contents/PlugIns/potassiumProviderActions.appex")) else {
+ throw FinderLiveError.unverifiedBuild
+ }
+ self.expectedActionsCodeHash = actionsHash
+ }
+
+ func remember(_ item: KDriveRemoteItem) {
+ owned[item.id] = item
+ subject(String(item.id))
+ }
+ func subject(_ identifier: String) { subjects.insert(StabilityDiagnosticIdentity.alias(for: identifier, runID: run.runID)) }
+ func require(_ value: T?) throws -> T { guard let value else { throw FinderLiveError.missingFixture }; return value }
+
+ func setup(scope: FinderFixtureNavigation.Scope) async throws {
+ try await context.verifySafety()
+ try await ui.navigate(to: context.rootURL)
+ try await signal(.rootContainer)
+ let rootSubject = StabilityDiagnosticIdentity.alias(for: NSFileProviderItemIdentifier.rootContainer.rawValue, runID: run.runID)
+ try await poll {
+ let callbacks = try self.diagnostics().filter { $0.source == .fileProviderExtension && $0.subjectAlias == rootSubject }
+ guard callbacks.allSatisfy({ $0.processCodeHash == self.expectedExtensionCodeHash && $0.processInstanceID != nil }) else {
+ throw FinderLiveError.unverifiedBuild
+ }
+ return callbacks.contains { $0.phase == .completed }
+ }
+ let name = "stability-run-" + run.runID.uuidString.lowercased()
+ let item = try await context.remote.createDirectory(driveID: context.domain.driveID, parentID: context.domain.rootFileID, name: name)
+ guard item.parentID == context.domain.rootFileID, item.id != context.domain.rootFileID, item.isDirectory else { throw FinderLiveError.unsafeTarget }
+ root = item; remember(item)
+ nested = try await createDirectory(name: "Nested", parent: item)
+ if scope == .fullSuite { deep = try await createDirectory(name: "Deep", parent: try require(nested)) }
+ sibling = try await createDirectory(name: "Sibling", parent: item)
+ if scope == .fullSuite { seed = try await upload(name: "remote-seed.txt", parent: try require(deep), data: bytes) }
+ // Replicated providers propagate remote changes through the working
+ // set. Signal the completed fixture batch once; native folder signals
+ // are ignored even when their acknowledgement reports success.
+ try await signalChanges(in: [.rootContainer] + [item, nested, deep, sibling].compactMap { $0 }
+ .map { NSFileProviderItemIdentifier(String($0.id)) })
+ // Resolving placeholders and verifying their provider identities is
+ // preparation; navigation starts after these read-only bindings finish.
+ let bind: (FinderFixtureNavigation.Target) async throws -> URL = { target in
+ print("finder stability fixture: resolving \(target.rawValue)")
+ do {
+ let fixture: KDriveRemoteItem = switch target {
+ case .root: item
+ case .nested: try self.require(self.nested)
+ case .deep: try self.require(self.deep)
+ case .sibling: try self.require(self.sibling)
+ case .seed: try self.require(self.seed)
+ }
+ let url = try await self.visible(fixture)
+ print("finder stability fixture: resolved \(target.rawValue)")
+ return url
+ } catch {
+ print("finder stability fixture: failed \(target.rawValue)")
+ throw error
+ }
+ }
+ if scope == .conflict {
+ // The selected race materializes its file and destinations itself.
+ // Deep navigation/hydration belongs to the original suite.
+ _ = try await FinderFixtureNavigation.resolveConflictRoot(using: ui, bind: bind)
+ } else {
+ navigationURLs = try await FinderFixtureNavigation.resolve(using: ui, bind: bind)
+ }
+ }
+
+ func createDirectory(name: String, parent: KDriveRemoteItem) async throws -> KDriveRemoteItem {
+ try await verifyOwned(parent)
+ let item = try await context.remote.createDirectory(driveID: context.domain.driveID, parentID: parent.id, name: name)
+ guard item.parentID == parent.id, item.isDirectory else { throw FinderLiveError.unsafeTarget }
+ remember(item)
+ return item
+ }
+
+ func upload(name: String, parent: KDriveRemoteItem, data: Data) async throws -> KDriveRemoteItem {
+ try await verifyOwned(parent)
+ let item = try await context.remote.uploadFile(driveID: context.domain.driveID, parentID: parent.id, fileName: name,
+ contents: data, lastModifiedAt: Date(), conflictStrategy: .error,
+ clientToken: KDriveMutationIdentity.clientToken([run.runID.uuidString, name]), contentHash: KDriveMutationIdentity.contentHash(data))
+ guard item.parentID == parent.id, !item.isDirectory else { throw FinderLiveError.unsafeTarget }
+ remember(item)
+ return item
+ }
+
+ func verifyOwned(_ item: KDriveRemoteItem) async throws {
+ guard owned[item.id] != nil, let root else { throw FinderLiveError.unsafeTarget }
+ try await context.verifySafety()
+ let remote = context.remote, driveID = context.domain.driveID
+ try await StabilityRunConfinement.verify(targetID: item.id, runRootID: root.id, labRootID: context.domain.rootFileID,
+ driveID: driveID, ownedIDs: Set(owned.keys)) { identifier in
+ try await remote.item(driveID: driveID, fileID: identifier)
+ }
+ }
+
+ func visible(_ item: KDriveRemoteItem, trashed: Bool = false) async throws -> URL {
+ guard owned[item.id] != nil else { throw FinderLiveError.unsafeTarget }
+ let manager = context.fileProviderManager
+ let url = try await StabilityCallbackWaiter().wait(timeout: deadline.remaining()) { completion in
+ manager.getUserVisibleURL(for: NSFileProviderItemIdentifier(String(item.id))) { url, error in
+ if let error { completion(.failure(error)) }
+ else if let url { completion(.success(url)) }
+ else { completion(.failure(FinderLiveError.missingFixture)) }
+ }
+ }
+ try await bind(url, item: item, trashed: trashed)
+ return url
+ }
+
+ func bind(_ url: URL, item: KDriveRemoteItem, trashed: Bool = false) async throws {
+ guard owned[item.id] != nil else { throw FinderLiveError.unsafeTarget }
+ if trashed {
+ try await context.verifySafety()
+ guard let actions = context.remote as? any KDriveContextActionProviding else { throw FinderLiveError.missingCapability }
+ let remote = try await actions.trashedItem(driveID: context.domain.driveID, fileID: item.id)
+ guard remote.id == item.id, remote.driveID == context.domain.driveID, owned[item.parentID] != nil else { throw FinderLiveError.unsafeTarget }
+ } else { try await verifyOwned(item) }
+ let binding = try await StabilityCallbackWaiter<(String, String)>().wait(timeout: deadline.remaining()) { completion in
+ NSFileProviderManager.getIdentifierForUserVisibleFile(at: url) { identifier, domain, error in
+ if let error { completion(.failure(error)) }
+ else if let identifier, let domain { completion(.success((identifier.rawValue, domain.rawValue))) }
+ else { completion(.failure(FinderLiveError.unsafeTarget)) }
+ }
+ }
+ guard FinderStabilityTargetBinding.matches(expectedFileID: item.id, expectedDomainIdentifier: context.domain.domainIdentifier,
+ actualItemIdentifier: binding.0, actualDomainIdentifier: binding.1) else { throw FinderLiveError.unsafeTarget }
+ subject(String(item.id)); subject(String(item.parentID))
+ ui.expectActionPanel(for: StabilityDiagnosticIdentity.alias(for: String(item.id), runID: run.runID))
+ lastVisibleURL = url
+ }
+
+ func list(_ parent: KDriveRemoteItem) async throws -> [KDriveRemoteItem] {
+ var cursor: String?, seen: Set = [], items: [KDriveRemoteItem] = []
+ for _ in 0..<100 {
+ let page = try await context.remote.listDirectory(driveID: context.domain.driveID, folderID: parent.id, cursor: cursor, limit: 200)
+ guard page.items.allSatisfy({ $0.parentID == parent.id }) else { throw FinderLiveError.unsafeTarget }
+ items += page.items
+ guard page.hasMore else { return items }
+ guard let next = page.nextCursor, seen.insert(next).inserted, !page.items.isEmpty else { throw FinderLiveError.invalidPagination }
+ cursor = next
+ }
+ throw FinderLiveError.invalidPagination
+ }
+
+ func waitItem(named name: String, parent: KDriveRemoteItem) async throws -> KDriveRemoteItem {
+ var result: KDriveRemoteItem?
+ try await poll { result = try await self.list(parent).first { $0.name == name }; return result != nil }
+ let item = try require(result)
+ remember(item)
+ return item
+ }
+
+ func waitMetadata(_ item: KDriveRemoteItem, predicate: @escaping (KDriveRemoteItem) -> Bool) async throws -> KDriveRemoteItem {
+ var result: KDriveRemoteItem?
+ try await poll {
+ let value = try await self.context.remote.item(driveID: self.context.domain.driveID, fileID: item.id)
+ if predicate(value) { result = value; return true }
+ return false
+ }
+ let resolved = try require(result)
+ remember(resolved)
+ return resolved
+ }
+
+ func waitBytes(_ item: KDriveRemoteItem, expected: Data) async throws {
+ try await poll {
+ try await self.context.remote.downloadFile(driveID: self.context.domain.driveID, fileID: item.id) == expected
+ }
+ }
+
+ /// Local append observations do not contact the server and must not inherit
+ /// the API poller's 2/4/8/10-second backoff during a short transfer.
+ func waitForLocalObservation(_ predicate: () throws -> Bool) async throws {
+ while deadline.remaining() > .zero {
+ try Task.checkCancellation()
+ if try predicate() { return }
+ try await Task.sleep(for: min(.milliseconds(50), deadline.remaining()))
+ }
+ throw FinderLiveError.timedOut
+ }
+
+ func diagnostics() throws -> [ProviderDiagnosticEvent] { try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL) }
+
+ /// A controlled diagnostic comparison after a failed navigation run. This
+ /// writes the unchanged timestamp of the untouched generated seed file;
+ /// its separate correlation can never satisfy a Finder scenario assertion.
+ func diagnoseDirectoryDateRejection() async {
+ guard let seed, let events = try? diagnostics() else { return }
+ let directoryAliases = Set(owned.values.filter(\.isDirectory).map {
+ StabilityDiagnosticIdentity.alias(for: String($0.id), runID: run.runID)
+ })
+ guard events.contains(where: { $0.operation == .updateModificationDate && $0.phase == .failed &&
+ $0.errorCode == 400 && $0.subjectAlias.map(directoryAliases.contains) == true }) else { return }
+ do {
+ try await ProviderDiagnosticCorrelationContext.withCorrelation(UUID()) { @MainActor in
+ try await self.verifyOwned(seed)
+ try await ProviderDiagnosticCorrelationContext.$subjectAlias.withValue(
+ StabilityDiagnosticIdentity.alias(for: String(seed.id), runID: self.run.runID)) {
+ try await self.context.remote.updateModificationDate(driveID: self.context.domain.driveID,
+ fileID: seed.id, date: seed.modifiedAt)
+ }
+ }
+ print("finder stability diagnostic probe: unchanged file timestamp accepted; directory timestamp rejected")
+ } catch {
+ print("finder stability diagnostic probe: unchanged file timestamp rejected; class \(ProviderDiagnosticErrorClassifier.classify(error).rawValue); status \(KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode ?? 0)")
+ }
+ }
+
+ func awaitDiagnostics(scenario: StabilityFinderScenario, startedAt: Date, actions: Int) async throws -> StabilityLiveStepEvidence {
+ let proof = evidence(actions: actions)
+ var lastWaitReason: StabilityLiveEvidenceError?
+ try await poll {
+ let result = StabilityFinderStepResult(sequenceNumber: 1, scenario: scenario, correlationID: self.correlationID,
+ startedAt: startedAt, finishedAt: Date(), outcome: .passed, assertions: [], liveEvidence: proof)
+ do { try StabilityLiveEvidenceValidator.validate(step: result, diagnostics: self.diagnostics()); return true }
+ catch StabilityLiveEvidenceError.wrongExtensionBuild { throw FinderLiveError.unverifiedBuild }
+ catch StabilityLiveEvidenceError.unexpectedFailure { throw StabilityLiveEvidenceError.unexpectedFailure }
+ catch StabilityLiveEvidenceError.contradictoryTerminal { throw StabilityLiveEvidenceError.contradictoryTerminal }
+ catch let error as StabilityLiveEvidenceError {
+ if error != lastWaitReason {
+ print("finder stability diagnostics pending: \(error.rawValue)")
+ lastWaitReason = error
+ }
+ return false
+ }
+ catch { return false }
+ }
+ return proof
+ }
+
+ func evidence(actions: Int, failure: StabilityFailureOrigin? = nil, reason: StabilityLiveFailureReason? = nil) -> StabilityLiveStepEvidence {
+ StabilityLiveStepEvidence(subjects: Array(subjects), observedUIActions: actions, expectedExtensionCodeHash: expectedExtensionCodeHash,
+ conflictBarrierReached: conflictReached, cancellationObserved: cancellationObserved,
+ workingSetMemberObserved: workingSetMember, failureOrigin: failure, failureReason: reason,
+ supportingSpanIDs: failure == nil ? nil : (try? Array(Set(diagnostics().filter { $0.correlationID == correlationID && $0.subjectAlias.map(subjects.contains) == true }.compactMap(\.spanID)))),
+ expectedActionsCodeHash: expectedActionsCodeHash, expectedWorkingSetMetadataAlias: expectedWorkingSetMetadataAlias)
+ }
+
+ func signal(_ identifier: NSFileProviderItemIdentifier) async throws {
+ try await signalChanges(in: [identifier])
+ }
+
+ private func signalChanges(in identifiers: [NSFileProviderItemIdentifier]) async throws {
+ let manager = context.fileProviderManager
+ try await FinderReplicatedRefresh.signal(changedContainers: identifiers,
+ recordSubject: { self.subject($0.rawValue) }) { identifier in
+ try await StabilityCallbackWaiter().wait(timeout: self.deadline.remaining()) { completion in
+ manager.signalEnumerator(for: identifier) { error in
+ if let error { completion(.failure(error)) } else { completion(.success(())) }
+ }
+ }
+ }
+ }
+
+ func poll(seconds: Int = 90, _ predicate: () async throws -> Bool) async throws {
+ let deadline = ContinuousClock.now.advanced(by: min(.seconds(seconds), self.deadline.remaining()))
+ var delay = 2
+ repeat {
+ try Task.checkCancellation()
+ do { if try await predicate() { return } }
+ catch {
+ guard let rejection = KDriveRemoteErrorClassifier.apiRejection(from: error), [408,429,500,502,503,504].contains(rejection.statusCode) else { throw error }
+ delay = max(delay, rejection.retryAfterSeconds ?? delay)
+ }
+ let remaining = ContinuousClock.now.duration(to: deadline)
+ guard remaining > .zero else { break }
+ try await Task.sleep(for: min(.seconds(delay), remaining))
+ delay = min(delay * 2, 10)
+ } while ContinuousClock.now < deadline
+ throw FinderLiveError.timedOut
+ }
+}
+
+enum FinderLiveError: Error {
+ case missingRun, missingFixture, unsafeTarget, unverifiedBuild, invalidPagination, timedOut
+ case assertionFailed, missingCapability, cancellationNotExercised
+}
+#endif
diff --git a/potassiumProvider/FinderLiveScenarioRunner.swift b/potassiumProvider/FinderLiveScenarioRunner.swift
new file mode 100644
index 0000000..e13c1ad
--- /dev/null
+++ b/potassiumProvider/FinderLiveScenarioRunner.swift
@@ -0,0 +1,296 @@
+#if os(macOS) && STABILITY
+import AppKit
+import FileProvider
+import PotassiumProviderCore
+
+@MainActor
+struct LiveFinderStabilityScenarioRunner: FinderStabilityScenarioRunning {
+ let ui: any FinderUIDriving
+ let conflictCase: StabilityLiveConflictCase?
+ init(ui: (any FinderUIDriving)? = nil, conflictCase: StabilityLiveConflictCase? = nil) {
+ self.ui = ui ?? SystemFinderUIDriver()
+ self.conflictCase = conflictCase
+ }
+
+ func run(context: FinderStabilityLiveContext) async -> FinderStabilityScenarioExecution {
+ var session: FinderLiveRunSession?
+ var steps: [StabilityFinderStepResult] = []
+ var observations: [StabilityFinderAPIObservation] = []
+ var failed = false
+ let selection = FinderStabilityScenarioSelection(conflictCase: conflictCase,
+ includePermanentDeletion: context.includePermanentDeletion)
+ for (index, scenario) in StabilityFinderScenario.allCases.enumerated() {
+ let startedAt = Date(), correlationID = UUID()
+ let actionStart = ui.actionCount
+ if let reason = selection.skipReason(for: scenario, afterFailure: failed) {
+ steps.append(step(index, scenario, correlationID, startedAt, .skipped(reason)))
+ if reason == .permanentDeletionNotSelected {
+ print("finder stability step 12/16: permanentDeletion deferred; continuing without re-trashing or deletion confirmation")
+ }
+ continue
+ }
+ var pointerActive = false
+ do {
+ try await context.beginStep(correlationID)
+ pointerActive = true
+ if session == nil { session = try FinderLiveRunSession(context: context, ui: ui) }
+ guard let session else { throw FinderLiveError.missingRun }
+ try StabilityLiveStatus(state: .running, scenario: scenario).write(to: session.run)
+ session.correlationID = correlationID
+ session.subjects = []
+ session.deadline = StabilityDeadline(budget: scenario == .cancellationAndProgress ? .seconds(600) : .seconds(90))
+ ui.useDeadline { [weak session] in session?.deadline.remaining() ?? .zero }
+ if scenario == .workingSetRefresh {
+ // A callback started during cancellation can deliver the
+ // next fixture under its original correlation. Settle that
+ // work before creating the new fixture; never relabel it or
+ // accept another scenario's callback as current evidence.
+ print("finder stability: settling prior callbacks before working-set fixture; 90-second preparation budget")
+ try await session.drain()
+ session.deadline = StabilityDeadline(budget: .seconds(90))
+ }
+ let (proof, stepObservations) = try await ProviderDiagnosticCorrelationContext.withCorrelation(correlationID) { @MainActor in
+ var stepObservations: [StabilityFinderAPIObservation] = []
+ if session.root == nil {
+ print("finder stability: preparing fixtures with a 90-second budget")
+ try await session.setup(scope: conflictCase == nil ? .fullSuite : .conflict)
+ guard session.deadline.remaining() > .zero else { throw StabilityDeadlineError.expired }
+ // Preparation is a separate bounded operation. Keep its
+ // evidence and elapsed time, then start the navigation
+ // budget once the generated hierarchy is available.
+ session.deadline = StabilityDeadline(budget: .seconds(90))
+ print("finder stability: fixture preparation complete; starting scenario budget")
+ }
+ let baseline = try await session.list(session.require(session.root))
+ stepObservations.append(StabilityFinderAPIObservation(scenario: scenario, correlationID: correlationID, phase: .baseline,
+ outcome: .passed, recordedAt: Date(), hasMore: false, itemCount: baseline.count))
+ try await context.verifySafety()
+ print("finder stability step \(index + 1)/16: \(scenario.rawValue)")
+ if let conflictCase { try await session.executeConflict(conflictCase) }
+ else { try await execute(scenario, session: session) }
+ print("finder stability: UI interaction complete; recording evidence")
+ if let url = session.lastVisibleURL, ![.trash, .permanentDeletion].contains(scenario) {
+ try await ui.select(url)
+ try await ui.capture(in: session.run.directoryURL.appendingPathComponent("visual-evidence"), sequence: index + 1)
+ }
+ let proof = try await session.awaitDiagnostics(scenario: scenario, startedAt: startedAt, actions: ui.actionCount - actionStart)
+ let post = try await session.list(session.require(session.root))
+ stepObservations.append(StabilityFinderAPIObservation(scenario: scenario, correlationID: correlationID, phase: .postcondition,
+ outcome: .passed, recordedAt: Date(), hasMore: false, itemCount: post.count))
+ return (proof, stepObservations)
+ }
+ observations += stepObservations
+ steps.append(step(index, scenario, correlationID, startedAt, .passed, proof: proof))
+ try await context.endStep(correlationID)
+ pointerActive = false
+ } catch {
+ if let session, (error as? FinderUIError) != .pointerTargetObstructed {
+ // The driver only captures its previously verified single
+ // generated selection; failure never broadens the region.
+ try? await ui.captureFailure(in: session.run.directoryURL.appendingPathComponent("visual-evidence"), sequence: 100 + index)
+ }
+ if pointerActive { try? await context.endStep(correlationID) }
+ let origin: StabilityFailureOrigin = (error as? FinderUIError)?.isEnvironmental == true ? .environment :
+ (error as? StabilityLiveEvidenceError) == .unexpectedFailure ? .provider : error is FinderUIError ? .automation :
+ (error is FinderLiveError || error is StabilityLiveEvidenceError || error is StabilityRunConfinementError || error is StabilityDeadlineError || error is StabilityDiagnosticTailError || error is ProviderDiagnosticStoreError ? .harness : .api)
+ let reason = failureReason(error)
+ let proof = session?.evidence(actions: ui.actionCount - actionStart, failure: origin, reason: reason)
+ steps.append(step(index, scenario, correlationID, startedAt, .failed(.operationFailed), proof: proof))
+ print("finder stability step failed: \(scenario.rawValue); origin \(origin.rawValue); reason \(reason.rawValue); class \(ProviderDiagnosticErrorClassifier.classify(error).rawValue)")
+ if let uiError = error as? FinderUIError { print("finder stability UI failure: \(uiError.rawValue)") }
+ if scenario == .enumerationAndChangeAnchors { await session?.diagnoseDirectoryDateRejection() }
+ failed = true
+ }
+ }
+ var canSeal = true
+ // Failure screenshots are already retained. Keep monitoring through
+ // window closure and the resulting provider callbacks before sealing.
+ do { try await ui.closeOwnedWindows(); print("finder stability: run-owned Finder windows closed") }
+ catch { canSeal = false; print("finder stability: Finder window cleanup failed; evidence will remain unsealed") }
+ if let session {
+ do { try StabilityLiveStatus(state: .settling).write(to: session.run); session.deadline = StabilityDeadline(budget: .seconds(600)); try await session.drain() }
+ catch { canSeal = false; print("finder stability: pending extension work; evidence will remain unsealed") }
+ }
+ return FinderStabilityScenarioExecution(stepResults: steps, observations: observations, canSeal: canSeal)
+ }
+
+ private func failureReason(_ error: Error) -> StabilityLiveFailureReason {
+ switch error {
+ case FinderUIError.selectionMismatch: return .selectionMismatch
+ case FinderUIError.windowMismatch: return .windowMismatch
+ case FinderUIError.screenshotUnavailable: return .screenshotUnavailable
+ case FinderUIError.controlUnavailable: return .unsupportedControl
+ case FinderUIError.operatorCancelled: return .operatorStopped
+ case FinderUIError.evictionResourceBusy: return .resourceBusy
+ case FinderUIError.timedOut, FinderLiveError.timedOut, StabilityDeadlineError.expired: return .deadline
+ case FinderLiveError.unsafeTarget, is StabilityRunConfinementError: return .unsafeTarget
+ case FinderLiveError.unverifiedBuild: return .unverifiedBuild
+ case FinderLiveError.missingFixture: return .missingFixture
+ case FinderLiveError.cancellationNotExercised: return .cancellationNotExercised
+ case FinderLiveError.assertionFailed: return .assertionFailed
+ case is FinderUIError: return .uiUnavailable
+ case StabilityLiveEvidenceError.unexpectedFailure: return .remoteError
+ case is StabilityLiveEvidenceError, is StabilityDiagnosticTailError, is ProviderDiagnosticStoreError: return .missingTelemetry
+ default: return .remoteError
+ }
+ }
+
+ private func step(_ index: Int, _ scenario: StabilityFinderScenario, _ correlationID: UUID, _ startedAt: Date,
+ _ outcome: StabilityFinderStepOutcome, proof: StabilityLiveStepEvidence? = nil) -> StabilityFinderStepResult {
+ let untested: StabilityFinderAssertionNotEvaluatedReason
+ if case .skipped = outcome { untested = .stepSkipped } else { untested = .operationDidNotReachAssertion }
+ return StabilityFinderStepResult(sequenceNumber: UInt16(index + 1), scenario: scenario, correlationID: correlationID,
+ startedAt: startedAt, finishedAt: Date(), outcome: outcome,
+ assertions: StabilityFinderAssertionClass.allCases.map {
+ StabilityFinderAssertionResult(assertionClass: $0, outcome: outcome == .passed ? .passed : .notEvaluated(untested))
+ }, liveEvidence: proof)
+ }
+
+ private func execute(_ scenario: StabilityFinderScenario, session s: FinderLiveRunSession) async throws {
+ let root = try s.require(s.root)
+ switch scenario {
+ case .enumerationAndChangeAnchors:
+ let urls = try s.require(s.navigationURLs)
+ let a = urls.nested, b = urls.deep, sibling = urls.sibling, rootURL = urls.root, seedURL = urls.seed
+ try await FinderNavigationSequence.execute(using: ui, root: rootURL, nested: a, deep: b, sibling: sibling) { index, folder in
+ // Select only a known generated child of the verified current
+ // folder. This preserves history and keeps sidebars out of capture.
+ let child: URL? = folder == rootURL ? a : folder == a ? b : folder == b ? seedURL : nil
+ if let child {
+ try await ui.select(child)
+ try await ui.capture(in: s.run.directoryURL.appendingPathComponent("visual-evidence"), sequence: 200 + index)
+ }
+ // The sibling milestone is captured with its remote-change
+ // fixture after that fixture becomes visible below.
+ }
+ print("finder stability: nested and history navigation verified")
+ let changed = try await s.upload(name: "remote-change.txt", parent: s.require(s.sibling), data: Data("remote change\n".utf8))
+ try await s.signal(NSFileProviderItemIdentifier(String(changed.parentID)))
+ let changedURL = try await s.visible(changed)
+ try await s.poll { try await ui.contains(changedURL) }
+ print("finder stability: remote change visible in Finder")
+ try await ui.select(changedURL)
+ case .hydrate:
+ let item = try s.require(s.seed), url = try await s.visible(item)
+ try await FinderHydrationSequence.execute(using: ui, url: url) {
+ try await s.waitBytes(item, expected: s.bytes)
+ guard try Data(contentsOf: url) == s.bytes else { throw FinderLiveError.assertionFailed }
+ }
+ case .evict:
+ // Hydration already verified this item's bytes and fetch completion,
+ // and closed its editor. Unrelated domain work is not a prerequisite
+ // for invoking this exact item's Finder action.
+ let url = try await s.visible(s.require(s.seed))
+ try await ui.contextAction("Remove Download", on: url)
+ // The menu state is inspected without opening or reading the evicted item.
+ try await s.poll { try await ui.hasContextAction("Download Now", on: url) }
+ guard try await ui.hasContextAction("Remove Download", on: url) == false else { throw FinderLiveError.assertionFailed }
+ // Metadata lookup is non-hydrating; a new fetch is mandatory in the next step.
+ _ = try await s.context.remote.item(driveID: s.context.domain.driveID, fileID: s.require(s.seed).id)
+ case .download:
+ let item = try s.require(s.seed), url = try await s.visible(item)
+ try await ui.contextAction("Download Now", on: url)
+ try await s.poll { try s.diagnostics().contains { $0.correlationID == s.correlationID && $0.operation == .fetchContents && $0.phase == .completed } }
+ guard try Data(contentsOf: url) == s.bytes else { throw FinderLiveError.assertionFailed }
+ case .fileCreate:
+ let temporary = FileManager.default.temporaryDirectory.appendingPathComponent(s.run.runID.uuidString, isDirectory: true)
+ try FileManager.default.createDirectory(at: temporary, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
+ let source = temporary.appendingPathComponent("created.txt")
+ try s.bytes.write(to: source, options: .withoutOverwriting)
+ try await ui.copy(source, to: s.visible(root))
+ s.file = try await s.waitItem(named: source.lastPathComponent, parent: root)
+ try await s.waitBytes(s.require(s.file), expected: s.bytes)
+ _ = try await s.visible(s.require(s.file))
+ case .directoryCreate:
+ let parent = try s.require(s.deep)
+ try await ui.createFolder(named: "Created Folder", in: s.visible(parent))
+ s.directory = try await s.waitItem(named: "Created Folder", parent: parent)
+ try await ui.navigate(to: s.visible(s.require(s.directory)))
+ case .editAndUpload:
+ let item = try s.require(s.file), url = try await s.visible(item)
+ s.bytes = Data("stability file version two\n".utf8)
+ try await ui.edit(url, contents: String(decoding: s.bytes, as: UTF8.self))
+ try await s.waitBytes(item, expected: s.bytes)
+ case .rename:
+ let item = try s.require(s.file), url = try await s.visible(item)
+ try await ui.rename(url, to: "renamed.txt")
+ s.file = try await s.waitMetadata(item) { $0.name == "renamed.txt" && $0.id == item.id }
+ try await s.poll { try await s.list(root).contains { $0.name == item.name } == false }
+ _ = try await s.visible(s.require(s.file))
+ case .move:
+ let item = try s.require(s.file), parent = try s.require(s.directory)
+ let source = try await s.visible(item), destination = try await s.visible(parent)
+ try await s.bind(source, item: item)
+ try await ui.move(source, to: destination)
+ s.file = try await s.waitMetadata(item) { $0.parentID == parent.id && $0.name == item.name }
+ try await s.waitBytes(item, expected: s.bytes)
+ _ = try await s.visible(s.require(s.file))
+ case .trash:
+ let item = try s.require(s.file), url = try await s.visible(item)
+ try await ui.select(url)
+ try await ui.capture(in: s.run.directoryURL.appendingPathComponent("visual-evidence"), sequence: 10)
+ try await ui.trash(url)
+ try await s.waitTrashed(item, exists: true)
+ case .restore:
+ let item = try s.require(s.file), url = try await s.visible(item, trashed: true)
+ try await FinderTrashedItemSequence.execute(reveal: { try await ui.revealTrashedItem(url) },
+ revalidate: { try await s.bind(url, item: item, trashed: true) },
+ action: { try await ui.contextAction("Restore from kDrive Trash", on: url) })
+ s.file = try await s.waitRestored(item)
+ try await s.waitBytes(item, expected: s.bytes)
+ let restored = try await s.waitRestoredLocation(s.require(s.file))
+ try await ui.select(restored)
+ case .permanentDeletion:
+ let item = try s.require(s.file), url = try await s.visible(item)
+ try await ui.trash(url)
+ try await s.waitTrashed(item, exists: true)
+ let trashed = try await s.visible(item, trashed: true)
+ try await FinderTrashedItemSequence.execute(reveal: { try await ui.revealTrashedItem(trashed) },
+ revalidate: { try await s.bind(trashed, item: item, trashed: true) },
+ action: { try await ui.select(trashed) })
+ try await ui.capture(in: s.run.directoryURL.appendingPathComponent("visual-evidence"), sequence: 12)
+ s.deadline.pause()
+ try await ui.confirmPermanentDeletion(trashed, fixtureAlias: StabilityDiagnosticIdentity.alias(for: String(item.id), runID: s.run.runID))
+ s.deadline.resume()
+ try await s.bind(trashed, item: item, trashed: true)
+ try await ui.contextAction("Delete Immediately…", on: trashed)
+ try await ui.panelAction(.confirmSystemDeletion)
+ try await s.waitDeleted(item)
+ case .concurrentRemotePreserveBoth:
+ try await s.preserveBoth()
+ case .cancellationAndProgress:
+ try await s.cancelTransfer()
+ case .workingSetRefresh:
+ // Independent of transfer cancellation: always use a fresh, small
+ // run-owned fixture, including when an earlier scenario failed.
+ let item = try await s.upload(name: "working-set-seed.bin", parent: root, data: Data("working-set fixture\n".utf8))
+ s.transfer = item
+ try await s.signal(NSFileProviderItemIdentifier(String(root.id)))
+ try await ui.select(s.visible(item))
+ try await s.verifyOwned(item)
+ // A fresh remote metadata change makes a cached membership event
+ // insufficient. The extension must deliver the new name through
+ // its actual working-set enumeration before Finder can pass.
+ try await s.context.remote.renameItem(driveID: s.context.domain.driveID, fileID: item.id, name: "working-set-current.bin")
+ let current = try await s.waitMetadata(item) { $0.name == "working-set-current.bin" && $0.parentID == item.parentID }
+ s.transfer = current
+ s.expectedWorkingSetMetadataAlias = StabilityDiagnosticIdentity.metadataAlias(for: current, runID: s.run.runID)
+ try await s.signal(.workingSet)
+ guard let remote = s.context.remote as? any KDriveWorkingSetRemoteProviding else { throw FinderLiveError.missingCapability }
+ try await s.poll {
+ let items = try await remote.listWorkingSetRelevantItems(driveID: s.context.domain.driveID, latestLimit: 100)
+ s.workingSetMember = items.contains { $0.id == item.id && $0.name == current.name && $0.parentID == current.parentID && $0.size == current.size }
+ return s.workingSetMember
+ }
+ try await s.poll { try s.diagnostics().contains { $0.source == .fileProviderExtension &&
+ $0.operation == .workingSetRefresh && $0.phase == .completed &&
+ $0.itemMetadataAlias == s.expectedWorkingSetMetadataAlias && $0.correlationID == s.correlationID } }
+ let currentURL = try await s.visible(current)
+ try await ui.select(currentURL)
+ guard try await ui.contains(currentURL) else { throw FinderLiveError.assertionFailed }
+ case .supportedContextualActions:
+ try await s.contextActions()
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderMenuCommandSequence.swift b/potassiumProvider/FinderMenuCommandSequence.swift
new file mode 100644
index 0000000..aad2ae3
--- /dev/null
+++ b/potassiumProvider/FinderMenuCommandSequence.swift
@@ -0,0 +1,19 @@
+#if os(macOS) && STABILITY
+@MainActor
+enum FinderMenuCommandSequence {
+ static func perform(command: String, press: () throws -> Void, click: () async throws -> Void,
+ waitForDismissal: () async throws -> Void, waitForResult: () async throws -> Void) async throws {
+ if command == "Download Now" {
+ // Dispatch the native click without awaiting the command's work.
+ // Callback/progress evidence owns transfer completion; cancellation
+ // must be able to run while the transfer is still active.
+ try await click()
+ try await waitForDismissal()
+ } else {
+ try press()
+ try await waitForDismissal()
+ try await waitForResult()
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderMenuWindowRefresh.swift b/potassiumProvider/FinderMenuWindowRefresh.swift
new file mode 100644
index 0000000..276dd4f
--- /dev/null
+++ b/potassiumProvider/FinderMenuWindowRefresh.swift
@@ -0,0 +1,17 @@
+#if os(macOS) && STABILITY
+/// A missing File Provider command can be cached in one Finder window. Permit
+/// one replacement of that owned window, never a broader/destructive command.
+struct FinderMenuWindowRefresh {
+ private var used = false
+
+ mutating func claim(command: String, matchingCommands: Int, elapsed: Duration) -> Bool {
+ guard !used, matchingCommands == 0, elapsed >= .seconds(2), [
+ "Download Now", "Remove Download", "Restore from kDrive Trash",
+ "Add to kDrive Favorites", "Remove from kDrive Favorites", "Duplicate on kDrive",
+ "Share kDrive Link…", "Version History…"
+ ].contains(command) else { return false }
+ used = true
+ return true
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderNavigationSequence.swift b/potassiumProvider/FinderNavigationSequence.swift
new file mode 100644
index 0000000..378b8c6
--- /dev/null
+++ b/potassiumProvider/FinderNavigationSequence.swift
@@ -0,0 +1,119 @@
+#if os(macOS) && STABILITY
+import Foundation
+import CoreGraphics
+
+struct FinderProcessIdentity: Equatable {
+ let pid: Int32
+ let launchedAt: Date
+}
+
+struct FinderWindowOwnership {
+ let windowID: Int32
+ let process: FinderProcessIdentity
+
+ func close(currentProcess: FinderProcessIdentity?, perform: (Int32) throws -> Void) rethrows {
+ // Finder restart destroys its old windows; a reused ID belongs to the
+ // new process and must not be touched by this run's cleanup.
+ guard currentProcess == process else { return }
+ try perform(windowID)
+ }
+}
+
+enum FinderUIURLIdentity {
+ static func matches(_ actual: URL?, _ expected: URL) -> Bool {
+ guard let actual, actual.isFileURL, expected.isFileURL,
+ [nil, "", "localhost"].contains(actual.host),
+ [nil, "", "localhost"].contains(expected.host) else { return false }
+ return actual.standardizedFileURL.path.precomposedStringWithCanonicalMapping ==
+ expected.standardizedFileURL.path.precomposedStringWithCanonicalMapping
+ }
+}
+
+enum FinderUINameObservation {
+ static func hasUniqueMatch(displayedName: String, rowNames: [String]) -> Bool {
+ !displayedName.isEmpty && rowNames.filter { $0 == displayedName }.count == 1
+ }
+}
+
+enum FinderAlertObservation {
+ static func isResourceBusyEviction(labels: [String]) -> Bool {
+ labels.contains("Unable to Remove Download") && labels.contains {
+ $0.localizedCaseInsensitiveContains("resource busy") || $0.localizedCaseInsensitiveContains("ressource busy")
+ }
+ }
+}
+
+enum FinderNameEditorObservation {
+ static func isConfined(value: String?, expected: String, editorBounds: CGRect, windowBounds: CGRect,
+ sameProcess: Bool, ownedWindowIsFront: Bool) -> Bool {
+ sameProcess && ownedWindowIsFront && !expected.isEmpty && value == expected &&
+ editorBounds.width > 1 && editorBounds.height > 1 && windowBounds.contains(editorBounds)
+ }
+}
+@MainActor
+enum FinderSelectionSequence {
+ static func execute(waitUntilVisible: () async throws -> Void,
+ assignSelection: () throws -> Void,
+ waitUntilSelected: () async throws -> Void) async throws {
+ // Finder may acknowledge a new window target before its rows exist.
+ // An early selection assignment is silently ignored and is not replayed.
+ try await waitUntilVisible()
+ try assignSelection()
+ try await waitUntilSelected()
+ }
+}
+
+@MainActor
+enum FinderNavigationSequence {
+ static func execute(using ui: any FinderUINavigating, root: URL, nested: URL, deep: URL, sibling: URL,
+ observe: ((Int, URL) async throws -> Void)? = nil) async throws {
+ try await ui.navigate(to: root)
+ try await observe?(0, root)
+ try await ui.navigate(to: nested)
+ try await observe?(1, nested)
+ try await ui.navigate(to: deep)
+ try await observe?(2, deep)
+ try await ui.navigateHistory(back: true, expectedURL: nested)
+ try await observe?(3, nested)
+ try await ui.navigateHistory(back: false, expectedURL: deep)
+ try await observe?(4, deep)
+ try await ui.navigateParent(expectedURL: nested)
+ try await observe?(5, nested)
+ try await ui.navigate(to: sibling)
+ try await observe?(6, sibling)
+ }
+}
+
+@MainActor
+enum FinderFixtureNavigation {
+ enum Scope { case fullSuite, conflict }
+ enum Target: String { case root, nested, deep, sibling, seed }
+
+ /// A targeted race owns its navigation and hydration. Preparation binds only
+ /// the run root so unrelated deep-hierarchy failures cannot block the race.
+ static func resolveConflictRoot(using ui: any FinderUINavigating,
+ bind: (Target) async throws -> URL) async throws -> URL {
+ let root = try await bind(.root)
+ try await ui.navigate(to: root)
+ return root
+ }
+
+ /// Open each verified parent before asking File Provider to materialize its
+ /// children. Resolving the entire unopened hierarchy can wait for separate
+ /// working-set crawls at every level.
+ static func resolve(using ui: any FinderUINavigating,
+ bind: (Target) async throws -> URL) async throws
+ -> (root: URL, nested: URL, deep: URL, sibling: URL, seed: URL) {
+ let root = try await bind(.root)
+ try await ui.navigate(to: root)
+ let nested = try await bind(.nested)
+ try await ui.navigate(to: nested)
+ let deep = try await bind(.deep)
+ try await ui.navigate(to: deep)
+ let seed = try await bind(.seed)
+ try await ui.navigate(to: root)
+ let sibling = try await bind(.sibling)
+ return (root, nested, deep, sibling, seed)
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderPointerClick.swift b/potassiumProvider/FinderPointerClick.swift
new file mode 100644
index 0000000..401216f
--- /dev/null
+++ b/potassiumProvider/FinderPointerClick.swift
@@ -0,0 +1,42 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+import Foundation
+
+@MainActor
+enum FinderPointerClick {
+ /// Move first so WindowServer and hover controls see the same target as the
+ /// subsequent click. Revalidate after that event; never inherit modifiers.
+ static func perform(at point: CGPoint, button: CGMouseButton,
+ mayClick: () throws -> Bool,
+ canPostEvents: () -> Bool = { CGPreflightPostEventAccess() },
+ post: (CGEvent) -> Void = { $0.post(tap: .cgSessionEventTap) },
+ settle: () async throws -> Void = { try await Task.sleep(for: .milliseconds(40)) }) async throws -> Bool {
+ let permitted = canPostEvents()
+ print("finder stability UI: native pointer permission=\(permitted)")
+ guard permitted else { throw FinderUIError.permissionRequired }
+ guard try mayClick() else { return false }
+ post(try event(.mouseMoved, at: point, button: button))
+ try await settle()
+ let cursor = CGEvent(source: nil)?.location
+ print("finder stability UI: session pointer position confirmed=\(cursor.map { abs($0.x - point.x) < 2 && abs($0.y - point.y) < 2 } == true)")
+ guard try mayClick() else { return false }
+ let down = try event(button == .right ? .rightMouseDown : .leftMouseDown, at: point, button: button)
+ let up = try event(button == .right ? .rightMouseUp : .leftMouseUp, at: point, button: button)
+ print("finder stability UI: native pointer down; timestamp=\(Date.now.timeIntervalSince1970)")
+ post(down)
+ // Release even if the task is cancelled while the button is down.
+ defer { post(up) }
+ try await settle()
+ return true
+ }
+
+ private static func event(_ type: CGEventType, at point: CGPoint, button: CGMouseButton) throws -> CGEvent {
+ guard let event = CGEvent(mouseEventSource: nil, mouseType: type, mouseCursorPosition: point, mouseButton: button) else {
+ throw FinderUIError.controlUnavailable
+ }
+ event.flags = []
+ event.setIntegerValueField(.mouseEventClickState, value: type == .mouseMoved ? 0 : 1)
+ return event
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderPointerTarget.swift b/potassiumProvider/FinderPointerTarget.swift
new file mode 100644
index 0000000..085a879
--- /dev/null
+++ b/potassiumProvider/FinderPointerTarget.swift
@@ -0,0 +1,39 @@
+#if os(macOS) && STABILITY
+import ApplicationServices
+import Foundation
+
+enum FinderPointerTarget {
+ /// Finder can be frontmost while a system window intercepts the pointer.
+ /// Do not read another application's UI or post through an obstructed target.
+ static func verify(at point: CGPoint, processIdentifier: pid_t, scope: AXUIElement) throws {
+ try verify(processIdentifier: processIdentifier, scope: scope, hitTest: {
+ var hit: AXUIElement?
+ let result = AXUIElementCopyElementAtPosition(AXUIElementCreateSystemWide(), Float(point.x), Float(point.y), &hit)
+ guard result == .success else { return nil }
+ return hit
+ }, owner: { element in
+ var pid: pid_t = 0
+ return AXUIElementGetPid(element, &pid) == .success ? pid : nil
+ }, parent: { element in
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, kAXParentAttribute as CFString, &value) == .success,
+ let value, CFGetTypeID(value) == AXUIElementGetTypeID() else { return nil }
+ return value as! AXUIElement
+ }, equal: { CFEqual($0, $1) })
+ }
+
+ static func verify(processIdentifier: pid_t, scope: Element,
+ hitTest: () -> Element?, owner: (Element) -> pid_t?,
+ parent: (Element) -> Element?, equal: (Element, Element) -> Bool) throws {
+ guard var element = hitTest() else { throw FinderUIError.pointerTargetObstructed }
+ for _ in 0..<32 {
+ // Never traverse a different app, including permission-alert hosts.
+ guard owner(element) == processIdentifier else { throw FinderUIError.pointerTargetObstructed }
+ if equal(element, scope) { return }
+ guard let next = parent(element) else { break }
+ element = next
+ }
+ throw FinderUIError.pointerTargetObstructed
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderPopupMenuDiscovery.swift b/potassiumProvider/FinderPopupMenuDiscovery.swift
new file mode 100644
index 0000000..898c830
--- /dev/null
+++ b/potassiumProvider/FinderPopupMenuDiscovery.swift
@@ -0,0 +1,25 @@
+#if os(macOS) && STABILITY
+/// Only transient root menus can originate the command on our bound selection.
+/// Expanded submenus and application menu-bar commands are not competing roots.
+enum FinderPopupMenuDiscovery {
+ enum Role { case menu, menuBar, other }
+
+ static func roots(from root: Element, limit: Int = 5_000,
+ role: (Element) -> Role, children: (Element) -> [Element],
+ visible: (Element) -> Bool) -> [Element] {
+ var queue = [root], roots: [Element] = [], visited = 0
+ while let element = queue.popLast() {
+ guard visited < limit else { return [] }
+ visited += 1
+ switch role(element) {
+ case .menu:
+ if visible(element) { roots.append(element) }
+ // Even a hidden submenu is subordinate to this root.
+ case .menuBar: break
+ case .other: queue += children(element)
+ }
+ }
+ return roots
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderReplicatedRefresh.swift b/potassiumProvider/FinderReplicatedRefresh.swift
new file mode 100644
index 0000000..e517cc5
--- /dev/null
+++ b/potassiumProvider/FinderReplicatedRefresh.swift
@@ -0,0 +1,19 @@
+#if os(macOS) && STABILITY
+import FileProvider
+
+@MainActor
+enum FinderReplicatedRefresh {
+ /// NSFileProviderReplicatedExtension ignores native signals for individual
+ /// containers. One working-set signal covers the completed mutation batch.
+ static func signal(changedContainers: [NSFileProviderItemIdentifier],
+ recordSubject: (NSFileProviderItemIdentifier) -> Void = { _ in },
+ using signal: (NSFileProviderItemIdentifier) async throws -> Void) async throws {
+ try Task.checkCancellation()
+ guard !changedContainers.isEmpty else { return }
+ // The native refresh target covers the entire domain, but the scenario
+ // owns only its intended fixtures. Global monitoring settles other work.
+ for identifier in changedContainers { recordSubject(identifier) }
+ try await signal(.workingSet)
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderRestoreObservation.swift b/potassiumProvider/FinderRestoreObservation.swift
new file mode 100644
index 0000000..d93d1f1
--- /dev/null
+++ b/potassiumProvider/FinderRestoreObservation.swift
@@ -0,0 +1,59 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+
+@MainActor
+enum FinderRestoreObservation {
+ static func matchesVisibleDestination(_ url: URL, parent: URL, name: String) -> Bool {
+ FinderUIURLIdentity.matches(url.deletingLastPathComponent(), parent) &&
+ url.lastPathComponent.precomposedStringWithCanonicalMapping == name.precomposedStringWithCanonicalMapping
+ }
+
+ static func observeVisibleDestination(parent: URL, name: String,
+ readVisible: () async throws -> URL) async throws -> URL? {
+ try await observeVisibleDestination(name: name, readVisible: readVisible) {
+ FinderUIURLIdentity.matches($0, parent)
+ }
+ }
+
+ static func observeVisibleDestination(name: String, readVisible: () async throws -> URL,
+ matchesParent: (URL) async throws -> Bool) async throws -> URL? {
+ let candidate: URL
+ do { candidate = try await readVisible() }
+ catch {
+ recordLookupFailure(error, phase: "resolveItemURL")
+ throw error
+ }
+ guard candidate.lastPathComponent.precomposedStringWithCanonicalMapping == name.precomposedStringWithCanonicalMapping else { return nil }
+ let parentMatches: Bool
+ do { parentMatches = try await matchesParent(candidate.deletingLastPathComponent()) }
+ catch {
+ recordLookupFailure(error, phase: "bindDestinationParent")
+ throw error
+ }
+ guard parentMatches else { return nil }
+ return candidate
+ }
+
+ private static func recordLookupFailure(_ error: any Error, phase: String) {
+ // Closed call-site labels and numeric codes only: a lookup error can
+ // carry the local URL in its description or user-info.
+ print("finder stability: destination lookup failed; phase=\(phase) class=\(ProviderDiagnosticErrorClassifier.classify(error).rawValue) code=\((error as NSError).code)")
+ }
+
+ /// A completed UI-originated callback gates verification. Active-item 404
+ /// while restoration becomes visible is pending, never positive evidence.
+ static func observe(callbackCompleted: Bool, expected: KDriveRemoteItem,
+ readActive: () async throws -> KDriveRemoteItem) async throws -> KDriveRemoteItem? {
+ guard callbackCompleted else { return nil }
+ do {
+ let current = try await readActive()
+ guard current.id == expected.id, current.driveID == expected.driveID,
+ current.parentID == expected.parentID else { throw FinderLiveError.unsafeTarget }
+ return current
+ } catch where KDriveRemoteErrorClassifier.isNotFound(error) {
+ return nil
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderRunnerPanel.swift b/potassiumProvider/FinderRunnerPanel.swift
new file mode 100644
index 0000000..c4d17a5
--- /dev/null
+++ b/potassiumProvider/FinderRunnerPanel.swift
@@ -0,0 +1,25 @@
+#if os(macOS) && STABILITY
+import AppKit
+
+@MainActor
+enum FinderRunnerPanel {
+ /// The caller retains its run lease and recorder while the sheet is open.
+ static func awaitResume(message: String) async -> Bool {
+ let panel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: 480, height: 100), styleMask: [.titled], backing: .buffered, defer: false)
+ panel.title = "Finder Stability — run paused"
+ panel.center()
+ panel.makeKeyAndOrderFront(nil)
+ NSApp.activate()
+ let alert = NSAlert()
+ alert.messageText = "This run is still being monitored"
+ alert.informativeText = message
+ alert.addButton(withTitle: "Recheck and continue")
+ alert.addButton(withTitle: "Stop run")
+ let response = await withCheckedContinuation { continuation in
+ alert.beginSheetModal(for: panel) { continuation.resume(returning: $0) }
+ }
+ panel.close()
+ return response == .alertFirstButtonReturn
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderStabilityCommand.swift b/potassiumProvider/FinderStabilityCommand.swift
new file mode 100644
index 0000000..e2fe310
--- /dev/null
+++ b/potassiumProvider/FinderStabilityCommand.swift
@@ -0,0 +1,339 @@
+#if os(macOS) && STABILITY
+import Darwin
+import AppKit
+import Foundation
+import PotassiumProviderCore
+
+enum FinderStabilityCommandLine {
+ nonisolated static let commandFlag = "--finder-stability"
+
+ nonisolated static func shouldHandle(arguments: [String]) -> Bool {
+ arguments.dropFirst().contains(commandFlag)
+ }
+
+ static func runInCurrentProcess(arguments: [String]) -> Int32 {
+ signal(SIGPIPE, SIG_IGN)
+ setvbuf(stdout, nil, _IOLBF, 0)
+ let application = NSApplication.shared
+ application.setActivationPolicy(.regular)
+ var exitCode: Int32?
+ Task { @MainActor in
+ exitCode = await run(arguments: arguments)
+ application.stop(nil)
+ if let wake = NSEvent.otherEvent(with: .applicationDefined, location: .zero,
+ modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, subtype: 0, data1: 0, data2: 0) {
+ application.postEvent(wake, atStart: true)
+ }
+ }
+ // NSApplication dispatches panel and permission events while Swift
+ // concurrency drives the suite. A bare CFRunLoop cannot operate sheets.
+ application.run()
+ return exitCode ?? 1
+ }
+
+ @MainActor
+ static func run(arguments: [String]) async -> Int32 {
+ await run(
+ arguments: arguments,
+ executor: SystemFinderStabilityCommandExecutor()
+ )
+ }
+
+ @MainActor
+ static func run(
+ arguments: [String],
+ executor: any FinderStabilityCommandExecuting
+ ) async -> Int32 {
+ do {
+ switch try FinderStabilityArgumentParser.parse(arguments: arguments) {
+ case .notRequested:
+ return 0
+ case .help:
+ print(usage)
+ return 0
+ case .execute(let options):
+ let result: FinderStabilityCommandResult
+ switch options.mode {
+ case .preflight:
+ result = await executor.preflight(
+ requestPermissions: options.requestPermissions
+ )
+ case .run:
+ result = await executor.run(requestPermissions: options.requestPermissions,
+ extensionLaunchMode: options.extensionLaunchMode,
+ includePermanentDeletion: options.includePermanentDeletion)
+ case .conflicts:
+ result = await executor.conflicts(requestPermissions: options.requestPermissions, selectedCase: options.conflictCase, extensionLaunchMode: options.extensionLaunchMode)
+ case .recover:
+ result = await executor.recoverStaleRun()
+ case .provision:
+ result = await executor.provision()
+ case .watch:
+ result = await executor.watch()
+ }
+ print(result.safeConsoleDescription)
+ return result.exitCode
+ }
+ } catch let error as FinderStabilityArgumentError {
+ fputs("finder stability command rejected: \(error.safeDescription)\n", stderr)
+ return 2
+ } catch {
+ fputs("finder stability command failed safely\n", stderr)
+ return 1
+ }
+ }
+
+ nonisolated static var usage: String {
+ """
+ Usage:
+ potassiumProvider --finder-stability provision --yes-live
+ potassiumProvider --finder-stability watch
+ potassiumProvider --finder-stability preflight [--request-permissions]
+ potassiumProvider --finder-stability run --yes-live [--include-permanent-deletion] [--extension-state fresh|running] [--request-permissions]
+ potassiumProvider --finder-stability conflicts --yes-live [--case CASE] [--extension-state fresh|running] [--request-permissions]
+ potassiumProvider --finder-stability recover --yes-recover
+
+ Options:
+ preflight Verify the Stability build, lab, File Provider domain, and macOS consent without Finder or remote mutation.
+ run Execute the verified, lab-scoped Finder scenario sequence and seal its evidence bundle.
+ recover Preserve and abandon evidence owned by a Finder runner process that is no longer alive.
+ --yes-live Required for run, provision, or conflicts. Confirms use of the saved development account and disposable lab root.
+ --yes-recover Required for recover. Performs local evidence lifecycle recovery only.
+ --request-permissions Ask macOS to present Accessibility, screen recording, and Finder Automation consent prompts when needed.
+ --include-permanent-deletion Include scenario 12 and its exact-item confirmation. Default: defer deletion and continue scenarios 13–16; exit 4 if all selected scenarios pass.
+ """
+ }
+}
+
+enum FinderStabilityCommandMode: String, Equatable, Sendable {
+ case preflight
+ case run
+ case recover
+ case provision
+ case watch
+ case conflicts
+}
+
+struct FinderStabilityCommandOptions: Equatable, Sendable {
+ let mode: FinderStabilityCommandMode
+ let requestPermissions: Bool
+ var conflictCase: StabilityLiveConflictCase? = nil
+ var extensionLaunchMode: StabilityExtensionLaunchMode? = nil
+ var includePermanentDeletion = false
+}
+
+enum FinderStabilityArgumentParseResult: Equatable, Sendable {
+ case notRequested
+ case help
+ case execute(FinderStabilityCommandOptions)
+}
+
+enum FinderStabilityArgumentError: Error, Equatable, Sendable {
+ case missingMode
+ case duplicateMode
+ case liveConfirmationRequired
+ case liveConfirmationNotAllowedForPreflight
+ case liveConfirmationNotAllowedForRecovery
+ case recoveryConfirmationRequired
+ case recoveryConfirmationNotAllowed
+ case permissionRequestNotAllowedForRecovery
+ case unknownOption
+
+ var safeDescription: String {
+ switch self {
+ case .missingMode:
+ "choose preflight, run, conflicts, provision, watch, or recover"
+ case .duplicateMode:
+ "choose exactly one mode"
+ case .liveConfirmationRequired:
+ "run, provision, and conflicts require --yes-live"
+ case .liveConfirmationNotAllowedForPreflight:
+ "--yes-live is accepted only with run, provision, or conflicts"
+ case .liveConfirmationNotAllowedForRecovery:
+ "--yes-live is not accepted with recover"
+ case .recoveryConfirmationRequired:
+ "recover requires --yes-recover"
+ case .recoveryConfirmationNotAllowed:
+ "--yes-recover is accepted only with recover"
+ case .permissionRequestNotAllowedForRecovery:
+ "--request-permissions is not accepted with recover"
+ case .unknownOption:
+ "unknown option"
+ }
+ }
+}
+
+enum FinderStabilityArgumentParser {
+ nonisolated static func parse(arguments: [String]) throws -> FinderStabilityArgumentParseResult {
+ guard let flagIndex = arguments.dropFirst().firstIndex(of: FinderStabilityCommandLine.commandFlag) else {
+ return .notRequested
+ }
+ let commandArguments = arguments[arguments.index(after: flagIndex)...]
+ if commandArguments.contains("--help") || commandArguments.contains("-h") {
+ return .help
+ }
+
+ var mode: FinderStabilityCommandMode?
+ var requestPermissions = false
+ var confirmedLiveRun = false
+ var confirmedRecovery = false
+ var conflictCase: StabilityLiveConflictCase?
+ var expectsCase = false
+ var extensionLaunchMode: StabilityExtensionLaunchMode?
+ var expectsLaunchMode = false
+ var includePermanentDeletion = false
+ for argument in commandArguments {
+ if expectsLaunchMode {
+ guard let value = StabilityExtensionLaunchMode(rawValue: argument), extensionLaunchMode == nil else { throw FinderStabilityArgumentError.unknownOption }
+ extensionLaunchMode = value; expectsLaunchMode = false; continue
+ }
+ if expectsCase {
+ guard let value = StabilityLiveConflictCase(rawValue: argument), conflictCase == nil else { throw FinderStabilityArgumentError.unknownOption }
+ conflictCase = value; expectsCase = false; continue
+ }
+ switch argument {
+ case FinderStabilityCommandMode.provision.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .provision
+ case FinderStabilityCommandMode.watch.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .watch
+ case FinderStabilityCommandMode.preflight.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .preflight
+ case FinderStabilityCommandMode.run.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .run
+ case FinderStabilityCommandMode.recover.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .recover
+ case "--extension-state":
+ expectsLaunchMode = true
+ case "--case":
+ expectsCase = true
+ case FinderStabilityCommandMode.conflicts.rawValue:
+ guard mode == nil else { throw FinderStabilityArgumentError.duplicateMode }
+ mode = .conflicts
+ case "--request-permissions":
+ requestPermissions = true
+ case "--yes-live":
+ confirmedLiveRun = true
+ case "--yes-recover":
+ confirmedRecovery = true
+ case "--include-permanent-deletion":
+ guard !includePermanentDeletion else { throw FinderStabilityArgumentError.unknownOption }
+ includePermanentDeletion = true
+ default:
+ throw FinderStabilityArgumentError.unknownOption
+ }
+ }
+
+ guard let mode else { throw FinderStabilityArgumentError.missingMode }
+ guard !expectsCase, !expectsLaunchMode,
+ !includePermanentDeletion || mode == .run,
+ conflictCase == nil || mode == .conflicts,
+ extensionLaunchMode == nil || mode == .conflicts || mode == .run else { throw FinderStabilityArgumentError.unknownOption }
+ switch mode {
+ case .conflicts where !confirmedLiveRun:
+ throw FinderStabilityArgumentError.liveConfirmationRequired
+ case .conflicts where confirmedRecovery:
+ throw FinderStabilityArgumentError.recoveryConfirmationNotAllowed
+ case .watch where confirmedLiveRun || confirmedRecovery || requestPermissions:
+ throw FinderStabilityArgumentError.unknownOption
+ case .provision where confirmedRecovery || requestPermissions:
+ throw FinderStabilityArgumentError.unknownOption
+ case .preflight where confirmedLiveRun:
+ throw FinderStabilityArgumentError.liveConfirmationNotAllowedForPreflight
+ case .preflight where confirmedRecovery:
+ throw FinderStabilityArgumentError.recoveryConfirmationNotAllowed
+ case .run where confirmedLiveRun == false, .provision where confirmedLiveRun == false:
+ throw FinderStabilityArgumentError.liveConfirmationRequired
+ case .run where confirmedRecovery:
+ throw FinderStabilityArgumentError.recoveryConfirmationNotAllowed
+ case .recover where confirmedRecovery == false:
+ throw FinderStabilityArgumentError.recoveryConfirmationRequired
+ case .recover where confirmedLiveRun:
+ throw FinderStabilityArgumentError.liveConfirmationNotAllowedForRecovery
+ case .recover where requestPermissions:
+ throw FinderStabilityArgumentError.permissionRequestNotAllowedForRecovery
+ default:
+ break
+ }
+ return .execute(FinderStabilityCommandOptions(
+ mode: mode,
+ requestPermissions: requestPermissions, conflictCase: conflictCase, extensionLaunchMode: extensionLaunchMode,
+ includePermanentDeletion: includePermanentDeletion
+ ))
+ }
+}
+
+enum FinderStabilityCommandResult: Equatable, Sendable {
+ case ready
+ case completed
+ case completedWithDeferredDeletion
+ case recovered
+ case checkpoint
+ case rejected
+ case failed
+
+ var exitCode: Int32 {
+ switch self {
+ case .ready, .completed, .recovered:
+ 0
+ case .checkpoint:
+ 3
+ case .completedWithDeferredDeletion:
+ 4
+ case .rejected:
+ 2
+ case .failed:
+ 1
+ }
+ }
+
+ var safeConsoleDescription: String {
+ switch self {
+ case .ready:
+ "finder stability preflight: ready"
+ case .completed:
+ "finder stability run: evidence bundle sealed"
+ case .completedWithDeferredDeletion:
+ "finder stability run: 15 scenarios passed; permanent deletion deferred; evidence bundle sealed; full acceptance incomplete"
+ case .recovered:
+ "finder stability recovery: stale run preserved and released"
+ case .checkpoint:
+ "finder stability checkpoint: macOS consent or variable Finder UI requires operator action"
+ case .rejected:
+ "finder stability rejected: lab or domain safety evidence did not pass"
+ case .failed:
+ "finder stability failed: inspect the redacted Stability evidence bundle"
+ }
+ }
+}
+
+@MainActor
+protocol FinderStabilityCommandExecuting {
+ func preflight(requestPermissions: Bool) async -> FinderStabilityCommandResult
+ func run(requestPermissions: Bool) async -> FinderStabilityCommandResult
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?, includePermanentDeletion: Bool) async -> FinderStabilityCommandResult
+ func conflicts(requestPermissions: Bool, selectedCase: StabilityLiveConflictCase?, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult
+ func recoverStaleRun() async -> FinderStabilityCommandResult
+ func provision() async -> FinderStabilityCommandResult
+ func watch() async -> FinderStabilityCommandResult
+}
+extension FinderStabilityCommandExecuting {
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?, includePermanentDeletion: Bool) async -> FinderStabilityCommandResult {
+ // Older injected executors cannot silently ignore explicit deletion selection.
+ guard !includePermanentDeletion else { return .rejected }
+ return await run(requestPermissions: requestPermissions, extensionLaunchMode: extensionLaunchMode)
+ }
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult {
+ guard extensionLaunchMode == nil else { return .rejected }
+ return await run(requestPermissions: requestPermissions)
+ }
+ func conflicts(requestPermissions: Bool, selectedCase: StabilityLiveConflictCase?, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult { .rejected }
+ func provision() async -> FinderStabilityCommandResult { .rejected }
+ func watch() async -> FinderStabilityCommandResult { .rejected }
+}
+#endif
diff --git a/potassiumProvider/FinderStabilityScenarioSelection.swift b/potassiumProvider/FinderStabilityScenarioSelection.swift
new file mode 100644
index 0000000..7c59744
--- /dev/null
+++ b/potassiumProvider/FinderStabilityScenarioSelection.swift
@@ -0,0 +1,22 @@
+#if os(macOS) && STABILITY
+import PotassiumProviderCore
+
+/// Selection happens before beginning a step, binding a target, or invoking UI.
+/// Deferral never changes the failure state of the subsequent scenarios.
+struct FinderStabilityScenarioSelection {
+ var conflictCase: StabilityLiveConflictCase? = nil
+ var includePermanentDeletion = false
+
+ func skipReason(for scenario: StabilityFinderScenario, afterFailure: Bool) -> StabilityFinderStepSkipReason? {
+ if let conflictCase, scenario != conflictCase.scenario { return .notSelectedForConflictProfile }
+ // These cases prepare their own fixture and repeat the normal safety preflight.
+ // Keep the earlier failed result; independence does not turn it into a pass.
+ let independent = conflictCase == nil && [.concurrentRemotePreserveBoth, .cancellationAndProgress, .workingSetRefresh, .supportedContextualActions].contains(scenario)
+ if afterFailure && !independent { return .earlierStepFailure }
+ if conflictCase == nil, scenario == .permanentDeletion, !includePermanentDeletion {
+ return .permanentDeletionNotSelected
+ }
+ return nil
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderTextEditSequence.swift b/potassiumProvider/FinderTextEditSequence.swift
new file mode 100644
index 0000000..7ca786f
--- /dev/null
+++ b/potassiumProvider/FinderTextEditSequence.swift
@@ -0,0 +1,27 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+/// Logical menu actions deliberately have no physical keyboard key codes.
+enum FinderTextEditAction: Equatable {
+ case selectAll, paste, save
+ var menuTitle: String { self == .save ? "File" : "Edit" }
+ var itemTitle: String {
+ switch self { case .selectAll: "Select All"; case .paste: "Paste"; case .save: "Save" }
+ }
+}
+
+@MainActor
+enum FinderTextEditSequence {
+ static func execute(menu: (FinderTextEditAction) async throws -> Void,
+ verifyText: () async throws -> Void,
+ verifySave: () async throws -> Void) async throws {
+ try await menu(.selectAll)
+ try await menu(.paste)
+ // A lost focus or ignored paste must never cause Save to succeed with
+ // the original bytes and be treated as a completed edit.
+ try await verifyText()
+ try await menu(.save)
+ try await verifySave()
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderToolbarActionTarget.swift b/potassiumProvider/FinderToolbarActionTarget.swift
new file mode 100644
index 0000000..f4c36cf
--- /dev/null
+++ b/potassiumProvider/FinderToolbarActionTarget.swift
@@ -0,0 +1,27 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+
+enum FinderToolbarActionTarget {
+ // Live Finder's toolbar omits provider commands and selected-item deletion.
+ // Use it only for the two built-in download commands verified through it.
+ static func supports(command: String) -> Bool {
+ ["Remove Download", "Download Now"].contains(command)
+ }
+
+ struct Button {
+ let description: String?
+ let enabled: Bool
+ let bounds: CGRect?
+ }
+
+ /// Candidates must come from the one toolbar of the bound Finder window.
+ static func index(window: CGRect, toolbar: CGRect, buttons: [Button]) -> Int? {
+ guard window.contains(toolbar) else { return nil }
+ let matches = buttons.indices.filter { buttons[$0].description == "Action" }
+ guard matches.count == 1, let index = matches.first, buttons[index].enabled,
+ let bounds = buttons[index].bounds, bounds.width > 1, bounds.height > 1,
+ toolbar.contains(bounds) else { return nil }
+ return index
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderTransferCancellationTarget.swift b/potassiumProvider/FinderTransferCancellationTarget.swift
new file mode 100644
index 0000000..daff260
--- /dev/null
+++ b/potassiumProvider/FinderTransferCancellationTarget.swift
@@ -0,0 +1,22 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+import Foundation
+
+enum FinderTransferCancellationTarget {
+ struct Indicator {
+ let fraction: Double?
+ let bounds: CGRect?
+ }
+
+ /// Finder exposes its transfer ring as AXProgressIndicator, without an AX
+ /// Cancel button. A native hit is allowed only on the one active indicator
+ /// inside the independently bound row/window. The callback proves cancellation.
+ static func point(window: CGRect, row: CGRect, indicators: [Indicator]) -> CGPoint? {
+ guard window.contains(row), indicators.count == 1, let indicator = indicators.first,
+ let fraction = indicator.fraction, fraction.isFinite, fraction > 0, fraction < 1,
+ let bounds = indicator.bounds, bounds.width > 1, bounds.height > 1,
+ row.contains(bounds) else { return nil }
+ return CGPoint(x: bounds.midX, y: bounds.midY)
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderTransferObservation.swift b/potassiumProvider/FinderTransferObservation.swift
new file mode 100644
index 0000000..cd35c1c
--- /dev/null
+++ b/potassiumProvider/FinderTransferObservation.swift
@@ -0,0 +1,48 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+
+/// Tracks one UI download attempt, from a cursor registered before dispatch.
+/// Correlation alone cannot associate progress with the selected fixture.
+@MainActor
+final class FinderTransferObservation {
+ let subject: UUID
+ let correlation: UUID
+ let codeHash: String
+ private var events: [ProviderDiagnosticEvent] = []
+ private var fetch: ProviderDiagnosticEvent?
+
+ init(subject: UUID, correlation: UUID, codeHash: String) {
+ self.subject = subject
+ self.correlation = correlation
+ self.codeHash = codeHash
+ }
+
+ func ingest(_ newEvents: [ProviderDiagnosticEvent]) throws {
+ events += newEvents.filter { $0.subjectAlias == subject && $0.correlationID == correlation &&
+ $0.processCodeHash == codeHash && $0.source == .fileProviderExtension &&
+ [.fetchContents, .downloadFile].contains($0.operation) }
+ let starts = events.filter { $0.operation == .fetchContents && $0.phase == .started }
+ guard starts.count <= 1 else { throw StabilityLiveEvidenceError.contradictoryTerminal }
+ fetch = starts.first
+ let terminals = related.filter { $0.operation == .fetchContents && [.completed, .cancelled, .failed].contains($0.phase) }
+ guard terminals.count <= 1 else { throw StabilityLiveEvidenceError.contradictoryTerminal }
+ guard !related.contains(where: { $0.phase == .failed }) else { throw StabilityLiveEvidenceError.unexpectedFailure }
+ }
+
+ private var related: [ProviderDiagnosticEvent] {
+ guard let fetch, let span = fetch.spanID, let process = fetch.processInstanceID else { return [] }
+ return events.filter { $0.processInstanceID == process &&
+ (($0.operation == .fetchContents && $0.spanID == span) ||
+ ($0.operation == .downloadFile && $0.parentSpanID == span)) }
+ }
+
+ var hasIntermediateProgress: Bool {
+ related.contains { $0.phase == .progress && (1..<100).contains($0.progressPercentBucket ?? 0) }
+ }
+ var downloadFinished: Bool { related.contains { $0.phase == .completed } }
+ var fetchCompleted: Bool { related.contains { $0.operation == .fetchContents && $0.phase == .completed } }
+ var fetchCancelled: Bool { related.contains { $0.operation == .fetchContents && $0.phase == .cancelled } }
+ var canCancel: Bool { hasIntermediateProgress && !downloadFinished && !fetchCancelled }
+}
+#endif
diff --git a/potassiumProvider/FinderTrashedItemSequence.swift b/potassiumProvider/FinderTrashedItemSequence.swift
new file mode 100644
index 0000000..8c1f426
--- /dev/null
+++ b/potassiumProvider/FinderTrashedItemSequence.swift
@@ -0,0 +1,16 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+@MainActor
+enum FinderTrashedItemSequence {
+ /// Navigation can take time. Bind the exact provider identity again after
+ /// revealing its parent and before any selected-item action.
+ static func execute(reveal: () async throws -> Void,
+ revalidate: () async throws -> Void,
+ action: () async throws -> Void) async throws {
+ try await reveal()
+ try await revalidate()
+ try await action()
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderUIDriver.swift b/potassiumProvider/FinderUIDriver.swift
new file mode 100644
index 0000000..b04e87c
--- /dev/null
+++ b/potassiumProvider/FinderUIDriver.swift
@@ -0,0 +1,1332 @@
+#if os(macOS) && STABILITY
+import AppKit
+import ApplicationServices
+import ScreenCaptureKit
+import Darwin
+import PotassiumProviderCore
+
+@MainActor
+protocol FinderUINavigating: AnyObject {
+ func navigate(to url: URL) async throws
+ func navigateHistory(back: Bool, expectedURL: URL) async throws
+ func navigateParent(expectedURL: URL) async throws
+}
+
+@MainActor
+protocol FinderDocumentUIDriving: AnyObject {
+ func edit(_ url: URL, contents: String?) async throws
+ func closeOwnedEditorDocuments() async throws
+}
+
+@MainActor
+protocol FinderUIDriving: FinderUINavigating, FinderDocumentUIDriving {
+ var actionCount: Int { get }
+ func useDeadline(_ remaining: @escaping @MainActor () -> Duration)
+ func expectActionPanel(for alias: UUID)
+ func closeOwnedWindows() async throws
+ func select(_ url: URL) async throws
+ func revealTrashedItem(_ url: URL) async throws
+ func contains(_ url: URL) async throws -> Bool
+ func createFolder(named name: String, in parent: URL) async throws
+ func copy(_ source: URL, to parent: URL) async throws
+ func rename(_ source: URL, to name: String) async throws
+ func move(_ source: URL, to parent: URL) async throws
+ func contextAction(_ title: String, on url: URL) async throws
+ func hasContextAction(_ title: String, on url: URL) async throws -> Bool
+ func trash(_ url: URL) async throws
+ func cancelDownload(_ url: URL, whilePending: () throws -> Bool) async throws -> Bool
+ func copyAndCancelDownload(_ source: URL, to parent: URL,
+ observation: () throws -> FinderCopyCancellationState) async throws -> Bool
+ func confirmPermanentDeletion(_ url: URL, fixtureAlias: UUID) async throws
+ func panelAction(_ action: FinderPanelAction) async throws
+ func capture(in directory: URL, sequence: Int) async throws
+ func captureFailure(in directory: URL, sequence: Int) async throws
+}
+
+enum FinderPanelAction {
+ case inheritAccess, createLink, toggleComments, saveLink, disableLink, confirmDisableLink, done
+ case restoreVersion(Int), confirmRestore, confirmSystemDeletion
+}
+
+enum FinderUIError: String, Error, Equatable {
+ case finderUnavailable, permissionRequired, windowMismatch, selectionMismatch
+ case controlUnavailable, timedOut, operatorCancelled, screenshotUnavailable
+ case editorUnavailable, automationFailed, finderBusy, evictionResourceBusy
+ case pointerTargetObstructed
+
+ var isEnvironmental: Bool { self == .evictionResourceBusy || self == .pointerTargetObstructed }
+}
+
+private enum FinderAppleEventOperation: String {
+ case observe, navigate, createWindow, changeView, activate, select
+}
+
+/// Owns a dedicated Finder window. Apple Events address its stable window ID;
+/// Accessibility actions resolve fresh elements and never reuse row indices.
+@MainActor
+final class SystemFinderUIDriver: FinderUIDriving {
+ private(set) var actionCount = 0
+ private var windowID: Int32?
+ private var currentURL: URL?
+ private var selectionURL: URL?
+ private var windowOwner: FinderWindowOwnership?
+ private var menuIsOpen = false
+ private var windowsBeforeAction: [AXUIElement] = []
+ private var windowBeforeAction: AXUIElement?
+ private var awaitingEvictionResult = false
+ private var remainingTime: @MainActor () -> Duration = { .seconds(90) }
+ private var actionPanelIdentifier: String?
+ private var selectedDeletionRequested = false
+ private var deletionDialogExpectation: FinderDeletionDialogExpectation?
+ private var lastDeletionDialogObservation: String?
+ private var lastRowObservation: String?
+ private var lastActionPanelObservation: String?
+ private var ownedEditorDocuments: [URL: FinderProcessIdentity] = [:]
+
+ func useDeadline(_ remaining: @escaping @MainActor () -> Duration) { remainingTime = remaining }
+ func expectActionPanel(for alias: UUID) { actionPanelIdentifier = "provider.stability.action." + alias.uuidString }
+
+ func closeOwnedWindows() async throws {
+ let deadline = StabilityDeadline(budget: .seconds(90))
+ remainingTime = { deadline.remaining() }
+ var failure: Error?
+ do {
+ if actionPanelScope() != nil {
+ try await pressControl(title: "Done")
+ try await wait { self.actionPanelScope() == nil }
+ }
+ } catch { failure = error }
+ do { try await closeOwnedEditorDocuments() } catch { failure = error }
+ do { try await closeOwnedFinderWindows() } catch { failure = failure ?? error }
+ if let failure { throw failure }
+ }
+
+ func closeOwnedEditorDocuments() async throws {
+ for (url, owner) in ownedEditorDocuments {
+ guard processIdentity(pid: owner.pid) == owner,
+ let editor = NSRunningApplication(processIdentifier: owner.pid),
+ editor.bundleIdentifier == "com.apple.TextEdit" else {
+ ownedEditorDocuments[url] = nil; continue
+ }
+ guard let document = editorDocument(url, editor: editor) else {
+ ownedEditorDocuments[url] = nil; continue
+ }
+ _ = AXUIElementPerformAction(document, kAXRaiseAction as CFString)
+ editor.activate()
+ try await wait { NSWorkspace.shared.frontmostApplication?.processIdentifier == editor.processIdentifier }
+ if let close = attribute(document, kAXCloseButtonAttribute), CFGetTypeID(close) == AXUIElementGetTypeID(),
+ (attribute(close as! AXUIElement, kAXEditedAttribute) as? Bool) == true {
+ // Preserve the generated document's current bytes on failure;
+ // never dismiss a save prompt by discarding or quitting TextEdit.
+ try await editorMenu("File", item: "Save", documentURL: url, editor: editor)
+ try await wait {
+ guard let fresh = self.editorDocument(url, editor: editor),
+ let button = self.attribute(fresh, kAXCloseButtonAttribute), CFGetTypeID(button) == AXUIElementGetTypeID() else { return false }
+ return (self.attribute(button as! AXUIElement, kAXEditedAttribute) as? Bool) == false
+ }
+ }
+ guard let fresh = editorDocument(url, editor: editor),
+ let button = attribute(fresh, kAXCloseButtonAttribute), CFGetTypeID(button) == AXUIElementGetTypeID(),
+ AXUIElementPerformAction(button as! AXUIElement, kAXPressAction as CFString) == .success else { throw FinderUIError.editorUnavailable }
+ try await wait { self.editorDocument(url, editor: editor) == nil }
+ ownedEditorDocuments[url] = nil
+ }
+ }
+
+ private func closeOwnedFinderWindows(preservingDeadline: Bool = false) async throws {
+ if !preservingDeadline {
+ let cleanupDeadline = StabilityDeadline(budget: .seconds(90))
+ remainingTime = { cleanupDeadline.remaining() }
+ }
+ guard let ownership = windowOwner else { return }
+ guard ownership.process == finderProcessIdentity() else {
+ windowID = nil; windowOwner = nil; currentURL = nil; selectionURL = nil; menuIsOpen = false
+ return
+ }
+ if menuIsOpen { try await dismissContextMenu(allowMissingWindow: true) }
+ try ownership.close(currentProcess: finderProcessIdentity()) { identifier in
+ // A manually closed window is already clean. Never close by title,
+ // front-window position, or a window from a relaunched Finder.
+ _ = try script("if exists Finder window id \(identifier) then close Finder window id \(identifier)")
+ }
+ windowID = nil
+ windowOwner = nil
+ currentURL = nil
+ selectionURL = nil
+ }
+
+ private func finderProcessIdentity() -> FinderProcessIdentity? {
+ guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else { return nil }
+ return processIdentity(pid: finder.processIdentifier)
+ }
+
+ private func processIdentity(pid: Int32) -> FinderProcessIdentity? {
+ // The kernel start time distinguishes PID reuse, even when
+ // LaunchServices omits the application's launch date.
+ var info = proc_bsdinfo()
+ let size = MemoryLayout.stride
+ guard proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, Int32(size)) == size else { return nil }
+ let launchedAt = Date(timeIntervalSince1970: Double(info.pbi_start_tvsec) + Double(info.pbi_start_tvusec) / 1_000_000)
+ return FinderProcessIdentity(pid: pid, launchedAt: launchedAt)
+ }
+
+ func navigate(to url: URL) async throws {
+ // Navigation binds the owned window and destination. A previous file
+ // selection may already have moved or been renamed by the provider.
+ selectionURL = nil
+ if windowID != nil {
+ try await navigateWithGoToFolder(url)
+ } else {
+ guard let owner = finderProcessIdentity() else { throw FinderUIError.finderUnavailable }
+ windowID = try script("set w to make new Finder window to (POSIX file \(quote(url.path)) as alias)\nreturn id of w", operation: .createWindow).int32Value
+ if let windowID { windowOwner = FinderWindowOwnership(windowID: windowID, process: owner) }
+ }
+ if let windowID { _ = try script("set current view of Finder window id \(windowID) to list view", operation: .changeView) }
+ currentURL = url
+ selectionURL = nil
+ try await wait { try self.verifyWindow(url) }
+ actionCount += 1
+ }
+
+ private func navigateWithGoToFolder(_ url: URL) async throws {
+ // Finder can acknowledge an Apple Event target change while its list
+ // remains busy indefinitely. Exercise Finder's normal navigation sheet
+ // and verify the original window identity after the UI accepts the path.
+ var phase = "activateOwnedWindow"
+ do {
+ try await activateFinder()
+ guard let window = finderWindowAX() else { throw FinderUIError.windowMismatch }
+ phase = "openNavigationSheet"
+ try key(5, flags: [.maskCommand, .maskShift]) // Go to Folder
+ var field: AXUIElement?
+ try await wait {
+ let sheets = self.elements(window).filter { self.string($0, kAXRoleAttribute) == kAXSheetRole }
+ guard sheets.count == 1, let sheet = sheets.first else { return false }
+ let fields = self.elements(sheet).filter { self.string($0, kAXRoleAttribute) == kAXTextFieldRole }
+ guard fields.count == 1, let candidate = fields.first else { return false }
+ field = candidate
+ return true
+ }
+ phase = "setNavigationPath"
+ guard let field, AXUIElementSetAttributeValue(field, kAXValueAttribute as CFString, url.path as CFString) == .success else {
+ throw FinderUIError.controlUnavailable
+ }
+ try await wait { self.string(field, kAXValueAttribute) == url.path }
+ phase = "submitNavigationPath"
+ try key(36)
+ phase = "verifyNavigationDestination"
+ var reportedPendingDestination = false
+ try await wait {
+ let matches = try self.verifyWindow(url)
+ if !matches && !reportedPendingDestination {
+ reportedPendingDestination = true
+ let sheets = self.elements(window).filter { self.string($0, kAXRoleAttribute) == kAXSheetRole }
+ let stillPrevious = self.currentURL.map { (try? self.verifyWindow($0)) == true } ?? false
+ print("finder stability UI: navigation pending; sheetCount=\(sheets.count) stillPreviousDestination=\(stillPrevious)")
+ }
+ return matches
+ }
+ print("finder stability UI: Go to Folder navigation verified")
+ } catch {
+ // Closed stage labels only. Paths, sheet contents, and error
+ // descriptions can contain generated or unrelated private data.
+ print("finder stability UI: navigation failed; phase=\(phase)")
+ throw error
+ }
+ }
+
+ func revealTrashedItem(_ url: URL) async throws {
+ guard let windowID, let ownership = windowOwner,
+ ownership.process == finderProcessIdentity() else { throw FinderUIError.windowMismatch }
+ let parent = url.deletingLastPathComponent()
+ selectionURL = nil
+ // Go to Folder can submit a provider Trash path without producing the
+ // requested target. Address the exact existing window through Finder's
+ // native target command, as used before navigation-sheet automation.
+ _ = try script("set target of Finder window id \(windowID) to (POSIX file \(quote(parent.path)) as alias)", operation: .navigate)
+ _ = try script("set current view of Finder window id \(windowID) to list view", operation: .changeView)
+ currentURL = parent
+ try await wait { try self.verifyWindow(parent) }
+ actionCount += 1
+ print("finder stability UI: bound Trash parent navigation verified")
+ }
+
+ func navigateHistory(back: Bool, expectedURL: URL) async throws {
+ try await activateFinder()
+ try key(back ? 33 : 30, flags: .maskCommand) // [ / ]
+ try await wait { try self.verifyWindow(expectedURL) }
+ currentURL = expectedURL
+ selectionURL = nil
+ actionCount += 1
+ }
+
+ func navigateParent(expectedURL: URL) async throws {
+ try await activateFinder()
+ try key(126, flags: .maskCommand)
+ try await wait { try self.verifyWindow(expectedURL) }
+ currentURL = expectedURL
+ selectionURL = nil
+ actionCount += 1
+ }
+
+ func select(_ url: URL) async throws {
+ let parent = url.deletingLastPathComponent()
+ if !FinderUIURLIdentity.matches(currentURL, parent) { try await navigate(to: parent) }
+ selectionURL = nil
+ lastRowObservation = nil
+ try await activateFinder()
+ try await FinderSelectionSequence.execute(waitUntilVisible: {
+ print("finder stability UI: waiting for generated selection row")
+ do { try await self.wait { try await self.contains(url) } }
+ catch {
+ // Use the last live observation; querying after the deadline
+ // would fail its guard and fabricate an apparent binding loss.
+ print("finder stability UI: last row observation \(self.lastRowObservation ?? "unavailable")")
+ throw error
+ }
+ }, assignSelection: {
+ guard let windowID = self.windowID, try self.verifyWindow(parent),
+ try self.script("get id of front Finder window").int32Value == windowID else {
+ throw FinderUIError.windowMismatch
+ }
+ // `select file` can reveal it in another Finder window. Assign the
+ // selection of the already verified front window without revealing.
+ print("finder stability UI: generated row visible; assigning selection")
+ _ = try self.script("set selection to {POSIX file \(self.quote(url.path)) as alias}", operation: .select)
+ self.selectionURL = url
+ }, waitUntilSelected: {
+ try await self.wait { try self.verifySelection(url) }
+ })
+ actionCount += 1
+ }
+
+ func contains(_ url: URL) async throws -> Bool {
+ guard let windowID else { throw FinderUIError.windowMismatch }
+ guard try verifyWindow(url.deletingLastPathComponent()) else { return false }
+ guard let window = finderWindowAX() else { throw FinderUIError.windowMismatch }
+ guard let displayedName = try displayedName(of: url) else { return false }
+ let nodes = elements(window)
+ let names = nodes.filter { string($0, kAXRoleAttribute) == kAXTextFieldRole }
+ .compactMap { string($0, kAXValueAttribute) }
+ let listView = try script("get current view of Finder window id \(windowID) is list view").booleanValue
+ let matches = names.filter { $0 == displayedName }.count
+ let namedElements = nodes.filter {
+ [string($0, kAXValueAttribute), string($0, kAXTitleAttribute), string($0, kAXDescriptionAttribute)].contains(displayedName)
+ }.count
+ let observation = "windowBound=true parentMatches=true listView=\(listView) matchingTextRows=\(matches) matchingNamedElements=\(namedElements)"
+ if lastRowObservation != observation {
+ print("finder stability UI: row observation " + observation)
+ lastRowObservation = observation
+ }
+ return FinderUINameObservation.hasUniqueMatch(displayedName: displayedName, rowNames: names)
+ }
+
+ func createFolder(named name: String, in parent: URL) async throws {
+ try await navigate(to: parent)
+ try await activateFinder()
+ guard let windowID else { throw FinderUIError.windowMismatch }
+ let before = try script("get URL of every item of target of Finder window id \(windowID)")
+ let priorURLs = (1...max(1, before.numberOfItems)).compactMap { before.atIndex($0)?.stringValue.flatMap(URL.init(string:)) }
+ try key(45, flags: [.maskCommand, .maskShift]) // N
+ try await wait {
+ let result = try self.script("set selectedItems to get selection\nif (count selectedItems) is not 1 then return {}\nreturn {URL of item 1 of selectedItems}")
+ guard result.numberOfItems == 1, let text = result.atIndex(1)?.stringValue, let created = URL(string: text),
+ FinderUIURLIdentity.matches(created.deletingLastPathComponent(), parent),
+ !priorURLs.contains(where: { FinderUIURLIdentity.matches($0, created) }) else { return false }
+ self.selectionURL = created
+ return true
+ }
+ try await enterFinderName(name)
+ try key(36)
+ try await wait { try await self.contains(parent.appendingPathComponent(name)) }
+ actionCount += 1
+ }
+
+ func copy(_ source: URL, to parent: URL) async throws {
+ try await select(source)
+ try await activateFinder()
+ try key(8, flags: .maskCommand)
+ try await navigate(to: parent)
+ try await activateFinder()
+ try key(9, flags: .maskCommand)
+ try await wait { try await self.contains(parent.appendingPathComponent(source.lastPathComponent)) }
+ actionCount += 1
+ }
+
+ func rename(_ source: URL, to name: String) async throws {
+ try await select(source)
+ try await activateFinder()
+ try key(36)
+ try await enterFinderName(name)
+ try key(36)
+ try await wait { try await self.contains(source.deletingLastPathComponent().appendingPathComponent(name)) }
+ actionCount += 1
+ }
+
+ func move(_ source: URL, to parent: URL) async throws {
+ try await select(source)
+ try await activateFinder()
+ try key(8, flags: .maskCommand)
+ try await navigate(to: parent)
+ try await activateFinder()
+ try key(9, flags: [.maskCommand, .maskAlternate])
+ try await wait { try await self.contains(parent.appendingPathComponent(source.lastPathComponent)) }
+ actionCount += 1
+ }
+
+ func trash(_ url: URL) async throws {
+ try await select(url)
+ try await activateFinder()
+ try key(51, flags: .maskCommand)
+ try await wait { try await self.contains(url) == false }
+ actionCount += 1
+ }
+
+ func contextAction(_ title: String, on url: URL) async throws {
+ try await select(url)
+ try await activateFinder()
+ windowsBeforeAction = attribute(finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []
+ windowBeforeAction = finderWindowAX()
+ awaitingEvictionResult = title == "Remove Download"
+ selectedDeletionRequested = title == "Delete Immediately…"
+ deletionDialogExpectation = nil
+ lastDeletionDialogObservation = nil
+ if selectedDeletionRequested {
+ guard let displayName = try displayedName(of: url), !displayName.isEmpty else { throw FinderUIError.selectionMismatch }
+ deletionDialogExpectation = FinderDeletionDialogExpectation(selectedURL: url, displayName: displayName)
+ print("finder stability UI: deletion display name bound; differsFromFilename=\(displayName != url.lastPathComponent)")
+ }
+ try await showSelectedContextMenu(for: title)
+ do {
+ var commandItem: AXUIElement?
+ var commandMenu: AXUIElement?
+ var lastMenuObservation: String?
+ var windowRefresh = FinderMenuWindowRefresh()
+ let menuOpenedAt = ContinuousClock.now
+ try await wait {
+ guard let menu = self.contextMenu() else {
+ let count = self.elements(self.finderAX()).filter { self.string($0, kAXRoleAttribute) == kAXMenuRole &&
+ self.rect($0).map { $0.width > 1 && $0.height > 1 } == true }.count
+ let observation = "visibleMenuCount=\(count)"
+ if observation != lastMenuObservation { print("finder stability UI: contextual menu pending; " + observation); lastMenuObservation = observation }
+ return false
+ }
+ let matches = (self.attribute(menu, kAXChildrenAttribute) as? [AXUIElement] ?? []).filter {
+ self.string($0, kAXRoleAttribute) == kAXMenuItemRole && self.string($0, kAXTitleAttribute) == title
+ }
+ let observation = "commandMatchCount=\(matches.count) enabled=\(matches.first.map { (self.attribute($0, kAXEnabledAttribute) as? Bool) != false } ?? false)"
+ if observation != lastMenuObservation { print("finder stability UI: contextual command observation; " + observation); lastMenuObservation = observation }
+ guard matches.count == 1, let item = matches.first else {
+ if windowRefresh.claim(command: title, matchingCommands: matches.count,
+ elapsed: menuOpenedAt.duration(to: .now)) {
+ try await self.dismissContextMenu()
+ guard try self.verifySelection(url), let oldWindow = self.finderWindowAX(),
+ !self.elements(oldWindow).contains(where: { self.string($0, kAXRoleAttribute) == kAXSheetRole }) else {
+ throw FinderUIError.windowMismatch
+ }
+ // Preserve this action's deadline and close only our
+ // recorded window ID, without touching editor documents.
+ try await self.closeOwnedFinderWindows(preservingDeadline: true)
+ try await self.navigate(to: url.deletingLastPathComponent())
+ try await self.select(url)
+ try await self.activateFinder()
+ self.windowsBeforeAction = self.attribute(self.finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []
+ self.windowBeforeAction = self.finderWindowAX()
+ try await self.showSelectedContextMenu(for: title)
+ lastMenuObservation = nil
+ print("finder stability UI: missing command retried in a new owned window")
+ }
+ return false
+ }
+ guard (self.attribute(item, kAXEnabledAttribute) as? Bool) != false else { return false }
+ commandItem = item; commandMenu = menu
+ return true
+ }
+ guard let item = commandItem, let menu = commandMenu else { throw FinderUIError.controlUnavailable }
+ let dispatchedAt = ContinuousClock.now
+ try await FinderMenuCommandSequence.perform(command: title, press: {
+ let result = AXUIElementPerformAction(item, kAXPressAction as CFString)
+ guard result == .success else {
+ print("finder stability UI: contextual press failed; AX code \(result.rawValue)")
+ throw FinderUIError.controlUnavailable
+ }
+ }, click: {
+ guard let menuBounds = self.rect(menu),
+ let point = FinderContextMenuTarget.point(displayedName: title, windowBounds: menuBounds,
+ fields: [.init(name: self.string(item, kAXTitleAttribute), bounds: self.rect(item))]),
+ NSWorkspace.shared.frontmostApplication?.bundleIdentifier == "com.apple.finder" else {
+ throw FinderUIError.selectionMismatch
+ }
+ guard try await FinderPointerClick.perform(at: point, button: .left, mayClick: {
+ guard self.remainingTime() > .zero,
+ let front = NSWorkspace.shared.frontmostApplication, front.bundleIdentifier == "com.apple.finder",
+ self.contextMenu().map({ CFEqual($0, menu) }) == true, self.rect(item)?.contains(point) == true else { return false }
+ try FinderPointerTarget.verify(at: point, processIdentifier: front.processIdentifier, scope: item)
+ return true
+ }) else { throw FinderUIError.selectionMismatch }
+ }, waitForDismissal: {
+ try await self.wait { self.contextMenu() == nil }
+ }, waitForResult: {
+ if ["Share kDrive Link…", "Version History…"].contains(title) {
+ try await self.wait { self.actionPanelScope() != nil }
+ } else if self.selectedDeletionRequested {
+ try await self.wait { self.selectedDeletionDialog() != nil }
+ } else { try await self.waitForFinderIdle() }
+ })
+ print("finder stability UI: contextual command invoked")
+ if title == "Download Now" {
+ print("finder stability UI: download dispatch returned; elapsed \(dispatchedAt.duration(to: ContinuousClock.now))")
+ }
+ menuIsOpen = false
+ actionCount += 1
+ } catch {
+ try? await dismissContextMenu()
+ throw error
+ }
+ }
+
+ func hasContextAction(_ title: String, on url: URL) async throws -> Bool {
+ try await select(url)
+ try await activateFinder()
+ try await showSelectedContextMenu(for: title)
+ do {
+ try await wait { self.contextMenu() != nil }
+ guard let menu = contextMenu() else { throw FinderUIError.controlUnavailable }
+ let found = (attribute(menu, kAXChildrenAttribute) as? [AXUIElement] ?? []).filter {
+ string($0, kAXRoleAttribute) == kAXMenuItemRole && string($0, kAXTitleAttribute) == title }
+ try await dismissContextMenu()
+ actionCount += 1
+ return found.count == 1
+ } catch {
+ try? await dismissContextMenu()
+ throw error
+ }
+ }
+
+ func edit(_ url: URL, contents: String?) async throws {
+ try await select(url)
+ _ = try script("open selection using application file id \"com.apple.TextEdit\"")
+ print("finder stability UI: TextEdit open requested")
+ try await wait {
+ guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.TextEdit").first else { return false }
+ let ax = AXUIElementCreateApplication(app.processIdentifier)
+ return self.elements(ax).contains { FinderUIURLIdentity.matches(self.string($0, kAXDocumentAttribute).flatMap(URL.init(string:)), url) }
+ }
+ guard let editor = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.TextEdit").first else { throw FinderUIError.editorUnavailable }
+ print("finder stability UI: TextEdit document located")
+ let editorAX = AXUIElementCreateApplication(editor.processIdentifier)
+ let documents = (attribute(editorAX, kAXWindowsAttribute) as? [AXUIElement] ?? []).filter {
+ FinderUIURLIdentity.matches(string($0, kAXDocumentAttribute).flatMap(URL.init(string:)), url)
+ }
+ guard documents.count == 1, let document = documents.first,
+ AXUIElementPerformAction(document, kAXRaiseAction as CFString) == .success else { throw FinderUIError.editorUnavailable }
+ guard let owner = processIdentity(pid: editor.processIdentifier) else { throw FinderUIError.editorUnavailable }
+ ownedEditorDocuments[url] = owner
+ editor.activate()
+ try await wait { NSWorkspace.shared.frontmostApplication?.processIdentifier == editor.processIdentifier }
+ guard let focused = attribute(editorAX, kAXFocusedWindowAttribute), CFEqual(focused, document) else { throw FinderUIError.editorUnavailable }
+ print("finder stability UI: TextEdit document focused")
+ if let contents {
+ let areas = elements(document).filter { string($0, kAXRoleAttribute) == kAXTextAreaRole }
+ guard areas.count == 1, let textArea = areas.first,
+ AXUIElementSetAttributeValue(textArea, kAXFocusedAttribute as CFString, kCFBooleanTrue) == .success else {
+ throw FinderUIError.editorUnavailable
+ }
+ // Native paste goes through TextEdit's editing/undo pipeline.
+ // AppKit may ignore a synthetic event's overridden Unicode string.
+ NSPasteboard.general.clearContents()
+ guard NSPasteboard.general.setString(contents, forType: .string) else { throw FinderUIError.controlUnavailable }
+ try await FinderTextEditSequence.execute { action in
+ try await self.editorMenu(action.menuTitle, item: action.itemTitle, documentURL: url, editor: editor)
+ } verifyText: {
+ try await self.wait {
+ guard let fresh = self.editorDocument(url, editor: editor) else { return false }
+ return self.elements(fresh).contains { self.string($0, kAXRoleAttribute) == kAXTextAreaRole && self.string($0, kAXValueAttribute) == contents }
+ }
+ print("finder stability UI: TextEdit fixture text verified")
+ } verifySave: {
+ try await self.wait {
+ guard let fresh = self.editorDocument(url, editor: editor) else { return false }
+ guard let close = self.attribute(fresh, kAXCloseButtonAttribute), CFGetTypeID(close) == AXUIElementGetTypeID() else { return false }
+ // AppKit exposes the document's edited state on its close
+ // button; TextEdit does not expose AXEdited on the window.
+ return (self.attribute(close as! AXUIElement, kAXEditedAttribute) as? Bool) == false
+ }
+ print("finder stability UI: TextEdit save acknowledged")
+ }
+ }
+ // Close only the verified document, releasing it before Remove Download.
+ guard let freshDocument = editorDocument(url, editor: editor),
+ let close = attribute(freshDocument, kAXCloseButtonAttribute), CFGetTypeID(close) == AXUIElementGetTypeID(),
+ AXUIElementPerformAction(close as! AXUIElement, kAXPressAction as CFString) == .success else {
+ throw FinderUIError.editorUnavailable
+ }
+ try await wait {
+ !(self.attribute(editorAX, kAXWindowsAttribute) as? [AXUIElement] ?? []).contains {
+ FinderUIURLIdentity.matches(self.string($0, kAXDocumentAttribute).flatMap(URL.init(string:)), url)
+ }
+ }
+ ownedEditorDocuments[url] = nil
+ actionCount += 1
+ }
+
+ private func editorDocument(_ url: URL, editor: NSRunningApplication) -> AXUIElement? {
+ guard !editor.isTerminated else { return nil }
+ let app = AXUIElementCreateApplication(editor.processIdentifier)
+ let matches = (attribute(app, kAXWindowsAttribute) as? [AXUIElement] ?? []).filter {
+ FinderUIURLIdentity.matches(string($0, kAXDocumentAttribute).flatMap(URL.init(string:)), url)
+ }
+ return matches.count == 1 ? matches.first : nil
+ }
+
+ private func editorMenu(_ menuTitle: String, item title: String, documentURL: URL, editor: NSRunningApplication) async throws {
+ let app = AXUIElementCreateApplication(editor.processIdentifier)
+ guard let document = editorDocument(documentURL, editor: editor),
+ NSWorkspace.shared.frontmostApplication?.processIdentifier == editor.processIdentifier,
+ attribute(app, kAXFocusedWindowAttribute).map({ CFEqual($0, document) }) == true,
+ let bar = attribute(app, kAXMenuBarAttribute), CFGetTypeID(bar) == AXUIElementGetTypeID() else {
+ throw FinderUIError.editorUnavailable
+ }
+ // Physical US key codes are layout-dependent: Command-A can become
+ // Quit on AZERTY. Resolve the native menu action in the bound editor.
+ let menus = (attribute(bar as! AXUIElement, kAXChildrenAttribute) as? [AXUIElement] ?? []).filter {
+ string($0, kAXRoleAttribute) == kAXMenuBarItemRole && string($0, kAXTitleAttribute) == menuTitle
+ }
+ guard menus.count == 1, let menu = menus.first else { throw FinderUIError.controlUnavailable }
+ try await wait {
+ // Native AXMenuItem press performs the command directly. Opening
+ // the menu bar first can leave TextEdit in its modal tracking loop.
+ guard !editor.isTerminated, NSWorkspace.shared.frontmostApplication?.processIdentifier == editor.processIdentifier else {
+ throw FinderUIError.editorUnavailable
+ }
+ let lists = self.attribute(menu, kAXChildrenAttribute) as? [AXUIElement] ?? []
+ let items = lists.flatMap { self.attribute($0, kAXChildrenAttribute) as? [AXUIElement] ?? [] }.filter {
+ self.string($0, kAXRoleAttribute) == kAXMenuItemRole && self.string($0, kAXTitleAttribute) == title &&
+ (self.attribute($0, kAXEnabledAttribute) as? Bool) == true }
+ guard items.count == 1, let item = items.first else { return false }
+ guard AXUIElementPerformAction(item, kAXPressAction as CFString) == .success else { throw FinderUIError.controlUnavailable }
+ return true
+ }
+ print("finder stability UI: TextEdit \(title) invoked")
+ try await wait {
+ guard let fresh = self.editorDocument(documentURL, editor: editor) else { return false }
+ return self.attribute(app, kAXFocusedWindowAttribute).map { CFEqual($0, fresh) } == true
+ }
+ }
+
+ func cancelDownload(_ url: URL, whilePending: () throws -> Bool) async throws -> Bool {
+ var phase = "verifyPending"
+ do {
+ guard try whilePending() else { return false }
+ phase = "verifyTransferSelection"
+ // Download Now already selected and activated this exact item.
+ // Do not reassign Finder selection while its short transfer runs.
+ guard FinderUIURLIdentity.matches(selectionURL, url), try verifySelection(url),
+ NSWorkspace.shared.frontmostApplication?.bundleIdentifier == "com.apple.finder" else {
+ throw FinderUIError.selectionMismatch
+ }
+ phase = "observeCancelControl"
+ var invoked = false
+ try await wait {
+ guard try whilePending() else { return true }
+ guard let selected = self.selectedElement() else { return false }
+ let nodes = self.elements(selected)
+ if let cancel = nodes.first(where: {
+ let label = self.string($0, kAXDescriptionAttribute) ?? self.string($0, kAXTitleAttribute) ?? ""
+ return label.localizedCaseInsensitiveContains("cancel")
+ }) {
+ guard try whilePending() else { return true }
+ phase = "pressCancelControl"
+ guard AXUIElementPerformAction(cancel, kAXPressAction as CFString) == .success else { throw FinderUIError.controlUnavailable }
+ } else {
+ guard let window = self.finderWindowAX(), let windowBounds = self.rect(window), let rowBounds = self.rect(selected),
+ let point = FinderTransferCancellationTarget.point(window: windowBounds, row: rowBounds,
+ indicators: nodes.filter { self.string($0, kAXRoleAttribute) == kAXProgressIndicatorRole }.map {
+ .init(fraction: (self.attribute($0, kAXValueAttribute) as? NSNumber)?.doubleValue, bounds: self.rect($0))
+ }) else { return false }
+ guard try whilePending() else { return true }
+ phase = "clickProgressIndicator"
+ if let indicator = nodes.first(where: { self.string($0, kAXRoleAttribute) == kAXProgressIndicatorRole }) {
+ var names: CFArray?
+ let result = AXUIElementCopyActionNames(indicator, &names)
+ let actions = names as? [String] ?? []
+ print("finder stability UI: transfer indicator; actionsAvailable=\(result == .success) press=\(actions.contains(kAXPressAction)) cancel=\(actions.contains(kAXCancelAction))")
+ }
+ if let bar = (self.attribute(self.finderAX(), kAXChildrenAttribute) as? [AXUIElement])?
+ .first(where: { self.string($0, kAXRoleAttribute) == kAXMenuBarRole }),
+ let windowMenu = (self.attribute(bar, kAXChildrenAttribute) as? [AXUIElement])?
+ .first(where: { self.string($0, kAXTitleAttribute) == "Window" }) {
+ let commands = self.elements(windowMenu).filter {
+ self.string($0, kAXRoleAttribute) == kAXMenuItemRole && self.string($0, kAXTitleAttribute) == "Show Progress Window"
+ }
+ print("finder stability UI: transfer progress window command; matchCount=\(commands.count) enabled=\(commands.first.map { (self.attribute($0, kAXEnabledAttribute) as? Bool) == true } ?? false)")
+ }
+ guard try await FinderPointerClick.perform(at: point, button: .left, mayClick: {
+ guard try whilePending() else { return false }
+ guard NSWorkspace.shared.frontmostApplication?.bundleIdentifier == "com.apple.finder",
+ let fresh = self.selectedElement(), CFEqual(fresh, selected),
+ let freshWindow = self.finderWindowAX(), let freshWindowBounds = self.rect(freshWindow),
+ let freshRowBounds = self.rect(fresh) else { throw FinderUIError.selectionMismatch }
+ let freshPoint = FinderTransferCancellationTarget.point(window: freshWindowBounds, row: freshRowBounds,
+ indicators: self.elements(fresh).filter { self.string($0, kAXRoleAttribute) == kAXProgressIndicatorRole }.map {
+ .init(fraction: (self.attribute($0, kAXValueAttribute) as? NSNumber)?.doubleValue, bounds: self.rect($0))
+ })
+ guard freshPoint.map({ abs($0.x - point.x) < 1 && abs($0.y - point.y) < 1 }) == true,
+ let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else { return false }
+ try FinderPointerTarget.verify(at: point, processIdentifier: finder.processIdentifier, scope: fresh)
+ // AX/Apple Events revalidation may outlast the transfer.
+ return try whilePending()
+ }) else { return true }
+ print("finder stability UI: active generated transfer indicator clicked; awaiting cancellation callback")
+ }
+ invoked = true
+ return true
+ }
+ if invoked { actionCount += 1 }
+ return invoked
+ } catch {
+ print("finder stability UI: cancellation failed; phase=\(phase)")
+ throw error
+ }
+ }
+
+ func copyAndCancelDownload(_ source: URL, to parent: URL,
+ observation: () throws -> FinderCopyCancellationState) async throws -> Bool {
+ try await select(source)
+ try await activateFinder()
+ let before = attribute(finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []
+ guard !before.contains(where: { string($0, kAXIdentifierAttribute) == "Progress" }),
+ (try FileManager.default.contentsOfDirectory(atPath: parent.path)).isEmpty else {
+ throw FinderUIError.controlUnavailable
+ }
+ try key(8, flags: .maskCommand)
+ try await navigate(to: parent)
+ try await activateFinder()
+ try key(9, flags: .maskCommand)
+ actionCount += 1
+ var invoked = false
+ var stoppedWindow: AXUIElement?
+ try await wait {
+ switch try observation() {
+ case .finished: return true
+ case .waiting: return false
+ case .cancellable: break
+ }
+ guard try self.verifyWindow(parent),
+ NSWorkspace.shared.frontmostApplication?.bundleIdentifier == "com.apple.finder" else {
+ throw FinderUIError.windowMismatch
+ }
+ let windows = (self.attribute(self.finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []).filter { candidate in
+ self.string(candidate, kAXIdentifierAttribute) == "Progress" && !before.contains(where: { CFEqual($0, candidate) })
+ }
+ guard windows.count == 1, let window = windows.first else { return false }
+ let nodes = self.elements(window)
+ let labels = nodes.flatMap {
+ [self.string($0, kAXValueAttribute), self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)].compactMap { $0 }
+ }
+ guard FinderCopyCancellationTarget.matches(sourceName: source.lastPathComponent,
+ destinationName: parent.lastPathComponent, labels: labels) else { return false }
+ let buttons = nodes.filter {
+ self.string($0, kAXRoleAttribute) == kAXButtonRole &&
+ [self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)].contains("stop progress") &&
+ (self.attribute($0, kAXEnabledAttribute) as? Bool) != false
+ }
+ guard buttons.count == 1, let button = buttons.first,
+ case .cancellable = try observation() else { return false }
+ guard AXUIElementPerformAction(button, kAXPressAction as CFString) == .success else {
+ throw FinderUIError.controlUnavailable
+ }
+ invoked = true
+ stoppedWindow = window
+ self.actionCount += 1
+ print("finder stability UI: exact generated copy Stop invoked; awaiting fetch cancellation")
+ return true
+ }
+ if let stoppedWindow {
+ try await wait {
+ !(self.attribute(self.finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []).contains { CFEqual($0, stoppedWindow) }
+ }
+ }
+ return invoked
+ }
+
+ func confirmPermanentDeletion(_ url: URL, fixtureAlias: UUID) async throws {
+ try await select(url)
+ let panel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: 480, height: 100), styleMask: [.titled], backing: .buffered, defer: false)
+ panel.title = "Stability run awaiting confirmation"
+ panel.center()
+ panel.makeKeyAndOrderFront(nil)
+ NSApp.activate()
+ let alert = NSAlert()
+ alert.messageText = "Permanently delete this generated test fixture?"
+ alert.informativeText = url.lastPathComponent + "\nRun item alias: " + fixtureAlias.uuidString + "\nThis cannot be undone. No other Trash item will be selected."
+ alert.addButton(withTitle: "Delete generated fixture")
+ alert.addButton(withTitle: "Stop run")
+ let result = await withCheckedContinuation { continuation in
+ alert.beginSheetModal(for: panel) { continuation.resume(returning: $0) }
+ }
+ panel.close()
+ guard result == .alertFirstButtonReturn else { throw FinderUIError.operatorCancelled }
+ // Caller revalidates identity after the prompt before executing deletion.
+ }
+
+ func panelAction(_ action: FinderPanelAction) async throws {
+ switch action {
+ case .inheritAccess:
+ try await pressControl(role: kAXPopUpButtonRole, identifier: "provider.share.access")
+ try await pressControl(title: "Inherit Access", role: kAXMenuItemRole)
+ case .createLink:
+ try await pressControl(title: "Create Link")
+ try await waitForShareResult("Created share link.")
+ case .toggleComments:
+ // The server can expose the newly created link before the panel
+ // finishes applying its response. Wait for the editable form, then
+ // verify the checkbox change before submitting another operation.
+ try await wait { self.panelControl(title: "Save Changes", role: kAXButtonRole) != nil }
+ guard let checkbox = panelControl(title: "Allow comments", role: kAXCheckBoxRole),
+ let value = attribute(checkbox, kAXValueAttribute) as? NSNumber,
+ value.boolValue == false else { throw FinderUIError.selectionMismatch }
+ try await pressControl(title: "Allow comments", role: kAXCheckBoxRole)
+ try await wait {
+ guard let checkbox = self.panelControl(title: "Allow comments", role: kAXCheckBoxRole) else { return false }
+ return (self.attribute(checkbox, kAXValueAttribute) as? NSNumber)?.boolValue == true
+ }
+ print("finder stability UI: comments checkbox verified enabled")
+ case .saveLink:
+ try await pressControl(title: "Save Changes")
+ try await waitForShareResult("Saved share-link settings.")
+ case .disableLink: try await pressControl(title: "Disable Link")
+ case .confirmDisableLink: try await pressActionConfirmation(.disableShareLink)
+ case .done: try await pressControl(title: "Done")
+ case .restoreVersion(let versionID):
+ try await pressControl(identifier: "provider.version.restore." + KDriveMutationIdentity.clientToken([String(versionID)]))
+ case .confirmRestore: try await pressActionConfirmation(.restoreVersion)
+ case .confirmSystemDeletion:
+ guard selectedDeletionRequested, let dialog = selectedDeletionDialog() else { throw FinderUIError.selectionMismatch }
+ let buttons = elements(dialog).filter { string($0, kAXRoleAttribute) == kAXButtonRole && string($0, kAXTitleAttribute) == "Delete" }
+ guard buttons.count == 1, let button = buttons.first,
+ AXUIElementPerformAction(button, kAXPressAction as CFString) == .success else { throw FinderUIError.controlUnavailable }
+ selectedDeletionRequested = false
+ deletionDialogExpectation = nil
+ }
+ actionCount += 1
+ }
+
+ private func panelControl(title: String, role: String) -> AXUIElement? {
+ guard let scope = actionPanelScope() else { return nil }
+ let matches = elements(scope).filter {
+ string($0, kAXRoleAttribute) == role &&
+ [string($0, kAXTitleAttribute), string($0, kAXDescriptionAttribute)].contains(title) &&
+ (attribute($0, kAXEnabledAttribute) as? Bool) != false
+ }
+ return matches.count == 1 ? matches.first : nil
+ }
+
+ private func waitForShareResult(_ successMessage: String) async throws {
+ try await wait {
+ guard let scope = self.actionPanelScope() else { return false }
+ let values = self.elements(scope).flatMap {
+ [self.string($0, kAXValueAttribute), self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)].compactMap { $0 }
+ }
+ if let mismatchMessage = KDriveContextActionError.shareLinkSettingsNotApplied.errorDescription,
+ values.contains(mismatchMessage) {
+ throw KDriveContextActionError.shareLinkSettingsNotApplied
+ }
+ return values.contains(successMessage)
+ }
+ }
+
+ private func pressActionConfirmation(_ action: FinderActionConfirmation) async throws {
+ var lastObservation: String?
+ try await wait {
+ guard let scope = self.actionPanelScope() else { return false }
+ let dialogs = self.elements(scope).filter { candidate in
+ self.string(candidate, kAXRoleAttribute) == kAXSheetRole &&
+ !self.elements(candidate).contains {
+ !CFEqual($0, candidate) && self.string($0, kAXRoleAttribute) == kAXSheetRole
+ }
+ }
+ let observations = dialogs.map { dialog in
+ let nodes = self.elements(dialog)
+ return FinderActionConfirmationObservation(
+ texts: nodes.flatMap {
+ [self.string($0, kAXValueAttribute), self.string($0, kAXTitleAttribute),
+ self.string($0, kAXDescriptionAttribute)].compactMap { $0 }
+ },
+ enabledButtonTitles: nodes.filter {
+ self.string($0, kAXRoleAttribute) == kAXButtonRole &&
+ (self.attribute($0, kAXEnabledAttribute) as? Bool) != false
+ }.compactMap {
+ [self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)]
+ .compactMap { $0 }.first { !$0.isEmpty }
+ })
+ }
+ let index = action.uniqueMatchIndex(in: observations)
+ let observation = "leafSheetCount=\(dialogs.count) exactMatch=\(index != nil)"
+ if observation != lastObservation {
+ print("finder stability UI: contextual confirmation; " + observation)
+ lastObservation = observation
+ }
+ guard let index else { return false }
+ let buttons = self.elements(dialogs[index]).filter {
+ self.string($0, kAXRoleAttribute) == kAXButtonRole &&
+ [self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)].contains(action.buttonTitle) &&
+ (self.attribute($0, kAXEnabledAttribute) as? Bool) != false
+ }
+ guard buttons.count == 1, let button = buttons.first else { return false }
+ guard !action.hasSuccessfulResult(in: self.panelTexts(scope)) else { throw FinderUIError.controlUnavailable }
+ let result = AXUIElementPerformAction(button, kAXPressAction as CFString)
+ print("finder stability UI: bound contextual confirmation attempted; AX code \(result.rawValue)")
+ // Closing a remote sheet can invalidate AXPress after the action
+ // executes. Never press again: require a new success result, then
+ // let the caller independently verify server state and exact bytes.
+ return true
+ }
+ try await wait {
+ guard let scope = self.actionPanelScope() else { return false }
+ return action.hasSuccessfulResult(in: self.panelTexts(scope))
+ }
+ print("finder stability UI: contextual confirmation result verified")
+ }
+
+ private func panelTexts(_ scope: AXUIElement) -> [String] {
+ elements(scope).flatMap {
+ [string($0, kAXValueAttribute), string($0, kAXTitleAttribute),
+ string($0, kAXDescriptionAttribute)].compactMap { $0 }
+ }
+ }
+
+ private func pressControl(title: String? = nil, role: String = kAXButtonRole, identifier: String? = nil) async throws {
+ var lastObservation: String?
+ try await wait {
+ guard let scope = self.actionPanelScope() else { return false }
+ let nodes = self.elements(scope)
+ let matches = nodes.filter {
+ if let identifier { return self.string($0, kAXIdentifierAttribute) == identifier }
+ return self.string($0, kAXRoleAttribute) == role &&
+ [self.string($0, kAXTitleAttribute), self.string($0, kAXDescriptionAttribute)].contains(title)
+ }
+ let enabled = matches.filter { (self.attribute($0, kAXEnabledAttribute) as? Bool) != false }
+ let observation = "roleCount=\(nodes.filter { self.string($0, kAXRoleAttribute) == role }.count) matchCount=\(matches.count) enabledCount=\(enabled.count)"
+ if observation != lastObservation {
+ print("finder stability UI: panel control \(title ?? "bound identifier"); \(observation)")
+ lastObservation = observation
+ }
+ guard enabled.count == 1, let element = enabled.first else { return false }
+ let result = AXUIElementPerformAction(element, kAXPressAction as CFString)
+ print("finder stability UI: panel control press; AX code \(result.rawValue)")
+ guard result == .success else { throw FinderUIError.controlUnavailable }
+ return true
+ }
+ }
+
+ private func actionPanelScope() -> AXUIElement? {
+ guard let actionPanelIdentifier, let host = finderWindowAX() else { return nil }
+ var windows: [AXUIElement] = [host]
+ let extensionURL = Bundle.main.bundleURL.appendingPathComponent("Contents/PlugIns/potassiumProviderActions.appex")
+ if let bundle = Bundle(url: extensionURL), let identifier = bundle.bundleIdentifier, let executable = bundle.executableURL {
+ let expectedHash = StabilityDiagnosticIdentity.codeHash(at: extensionURL)
+ for app in NSRunningApplication.runningApplications(withBundleIdentifier: identifier) {
+ guard FinderActionPanelTarget.isExpectedProcess(executableURL: app.executableURL, expectedURL: executable,
+ codeHash: StabilityDiagnosticIdentity.codeHash(forProcessIdentifier: app.processIdentifier), expectedCodeHash: expectedHash) else { continue }
+ let application = AXUIElementCreateApplication(app.processIdentifier)
+ let listed = attribute(application, kAXWindowsAttribute) as? [AXUIElement] ?? []
+ let mainValue = attribute(application, kAXMainWindowAttribute)
+ let main = mainValue.flatMap { CFGetTypeID($0) == AXUIElementGetTypeID() ? ($0 as! AXUIElement) : nil }
+ windows += FinderActionPanelTarget.candidates(listed: listed, main: main, equal: { CFEqual($0, $1) })
+ }
+ }
+ var uniqueWindows: [AXUIElement] = []
+ for window in windows where !uniqueWindows.contains(where: { CFEqual($0, window) }) { uniqueWindows.append(window) }
+ let panels = uniqueWindows.map { window in
+ // Stop at the native alias root; a nested SwiftUI alias is the
+ // same panel, while sibling roots must remain distinguishable.
+ var result: [AXUIElement] = [], queue = [window], visited = 0
+ while let node = queue.popLast(), visited < 5_000 {
+ visited += 1
+ if string(node, kAXIdentifierAttribute) == actionPanelIdentifier { result.append(node) }
+ else { queue += attribute(node, kAXChildrenAttribute) as? [AXUIElement] ?? [] }
+ }
+ return result
+ }
+ let index = FinderActionPanelTarget.windowIndex(alias: actionPanelIdentifier, panels: panels,
+ identifier: { string($0, kAXIdentifierAttribute) }, equal: { CFEqual($0, $1) })
+ let observation = "candidateCount=\(uniqueWindows.count) matchingWindowCount=\(panels.filter { !$0.isEmpty }.count) panelRootCount=\(panels.map(\.count)) bound=\(index != nil)"
+ if observation != lastActionPanelObservation {
+ print("finder stability UI: action panel observation; \(observation)")
+ lastActionPanelObservation = observation
+ }
+ guard let index else { return nil }
+ return uniqueWindows[index]
+ }
+
+ private func selectedDeletionDialog() -> AXUIElement? {
+ guard selectedDeletionRequested, let selectionURL, let expectation = deletionDialogExpectation else { return nil }
+ let windows = attribute(finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []
+ var candidates = windows.filter { candidate in !windowsBeforeAction.contains { CFEqual($0, candidate) } }
+ if let windowBeforeAction { candidates += elements(windowBeforeAction).filter { string($0, kAXRoleAttribute) == kAXSheetRole } }
+ // A sheet can also occur in AXWindows. Deduplicate the same AX object;
+ // two distinct matching dialogs remain ambiguous and must be rejected.
+ var uniqueCandidates: [AXUIElement] = []
+ for candidate in candidates where !uniqueCandidates.contains(where: { CFEqual($0, candidate) }) {
+ uniqueCandidates.append(candidate)
+ }
+ let observations = uniqueCandidates.map { candidate in
+ let values = elements(candidate).flatMap { [string($0, kAXTitleAttribute), string($0, kAXValueAttribute)] }.compactMap { $0 }
+ let buttons = elements(candidate).filter { string($0, kAXRoleAttribute) == kAXButtonRole }.compactMap { string($0, kAXTitleAttribute) }
+ return FinderDeletionDialogObservation(texts: values, buttonTitles: buttons)
+ }
+ let match = expectation.uniqueMatchIndex(selection: selectionURL, dialogs: observations)
+ let observation = "candidateCount=\(uniqueCandidates.count) exactMatch=\(match != nil)"
+ if observation != lastDeletionDialogObservation {
+ print("finder stability UI: deletion dialog observation; " + observation)
+ lastDeletionDialogObservation = observation
+ }
+ return match.map { uniqueCandidates[$0] }
+ }
+
+ func captureFailure(in directory: URL, sequence: Int) async throws {
+ // An expired operation budget must not also suppress its failure image.
+ // Observation cannot extend the failed operation or turn it into a pass.
+ let operationRemaining = remainingTime
+ let observationDeadline = StabilityDeadline(budget: .seconds(15))
+ remainingTime = { observationDeadline.remaining() }
+ defer { remainingTime = operationRemaining }
+ if menuIsOpen { try await dismissContextMenu(allowMissingWindow: true) }
+ try await capture(in: directory, sequence: sequence)
+ }
+
+ func capture(in directory: URL, sequence: Int) async throws {
+ guard CGPreflightScreenCaptureAccess(), let windowID, let selected = selectedElement(),
+ let bounds = rect(selected) else { throw FinderUIError.screenshotUnavailable }
+ let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
+ guard let window = content.windows.first(where: { $0.windowID == UInt32(windowID) && $0.owningApplication?.bundleIdentifier == "com.apple.finder" }) else {
+ throw FinderUIError.screenshotUnavailable
+ }
+ // Capture only the selected generated row, excluding sidebar/path and other rows.
+ let region = bounds.intersection(window.frame)
+ guard !region.isEmpty, region.width > 1, region.height > 1 else { throw FinderUIError.screenshotUnavailable }
+ let config = SCStreamConfiguration()
+ config.sourceRect = region.offsetBy(dx: -window.frame.minX, dy: -window.frame.minY)
+ config.width = Int(region.width * 2)
+ config.height = Int(region.height * 2)
+ config.showsCursor = false
+ let image = try await SCScreenshotManager.captureImage(contentFilter: SCContentFilter(desktopIndependentWindow: window), configuration: config)
+ let data = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:])
+ guard let data else { throw FinderUIError.screenshotUnavailable }
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
+ try data.write(to: directory.appendingPathComponent(String(format: "%03d.png", sequence)), options: .withoutOverwriting)
+ }
+
+ private func script(_ body: String, operation: FinderAppleEventOperation = .observe) throws -> NSAppleEventDescriptor {
+ guard remainingTime() > .zero else { throw FinderUIError.timedOut }
+ let source = "with timeout of 10 seconds\ntell application id \"com.apple.finder\"\n" + body + "\nend tell\nend timeout"
+ var error: NSDictionary?
+ guard let script = NSAppleScript(source: source) else { throw FinderUIError.automationFailed }
+ let value = script.executeAndReturnError(&error)
+ guard error == nil else {
+ if (error?[NSAppleScript.errorNumber] as? Int) == -15260 { throw FinderUIError.finderBusy }
+ print("finder stability UI: Apple Event failed; operation \(operation.rawValue); code \((error?[NSAppleScript.errorNumber] as? Int) ?? 0)")
+ throw FinderUIError.automationFailed
+ }
+ return value
+ }
+
+ private func quote(_ value: String) -> String { "\"" + value.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\"" }
+
+ private func verifyWindow(_ url: URL) throws -> Bool {
+ guard let windowID, let value = try script("get URL of target of Finder window id \(windowID)").stringValue,
+ let actual = URL(string: value) else { throw FinderUIError.windowMismatch }
+ return FinderUIURLIdentity.matches(actual, url)
+ }
+
+ private func verifySelection(_ url: URL) throws -> Bool {
+ // Finder's optimized `count selection` event returns zero on this OS
+ // even when its selection list contains one item. Fetch the list once
+ // and count that snapshot before reading the one selected URL.
+ let selected = try script("set selectedItems to get selection\nif (count selectedItems) is not 1 then return {}\nreturn {URL of item 1 of selectedItems}")
+ guard selected.numberOfItems == 1,
+ let value = selected.atIndex(1)?.stringValue,
+ let actual = URL(string: value) else { return false }
+ return FinderUIURLIdentity.matches(actual, url)
+ }
+
+ private func displayedName(of url: URL) throws -> String? {
+ // Finder may hide a filename extension. Ask for the display label of
+ // the exact bound URL; never infer identity by stripping extensions.
+ let file = "POSIX file " + quote(url.path)
+ let value = try script("if not (exists \(file)) then return \"\"\nget displayed name of (\(file) as alias)")
+ guard let name = value.stringValue else { throw FinderUIError.selectionMismatch }
+ return name.isEmpty ? nil : name
+ }
+
+ private func activateFinder() async throws {
+ guard let windowID, let currentURL, try verifyWindow(currentURL) else { throw FinderUIError.windowMismatch }
+ _ = try script("set index of Finder window id \(windowID) to 1\nactivate", operation: .activate)
+ try await wait { NSWorkspace.shared.frontmostApplication?.bundleIdentifier == "com.apple.finder" }
+ if let selectionURL, try verifySelection(selectionURL) == false { throw FinderUIError.selectionMismatch }
+ }
+
+ private func finderAX() -> AXUIElement {
+ AXUIElementCreateApplication(NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first?.processIdentifier ?? 0)
+ }
+
+ private func finderWindowAX() -> AXUIElement? {
+ guard let windowID, let currentURL, (try? verifyWindow(currentURL)) == true,
+ let bounds = try? script("get bounds of Finder window id \(windowID)"), bounds.numberOfItems == 4 else { return nil }
+ let x = CGFloat(bounds.atIndex(1)?.int32Value ?? 0), y = CGFloat(bounds.atIndex(2)?.int32Value ?? 0)
+ let expected = CGRect(x: x, y: y, width: CGFloat(bounds.atIndex(3)?.int32Value ?? 0) - x,
+ height: CGFloat(bounds.atIndex(4)?.int32Value ?? 0) - y)
+ let matches = (attribute(finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []).filter {
+ guard let frame = rect($0) else { return false }
+ return abs(frame.minX - expected.minX) < 3 && abs(frame.minY - expected.minY) < 3 &&
+ abs(frame.width - expected.width) < 3 && abs(frame.height - expected.height) < 3
+ }
+ if matches.count == 1 { return matches.first }
+ // Two retained run windows can have identical frames and titles. The
+ // front window is usable only after its Apple Events ID is verified;
+ // AX focus then disambiguates those otherwise identical windows.
+ guard (try? script("get id of front Finder window").int32Value) == windowID,
+ let focused = attribute(finderAX(), kAXFocusedWindowAttribute) else { return nil }
+ return matches.first { CFEqual($0, focused) }
+ }
+
+ private func selectedElement() -> AXUIElement? {
+ guard let selectionURL, (try? verifySelection(selectionURL)) == true, let window = finderWindowAX(),
+ let displayedName = try? displayedName(of: selectionURL) else { return nil }
+ let candidates = elements(window).filter { element in
+ (attribute(element, kAXSelectedAttribute) as? Bool) == true && elements(element).contains {
+ [string($0, kAXTitleAttribute), string($0, kAXValueAttribute), string($0, kAXDescriptionAttribute)].contains(displayedName)
+ }
+ }
+ // The complete row includes progress controls; its contents belong only
+ // to the verified single selection. Never search another Finder window.
+ return candidates.first { string($0, kAXRoleAttribute) == kAXRowRole } ?? candidates.first
+ }
+
+ private func selectedMenuPoint() -> CGPoint? {
+ guard let selected = selectedElement(), let selectionURL,
+ let name = try? displayedName(of: selectionURL),
+ let window = finderWindowAX(), let windowFrame = rect(window) else { return nil }
+ let candidates = elements(selected).filter {
+ string($0, kAXRoleAttribute) == kAXTextFieldRole
+ }
+ return FinderContextMenuTarget.point(displayedName: name, windowBounds: windowFrame,
+ fields: candidates.map { .init(name: string($0, kAXValueAttribute), bounds: rect($0)) })
+ }
+
+ private func showSelectedContextMenu(for title: String) async throws {
+ // Finder exposes the selected item's contextual commands through its
+ // Actions toolbar menu too. Prefer this named Accessibility control.
+ guard let selectionURL, try verifySelection(selectionURL), let window = finderWindowAX() else {
+ throw FinderUIError.selectionMismatch
+ }
+ let toolbars = elements(window).filter { string($0, kAXRoleAttribute) == kAXToolbarRole }
+ if FinderToolbarActionTarget.supports(command: title), toolbars.count == 1, let toolbar = toolbars.first, let windowBounds = rect(window), let toolbarBounds = rect(toolbar) {
+ let buttons = elements(toolbar).filter { string($0, kAXRoleAttribute) == kAXMenuButtonRole }
+ if let index = FinderToolbarActionTarget.index(window: windowBounds, toolbar: toolbarBounds,
+ buttons: buttons.map { .init(description: string($0, kAXDescriptionAttribute),
+ enabled: (attribute($0, kAXEnabledAttribute) as? Bool) != false, bounds: rect($0)) }) {
+ menuIsOpen = true
+ let button = buttons[index]
+ // Menu tracking may outlive the AX request. Its observed popup
+ // is authoritative; do not block the driver inside that request.
+ let remaining = remainingTime().components
+ let seconds = Float(remaining.seconds) + Float(remaining.attoseconds) / 1e18
+ guard seconds > 0, AXUIElementSetMessagingTimeout(button, min(2, seconds)) == .success else {
+ throw FinderUIError.controlUnavailable
+ }
+ let result = AXUIElementPerformAction(button, kAXPressAction as CFString)
+ print("finder stability UI: bound Actions toolbar menu requested; AX code \(result.rawValue)")
+ guard result == .success || contextMenu() != nil else { throw FinderUIError.controlUnavailable }
+ return
+ }
+ }
+ print("finder stability UI: locating confined context-menu anchor")
+ try await wait { self.selectedMenuPoint() != nil }
+ guard let point = selectedMenuPoint(),
+ let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else {
+ throw FinderUIError.controlUnavailable
+ }
+ // Finder may omit AXShowMenu or reject that action. Secondary-click
+ // routing needs only the unique name field of the verified selection,
+ // with fresh geometry confined to the bound window.
+ menuIsOpen = true
+ guard try await FinderPointerClick.perform(at: point, button: .right, mayClick: {
+ guard self.remainingTime() > .zero,
+ NSWorkspace.shared.frontmostApplication?.processIdentifier == finder.processIdentifier,
+ let fresh = self.selectedMenuPoint(), abs(fresh.x - point.x) < 1, abs(fresh.y - point.y) < 1 else {
+ throw FinderUIError.windowMismatch
+ }
+ guard let row = self.selectedElement() else { throw FinderUIError.selectionMismatch }
+ try FinderPointerTarget.verify(at: point, processIdentifier: finder.processIdentifier, scope: row)
+ return true
+ }) else { throw FinderUIError.controlUnavailable }
+ print("finder stability UI: context-menu mouse request posted")
+ }
+
+ private func contextMenu() -> AXUIElement? {
+ // The application menu bar contains similarly named commands. Only
+ // the transient popup opened on our verified selection is actionable.
+ let menus = FinderPopupMenuDiscovery.roots(from: finderAX(), role: {
+ switch self.string($0, kAXRoleAttribute) {
+ case kAXMenuRole: .menu
+ case kAXMenuBarRole: .menuBar
+ default: .other
+ }
+ }, children: { self.attribute($0, kAXChildrenAttribute) as? [AXUIElement] ?? [] }, visible: {
+ self.rect($0).map { $0.width > 1 && $0.height > 1 } == true
+ })
+ return menus.count == 1 ? menus.first : nil
+ }
+
+ private func waitForFinderIdle(allowMissingWindow: Bool = false) async throws {
+ guard let windowID else { throw FinderUIError.windowMismatch }
+ try await wait {
+ try self.detectEvictionFailure()
+ _ = try self.script(allowMissingWindow ? "exists Finder window id \(windowID)" : "get id of Finder window id \(windowID)")
+ return true
+ }
+ }
+
+ private func detectEvictionFailure() throws {
+ guard awaitingEvictionResult else { return }
+ let windows = attribute(finderAX(), kAXWindowsAttribute) as? [AXUIElement] ?? []
+ var candidates = windows.filter { candidate in !windowsBeforeAction.contains { CFEqual($0, candidate) } }
+ if let windowBeforeAction {
+ candidates += elements(windowBeforeAction).filter { string($0, kAXRoleAttribute) == kAXSheetRole }
+ }
+ let alerts = candidates.filter { candidate in
+ let labels = elements(candidate).flatMap { [string($0, kAXTitleAttribute), string($0, kAXValueAttribute)] }.compactMap { $0 }
+ return FinderAlertObservation.isResourceBusyEviction(labels: labels)
+ }
+ guard alerts.count == 1, let alert = alerts.first else { return }
+ let buttons = elements(alert).filter { string($0, kAXRoleAttribute) == kAXButtonRole && string($0, kAXTitleAttribute) == "OK" }
+ guard buttons.count == 1, let button = buttons.first,
+ AXUIElementPerformAction(button, kAXPressAction as CFString) == .success else { throw FinderUIError.controlUnavailable }
+ print("finder stability UI: generated eviction rejected; resource busy; alert dismissed")
+ awaitingEvictionResult = false
+ throw FinderUIError.evictionResourceBusy
+ }
+
+ private func dismissContextMenu(allowMissingWindow: Bool = false) async throws {
+ try key(53)
+ try await waitForFinderIdle(allowMissingWindow: allowMissingWindow)
+ menuIsOpen = false
+ }
+
+ private func attribute(_ element: AXUIElement, _ name: String) -> CFTypeRef? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, name as CFString, &value) == .success else { return nil }
+ return value
+ }
+ private func string(_ element: AXUIElement, _ name: String) -> String? { attribute(element, name) as? String }
+ private func elements(_ root: AXUIElement) -> [AXUIElement] {
+ var result: [AXUIElement] = [], queue = [root]
+ while !queue.isEmpty && result.count < 5_000 {
+ let value = queue.removeLast()
+ result.append(value)
+ queue.append(contentsOf: attribute(value, kAXChildrenAttribute) as? [AXUIElement] ?? [])
+ }
+ return result
+ }
+ private func rect(_ element: AXUIElement) -> CGRect? {
+ guard let position = attribute(element, kAXPositionAttribute), let size = attribute(element, kAXSizeAttribute),
+ CFGetTypeID(position) == AXValueGetTypeID(), CFGetTypeID(size) == AXValueGetTypeID() else { return nil }
+ var point = CGPoint.zero, extent = CGSize.zero
+ guard AXValueGetValue(position as! AXValue, .cgPoint, &point), AXValueGetValue(size as! AXValue, .cgSize, &extent) else { return nil }
+ return CGRect(origin: point, size: extent)
+ }
+ private func key(_ code: CGKeyCode, flags: CGEventFlags = []) throws {
+ guard remainingTime() > .zero else { throw FinderUIError.timedOut }
+ guard AXIsProcessTrusted(), let target = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first?.processIdentifier,
+ let down = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: true),
+ let up = CGEvent(keyboardEventSource: nil, virtualKey: code, keyDown: false) else { throw FinderUIError.permissionRequired }
+ down.flags = flags; up.flags = flags
+ down.postToPid(target); up.postToPid(target)
+ }
+ private func enterFinderName(_ name: String) async throws {
+ var editor: AXUIElement?
+ do { try await wait {
+ guard let focused = self.attribute(self.finderAX(), kAXFocusedUIElementAttribute),
+ CFGetTypeID(focused) == AXUIElementGetTypeID() else { return false }
+ let field = focused as! AXUIElement
+ guard self.string(field, kAXRoleAttribute) == kAXTextFieldRole,
+ let window = self.finderWindowAX(),
+ self.belongsToWindow(field, window: window) || self.belongsToSelectedRowOverlay(field, window: window) ||
+ self.isConfinedNameEditor(field, window: window) else { return false }
+ var settable = DarwinBoolean(false)
+ guard AXUIElementIsAttributeSettable(field, kAXValueAttribute as CFString, &settable) == .success,
+ settable.boolValue else { return false }
+ editor = field
+ return true
+ } } catch {
+ if let value = attribute(finderAX(), kAXFocusedUIElementAttribute), CFGetTypeID(value) == AXUIElementGetTypeID() {
+ let field = value as! AXUIElement
+ let role = string(field, kAXRoleAttribute) ?? ""
+ let knownRoles = [kAXTextFieldRole, kAXTextAreaRole, kAXOutlineRole, kAXRowRole, kAXGroupRole, kAXWindowRole, kAXComboBoxRole]
+ var settable = DarwinBoolean(false)
+ let result = AXUIElementIsAttributeSettable(field, kAXValueAttribute as CFString, &settable)
+ let window = finderWindowAX()
+ print("finder stability UI name field: role \(knownRoles.contains(role) ? role : "other"); settable \(settable.boolValue); status \(result.rawValue); window \(window != nil); ownership \(window.map { belongsToWindow(field, window: $0) } ?? false)")
+ let row = selectedElement()
+ let focusedWindow = attribute(finderAX(), kAXFocusedWindowAttribute)
+ print("finder stability UI name geometry: row \(row != nil); focused window \(window.map { w in focusedWindow.map { CFEqual($0, w) } ?? false } ?? false); editor bounds \(rect(field) != nil); contained \(window.flatMap(rect).map { w in rect(field).map(w.contains) ?? false } ?? false); overlaps row \(row.flatMap(rect).map { r in rect(field).map(r.intersects) ?? false } ?? false)")
+ } else { print("finder stability UI name field: no focused element") }
+ throw error
+ }
+ guard let editor,
+ AXUIElementSetAttributeValue(editor, kAXValueAttribute as CFString, name as CFString) == .success else {
+ throw FinderUIError.controlUnavailable
+ }
+ }
+
+ private func belongsToWindow(_ element: AXUIElement, window: AXUIElement) -> Bool {
+ // Finder's inline name editor is an overlay omitted from AXChildren.
+ // Its AXWindow/parent links still provide an explicit ownership edge.
+ if let owner = attribute(element, kAXWindowAttribute), CFEqual(owner, window) { return true }
+ var current = element
+ for _ in 0..<16 {
+ guard let parent = attribute(current, kAXParentAttribute), CFGetTypeID(parent) == AXUIElementGetTypeID() else { return false }
+ if CFEqual(parent, window) { return true }
+ current = parent as! AXUIElement
+ }
+ return false
+ }
+
+ private func belongsToSelectedRowOverlay(_ field: AXUIElement, window: AXUIElement) -> Bool {
+ // Finder omits both child and parent links for its inline editor on
+ // some OS versions. Bind the focused editor to the independently
+ // verified single selection and focused window, using fresh geometry.
+ guard let focusedWindow = attribute(finderAX(), kAXFocusedWindowAttribute), CFEqual(focusedWindow, window),
+ let selected = selectedElement(), let row = rect(selected), let editor = rect(field),
+ let bounds = rect(window), bounds.contains(editor), row.intersects(editor) else { return false }
+ var editorPID: pid_t = 0, windowPID: pid_t = 0
+ return AXUIElementGetPid(field, &editorPID) == .success && AXUIElementGetPid(window, &windowPID) == .success && editorPID == windowPID
+ }
+
+ private func isConfinedNameEditor(_ field: AXUIElement, window: AXUIElement) -> Bool {
+ guard let selectionURL, (try? verifySelection(selectionURL)) == true,
+ let expected = try? displayedName(of: selectionURL), let editorBounds = rect(field), let windowBounds = rect(window),
+ let windowID else { return false }
+ var editorPID: pid_t = 0, windowPID: pid_t = 0
+ let sameProcess = AXUIElementGetPid(field, &editorPID) == .success && AXUIElementGetPid(window, &windowPID) == .success && editorPID == windowPID
+ return FinderNameEditorObservation.isConfined(value: string(field, kAXValueAttribute), expected: expected,
+ editorBounds: editorBounds, windowBounds: windowBounds, sameProcess: sameProcess,
+ ownedWindowIsFront: (try? script("get id of front Finder window").int32Value) == windowID)
+ }
+ private func wait(_ predicate: () async throws -> Bool) async throws {
+ guard remainingTime() > .zero else { throw FinderUIError.timedOut }
+ // Transfer callbacks have their own ten-minute budget. An ordinary UI
+ // control lookup must still stop within 90 seconds.
+ let deadline = FinderUIObservationDeadline.make(remaining: remainingTime())
+ repeat {
+ try Task.checkCancellation()
+ do { if try await predicate() { return } }
+ catch FinderUIError.finderBusy { /* Menu tracking is transient; retry only the bounded observation. */ }
+ try await Task.sleep(for: .milliseconds(100))
+ } while deadline.remaining() > .zero
+ throw FinderUIError.timedOut
+ }
+}
+#endif
diff --git a/potassiumProvider/FinderUIObservationDeadline.swift b/potassiumProvider/FinderUIObservationDeadline.swift
new file mode 100644
index 0000000..e5e23ae
--- /dev/null
+++ b/potassiumProvider/FinderUIObservationDeadline.swift
@@ -0,0 +1,11 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+
+/// Ordinary UI observations remain bounded even inside a long transfer scenario.
+enum FinderUIObservationDeadline {
+ static func make(remaining: Duration, now: ContinuousClock.Instant = .now) -> StabilityDeadline {
+ StabilityDeadline(budget: min(max(.zero, remaining), .seconds(90)), now: now)
+ }
+}
+#endif
diff --git a/potassiumProvider/LiveFinderStabilityScenarioRunner.swift b/potassiumProvider/LiveFinderStabilityScenarioRunner.swift
new file mode 100644
index 0000000..149ce73
--- /dev/null
+++ b/potassiumProvider/LiveFinderStabilityScenarioRunner.swift
@@ -0,0 +1,50 @@
+#if os(macOS) && STABILITY
+import AppKit
+import ApplicationServices
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+
+enum FinderStabilityTargetBinding {
+ static func resolve(
+ expectedFileID: Int,
+ expectedDomainIdentifier: String,
+ using resolver: () async throws -> (itemIdentifier: String, domainIdentifier: String)
+ ) async throws -> String {
+ let actual = try await resolver()
+ guard matches(
+ expectedFileID: expectedFileID,
+ expectedDomainIdentifier: expectedDomainIdentifier,
+ actualItemIdentifier: actual.itemIdentifier,
+ actualDomainIdentifier: actual.domainIdentifier
+ ) else {
+ throw FinderLiveError.unsafeTarget
+ }
+ return actual.itemIdentifier
+ }
+
+ static func matches(
+ expectedFileID: Int,
+ expectedDomainIdentifier: String,
+ actualItemIdentifier: String,
+ actualDomainIdentifier: String
+ ) -> Bool {
+ actualItemIdentifier == String(expectedFileID)
+ && actualDomainIdentifier == expectedDomainIdentifier
+ }
+}
+
+@MainActor
+enum FinderStabilityScenarioGate {
+ static func run(
+ baseline: () async throws -> Baseline,
+ verifySafety: () async throws -> Void,
+ execute: () async throws -> Outcome
+ ) async throws -> (Baseline, Outcome) {
+ let baselineValue = try await baseline()
+ try await verifySafety()
+ let outcome = try await execute()
+ return (baselineValue, outcome)
+ }
+}
+#endif
diff --git a/potassiumProvider/PotassiumProviderAppModel.swift b/potassiumProvider/PotassiumProviderAppModel.swift
index 548d347..95c1194 100644
--- a/potassiumProvider/PotassiumProviderAppModel.swift
+++ b/potassiumProvider/PotassiumProviderAppModel.swift
@@ -40,6 +40,9 @@ final class PotassiumProviderAppModel: ObservableObject {
private static let log = ProviderLog.app
static let encryptedVaultRiskWarningDelaySeconds: TimeInterval = 5
+ private(set) var lastDriveDiscoveryErrorClass: ProviderDiagnosticErrorClass?
+ private(set) var lastDriveDiscoveryErrorCode: Int?
+
@Published private(set) var accounts: [ProviderAccount] = []
@Published private(set) var drivesByAccountIdentifier: [String: [KDriveDriveSummary]] = [:]
@Published private(set) var domains: [ProviderDomainConfiguration] = []
@@ -67,6 +70,9 @@ final class PotassiumProviderAppModel: ObservableObject {
@Published private(set) var encryptedVaultsEnabled: Bool
@Published private(set) var encryptedVaultICloudKeychainEnabled: Bool
@Published private(set) var statusMessage: String?
+ @Published private(set) var stabilityLabPreflightResult: StabilityLabPreflightResult?
+ @Published private(set) var isPerformingStabilityLabOperation = false
+ @Published private(set) var activeStabilityRunID: UUID?
@Published var errorMessage: String?
@Published var manualAccessToken = ""
@Published var selectedDriveIDs: [String: Int] = [:]
@@ -79,7 +85,8 @@ final class PotassiumProviderAppModel: ObservableObject {
private let oauthAuthenticator: any KDriveOAuthAuthenticating
private let domainRegistrar: any ProviderDomainRegistering
private let snapshotStore: (any KDriveSnapshotStoring)?
- private let eventStore: (any KDriveProviderEventStoring)?
+ private let injectedEventStore: (any KDriveProviderEventStoring)?
+ private let defaultEventStore: (any KDriveProviderEventStoring)?
private let fileProviderFactory: (String) -> any KDriveFileProviding
private let objectStoreFactory: (Int, String) -> any KDriveObjectStoreProviding
private let vaultKeyStore: any VaultKeyStoring
@@ -97,6 +104,20 @@ final class PotassiumProviderAppModel: ObservableObject {
private var automaticallyLoadedDriveAccountIdentifiers: Set = []
private var fileProviderDomainChangeCancellable: AnyCancellable?
+ var stabilityLabConfiguration: ProviderDomainConfiguration? {
+ domains.first { $0.purpose == .stabilityLab }
+ }
+
+ private var eventStore: (any KDriveProviderEventStoring)? {
+ if let injectedEventStore {
+ return injectedEventStore
+ }
+ if ProviderRuntimeProfile.current == .stability {
+ return Self.makeDefaultEventStore()
+ }
+ return defaultEventStore
+ }
+
init(
accountStore: (any ProviderAccountStoring)? = nil,
domainStore: (any DomainConfigurationStoring)? = nil,
@@ -109,7 +130,7 @@ final class PotassiumProviderAppModel: ObservableObject {
initialAccounts: [ProviderAccount] = [],
initialDrivesByAccountIdentifier: [String: [KDriveDriveSummary]] = [:],
initialDomains: [ProviderDomainConfiguration] = [],
- fileProviderFactory: @escaping (String) -> any KDriveFileProviding = { PotassiumKDriveService(bearerToken: $0) },
+ fileProviderFactory: ((String) -> any KDriveFileProviding)? = nil,
objectStoreFactory: @escaping (Int, String) -> any KDriveObjectStoreProviding = {
PotassiumKDriveObjectStore(driveID: $0, bearerToken: $1)
},
@@ -135,8 +156,27 @@ final class PotassiumProviderAppModel: ObservableObject {
self.oauthAuthenticator = oauthAuthenticator ?? KDriveOAuthWebAuthenticator()
self.domainRegistrar = domainRegistrar ?? FileProviderDomainRegistrar()
self.snapshotStore = snapshotStore ?? Self.makeDefaultSnapshotStore()
- self.eventStore = eventStore ?? Self.makeDefaultEventStore()
- self.fileProviderFactory = fileProviderFactory
+ let resolvedEventStore = eventStore ?? Self.makeDefaultEventStore()
+ self.injectedEventStore = eventStore
+ self.defaultEventStore = resolvedEventStore
+ if let injectedRecorder = eventStore as? any ProviderDiagnosticRecording {
+ self.fileProviderFactory = fileProviderFactory ?? { token in
+ PotassiumKDriveService(
+ bearerToken: token,
+ diagnosticRecorder: injectedRecorder,
+ diagnosticSource: .app
+ )
+ }
+ } else {
+ self.fileProviderFactory = fileProviderFactory ?? { token in
+ PotassiumKDriveService(
+ bearerToken: token,
+ diagnosticRecorder: Self.makeDefaultEventStore()
+ as? any ProviderDiagnosticRecording,
+ diagnosticSource: .app
+ )
+ }
+ }
self.objectStoreFactory = objectStoreFactory
let defaultVaultKeyStore = KeychainVaultKeyStore(
accessGroup: ProviderConstants.keychainAccessGroup
@@ -438,6 +478,8 @@ final class PotassiumProviderAppModel: ObservableObject {
return
}
+ lastDriveDiscoveryErrorClass = nil
+ lastDriveDiscoveryErrorCode = nil
loadingDriveAccountIdentifiers.insert(accountIdentifier)
defer { loadingDriveAccountIdentifiers.remove(accountIdentifier) }
@@ -458,6 +500,8 @@ final class PotassiumProviderAppModel: ObservableObject {
? "No usable kDrives found for \(account.displayName)."
: "Loaded \(usableDrives.count) usable kDrive\(usableDrives.count == 1 ? "" : "s") for \(account.displayName)."
} catch {
+ lastDriveDiscoveryErrorClass = ProviderDiagnosticErrorClassifier.classify(error)
+ lastDriveDiscoveryErrorCode = KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode
await recordAppFailure(
kind: .driveDiscovery,
summary: "Could not load kDrives.",
@@ -470,6 +514,11 @@ final class PotassiumProviderAppModel: ObservableObject {
}
func addDomain(accountIdentifier: String) async {
+ guard ProviderRuntimeProfile.current == .standard else {
+ errorMessage = "The Stability build registers only a verified Stability Lab root. Use the Stability Lab tab."
+ statusMessage = nil
+ return
+ }
guard let account = account(accountIdentifier: accountIdentifier) else {
errorMessage = "Choose an account before adding a domain."
statusMessage = nil
@@ -539,6 +588,192 @@ final class PotassiumProviderAppModel: ObservableObject {
await addDomain(accountIdentifier: accountIdentifier)
}
+ func provisionStabilityLab(
+ accountIdentifier: String,
+ drive: KDriveDriveSummary
+ ) async {
+ guard beginStabilityLabOperation() else { return }
+ defer { isPerformingStabilityLabOperation = false }
+
+ do {
+ try requireStabilityRuntime()
+ guard drive.isUsableInternalDrive else {
+ throw StabilityLabAppError.driveAccessNotVerified
+ }
+ let storedConfigurations = try await domainStore.allConfigurations()
+ let registeredIdentifiers = try await domainRegistrar.registeredDomainIdentifiers()
+ guard storedConfigurations.isEmpty, registeredIdentifiers.isEmpty else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+
+ let token = try await usableToken(accountIdentifier: accountIdentifier)
+ let remote = fileProviderFactory(token.accessToken)
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+ let privateParentID = try await KDrivePrivateDirectoryResolver.resolveFileID(
+ driveID: drive.id, rootFileID: ProviderConstants.defaultRootFileID, remote: remote
+ )
+ let provisioned = try await coordinator.provision(
+ driveID: drive.id,
+ driveRootFileID: ProviderConstants.defaultRootFileID,
+ privateParentFileID: privateParentID,
+ registeredDomainsProvider: { [weak self] in
+ guard let self else { throw StabilityLabAppError.domainIsolationRequired }
+ return try await self.provisioningDomainEvidence()
+ }
+ )
+ let now = Date()
+ let configuration = ProviderDomainConfiguration(
+ accountIdentifier: accountIdentifier,
+ displayName: "Potassium Stability Lab",
+ driveID: provisioned.driveID,
+ driveName: drive.name,
+ rootFileID: provisioned.rootFileID,
+ knownFolderLayout: .machineNamespace,
+ encryptionMode: .legacyPlaintext,
+ purpose: .stabilityLab,
+ stabilityLab: ProviderStabilityLabConfiguration(
+ driveRootFileID: provisioned.driveRootFileID,
+ markerFileID: provisioned.ownershipMarkerFileID,
+ ownershipMarker: provisioned.ownershipMarker
+ ),
+ createdAt: now,
+ updatedAt: now
+ )
+
+ // Persist ownership evidence before registering the domain. If
+ // registration fails, keep the record so the remote root never
+ // becomes an untracked automatic-cleanup candidate.
+ try await domainStore.save(configuration)
+ domains = try await domainStore.allConfigurations()
+ do {
+ let finalStoredConfigurations = try await domainStore.allConfigurations()
+ let finalRegisteredIdentifiers = try await domainRegistrar.registeredDomainIdentifiers()
+ guard finalStoredConfigurations == [configuration],
+ finalRegisteredIdentifiers.isEmpty else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ try await domainRegistrar.addDomain(for: configuration)
+ } catch {
+ throw StabilityLabAppError.domainRegistrationFailed
+ }
+ stabilityLabPreflightResult = nil
+ errorMessage = nil
+ statusMessage = "Stability Lab provisioned. Verify it before starting a Finder run."
+ } catch {
+ await refreshDomainListAfterStabilityLabOperation()
+#if STABILITY
+ print("stability lab failure: type \(String(reflecting: type(of: error))); class \(ProviderDiagnosticErrorClassifier.classify(error).rawValue); code \(KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode ?? 0)")
+#endif
+ errorMessage = stabilityLabMessage(for: error)
+ statusMessage = nil
+ }
+ }
+
+ /// Retry only registration for a previously provisioned, positively identified lab.
+ func resumeStabilityLabRegistration() async throws {
+ guard beginStabilityLabOperation() else { throw StabilityLabAppError.domainIsolationRequired }
+ defer { isPerformingStabilityLabOperation = false }
+ let context = try await makeStabilityLabContext(requireRegisteredDomain: false)
+ _ = try await context.coordinator.observe(configuration: context.remoteConfiguration)
+ let registered = try await domainRegistrar.registeredDomainIdentifiers()
+ guard registered.isEmpty || registered == [context.domain.domainIdentifier] else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ if registered.isEmpty { try await domainRegistrar.addDomain(for: context.domain) }
+ _ = try await verifyStabilityLabWhileHoldingOperationGate()
+ }
+
+ func verifyStabilityLab() async {
+ guard beginStabilityLabOperation() else { return }
+ defer { isPerformingStabilityLabOperation = false }
+
+ do {
+ _ = try await verifyStabilityLabWhileHoldingOperationGate()
+ errorMessage = nil
+ statusMessage = "Stability Lab ownership, root, marker, and domain isolation are verified."
+ } catch {
+ stabilityLabPreflightResult = nil
+ errorMessage = stabilityLabMessage(for: error)
+ statusMessage = nil
+ }
+ }
+
+ func resetStabilityLab(typedConfirmation: String) async {
+ guard beginStabilityLabOperation() else { return }
+ defer { isPerformingStabilityLabOperation = false }
+
+ do {
+ let runCoordinator = try StabilityRunCoordinator()
+ let context = try await makeStabilityLabContext(requireRegisteredDomain: true)
+ let confirmation = try StabilityLabResetConfirmation(
+ typedPhrase: typedConfirmation,
+ marker: context.remoteConfiguration.ownershipMarker
+ )
+ let result = try await runCoordinator.withInactiveRunLease {
+ try await context.coordinator.reset(
+ configuration: context.remoteConfiguration,
+ configuredEncryptionMode: context.domain.encryptionMode,
+ registeredDomainsProvider: { [weak self] in
+ guard let self else { throw StabilityLabAppError.domainIsolationRequired }
+ return try await self.registeredLabDomainEvidence(for: context.domain)
+ },
+ confirmation: confirmation
+ )
+ }
+ stabilityLabPreflightResult = nil
+ errorMessage = nil
+ statusMessage = "Stability Lab reset completed safely; \(result.trashedImmediateChildCount) immediate item(s) moved to trash. The root and ownership marker were preserved."
+ } catch {
+ stabilityLabPreflightResult = nil
+ errorMessage = stabilityLabMessage(for: error)
+ statusMessage = nil
+ }
+ }
+
+ func startStabilityRun() async {
+ guard beginStabilityLabOperation() else { return }
+ defer { isPerformingStabilityLabOperation = false }
+
+ do {
+ _ = try await verifyStabilityLabWhileHoldingOperationGate()
+ let coordinator = try StabilityRunCoordinator()
+ let handle = try await coordinator.startRun()
+ activeStabilityRunID = handle.runID
+ statusMessage = "Stability diagnostics run started."
+ } catch {
+ stabilityLabPreflightResult = nil
+ errorMessage = stabilityLabMessage(for: error)
+ statusMessage = nil
+ }
+ }
+
+ func finishStabilityRun() async {
+ guard beginStabilityLabOperation() else { return }
+ defer { isPerformingStabilityLabOperation = false }
+ do {
+ let coordinator = try StabilityRunCoordinator()
+ guard let handle = try await coordinator.activeRun() else {
+ activeStabilityRunID = nil
+ throw StabilityLabAppError.noActiveRun
+ }
+ try await coordinator.finishRun(
+ runID: handle.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ _ = try await coordinator.pruneCompletedRuns()
+ activeStabilityRunID = nil
+ errorMessage = nil
+ statusMessage = "Stability diagnostics run finished and sealed."
+ } catch {
+ errorMessage = stabilityLabMessage(for: error)
+ statusMessage = nil
+ }
+ }
+
/// Begins encrypted-vault onboarding without creating remote objects. The
/// unsupported-feature warning must remain visible for the configured delay
/// before `acceptEncryptedVaultRiskAndPrepare` can prepare the vault.
@@ -1911,6 +2146,156 @@ final class PotassiumProviderAppModel: ObservableObject {
}
}
+ private func beginStabilityLabOperation() -> Bool {
+ guard isPerformingStabilityLabOperation == false else { return false }
+ isPerformingStabilityLabOperation = true
+ errorMessage = nil
+ return true
+ }
+
+ private func requireStabilityRuntime() throws {
+ guard ProviderRuntimeProfile.current == .stability else {
+ throw StabilityLabAppError.stabilityBuildRequired
+ }
+ }
+
+ private func makeStabilityLabContext(
+ requireRegisteredDomain: Bool
+ ) async throws -> StabilityLabAppContext {
+ try requireStabilityRuntime()
+ let storedConfigurations = try await domainStore.allConfigurations()
+ guard storedConfigurations.contains(where: { $0.purpose == .ordinary }) == false else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ let labConfigurations = storedConfigurations.filter { $0.purpose == .stabilityLab }
+ guard labConfigurations.count == 1,
+ let domain = labConfigurations.first else {
+ throw StabilityLabAppError.singleLabRequired
+ }
+ guard domain.hasConsistentPurposeConfiguration,
+ let lab = domain.stabilityLab else {
+ throw StabilityLabAppError.invalidLocalOwnershipEvidence
+ }
+
+ let registeredIdentifiers = try await domainRegistrar.registeredDomainIdentifiers()
+ if requireRegisteredDomain {
+ guard registeredIdentifiers == [domain.domainIdentifier] else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ } else if registeredIdentifiers.subtracting([domain.domainIdentifier]).isEmpty == false {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ let token = try await usableToken(accountIdentifier: domain.accountIdentifier)
+ let remote = fileProviderFactory(token.accessToken)
+ let remoteConfiguration = StabilityLabRemoteConfiguration(
+ driveID: domain.driveID,
+ driveRootFileID: lab.driveRootFileID,
+ rootFileID: domain.rootFileID,
+ ownershipMarkerFileID: lab.markerFileID,
+ ownershipMarker: lab.ownershipMarker
+ )
+ return StabilityLabAppContext(
+ domain: domain,
+ remoteConfiguration: remoteConfiguration,
+ coordinator: StabilityLabRemoteCoordinator(remote: remote)
+ )
+ }
+
+ private func verifyStabilityLabWhileHoldingOperationGate() async throws
+ -> StabilityLabAppContext
+ {
+ let context = try await makeStabilityLabContext(requireRegisteredDomain: true)
+ let observation = try await context.coordinator.observe(
+ configuration: context.remoteConfiguration
+ )
+ let freshRegisteredDomains = try await registeredLabDomainEvidence(
+ for: context.domain
+ )
+ let result = StabilityLabSafety.preflight(StabilityLabPreflightInput(
+ expectedMarker: context.remoteConfiguration.ownershipMarker,
+ expectedOwnershipMarkerFileID: context.remoteConfiguration.ownershipMarkerFileID,
+ configuredEncryptionMode: context.domain.encryptionMode,
+ root: observation,
+ registeredDomains: freshRegisteredDomains
+ ))
+ stabilityLabPreflightResult = result
+ guard result.isAllowed else {
+ throw StabilityLabAppError.preflightRejected
+ }
+ return context
+ }
+
+ private func provisioningDomainEvidence() async throws -> [StabilityLabRegisteredDomain] {
+ let storedConfigurations = try await domainStore.allConfigurations()
+ let registeredIdentifiers = try await domainRegistrar.registeredDomainIdentifiers()
+ guard storedConfigurations.isEmpty, registeredIdentifiers.isEmpty else {
+ return [StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: 0,
+ rootFileID: 0,
+ encryptionMode: .legacyPlaintext
+ )]
+ }
+ return []
+ }
+
+ private func registeredLabDomainEvidence(
+ for expectedDomain: ProviderDomainConfiguration
+ ) async throws -> [StabilityLabRegisteredDomain] {
+ let storedConfigurations = try await domainStore.allConfigurations()
+ let registeredIdentifiers = try await domainRegistrar.registeredDomainIdentifiers()
+ guard storedConfigurations.count == 1,
+ storedConfigurations.first == expectedDomain,
+ registeredIdentifiers == [expectedDomain.domainIdentifier],
+ let lab = expectedDomain.stabilityLab,
+ expectedDomain.hasConsistentPurposeConfiguration else {
+ throw StabilityLabAppError.domainIsolationRequired
+ }
+ return [StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: expectedDomain.driveID,
+ rootFileID: expectedDomain.rootFileID,
+ encryptionMode: expectedDomain.encryptionMode,
+ ownershipMarkerIdentifier: lab.ownershipMarker.identifier
+ )]
+ }
+
+ private func refreshDomainListAfterStabilityLabOperation() async {
+ if let configurations = try? await domainStore.allConfigurations() {
+ domains = configurations
+ }
+ }
+
+ private func stabilityLabMessage(for error: Error) -> String {
+ switch error {
+ case StabilityLabAppError.domainIsolationRequired:
+ return "Stability Lab isolation failed. Run scripts/uninstall-file-provider.sh --dry-run, review it, then run the explicit safe --yes cleanup. Hard purge is never automatic."
+ case StabilityLabAppError.stabilityBuildRequired:
+ return "Open the macOS Stability build to use the Stability Lab."
+ case StabilityLabAppError.driveAccessNotVerified:
+ return "Choose an available internal drive reached through the dedicated development account."
+ case StabilityLabAppError.singleLabRequired:
+ return "Exactly one saved Stability Lab configuration is required."
+ case StabilityLabAppError.invalidLocalOwnershipEvidence:
+ return "The saved Stability Lab ownership evidence is incomplete or inconsistent. No remote change was made."
+ case StabilityLabAppError.domainRegistrationFailed:
+ return "The lab root was provisioned and its ownership evidence was saved, but File Provider registration failed. The root was not cleaned automatically."
+ case StabilityLabAppError.preflightRejected:
+ return "Stability Lab preflight rejected the root or domain state. No remote change was made."
+ case StabilityLabAppError.noActiveRun:
+ return "There is no active Stability diagnostics run."
+ case ProviderDiagnosticStoreError.runAlreadyActive:
+ return "Finish and seal the active Stability diagnostics run before resetting its lab contents."
+ case is StabilityLabResetConfirmationError:
+ return "Type the exact reset phrase before moving lab contents to trash."
+ case is StabilityLabRemoteCoordinatorError,
+ is StabilityLabResetPlanningError:
+ return "The Stability Lab safety coordinator rejected the operation. No unverified target was changed."
+ default:
+ return "The Stability Lab operation could not be completed safely."
+ }
+ }
+
private func usableToken(accountIdentifier: String) async throws -> KDriveOAuthToken {
guard account(accountIdentifier: accountIdentifier) != nil else {
throw PotassiumProviderAppModelError.missingAccount
@@ -2078,7 +2463,7 @@ final class PotassiumProviderAppModel: ObservableObject {
}
private static func makeDefaultEventStore() -> (any KDriveProviderEventStoring)? {
- try? KDriveProviderEventSQLiteStore(appGroupIdentifier: ProviderConstants.appGroupIdentifier)
+ try? ProviderEventStoreFactory.makeDefault()
}
private func trimmed(_ value: String) -> String {
@@ -2152,6 +2537,11 @@ final class PotassiumProviderAppModel: ObservableObject {
continue
}
+ // Stability Lab domains and ordinary user domains have disjoint
+ // build identities. Never let launching the wrong profile silently
+ // register the other profile's saved domain.
+ guard configurations[index].isCompatible(with: .current) else { continue }
+
do {
try await domainRegistrar.addDomain(for: configurations[index])
} catch {
@@ -2373,6 +2763,23 @@ enum PotassiumProviderAppModelError: Error, Equatable, LocalizedError {
}
}
+private struct StabilityLabAppContext {
+ let domain: ProviderDomainConfiguration
+ let remoteConfiguration: StabilityLabRemoteConfiguration
+ let coordinator: StabilityLabRemoteCoordinator
+}
+
+private enum StabilityLabAppError: Error, Equatable {
+ case stabilityBuildRequired
+ case driveAccessNotVerified
+ case domainIsolationRequired
+ case singleLabRequired
+ case invalidLocalOwnershipEvidence
+ case domainRegistrationFailed
+ case preflightRejected
+ case noActiveRun
+}
+
private enum VaultDomainRegistrationError: Error, LocalizedError {
case vaultAlreadyRegistered
diff --git a/potassiumProvider/ProviderSetupView.swift b/potassiumProvider/ProviderSetupView.swift
index 03c9484..46960fa 100644
--- a/potassiumProvider/ProviderSetupView.swift
+++ b/potassiumProvider/ProviderSetupView.swift
@@ -409,6 +409,7 @@ private struct ProviderAccountRow: View {
private struct ProviderAddAccountView: View {
@ObservedObject var model: PotassiumProviderAppModel
@Environment(\.dismiss) private var dismiss
+ @State private var isAdvancedExpanded = false
var body: some View {
#if os(macOS)
@@ -518,7 +519,7 @@ private struct ProviderAddAccountView: View {
.accessibilityIdentifier("addAccount.oauth")
}
- DisclosureGroup("Advanced") {
+ DisclosureGroup(isExpanded: $isAdvancedExpanded) {
VStack(alignment: .leading, spacing: 12) {
Text("Manual tokens are intended for development and may stop working when they expire.")
.font(.subheadline)
@@ -545,8 +546,11 @@ private struct ProviderAddAccountView: View {
.accessibilityIdentifier("addAccount.saveManualToken")
}
.padding(.top, 10)
+ } label: {
+ Text("Advanced")
+ .contentShape(Rectangle())
+ .onTapGesture { isAdvancedExpanded.toggle() }
}
- .accessibilityIdentifier("addAccount.advanced")
}
}
.navigationTitle("Add Account")
@@ -766,6 +770,7 @@ private struct ProviderAccountManagementView: View {
ForEach(Array(driveDescriptors.enumerated()), id: \.element.id) { index, descriptor in
NavigationLink(value: ProviderSetupRoute.drive(descriptor.id)) {
ProviderDriveRow(descriptor: descriptor)
+ .contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityIdentifier("account.drive.\(descriptor.driveID)")
@@ -1024,7 +1029,7 @@ private struct ProviderDriveManagementView: View {
}
private func driveForm(_ descriptor: ProviderDriveDescriptor) -> some View {
- List {
+ let content = Group {
Section("Drive") {
LabeledContent("Name", value: descriptor.name)
LabeledContent("Drive ID", value: String(descriptor.driveID))
@@ -1244,6 +1249,13 @@ private struct ProviderDriveManagementView: View {
}
}
}
+ #if os(macOS)
+ // A grouped form exposes its controls individually to Accessibility;
+ // macOS List rows otherwise combine the action into an unnamed row.
+ return Form { content }.formStyle(.grouped)
+ #else
+ return List { content }
+ #endif
}
@ViewBuilder
diff --git a/potassiumProvider/ProviderUITestFixture.swift b/potassiumProvider/ProviderUITestFixture.swift
index 286500e..bc925f5 100644
--- a/potassiumProvider/ProviderUITestFixture.swift
+++ b/potassiumProvider/ProviderUITestFixture.swift
@@ -1,4 +1,4 @@
-#if DEBUG
+#if DEBUG && !STABILITY
import Foundation
import PotassiumProviderCore
diff --git a/potassiumProvider/StabilityActionRegistration.swift b/potassiumProvider/StabilityActionRegistration.swift
new file mode 100644
index 0000000..4bedf1d
--- /dev/null
+++ b/potassiumProvider/StabilityActionRegistration.swift
@@ -0,0 +1,57 @@
+#if os(macOS) && STABILITY
+import Foundation
+
+enum StabilityActionRegistrationError: Error {
+ case unavailable, ambiguous, timedOut
+}
+
+/// Actions extensions are discovered separately from the replicated instance.
+/// Attesting the latter does not exclude an older Actions binary with the same ID.
+@MainActor
+enum StabilityActionRegistration {
+ static func verify(appURL: URL) async throws {
+ try StabilityAppGroupProvisioning.verify(appURL: appURL)
+ let extensionURL = appURL.appendingPathComponent("Contents/PlugIns/potassiumProviderActions.appex")
+ guard let identifier = Bundle(url: extensionURL)?.bundleIdentifier else { throw StabilityActionRegistrationError.unavailable }
+ let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("stability-registration-\(UUID().uuidString)")
+ guard FileManager.default.createFile(atPath: outputURL.path, contents: nil, attributes: [.posixPermissions: 0o600]) else {
+ throw StabilityActionRegistrationError.unavailable
+ }
+ defer { try? FileManager.default.removeItem(at: outputURL) }
+ let output = try FileHandle(forWritingTo: outputURL)
+ defer { try? output.close() }
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/pluginkit")
+ process.arguments = ["-m", "-A", "-D", "-v", "-i", identifier]
+ process.standardOutput = output
+ process.standardError = FileHandle.nullDevice
+ try process.run()
+ defer { if process.isRunning { process.terminate() } }
+ let deadline = ContinuousClock.now.advanced(by: .seconds(10))
+ while process.isRunning {
+ try Task.checkCancellation()
+ guard ContinuousClock.now < deadline else { throw StabilityActionRegistrationError.timedOut }
+ try await Task.sleep(for: .milliseconds(100))
+ }
+ guard process.terminationStatus == 0,
+ (try outputURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? Int.max) <= 1_024 * 1_024,
+ let listing = String(data: try Data(contentsOf: outputURL), encoding: .utf8) else {
+ throw StabilityActionRegistrationError.unavailable
+ }
+ do { try validate(listing: listing, identifier: identifier, expectedURL: extensionURL) }
+ catch {
+ print("finder stability preflight: Actions extension registration is missing or ambiguous; register only the selected app's Actions extension before retrying")
+ throw error
+ }
+ }
+
+ static func validate(listing: String, identifier: String, expectedURL: URL) throws {
+ let registrations = listing.split(separator: "\n").filter { $0.contains(identifier + "(") }
+ guard registrations.count == 1, let line = registrations.first else { throw StabilityActionRegistrationError.ambiguous }
+ let columns = line.split(separator: "\t", omittingEmptySubsequences: false)
+ guard columns.count == 4, let path = columns.last, path.hasPrefix("/"),
+ URL(fileURLWithPath: String(path)).standardizedFileURL.resolvingSymlinksInPath() ==
+ expectedURL.standardizedFileURL.resolvingSymlinksInPath() else { throw StabilityActionRegistrationError.ambiguous }
+ }
+}
+#endif
diff --git a/potassiumProvider/StabilityAppGroupProvisioning.swift b/potassiumProvider/StabilityAppGroupProvisioning.swift
new file mode 100644
index 0000000..c9c206d
--- /dev/null
+++ b/potassiumProvider/StabilityAppGroupProvisioning.swift
@@ -0,0 +1,84 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+import Security
+
+enum StabilityAppGroupProvisioningError: Error {
+ case unreadableSignature, unreadableProfile, unauthorizedGroup, invalidApplicationIdentity, expiredProfile
+}
+
+/// A valid code signature alone does not authorize macOS app-group access.
+/// Keep the qualification read-only and never export the profile or its identifiers.
+enum StabilityAppGroupProvisioning {
+ static func verify(appURL: URL) throws {
+ for (role, url) in [
+ ("app", appURL),
+ ("provider", appURL.appendingPathComponent("Contents/PlugIns/potassiumProviderFileProvider.appex")),
+ ("actions", appURL.appendingPathComponent("Contents/PlugIns/potassiumProviderActions.appex")),
+ ] {
+ do { try verifyBundle(at: url) }
+ catch {
+ print("finder stability preflight: \(role) app-group provisioning failed; rebuild with registered App Groups and updated provisioning profiles")
+ throw error
+ }
+ }
+ }
+
+ private static func verifyBundle(at url: URL) throws {
+ var code: SecStaticCode?
+ guard SecStaticCodeCreateWithPath(url as CFURL, [], &code) == errSecSuccess, let code,
+ SecStaticCodeCheckValidity(code, [], nil) == errSecSuccess else {
+ throw StabilityAppGroupProvisioningError.unreadableSignature
+ }
+ var information: CFDictionary?
+ guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &information) == errSecSuccess,
+ let information = information as? [String: Any],
+ let entitlements = information[kSecCodeInfoEntitlementsDict as String] as? [String: Any],
+ let bundleIdentifier = Bundle(url: url)?.bundleIdentifier else {
+ throw StabilityAppGroupProvisioningError.unreadableSignature
+ }
+ let profileURL = url.appendingPathComponent("Contents/embedded.provisionprofile")
+ guard let size = try? profileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize,
+ size <= 1_024 * 1_024, let data = try? Data(contentsOf: profileURL) else {
+ throw StabilityAppGroupProvisioningError.unreadableProfile
+ }
+ var decoder: CMSDecoder?
+ guard CMSDecoderCreate(&decoder) == errSecSuccess, let decoder,
+ data.withUnsafeBytes({ bytes in
+ guard let base = bytes.baseAddress else { return errSecDecode }
+ return CMSDecoderUpdateMessage(decoder, base, bytes.count)
+ }) == errSecSuccess,
+ CMSDecoderFinalizeMessage(decoder) == errSecSuccess else {
+ throw StabilityAppGroupProvisioningError.unreadableProfile
+ }
+ var content: CFData?
+ guard CMSDecoderCopyContent(decoder, &content) == errSecSuccess, let content,
+ let profile = try PropertyListSerialization.propertyList(from: content as Data, format: nil) as? [String: Any] else {
+ throw StabilityAppGroupProvisioningError.unreadableProfile
+ }
+ // This qualifies the installed profile's claims; macOS remains the
+ // authority for CMS trust, device eligibility and runtime validation.
+ try validate(entitlements: entitlements, profile: profile, bundleIdentifier: bundleIdentifier)
+ }
+
+ static func validate(entitlements: [String: Any], profile: [String: Any], bundleIdentifier: String, now: Date = Date()) throws {
+ guard let grants = profile["Entitlements"] as? [String: Any],
+ let expiration = profile["ExpirationDate"] as? Date else {
+ throw StabilityAppGroupProvisioningError.unreadableProfile
+ }
+ guard expiration > now else { throw StabilityAppGroupProvisioningError.expiredProfile }
+ guard let team = entitlements["com.apple.developer.team-identifier"] as? String, !team.isEmpty,
+ let application = entitlements["com.apple.application-identifier"] as? String,
+ !application.contains("$("), application == team + "." + bundleIdentifier,
+ grants["com.apple.application-identifier"] as? String == application,
+ grants["com.apple.developer.team-identifier"] as? String == team else {
+ throw StabilityAppGroupProvisioningError.invalidApplicationIdentity
+ }
+ let group = ProviderConstants.appGroupIdentifier
+ guard (entitlements["com.apple.security.application-groups"] as? [String])?.contains(group) == true,
+ (grants["com.apple.security.application-groups"] as? [String])?.contains(group) == true else {
+ throw StabilityAppGroupProvisioningError.unauthorizedGroup
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/StabilityExtensionProcessController.swift b/potassiumProvider/StabilityExtensionProcessController.swift
new file mode 100644
index 0000000..143504b
--- /dev/null
+++ b/potassiumProvider/StabilityExtensionProcessController.swift
@@ -0,0 +1,139 @@
+#if os(macOS) && STABILITY
+import Darwin
+import Foundation
+import PotassiumProviderCore
+
+/// Operates only on the exact signed extension embedded in this ordinary app.
+/// Domain/account isolation is verified by the caller before any termination.
+@MainActor
+final class StabilityExtensionProcessController {
+ private struct ProcessIdentity: Equatable {
+ let pid: Int32
+ let startedAt: Date
+ }
+ private let mode: StabilityExtensionLaunchMode
+ private let run: StabilityRunHandle
+ private let executable: URL
+ private let expectedCodeHash: String
+ private let initial: ProcessIdentity?
+ private let createdAt = Date()
+ private var preparedAt: Date?
+
+ init(mode: StabilityExtensionLaunchMode, run: StabilityRunHandle) throws {
+ self.mode = mode; self.run = run
+ let extensionURL = Bundle.main.bundleURL.appendingPathComponent("Contents/PlugIns/potassiumProviderFileProvider.appex")
+ guard let executable = Bundle(url: extensionURL)?.executableURL,
+ let hash = StabilityDiagnosticIdentity.codeHash(at: extensionURL) else { throw FinderLiveError.unverifiedBuild }
+ self.executable = executable; expectedCodeHash = hash
+ initial = try Self.observe(executable: executable, codeHash: hash)
+ if mode == .running, initial == nil { throw StabilityLaunchPreparationError.initialProcessAbsent }
+ }
+
+ func prepare(verifySafety: @MainActor () async throws -> Void,
+ requestObservation: @MainActor (Duration) async throws -> Void) async throws {
+ try await verifySafety()
+ if mode == .running {
+ let end = ContinuousClock.now.advanced(by: .seconds(90))
+ // Resolving an already-known root can be answered by the daemon's
+ // cache. Explicitly request a callback instead of waiting for an
+ // incidental poll while an idle extension may be discarded.
+ try await StabilityWarmLaunchObservation.request(
+ verifyProcess: {
+ guard try self.observe() == self.initial else { throw StabilityLaunchPreparationError.initialProcessChanged }
+ },
+ signalWorkingSet: { try await requestObservation(ContinuousClock.now.duration(to: end)) })
+ while true {
+ try Task.checkCancellation()
+ guard try observe() == initial else { throw StabilityLaunchPreparationError.initialProcessChanged }
+ guard ContinuousClock.now < end else { throw StabilityDeadlineError.expired }
+ let boundary = Date(timeIntervalSince1970: floor(Date().timeIntervalSince1970))
+ let events = try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL)
+ if boundary >= createdAt, events.contains(where: { $0.source == .fileProviderExtension &&
+ $0.phase == .completed && $0.occurredAt < boundary &&
+ $0.processCodeHash == expectedCodeHash && $0.processInstanceID != nil }) {
+ preparedAt = boundary
+ return
+ }
+ try await Task.sleep(for: .milliseconds(250))
+ }
+ }
+ // Wait for all recorded work to finish before stopping this one process.
+ // Any callback racing this fence remains visible and prevents certification.
+ let end = ContinuousClock.now.advanced(by: .seconds(90))
+ var quietSince: ContinuousClock.Instant?
+ var lastEventID: UUID?
+ while true {
+ try Task.checkCancellation()
+ guard ContinuousClock.now < end else { throw StabilityDeadlineError.expired }
+ let events = try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL)
+ let starts = events.filter { $0.phase == .started }
+ guard starts.allSatisfy({ $0.spanID != nil }),
+ !FileManager.default.fileExists(atPath: run.directoryURL.appendingPathComponent("diagnostic-health.failed").path) else {
+ throw StabilityLiveEvidenceError.pendingOperations
+ }
+ let started = Set(starts.compactMap(\.spanID))
+ let terminal = Set(events.filter { [.completed, .failed, .cancelled].contains($0.phase) }.compactMap(\.spanID))
+ if events.last?.id != lastEventID { quietSince = nil; lastEventID = events.last?.id }
+ if started.isSubset(of: terminal) {
+ quietSince = quietSince ?? .now
+ if let quietSince, quietSince.duration(to: .now) >= .seconds(2) { break }
+ } else { quietSince = nil }
+ try await Task.sleep(for: .milliseconds(200))
+ }
+ let current = try observe()
+ // JSONL event times use whole seconds. Two seconds without any new
+ // event leaves a clean whole-second fence without reclassifying old
+ // callbacks as belonging to the replacement process.
+ preparedAt = Date(timeIntervalSince1970: floor(Date().timeIntervalSince1970))
+ if let current {
+ guard try observe() == current, kill(current.pid, SIGTERM) == 0 else { throw FinderLiveError.unverifiedBuild }
+ while Self.identity(pid: current.pid) == current {
+ try Task.checkCancellation()
+ guard ContinuousClock.now < end else { throw StabilityDeadlineError.expired }
+ try await Task.sleep(for: .milliseconds(100))
+ }
+ }
+ print("finder conflict: fresh extension launch requested after recorded work settled")
+ }
+
+ func evidence(report: StabilityFinderRunReport) throws -> StabilityExtensionLaunchEvidence {
+ guard let preparedAt, let current = try observe() else { throw FinderLiveError.unverifiedBuild }
+ if mode == .running, current != initial { throw FinderLiveError.unverifiedBuild }
+ let events = try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL)
+ let ids = Set(events.filter { $0.source == .fileProviderExtension && $0.occurredAt >= preparedAt }.compactMap(\.processInstanceID))
+ guard ids.count == 1, let instance = ids.first else { throw FinderLiveError.unverifiedBuild }
+ let evidence = StabilityExtensionLaunchEvidence(runID: run.runID, mode: mode, recordingStartedAt: report.startedAt, preparedAt: preparedAt,
+ processStartedAt: current.startedAt, processInstanceID: instance, expectedCodeHash: expectedCodeHash)
+ try evidence.validate(runID: run.runID, report: report, diagnostics: events)
+ return evidence
+ }
+
+ private func observe() throws -> ProcessIdentity? { try Self.observe(executable: executable, codeHash: expectedCodeHash) }
+ private static func observe(executable: URL, codeHash: String) throws -> ProcessIdentity? {
+ let capacity = Int(proc_listallpids(nil, 0)) + 64
+ guard capacity > 64 else { throw FinderLiveError.unverifiedBuild }
+ var pids = [Int32](repeating: 0, count: capacity)
+ let count = pids.withUnsafeMutableBytes { proc_listallpids($0.baseAddress, Int32($0.count)) }
+ guard count >= 0, count <= capacity else { throw FinderLiveError.unverifiedBuild }
+ var matches: [ProcessIdentity] = []
+ for pid in pids.prefix(Int(count)) where pid > 0 {
+ var path = [CChar](repeating: 0, count: 4 * Int(MAXPATHLEN))
+ let length = path.withUnsafeMutableBytes { proc_pidpath(pid, $0.baseAddress, UInt32($0.count)) }
+ guard length > 0, String(cString: path) == executable.path else { continue }
+ guard let identity = identity(pid: pid), StabilityDiagnosticIdentity.codeHash(forProcessIdentifier: pid) == codeHash else {
+ throw FinderLiveError.unverifiedBuild
+ }
+ matches.append(identity)
+ }
+ guard matches.count <= 1 else { throw FinderLiveError.unverifiedBuild }
+ return matches.first
+ }
+ private static func identity(pid: Int32) -> ProcessIdentity? {
+ var info = proc_bsdinfo()
+ let size = MemoryLayout.stride
+ guard proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, Int32(size)) == size else { return nil }
+ return ProcessIdentity(pid: pid, startedAt: Date(timeIntervalSince1970:
+ Double(info.pbi_start_tvsec) + Double(info.pbi_start_tvusec) / 1_000_000))
+ }
+}
+#endif
diff --git a/potassiumProvider/StabilityLabView.swift b/potassiumProvider/StabilityLabView.swift
new file mode 100644
index 0000000..a93dd83
--- /dev/null
+++ b/potassiumProvider/StabilityLabView.swift
@@ -0,0 +1,206 @@
+#if os(macOS) && STABILITY
+import PotassiumProviderCore
+import SwiftUI
+
+struct StabilityLabView: View {
+ @ObservedObject var model: PotassiumProviderAppModel
+ @State private var selectedAccountIdentifier: String?
+ @State private var selectedDriveID: Int?
+ @State private var resetConfirmation = ""
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ introduction
+ isolationState
+ if model.stabilityLabConfiguration == nil {
+ provisioning
+ } else {
+ verificationAndReset
+ runControls
+ }
+ feedback
+ }
+ .frame(maxWidth: 720, alignment: .leading)
+ .frame(maxWidth: .infinity)
+ .padding(28)
+ }
+ .navigationTitle("Stability Lab")
+ .task { await prepareSelection() }
+ }
+ }
+
+ private var introduction: some View {
+ GroupBox("Disposable development root") {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("This opt-in lab uses the selected account’s saved Keychain login and registers one verified plaintext folder inside Private.")
+ Text("Use only a dedicated development account containing no customer data. Live checks and Finder mutations remain outside CI.")
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ private var isolationState: some View {
+ GroupBox("Isolation gate") {
+ VStack(alignment: .leading, spacing: 8) {
+ if model.domains.contains(where: { $0.purpose == .ordinary }) {
+ Label("Ordinary saved domains are present. Provisioning and reset are blocked.", systemImage: "exclamationmark.triangle.fill")
+ .foregroundStyle(.orange)
+ Text("Run scripts/uninstall-file-provider.sh --dry-run, inspect the plan, then use the explicit safe --yes cleanup. The lab never invokes hard purge.")
+ .foregroundStyle(.secondary)
+ } else {
+ Label("No ordinary saved domain is visible to this build.", systemImage: "checkmark.shield")
+ .foregroundStyle(.green)
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ private var provisioning: some View {
+ GroupBox("Provision") {
+ VStack(alignment: .leading, spacing: 12) {
+ if model.accounts.isEmpty {
+ Text("Add the dedicated development account in Setup with OAuth or a manual token, then return here.")
+ .foregroundStyle(.secondary)
+ } else {
+ Picker("Account", selection: $selectedAccountIdentifier) {
+ Text("Choose an account").tag(String?.none)
+ ForEach(model.accounts) { account in
+ Text(account.displayName).tag(String?.some(account.accountIdentifier))
+ }
+ }
+ Picker("Internal drive", selection: $selectedDriveID) {
+ Text("Choose a drive").tag(Int?.none)
+ ForEach(selectedDrives) { drive in
+ Text(drive.name).tag(Int?.some(drive.id))
+ }
+ }
+
+ HStack {
+ Button("Refresh drives") {
+ guard let selectedAccountIdentifier else { return }
+ Task {
+ await model.loadDrives(accountIdentifier: selectedAccountIdentifier)
+ normalizeDriveSelection()
+ }
+ }
+ Button("Provision verified lab root") {
+ guard let selectedAccountIdentifier,
+ let drive = selectedDrive else { return }
+ Task {
+ await model.provisionStabilityLab(
+ accountIdentifier: selectedAccountIdentifier,
+ drive: drive
+ )
+ }
+ }
+ .buttonStyle(.borderedProminent)
+ .disabled(
+ selectedDrive == nil ||
+ model.isPerformingStabilityLabOperation ||
+ model.domains.isEmpty == false
+ )
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ private var verificationAndReset: some View {
+ GroupBox("Verify and reset") {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Every verification reloads server-authoritative root and marker metadata. Reset additionally revalidates them and each immediate child's parent before moving that child to trash.")
+ .foregroundStyle(.secondary)
+
+ Button("Verify lab safety gates") {
+ Task { await model.verifyStabilityLab() }
+ }
+ .disabled(model.isPerformingStabilityLabOperation)
+
+ Divider()
+ Text("Reset preserves both the lab root and its ownership marker. It never permanently deletes remote items.")
+ Text("Type: \(StabilityLabResetConfirmation.requiredPhrase)")
+ .font(.callout.monospaced())
+ TextField("Exact reset confirmation", text: $resetConfirmation)
+ .textFieldStyle(.roundedBorder)
+ Button("Move verified lab contents to Trash", role: .destructive) {
+ let phrase = resetConfirmation
+ resetConfirmation = ""
+ Task { await model.resetStabilityLab(typedConfirmation: phrase) }
+ }
+ .disabled(
+ resetConfirmation != StabilityLabResetConfirmation.requiredPhrase ||
+ model.isPerformingStabilityLabOperation ||
+ model.activeStabilityRunID != nil
+ )
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ private var runControls: some View {
+ GroupBox("Diagnostic run") {
+ HStack {
+ if model.activeStabilityRunID == nil {
+ Button("Verify and start run") {
+ Task { await model.startStabilityRun() }
+ }
+ .buttonStyle(.borderedProminent)
+ } else {
+ Label("Run active", systemImage: "record.circle")
+ .foregroundStyle(.red)
+ Button("Finish and seal run") {
+ Task { await model.finishStabilityRun() }
+ }
+ }
+ }
+ .disabled(model.isPerformingStabilityLabOperation)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+
+ @ViewBuilder
+ private var feedback: some View {
+ if let error = model.errorMessage {
+ Label(error, systemImage: "xmark.octagon.fill")
+ .foregroundStyle(.red)
+ } else if let status = model.statusMessage {
+ Label(status, systemImage: "checkmark.circle.fill")
+ .foregroundStyle(.secondary)
+ }
+ if model.isPerformingStabilityLabOperation {
+ ProgressView("Checking Stability Lab safety state…")
+ }
+ }
+
+ private var selectedDrives: [KDriveDriveSummary] {
+ guard let selectedAccountIdentifier else { return [] }
+ return model.drives(for: selectedAccountIdentifier).filter(\.isUsableInternalDrive)
+ }
+
+ private var selectedDrive: KDriveDriveSummary? {
+ selectedDrives.first { $0.id == selectedDriveID }
+ }
+
+ @MainActor
+ private func prepareSelection() async {
+ if selectedAccountIdentifier == nil {
+ selectedAccountIdentifier = model.accounts.first?.accountIdentifier
+ }
+ if let selectedAccountIdentifier {
+ await model.loadDrives(accountIdentifier: selectedAccountIdentifier)
+ }
+ normalizeDriveSelection()
+ }
+
+ private func normalizeDriveSelection() {
+ if selectedDrives.contains(where: { $0.id == selectedDriveID }) == false {
+ selectedDriveID = selectedDrives.first?.id
+ }
+ }
+}
+#endif
diff --git a/potassiumProvider/StabilityLaunchPreparation.swift b/potassiumProvider/StabilityLaunchPreparation.swift
new file mode 100644
index 0000000..c4848b7
--- /dev/null
+++ b/potassiumProvider/StabilityLaunchPreparation.swift
@@ -0,0 +1,19 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+
+@MainActor
+enum StabilityWarmLaunchObservation {
+ static func request(verifyProcess: () throws -> Void,
+ signalWorkingSet: () async throws -> Void) async throws {
+ try Task.checkCancellation()
+ try verifyProcess()
+ try await signalWorkingSet()
+ try Task.checkCancellation()
+ try verifyProcess()
+ // Signal completion is only acknowledgement. The controller still
+ // waits for a real, correctly signed provider callback to complete.
+ }
+}
+
+#endif
diff --git a/potassiumProvider/SystemFinderStabilityCommandExecutor.swift b/potassiumProvider/SystemFinderStabilityCommandExecutor.swift
new file mode 100644
index 0000000..7d625d1
--- /dev/null
+++ b/potassiumProvider/SystemFinderStabilityCommandExecutor.swift
@@ -0,0 +1,755 @@
+#if os(macOS) && STABILITY
+import AppKit
+import ApplicationServices
+import CoreServices
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+
+@MainActor
+final class SystemFinderStabilityCommandExecutor: FinderStabilityCommandExecuting {
+ private let contextLoader: FinderStabilityContextLoader
+ private let permissionChecker: any FinderStabilityPermissionChecking
+ private let scenarioRunner: any FinderStabilityScenarioRunning
+ private let runCoordinatorProvider: () throws -> StabilityRunCoordinator
+ private let statusWriter: (StabilityLiveStatus, StabilityRunHandle) throws -> Void
+ private let registrationChecker: () async throws -> Void
+
+ init() {
+ self.contextLoader = FinderStabilityContextLoader()
+ self.permissionChecker = SystemFinderStabilityPermissionChecker()
+ self.scenarioRunner = LiveFinderStabilityScenarioRunner()
+ self.runCoordinatorProvider = { try StabilityRunCoordinator() }
+ self.statusWriter = { try $0.write(to: $1) }
+ self.registrationChecker = { try await StabilityActionRegistration.verify(appURL: Bundle.main.bundleURL) }
+ }
+
+ init(
+ contextLoader: FinderStabilityContextLoader,
+ permissionChecker: any FinderStabilityPermissionChecking,
+ scenarioRunner: any FinderStabilityScenarioRunning,
+ runCoordinatorProvider: @escaping () throws -> StabilityRunCoordinator = { try StabilityRunCoordinator() },
+ statusWriter: @escaping (StabilityLiveStatus, StabilityRunHandle) throws -> Void = { try $0.write(to: $1) },
+ registrationChecker: @escaping () async throws -> Void = { try await StabilityActionRegistration.verify(appURL: Bundle.main.bundleURL) }
+ ) {
+ self.contextLoader = contextLoader
+ self.permissionChecker = permissionChecker
+ self.scenarioRunner = scenarioRunner
+ self.runCoordinatorProvider = runCoordinatorProvider
+ self.statusWriter = statusWriter
+ self.registrationChecker = registrationChecker
+ }
+
+ func provision() async -> FinderStabilityCommandResult {
+ let model = PotassiumProviderAppModel(automaticallyReloadStoredState: false)
+ await model.reloadStoredState()
+ if model.domains.count == 1, model.stabilityLabConfiguration != nil {
+ do {
+ try await model.resumeStabilityLabRegistration()
+ print("finder stability provision: existing lab verified and registered")
+ return .ready
+ } catch {
+ if let safety = error as? StabilityLabRemoteCoordinatorError { print(safety.localizedDescription) }
+ print("finder stability registration: type \(String(reflecting: type(of: error))); class \(ProviderDiagnosticErrorClassifier.classify(error).rawValue); code \((error as NSError).code)")
+ return .failed
+ }
+ }
+ guard model.accounts.count == 1, model.domains.isEmpty, let account = model.accounts.first else {
+ print("finder stability provision: requires one saved account and no domain")
+ return .rejected
+ }
+ await model.loadDrives(accountIdentifier: account.accountIdentifier)
+ let drives = model.drives(for: account.accountIdentifier).filter(\.isUsableInternalDrive)
+ guard let drive = drives.first(where: { $0.id == model.selectedDriveIDs[account.accountIdentifier] }) ?? drives.first else {
+ print("finder stability provision: available drive count \(drives.count); error \(model.lastDriveDiscoveryErrorClass?.rawValue ?? "none") \(model.lastDriveDiscoveryErrorCode.map(String.init) ?? "")")
+ return .rejected
+ }
+ await model.provisionStabilityLab(accountIdentifier: account.accountIdentifier, drive: drive)
+ guard model.stabilityLabConfiguration != nil, model.errorMessage == nil else {
+ print("finder stability provision: " + (model.errorMessage ?? "root creation or domain registration failed"))
+ return .failed
+ }
+ print("finder stability provision: verified lab created inside Private")
+ return .ready
+ }
+
+ func watch() async -> FinderStabilityCommandResult {
+ do {
+ guard let run = try StabilityDiagnosticIdentity.activeRun() else {
+ print("finder stability watch: no active run")
+ return .ready
+ }
+ var seen: Set = []
+ var previousStatus: StabilityLiveStatus?
+ repeat {
+ if let status = try StabilityLiveStatus.read(from: run), status != previousStatus {
+ print("run \(status.state.rawValue): \(status.scenario?.rawValue ?? "preflight")")
+ previousStatus = status
+ }
+ let events = try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL)
+ for event in events where seen.insert(event.id).inserted {
+ guard [.failed, .cancelled, .checkpoint].contains(event.phase) ||
+ [.runtimeInitialize, .runtimeInvalidate].contains(event.operation) else { continue }
+ print("\(event.source.rawValue) \(event.operation.rawValue) \(event.phase.rawValue) \(event.errorClass?.rawValue ?? "") \(event.errorCode.map(String.init) ?? "")")
+ }
+ if try StabilityDiagnosticIdentity.activeRun()?.runID != run.runID { break }
+ try await Task.sleep(for: .seconds(1))
+ } while !Task.isCancelled
+ return .ready
+ } catch { return .failed }
+ }
+
+ func preflight(requestPermissions: Bool) async -> FinderStabilityCommandResult {
+ do {
+ try await registrationChecker()
+ let context = try await contextLoader.loadPreflight()
+ let results = FinderStabilityPreflightEvaluator.results(
+ permissions: permissionChecker.check(requestPermissions: requestPermissions),
+ hasFileProviderRegistration: true,
+ hasVerifiedFileProviderConsent: context.hasVerifiedFileProviderConsent,
+ labSafetyAllowed: true,
+ recordedAt: Date()
+ )
+ for result in results { print("finder stability permission: \(result.check.rawValue) \(result.outcome)") }
+ return FinderStabilityPreflightEvaluator.commandResult(for: results)
+ } catch {
+ return .rejected
+ }
+ }
+
+ func conflicts(requestPermissions: Bool, selectedCase: StabilityLiveConflictCase?, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult {
+ var failed = false
+ for conflict in selectedCase.map({ [$0] }) ?? StabilityLiveConflictCase.allCases {
+ print("finder conflict case: " + conflict.rawValue)
+ let result = await run(requestPermissions: requestPermissions, conflictCase: conflict, extensionLaunchMode: extensionLaunchMode)
+ if result != .completed { failed = true }
+ // An unsealed run still owns the recorder. Do not repurpose its
+ // evidence or recover it automatically while the owner is alive.
+ if (try? await runCoordinatorProvider().activeRun()) != nil { return .failed }
+ }
+ return failed ? .failed : .completed
+ }
+
+ func run(requestPermissions: Bool) async -> FinderStabilityCommandResult {
+ await run(requestPermissions: requestPermissions, conflictCase: nil)
+ }
+
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?) async -> FinderStabilityCommandResult {
+ await run(requestPermissions: requestPermissions, conflictCase: nil, extensionLaunchMode: extensionLaunchMode)
+ }
+
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?, includePermanentDeletion: Bool) async -> FinderStabilityCommandResult {
+ await run(requestPermissions: requestPermissions, conflictCase: nil, extensionLaunchMode: extensionLaunchMode,
+ includePermanentDeletion: includePermanentDeletion)
+ }
+
+ private func run(requestPermissions: Bool, conflictCase: StabilityLiveConflictCase?, extensionLaunchMode: StabilityExtensionLaunchMode? = nil, includePermanentDeletion: Bool = false) async -> FinderStabilityCommandResult {
+ let reportStartedAt = Date()
+ let runCoordinator: StabilityRunCoordinator
+ let ownedRun: StabilityOwnedRunHandle
+ let launchController: StabilityExtensionProcessController?
+ do {
+ // Domain discovery or visible-root resolution may launch the
+ // extension. Its first callback must already have a recorder.
+ runCoordinator = try runCoordinatorProvider()
+ ownedRun = try await runCoordinator.startOwnedRun()
+ try statusWriter(StabilityLiveStatus(state: .preflight), ownedRun.run)
+ if let conflictCase {
+ try await runCoordinator.selectConflictProfile(conflictCase, ownedRun: ownedRun, extensionLaunchMode: extensionLaunchMode)
+ } else if let extensionLaunchMode {
+ try await runCoordinator.requireExtensionLaunch(extensionLaunchMode, ownedRun: ownedRun)
+ }
+ do {
+ launchController = try extensionLaunchMode.map { try StabilityExtensionProcessController(mode: $0, run: ownedRun.run) }
+ } catch {
+ StabilityLaunchPreparationFailure.record(stage: .initialProcessObservation, error: error, run: ownedRun.run)
+ throw error
+ }
+ } catch {
+ print("finder conflict: extension launch preparation could not establish the requested initial state")
+ return .failed
+ }
+ let preflightContext: FinderStabilityPreflightContext
+ do {
+ try await registrationChecker()
+ preflightContext = try await contextLoader.loadPreflight()
+ } catch {
+ try? statusWriter(StabilityLiveStatus(state: .failed), ownedRun.run)
+ StabilityLaunchPreparationFailure.record(stage: .labPreflight, error: error, run: ownedRun.run)
+ // Preserve preflight diagnostics and the unsealed owned run for
+ // explicit stale-owner recovery; missing context cannot certify it.
+ return .rejected
+ }
+
+ var preflight = FinderStabilityPreflightEvaluator.results(
+ permissions: permissionChecker.check(requestPermissions: false),
+ hasFileProviderRegistration: true,
+ hasVerifiedFileProviderConsent: preflightContext.hasVerifiedFileProviderConsent,
+ labSafetyAllowed: true,
+ recordedAt: Date()
+ )
+ var preflightCommandResult = FinderStabilityPreflightEvaluator.commandResult(for: preflight)
+ while preflightCommandResult == .checkpoint {
+ let missing = preflight.filter { if case .checkpoint = $0.outcome { return true }; return false }.map { $0.check.rawValue }.joined(separator: ", ")
+ try? statusWriter(StabilityLiveStatus(state: .awaitingPermissions), ownedRun.run)
+ print("finder stability paused: " + missing)
+ if requestPermissions { _ = permissionChecker.check(requestPermissions: true) }
+ guard await FinderRunnerPanel.awaitResume(message: "Allow the required macOS permissions, then continue. Pending: " + missing) else { break }
+ do {
+ try await preflightContext.verifySafety()
+ preflight = FinderStabilityPreflightEvaluator.results(
+ permissions: permissionChecker.check(requestPermissions: false), hasFileProviderRegistration: true,
+ hasVerifiedFileProviderConsent: true, labSafetyAllowed: true, recordedAt: Date())
+ preflightCommandResult = FinderStabilityPreflightEvaluator.commandResult(for: preflight)
+ } catch { preflightCommandResult = .rejected; break }
+ }
+ var context: FinderStabilityLiveContext
+ var preparationStage = StabilityLaunchPreparationFailure.Stage.launchPreparation
+ do {
+ // Reconstruct after the run starts so typed network spans bind to
+ // this run's active JSONL recorder instead of a cached nil sink.
+ if preflightCommandResult == .ready {
+ try await launchController?.prepare(verifySafety: preflightContext.verifySafety,
+ requestObservation: preflightContext.requestWorkingSetRefresh)
+ }
+ preparationStage = .liveContext
+ context = try await contextLoader.load(
+ ownedRun: ownedRun,
+ runCoordinator: runCoordinator
+ )
+ context.includePermanentDeletion = includePermanentDeletion
+ } catch {
+ StabilityLaunchPreparationFailure.record(stage: preparationStage, error: error, run: ownedRun.run)
+ // Never seal a run without its required Finder report. The active
+ // owner marker and partial bundle remain intact for explicit stale
+ // owner recovery after this process exits.
+ return .failed
+ }
+
+ let execution: FinderStabilityScenarioExecution
+ if preflightCommandResult == .ready {
+ if let conflictCase {
+ execution = await LiveFinderStabilityScenarioRunner(conflictCase: conflictCase).run(context: context)
+ } else { execution = await scenarioRunner.run(context: context) }
+ } else {
+ let skipReason: StabilityFinderStepSkipReason = preflightCommandResult == .checkpoint
+ ? .preflightCheckpoint
+ : .preflightFailure
+ execution = FinderStabilityScenarioExecution.skippingAll(
+ reason: skipReason,
+ startedAt: reportStartedAt
+ )
+ }
+
+ var candidateReport: StabilityFinderRunReport?
+ do {
+ let report = try StabilityFinderRunReport(
+ schemaVersion: StabilityFinderRunReport.selectiveSchemaVersion,
+ correlationID: UUID(),
+ startedAt: reportStartedAt,
+ finishedAt: Date(),
+ preflightResults: preflight,
+ stepResults: execution.stepResults
+ )
+ candidateReport = report
+ guard execution.canSeal else { throw StabilityLiveEvidenceError.incompleteRun }
+ if report.stepSummary.passed > 0, let launchController {
+ let launch = try launchController.evidence(report: report)
+ try await runCoordinator.recordExtensionLaunch(launch, ownedRun: ownedRun)
+ }
+ try await runCoordinator.writeFinderEvidence(
+ ownedRun: ownedRun,
+ report: report,
+ observations: execution.observations
+ )
+ _ = try await runCoordinator.finishOwnedRun(
+ ownedRun,
+ summary: StabilityRunSummary(
+ assertionCount: report.stepResults.flatMap(\.assertions).count,
+ failedAssertionCount: report.stepResults.flatMap(\.assertions).filter {
+ if case .failed = $0.outcome { return true }
+ return false
+ }.count,
+ checkpointCount: report.preflightSummary.checkpointed
+ + report.stepSummary.checkpointed
+ )
+ )
+ // Live failures are comparison evidence. Retention is an explicit
+ // maintenance operation; never prune them after a later run.
+
+ if report.preflightSummary.failed > 0 || report.stepSummary.failed > 0 {
+ return .failed
+ }
+ if report.preflightSummary.checkpointed > 0 || report.stepSummary.checkpointed > 0 {
+ return .checkpoint
+ }
+ if let conflictCase {
+ guard report.stepResults.first(where: { $0.scenario == conflictCase.scenario })?.outcome == .passed else { return .failed }
+ } else {
+ if report.hasOnlyDeferredPermanentDeletion { return .completedWithDeferredDeletion }
+ guard report.stepSummary.passed == StabilityFinderScenario.allCases.count else { return .failed }
+ }
+ return .completed
+ } catch {
+ // A report is the commit marker for the two evidence JSONL files.
+ // Do not create summary.json after any assembly/finalization
+ // failure, because that would seal a partial bundle.
+ print("finder stability: evidence sealing rejected; reason \((error as? StabilityLiveEvidenceError)?.rawValue ?? "assemblyFailure")")
+ if let candidateReport {
+ do {
+ try await runCoordinator.recordFinderEvidenceRejection(
+ StabilityFinderEvidenceRejection(report: candidateReport, observations: execution.observations, error: error),
+ ownedRun: ownedRun)
+ } catch { print("finder stability: rejected candidate could not be retained") }
+ }
+ return .failed
+ }
+ }
+
+ func recoverStaleRun() async -> FinderStabilityCommandResult {
+ do {
+ let runCoordinator = try StabilityRunCoordinator()
+ _ = try await runCoordinator.abandonStaleOwnedRun()
+ return .recovered
+ } catch ProviderDiagnosticStoreError.finderOwnerProcessStillRunning {
+ return .rejected
+ } catch ProviderDiagnosticStoreError.noStaleFinderRun {
+ return .rejected
+ } catch {
+ return .failed
+ }
+ }
+
+}
+
+enum FinderStabilityPreflightEvaluator {
+ static func results(
+ permissions: FinderStabilityPermissionSnapshot,
+ hasFileProviderRegistration: Bool,
+ hasVerifiedFileProviderConsent: Bool,
+ labSafetyAllowed: Bool,
+ recordedAt: Date
+ ) -> [StabilityFinderPreflightResult] {
+ let outcomes: [StabilityFinderPreflightCheck: StabilityFinderPreflightOutcome] = [
+ .accessibilityPermission: permissions.accessibilityOutcome,
+ .finderAutomationPermission: permissions.automationOutcome,
+ .screenRecordingPermission: permissions.recordingOutcome,
+ .fileProviderRegistration: hasFileProviderRegistration
+ ? .passed
+ : .failed(.fileProviderNotRegistered),
+ .fileProviderConsent: hasVerifiedFileProviderConsent
+ ? .passed
+ : .checkpoint(.fileProviderConsentRequired),
+ .stabilityLabSafety: labSafetyAllowed
+ ? .passed
+ : .failed(.stabilityLabRejected),
+ ]
+ return StabilityFinderPreflightCheck.allCases.map { check in
+ StabilityFinderPreflightResult(
+ check: check,
+ outcome: outcomes[check] ?? .failed(.permissionStateUnavailable),
+ recordedAt: recordedAt
+ )
+ }
+ }
+
+ static func commandResult(
+ for results: [StabilityFinderPreflightResult]
+ ) -> FinderStabilityCommandResult {
+ if results.contains(where: {
+ if case .failed = $0.outcome { return true }
+ return false
+ }) {
+ return .rejected
+ }
+ if results.contains(where: {
+ if case .checkpoint = $0.outcome { return true }
+ return false
+ }) {
+ return .checkpoint
+ }
+ return .ready
+ }
+}
+
+struct FinderStabilityPermissionSnapshot: Equatable, Sendable {
+ let accessibilityOutcome: StabilityFinderPreflightOutcome
+ let automationOutcome: StabilityFinderPreflightOutcome
+ var recordingOutcome: StabilityFinderPreflightOutcome = .passed
+}
+
+protocol FinderStabilityPermissionChecking: Sendable {
+ nonisolated func check(requestPermissions: Bool) -> FinderStabilityPermissionSnapshot
+}
+
+struct SystemFinderStabilityPermissionChecker: FinderStabilityPermissionChecking {
+ nonisolated func check(requestPermissions: Bool) -> FinderStabilityPermissionSnapshot {
+ let accessibilityOptions = [
+ kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: requestPermissions,
+ ] as CFDictionary
+ let accessibilityTrusted = AXIsProcessTrustedWithOptions(accessibilityOptions)
+ let accessibility: StabilityFinderPreflightOutcome = accessibilityTrusted
+ ? .passed
+ : .checkpoint(.accessibilityConsentRequired)
+
+ let automation: StabilityFinderPreflightOutcome
+ var target = AEAddressDesc()
+ let finderBundleIdentifier = Data("com.apple.finder".utf8)
+ let descriptorStatus = finderBundleIdentifier.withUnsafeBytes { bytes in
+ AECreateDesc(
+ DescType(typeApplicationBundleID),
+ bytes.baseAddress,
+ finderBundleIdentifier.count,
+ &target
+ )
+ }
+ if descriptorStatus != noErr {
+ automation = .failed(.permissionStateUnavailable)
+ } else {
+ defer { AEDisposeDesc(&target) }
+ let status = AEDeterminePermissionToAutomateTarget(
+ &target,
+ AEEventClass(kAECoreSuite),
+ AEEventID(kAEGetData),
+ requestPermissions
+ )
+ switch status {
+ case noErr:
+ automation = .passed
+ case OSStatus(errAEEventNotPermitted), OSStatus(errAEEventWouldRequireUserConsent):
+ automation = .checkpoint(.finderAutomationConsentRequired)
+ default:
+ automation = .failed(.finderUnavailable)
+ }
+ }
+ return FinderStabilityPermissionSnapshot(
+ accessibilityOutcome: accessibility,
+ automationOutcome: automation,
+ recordingOutcome: (requestPermissions ? CGRequestScreenCaptureAccess() : CGPreflightScreenCaptureAccess()) ? .passed : .checkpoint(.screenRecordingConsentRequired)
+ )
+ }
+}
+
+@MainActor
+struct FinderStabilityLiveContext {
+ let domain: ProviderDomainConfiguration
+ let remoteConfiguration: StabilityLabRemoteConfiguration
+ let remote: any KDriveFileProviding
+ let rootURL: URL
+ let fileProviderManager: NSFileProviderManager
+ let hasVerifiedFileProviderConsent: Bool
+ let verifySafety: @MainActor () async throws -> Void
+ let beginStep: @MainActor (UUID) async throws -> Void
+ let endStep: @MainActor (UUID) async throws -> Void
+ var includePermanentDeletion = false
+}
+
+@MainActor
+struct FinderStabilityPreflightContext {
+ let hasVerifiedFileProviderConsent: Bool
+ let verifySafety: @MainActor () async throws -> Void
+ let requestWorkingSetRefresh: @MainActor (Duration) async throws -> Void
+}
+
+struct FinderStabilityContextLoader {
+ typealias RemoteFactory = @MainActor (
+ _ accessToken: String,
+ _ recorder: (any ProviderDiagnosticRecording)?
+ ) -> any KDriveFileProviding
+ typealias VisibleDomainResolver = @MainActor (
+ _ rootURL: URL
+ ) async throws -> FinderStabilityVisibleItemBinding
+
+ private let accountStoreProvider: @MainActor () throws -> any ProviderAccountStoring
+ private let domainStoreProvider: @MainActor () throws -> any DomainConfigurationStoring
+ private let tokenStore: any OAuthTokenStoring
+ private let registrar: any ProviderDomainRegistering
+ private let remoteFactory: RemoteFactory
+ private let visibleDomainResolver: VisibleDomainResolver
+
+ @MainActor
+ init() {
+ self.accountStoreProvider = { try ProviderAccountFileStore() }
+ self.domainStoreProvider = { try DomainConfigurationFileStore() }
+ self.tokenStore = KeychainOAuthTokenStore(
+ accessGroup: ProviderConstants.keychainAccessGroup
+ )
+ self.registrar = FileProviderDomainRegistrar()
+ self.remoteFactory = { accessToken, recorder in
+ PotassiumKDriveService(
+ bearerToken: accessToken,
+ diagnosticRecorder: recorder,
+ diagnosticSource: .finderRunner
+ )
+ }
+ self.visibleDomainResolver = { rootURL in
+ let resolved = try await Self.identifier(for: rootURL)
+ return FinderStabilityVisibleItemBinding(
+ itemIdentifier: resolved.0.rawValue,
+ domainIdentifier: resolved.1.rawValue
+ )
+ }
+ }
+
+ @MainActor
+ init(
+ accountStore: any ProviderAccountStoring,
+ domainStore: any DomainConfigurationStoring,
+ tokenStore: any OAuthTokenStoring,
+ registrar: any ProviderDomainRegistering,
+ remoteFactory: @escaping RemoteFactory,
+ visibleDomainResolver: @escaping VisibleDomainResolver
+ ) {
+ self.accountStoreProvider = { accountStore }
+ self.domainStoreProvider = { domainStore }
+ self.tokenStore = tokenStore
+ self.registrar = registrar
+ self.remoteFactory = remoteFactory
+ self.visibleDomainResolver = visibleDomainResolver
+ }
+
+ @MainActor
+ func loadPreflight() async throws -> FinderStabilityPreflightContext {
+ let base = try await loadBase()
+ return FinderStabilityPreflightContext(
+ hasVerifiedFileProviderConsent: base.hasVerifiedFileProviderConsent,
+ verifySafety: base.verifySafety,
+ requestWorkingSetRefresh: { timeout in
+ let domain = NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier(rawValue: base.domain.domainIdentifier),
+ displayName: base.domain.displayName)
+ guard let manager = NSFileProviderManager(for: domain) else { throw FinderStabilityContextError.fileProviderNotRegistered }
+ try await StabilityCallbackWaiter().wait(timeout: timeout) { completion in
+ manager.signalEnumerator(for: .workingSet) { error in
+ if let error { completion(.failure(error)) } else { completion(.success(())) }
+ }
+ }
+ }
+ )
+ }
+
+ @MainActor
+ func load(
+ ownedRun: StabilityOwnedRunHandle,
+ runCoordinator: StabilityRunCoordinator
+ ) async throws -> FinderStabilityLiveContext {
+ let base = try await loadBase()
+ let fileProviderDomain = NSFileProviderDomain(
+ identifier: NSFileProviderDomainIdentifier(rawValue: base.domain.domainIdentifier),
+ displayName: base.domain.displayName
+ )
+ guard let manager = NSFileProviderManager(for: fileProviderDomain) else {
+ throw FinderStabilityContextError.fileProviderNotRegistered
+ }
+
+ return FinderStabilityLiveContext(
+ domain: base.domain,
+ remoteConfiguration: base.remoteConfiguration,
+ remote: base.remote,
+ rootURL: base.rootURL,
+ fileProviderManager: manager,
+ hasVerifiedFileProviderConsent: base.hasVerifiedFileProviderConsent,
+ verifySafety: base.verifySafety,
+ beginStep: { correlationID in
+ try await runCoordinator.beginFinderStep(
+ ownedRun: ownedRun,
+ correlationID: correlationID
+ )
+ },
+ endStep: { correlationID in
+ try await runCoordinator.endFinderStep(
+ ownedRun: ownedRun,
+ correlationID: correlationID
+ )
+ }
+ )
+ }
+
+ @MainActor
+ private func loadBase() async throws -> FinderStabilityBaseContext {
+ guard ProviderRuntimeProfile.current == .stability else {
+ throw FinderStabilityContextError.stabilityBuildRequired
+ }
+ let accountStore = try accountStoreProvider()
+ let domainStore = try domainStoreProvider()
+ print("finder stability preflight: stored configuration")
+ let accounts = try await accountStore.allAccounts()
+ let domains = try await domainStore.allConfigurations()
+ guard domains.count == 1,
+ let domain = domains.first,
+ domain.isCompatible(with: .stability),
+ let lab = domain.stabilityLab,
+ accounts.contains(where: { $0.accountIdentifier == domain.accountIdentifier }) else {
+ throw FinderStabilityContextError.invalidLabConfiguration
+ }
+
+ print("finder stability preflight: domain registration")
+ guard try await registrar.registeredDomainIdentifiers() == [domain.domainIdentifier] else {
+ throw FinderStabilityContextError.fileProviderNotRegistered
+ }
+ print("finder stability preflight: Keychain authentication")
+ guard var token = try await tokenStore.loadToken(
+ accountIdentifier: domain.accountIdentifier
+ ), token.accessToken.isEmpty == false else {
+ throw FinderStabilityContextError.credentialUnavailable
+ }
+ if token.shouldRefresh() {
+ guard let refreshToken = token.refreshToken else {
+ throw FinderStabilityContextError.credentialUnavailable
+ }
+ token = try await KDriveOAuthClient.refresh(refreshToken: refreshToken)
+ try await tokenStore.saveToken(token, accountIdentifier: domain.accountIdentifier)
+ }
+
+ let recorder = try ProviderEventStoreFactory.makeDefault(
+ profile: .stability
+ ) as? any ProviderDiagnosticRecording
+ let remote = remoteFactory(token.accessToken, recorder)
+ let remoteConfiguration = StabilityLabRemoteConfiguration(
+ driveID: domain.driveID,
+ driveRootFileID: lab.driveRootFileID,
+ rootFileID: domain.rootFileID,
+ ownershipMarkerFileID: lab.markerFileID,
+ ownershipMarker: lab.ownershipMarker
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+ let verifyRemoteSafety: @MainActor () async throws -> Void = {
+ guard try await domainStore.allConfigurations() == [domain],
+ try await registrar.registeredDomainIdentifiers() == [domain.domainIdentifier] else {
+ throw FinderStabilityContextError.fileProviderNotRegistered
+ }
+ let observation = try await coordinator.observe(configuration: remoteConfiguration)
+ let safetyResult = StabilityLabSafety.preflight(StabilityLabPreflightInput(
+ expectedMarker: lab.ownershipMarker,
+ expectedOwnershipMarkerFileID: lab.markerFileID,
+ configuredEncryptionMode: domain.encryptionMode,
+ root: observation,
+ registeredDomains: [StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: domain.driveID,
+ rootFileID: domain.rootFileID,
+ encryptionMode: domain.encryptionMode,
+ ownershipMarkerIdentifier: lab.ownershipMarker.identifier
+ )]
+ ))
+ guard safetyResult.isAllowed else {
+ throw FinderStabilityContextError.invalidLabConfiguration
+ }
+ }
+ try await verifyRemoteSafety()
+ print("finder stability preflight: remote ownership verified; resolving visible root")
+
+ let rootURL = try await registrar.userVisibleRootURL(for: domain)
+ print("finder stability preflight: binding visible domain")
+ let visibleRootBinding = try await visibleDomainResolver(rootURL)
+ let consentMatches = FinderStabilityRootBinding.matches(
+ expectedDomainIdentifier: domain.domainIdentifier,
+ actualItemIdentifier: visibleRootBinding.itemIdentifier,
+ actualDomainIdentifier: visibleRootBinding.domainIdentifier
+ )
+ let verifySafety: @MainActor () async throws -> Void = {
+ try await verifyRemoteSafety()
+ let currentRootBinding = try await visibleDomainResolver(rootURL)
+ guard FinderStabilityRootBinding.matches(
+ expectedDomainIdentifier: domain.domainIdentifier,
+ actualItemIdentifier: currentRootBinding.itemIdentifier,
+ actualDomainIdentifier: currentRootBinding.domainIdentifier
+ ) else {
+ throw FinderStabilityContextError.fileProviderNotRegistered
+ }
+ }
+
+ return FinderStabilityBaseContext(
+ domain: domain,
+ remoteConfiguration: remoteConfiguration,
+ remote: remote,
+ rootURL: rootURL,
+ hasVerifiedFileProviderConsent: consentMatches,
+ verifySafety: verifySafety
+ )
+ }
+
+ private static func identifier(
+ for url: URL
+ ) async throws -> (NSFileProviderItemIdentifier, NSFileProviderDomainIdentifier) {
+ try await StabilityCallbackWaiter<(NSFileProviderItemIdentifier, NSFileProviderDomainIdentifier)>().wait { completion in
+ NSFileProviderManager.getIdentifierForUserVisibleFile(at: url) {
+ itemIdentifier, domainIdentifier, error in
+ if let error {
+ completion(.failure(error))
+ } else if let itemIdentifier, let domainIdentifier {
+ completion(.success((itemIdentifier, domainIdentifier)))
+ } else {
+ completion(.failure(FinderStabilityContextError.fileProviderNotRegistered))
+ }
+ }
+ }
+ }
+}
+
+struct FinderStabilityVisibleItemBinding: Equatable, Sendable {
+ let itemIdentifier: String
+ let domainIdentifier: String
+}
+
+enum FinderStabilityRootBinding {
+ static func matches(
+ expectedDomainIdentifier: String,
+ actualItemIdentifier: String,
+ actualDomainIdentifier: String
+ ) -> Bool {
+ actualItemIdentifier == NSFileProviderItemIdentifier.rootContainer.rawValue
+ && actualDomainIdentifier == expectedDomainIdentifier
+ }
+}
+
+@MainActor
+private struct FinderStabilityBaseContext {
+ let domain: ProviderDomainConfiguration
+ let remoteConfiguration: StabilityLabRemoteConfiguration
+ let remote: any KDriveFileProviding
+ let rootURL: URL
+ let hasVerifiedFileProviderConsent: Bool
+ let verifySafety: @MainActor () async throws -> Void
+}
+
+enum FinderStabilityContextError: Error, Equatable, Sendable {
+ case stabilityBuildRequired
+ case invalidLabConfiguration
+ case fileProviderNotRegistered
+ case credentialUnavailable
+}
+
+@MainActor
+protocol FinderStabilityScenarioRunning {
+ func run(context: FinderStabilityLiveContext) async -> FinderStabilityScenarioExecution
+}
+
+struct FinderStabilityScenarioExecution: Sendable {
+ let stepResults: [StabilityFinderStepResult]
+ let observations: [StabilityFinderAPIObservation]
+ var canSeal = true
+
+ static func skippingAll(
+ reason: StabilityFinderStepSkipReason,
+ startedAt: Date
+ ) -> FinderStabilityScenarioExecution {
+ let steps = StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ StabilityFinderStepResult(
+ sequenceNumber: UInt16(index + 1),
+ scenario: scenario,
+ correlationID: UUID(),
+ startedAt: startedAt,
+ finishedAt: startedAt,
+ outcome: .skipped(reason),
+ assertions: StabilityFinderAssertionClass.allCases.map {
+ StabilityFinderAssertionResult(
+ assertionClass: $0,
+ outcome: .notEvaluated(.stepSkipped)
+ )
+ }
+ )
+ }
+ return FinderStabilityScenarioExecution(stepResults: steps, observations: [])
+ }
+}
+#endif
diff --git a/potassiumProvider/potassiumProviderApp.swift b/potassiumProvider/potassiumProviderApp.swift
index b786f8d..ffe6f14 100644
--- a/potassiumProvider/potassiumProviderApp.swift
+++ b/potassiumProvider/potassiumProviderApp.swift
@@ -8,7 +8,7 @@ struct potassiumProviderApp: App {
#endif
init() {
- #if DEBUG
+ #if DEBUG && !STABILITY
_model = StateObject(wrappedValue: ProviderUITestFixture.makeModel() ?? PotassiumProviderAppModel())
#else
_model = StateObject(wrappedValue: PotassiumProviderAppModel())
@@ -36,6 +36,11 @@ enum PotassiumProviderMain {
if FileProviderUninstallCommandLine.shouldHandle(arguments: CommandLine.arguments) {
exit(FileProviderUninstallCommandLine.runInCurrentProcess(arguments: CommandLine.arguments))
}
+ #if os(macOS) && STABILITY
+ if FinderStabilityCommandLine.shouldHandle(arguments: CommandLine.arguments) {
+ exit(FinderStabilityCommandLine.runInCurrentProcess(arguments: CommandLine.arguments))
+ }
+ #endif
potassiumProviderApp.main()
}
diff --git a/potassiumProviderActions/ProviderActionViewController.swift b/potassiumProviderActions/ProviderActionViewController.swift
index 7e62158..8385508 100644
--- a/potassiumProviderActions/ProviderActionViewController.swift
+++ b/potassiumProviderActions/ProviderActionViewController.swift
@@ -5,6 +5,9 @@ import SwiftUI
#if os(macOS)
import AppKit
+#if STABILITY
+import Combine
+#endif
#else
import UIKit
#endif
@@ -12,6 +15,12 @@ import UIKit
@objc(ProviderActionViewController)
public final class ProviderActionViewController: FPUIActionExtensionViewController {
private var actionModel: ProviderActionViewModel?
+ private var loadTask: Task?
+ #if os(macOS) && STABILITY
+ private var panelIdentityObservation: AnyCancellable?
+ #endif
+
+ deinit { loadTask?.cancel() }
#if os(macOS)
public override func loadView() {
@@ -27,6 +36,7 @@ public final class ProviderActionViewController: FPUIActionExtensionViewControll
forAction actionIdentifier: String,
itemIdentifiers: [NSFileProviderItemIdentifier]
) {
+ loadTask?.cancel()
guard let domainIdentifier = extensionContext.domainIdentifier?.rawValue,
itemIdentifiers.count == 1,
let itemIdentifier = itemIdentifiers.first,
@@ -44,10 +54,24 @@ public final class ProviderActionViewController: FPUIActionExtensionViewControll
install(
ProviderActionRootView(
model: model,
- complete: { [weak self] in self?.extensionContext.completeRequest() }
+ complete: { [weak self] in
+ self?.loadTask?.cancel()
+ self?.extensionContext.completeRequest()
+ }
)
)
- Task { await model.load() }
+ #if os(macOS) && STABILITY
+ // FileProviderUI's hosted NavigationStack does not expose its SwiftUI
+ // identifier through AX. Publish the verified identity on the native root.
+ view.setAccessibilityElement(true)
+ view.setAccessibilityRole(.group)
+ view.setAccessibilityIdentifier(nil)
+ panelIdentityObservation = model.$stabilityPanelAlias.sink { [weak self] alias in
+ guard let alias else { return }
+ self?.view.setAccessibilityIdentifier("provider.stability.action." + alias.uuidString)
+ }
+ #endif
+ loadTask = Task { await model.load() }
}
private func cancel(with message: String) {
@@ -61,6 +85,10 @@ public final class ProviderActionViewController: FPUIActionExtensionViewControll
}
private func install(_ content: Content) {
+ for child in children {
+ child.view.removeFromSuperview()
+ child.removeFromParent()
+ }
#if os(macOS)
let hostingController = NSHostingController(rootView: content)
addChild(hostingController)
diff --git a/potassiumProviderActions/ProviderActionViewModel.swift b/potassiumProviderActions/ProviderActionViewModel.swift
index 2479a8a..163a099 100644
--- a/potassiumProviderActions/ProviderActionViewModel.swift
+++ b/potassiumProviderActions/ProviderActionViewModel.swift
@@ -29,8 +29,11 @@ final class ProviderActionViewModel: ObservableObject {
let mode: Mode
let domainIdentifier: String
- let itemIdentifier: NSFileProviderItemIdentifier
+ @Published private(set) var itemIdentifier: NSFileProviderItemIdentifier
+ #if STABILITY
+ @Published private(set) var stabilityPanelAlias: UUID?
+ #endif
@Published private(set) var item: KDriveRemoteItem?
@Published private(set) var vaultItem: VaultItem?
@Published private(set) var shareLink: KDriveShareLinkSummary?
@@ -66,6 +69,13 @@ final class ProviderActionViewModel: ObservableObject {
defer { isLoading = false }
do {
let runtime = try await ProviderActionRuntime.load(domainIdentifier: domainIdentifier)
+ let resolvedIdentifier = try await ProviderActionItemResolver.resolve(itemIdentifier, configuration: runtime.configuration)
+ try Task.checkCancellation()
+ itemIdentifier = resolvedIdentifier
+ #if STABILITY
+ stabilityPanelAlias = try await StabilityActionPanelIdentity.resolve(for: resolvedIdentifier.rawValue)
+ try Task.checkCancellation()
+ #endif
if let vault = runtime.encryptedVault {
guard let identifier = VaultItemIdentifier(
fileProviderIdentifier: itemIdentifier.rawValue
@@ -103,6 +113,8 @@ final class ProviderActionViewModel: ObservableObject {
case .versionHistory:
try await loadNextVersionPage()
}
+ } catch is CancellationError {
+ return
} catch {
errorMessage = error.localizedDescription
initialLoadErrorMessage = error.localizedDescription
@@ -139,6 +151,13 @@ final class ProviderActionViewModel: ObservableObject {
self.shareLink = link
self.apply(link.configuration)
self.password = ""
+ // The service can acknowledge an update while retaining a different
+ // capability value. Show the authoritative form, but never report
+ // that the requested settings were saved in that case.
+ guard requestConfiguration.hasSameReportedSettings(as: link.configuration) else {
+ await self.signalParentAndWorkingSet(runtime: runtime, parentID: item.parentID)
+ throw KDriveContextActionError.shareLinkSettingsNotApplied
+ }
self.message = createsLink ? "Created share link." : "Saved share-link settings."
await self.record(
kind: .shareLink,
@@ -293,7 +312,10 @@ final class ProviderActionViewModel: ObservableObject {
message = nil
defer { isWorking = false }
do {
- try await operation()
+ let subject = item.flatMap { StabilityDiagnosticIdentity.activeAlias(for: String($0.id)) }
+ try await ProviderDiagnosticCorrelationContext.$subjectAlias.withValue(subject) {
+ try await operation()
+ }
} catch {
errorMessage = error.localizedDescription
await recordFailure(error)
diff --git a/potassiumProviderActions/ProviderActionViews.swift b/potassiumProviderActions/ProviderActionViews.swift
index 746bef1..c6ae69c 100644
--- a/potassiumProviderActions/ProviderActionViews.swift
+++ b/potassiumProviderActions/ProviderActionViews.swift
@@ -1,4 +1,5 @@
import PotassiumProviderCore
+import FileProvider
import SwiftUI
struct ProviderActionRootView: View {
@@ -44,14 +45,32 @@ struct ProviderActionRootView: View {
}
}
.navigationTitle(navigationTitle)
+ #if os(macOS)
+ // A hosted FPUI sheet does not install a NavigationStack toolbar in
+ // Finder's window. Keep dismissal in the actual hosted view tree.
+ .safeAreaInset(edge: .bottom) {
+ HStack {
+ Spacer()
+ Button("Done", action: complete)
+ .disabled(model.isWorking)
+ }
+ .padding()
+ }
+ #else
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: complete)
- .disabled(model.isLoading || model.isWorking)
+ .disabled(model.isWorking)
}
}
+ #endif
}
.frame(minWidth: 360, minHeight: 440)
+ #if STABILITY
+ .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("provider.stability.action." +
+ (model.stabilityPanelAlias?.uuidString ?? "unbound"))
+ #endif
}
private var navigationTitle: String {
@@ -170,8 +189,10 @@ private struct ShareLinkActionView: View {
Section("Access") {
Picker("Access", selection: $model.configuration.access) {
Text("Public").tag(KDriveShareLinkConfiguration.Access.public)
+ Text("Inherit Access").tag(KDriveShareLinkConfiguration.Access.inherit)
Text("Password Protected").tag(KDriveShareLinkConfiguration.Access.password)
}
+ .accessibilityIdentifier("provider.share.access")
if model.configuration.access == .password {
SecureField(
model.shareLink == nil ? "Password" : "New password (leave blank to keep current)",
@@ -247,7 +268,9 @@ private struct VersionHistoryActionView: View {
@State private var pendingRestore: KDriveFileVersionSummary?
var body: some View {
- List {
+ // A hosted macOS List flattens the row and hides its Restore control.
+ // Form preserves the separate button and its stable accessibility identity.
+ Form {
Section {
Label(item.name, systemImage: "doc")
.lineLimit(2)
@@ -323,7 +346,9 @@ private struct VersionRow: View {
}
Spacer()
Button("Restore", action: restore)
+ .accessibilityIdentifier("provider.version.restore." + KDriveMutationIdentity.clientToken([String(version.id)]))
.buttonStyle(.borderless)
}
+ .accessibilityElement(children: .contain)
}
}
diff --git a/potassiumProviderFileProvider/FileProviderEnumerator.swift b/potassiumProviderFileProvider/FileProviderEnumerator.swift
index 170486f..ca57d2a 100644
--- a/potassiumProviderFileProvider/FileProviderEnumerator.swift
+++ b/potassiumProviderFileProvider/FileProviderEnumerator.swift
@@ -6,15 +6,40 @@ import PotassiumProviderCore
final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private let containerItemIdentifier: NSFileProviderItemIdentifier
private let domain: NSFileProviderDomain
+ private var diagnosticRecorder: (any ProviderDiagnosticRecording)? {
+ FileProviderRuntime.makeEventStore() as? any ProviderDiagnosticRecording
+ }
init(containerItemIdentifier: NSFileProviderItemIdentifier, domain: NSFileProviderDomain) {
self.containerItemIdentifier = containerItemIdentifier
self.domain = domain
super.init()
+ if let diagnosticRecorder {
+ Task {
+ let span = await ProviderDiagnosticSpan.start(
+ itemIdentifier: self.containerItemIdentifier.rawValue,
+ source: .fileProviderExtension,
+ operation: .enumeratorInitialize,
+ recorder: diagnosticRecorder
+ )
+ await span.complete(statusClass: .success)
+ }
+ }
FileProviderLog.enumeration.debug("init enumerator container(\(self.containerItemIdentifier.rawValue, privacy: .public)) kind(\(self.snapshotContainerIdentifier, privacy: .public)) domain(\(self.domain.identifier.rawValue, privacy: .public))")
}
func invalidate() {
+ if let diagnosticRecorder {
+ Task {
+ let span = await ProviderDiagnosticSpan.start(
+ itemIdentifier: self.containerItemIdentifier.rawValue,
+ source: .fileProviderExtension,
+ operation: .enumeratorInvalidate,
+ recorder: diagnosticRecorder
+ )
+ await span.complete(statusClass: .success)
+ }
+ }
FileProviderLog.enumeration.debug("invalidate enumerator container(\(self.containerItemIdentifier.rawValue, privacy: .public)) domain(\(self.domain.identifier.rawValue, privacy: .public))")
}
@@ -22,6 +47,14 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
let cursor = FileProviderPageCodec.cursor(from: page)
FileProviderLog.enumeration.debug("enumerateItems start container(\(self.containerItemIdentifier.rawValue, privacy: .public)) kind(\(self.snapshotContainerIdentifier, privacy: .public)) cursorPresent(\(cursor != nil, privacy: .public))")
Task {
+ let span = await ProviderDiagnosticSpan.start(
+ itemIdentifier: self.containerItemIdentifier.rawValue,
+ source: .fileProviderExtension,
+ operation: .enumerateItems,
+ optionShape: cursor == nil ? [.pageLimit] : [.paginationCursor, .pageLimit],
+ recorder: diagnosticRecorder
+ )
+ await span.withCorrelation {
var runtime: FileProviderRuntime?
do {
let loadedRuntime = try await FileProviderRuntime.load(domain: self.domain)
@@ -33,12 +66,20 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
startingAt: page
)
observer.didEnumerate(vaultPage.items.map(FileProviderItem.init(vaultItem:)))
+ await span.complete(
+ statusClass: .success,
+ hasCursor: vaultPage.nextCursor != nil,
+ hasMore: vaultPage.nextCursor != nil
+ )
observer.finishEnumerating(
upTo: FileProviderPageCodec.page(from: vaultPage.nextCursor)
)
return
}
let itemPage = try await self.listItems(runtime: loadedRuntime, startingAt: page)
+ if self.containerItemIdentifier == .workingSet {
+ await self.recordWorkingSetMembers(itemPage.items, runtime: loadedRuntime)
+ }
let enumeratesTrash = self.containerItemIdentifier == .trashContainer
observer.didEnumerate(itemPage.items.map {
FileProviderItem(
@@ -56,6 +97,11 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
itemPath: nil,
summary: "Enumerated \(itemPage.items.count) item(s) in \(self.snapshotContainerIdentifier)."
)
+ await span.complete(
+ statusClass: .success,
+ hasCursor: itemPage.nextCursor != nil,
+ hasMore: itemPage.hasMore
+ )
observer.finishEnumerating(upTo: FileProviderPageCodec.page(from: itemPage.nextCursor))
} catch {
let mappedError = await self.recordFailure(
@@ -65,13 +111,22 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
summary: "enumerate folder items."
)
FileProviderLog.enumeration.error("enumerateItems failed container(\(self.containerItemIdentifier.rawValue, privacy: .public)): \(mappedError.localizedDescription, privacy: .public)")
+ await span.fail(error: mappedError)
observer.finishEnumeratingWithError(mappedError)
}
+ }
}
}
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
Task {
+ let span = await ProviderDiagnosticSpan.start(
+ itemIdentifier: self.containerItemIdentifier.rawValue,
+ source: .fileProviderExtension,
+ operation: .currentSyncAnchor,
+ recorder: diagnosticRecorder
+ )
+ await span.withCorrelation {
var domainIdentifier = domain.identifier.rawValue
var driveID = 0
do {
@@ -84,6 +139,7 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
throw NSFileProviderError(.notAuthenticated)
}
let frontier = try await vault.synchronize()
+ await span.complete(statusClass: .success, hasAnchor: true)
completionHandler(FileProviderPageCodec.anchor(
from: frontier.anchorString
))
@@ -95,6 +151,10 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
let snapshot = try await stateStore.workingSetSnapshot(
domainIdentifier: configuration.domainIdentifier
)
+ await span.complete(
+ statusClass: .success,
+ hasAnchor: snapshot != nil
+ )
completionHandler(snapshot.map { FileProviderPageCodec.anchor(from: $0.anchor) })
return
}
@@ -109,11 +169,17 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
snapshot.usesAdvancedListing,
snapshot.isFullyEnumerated,
let serverCursor = snapshot.serverCursor else {
+ await span.complete(statusClass: .success, hasAnchor: false)
completionHandler(nil)
return
}
+ await span.complete(statusClass: .success, hasAnchor: true)
completionHandler(FileProviderPageCodec.anchor(from: serverCursor))
} else {
+ await span.complete(
+ statusClass: .success,
+ hasAnchor: snapshot != nil
+ )
completionHandler(snapshot.map { FileProviderPageCodec.anchor(from: $0.anchor) })
}
} catch {
@@ -132,8 +198,10 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
)
}
FileProviderLog.enumeration.error("currentSyncAnchor failed container(\(self.containerItemIdentifier.rawValue, privacy: .public)): \(mapping.mappedError.localizedDescription, privacy: .public)")
+ await span.fail(error: mapping.mappedError)
completionHandler(nil)
}
+ }
}
}
@@ -141,6 +209,14 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
let requestedAnchor = FileProviderPageCodec.anchorString(from: anchor)
FileProviderLog.enumeration.debug("enumerateChanges start container(\(self.containerItemIdentifier.rawValue, privacy: .public)) requestedAnchorPresent(\(requestedAnchor != nil, privacy: .public))")
Task {
+ let span = await ProviderDiagnosticSpan.start(
+ itemIdentifier: self.containerItemIdentifier.rawValue,
+ source: .fileProviderExtension,
+ operation: .enumerateChanges,
+ optionShape: requestedAnchor == nil ? [] : [.paginationCursor],
+ recorder: diagnosticRecorder
+ )
+ await span.withCorrelation {
var runtime: FileProviderRuntime?
do {
let loadedRuntime = try await FileProviderRuntime.load(domain: self.domain)
@@ -163,6 +239,11 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
NSFileProviderItemIdentifier($0.fileProviderIdentifier)
})
}
+ await span.complete(
+ statusClass: .success,
+ hasMore: false,
+ hasAnchor: true
+ )
observer.finishEnumeratingChanges(
upTo: FileProviderPageCodec.anchor(
from: changes.frontier.anchorString
@@ -176,7 +257,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
try await self.enumerateWorkingSetChanges(
for: observer,
runtime: loadedRuntime,
- requestedAnchor: requestedAnchor
+ requestedAnchor: requestedAnchor,
+ diagnosticSpan: span
)
return
}
@@ -184,7 +266,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
try await self.enumerateAdvancedChanges(
for: observer,
runtime: loadedRuntime,
- requestedCursor: requestedAnchor
+ requestedCursor: requestedAnchor,
+ diagnosticSpan: span
)
return
}
@@ -196,7 +279,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
try await self.enumerateStoredChanges(
for: observer,
runtime: loadedRuntime,
- requestedAnchor: requestedAnchor
+ requestedAnchor: requestedAnchor,
+ diagnosticSpan: span
)
return
}
@@ -218,7 +302,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
try await self.enumerateStoredChanges(
for: observer,
runtime: loadedRuntime,
- requestedAnchor: requestedAnchor
+ requestedAnchor: requestedAnchor,
+ diagnosticSpan: span
)
} catch {
let mappedError = await self.recordFailure(
@@ -228,8 +313,10 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
summary: "enumerate folder changes."
)
FileProviderLog.enumeration.error("enumerateChanges failed container(\(self.containerItemIdentifier.rawValue, privacy: .public)): \(mappedError.localizedDescription, privacy: .public)")
+ await span.fail(error: mappedError)
observer.finishEnumeratingWithError(mappedError)
}
+ }
}
}
@@ -543,7 +630,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private func enumerateAdvancedChanges(
for observer: NSFileProviderChangeObserver,
runtime: FileProviderRuntime,
- requestedCursor: String?
+ requestedCursor: String?,
+ diagnosticSpan: ProviderDiagnosticSpan
) async throws {
guard let requestedCursor else {
throw NSFileProviderError(.syncAnchorExpired)
@@ -596,6 +684,11 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
itemPath: nil,
summary: "Synced \(result.changes.updatedItems.count) update(s) and \(result.changes.deletedItemIDs.count) delete(s)."
)
+ await diagnosticSpan.complete(
+ statusClass: .success,
+ hasMore: response.hasMore,
+ hasAnchor: true
+ )
observer.finishEnumeratingChanges(upTo: FileProviderPageCodec.anchor(from: newCursor), moreComing: response.hasMore)
} catch let error as KDriveListingValidationError {
FileProviderLog.enumeration.error("enumerateAdvancedChanges invalid listing payload container(\(self.containerItemIdentifier.rawValue, privacy: .public)): \(error.localizedDescription, privacy: .public)")
@@ -625,27 +718,55 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
itemPath: nil,
summary: "Rebuilt sync state with \(changes.updatedItems.count) update(s) and \(changes.deletedItemIDs.count) delete(s)."
)
+ await diagnosticSpan.complete(
+ statusClass: .success,
+ hasMore: false,
+ hasAnchor: true
+ )
observer.finishEnumeratingChanges(upTo: FileProviderPageCodec.anchor(from: rebuiltSnapshot.anchor), moreComing: false)
}
}
+ /// Emits membership only for items delivered by a real working-set enumeration.
+ private func recordWorkingSetMembers(_ items: [KDriveRemoteItem], runtime: FileProviderRuntime) async {
+ for item in items {
+ let member = await ProviderDiagnosticSpan.start(
+ itemIdentifier: String(item.id), source: .fileProviderExtension, operation: .workingSetRefresh,
+ recorder: runtime.eventStore as? any ProviderDiagnosticRecording
+ )
+ let metadataAlias = (try? StabilityDiagnosticIdentity.activeRun()).map {
+ StabilityDiagnosticIdentity.metadataAlias(for: item, runID: $0.runID)
+ }
+ await member.complete(statusClass: .success, itemMetadataAlias: metadataAlias)
+ }
+ }
+
private func enumerateWorkingSetChanges(
for observer: NSFileProviderChangeObserver,
runtime: FileProviderRuntime,
- requestedAnchor: String?
+ requestedAnchor: String?,
+ diagnosticSpan: ProviderDiagnosticSpan
) async throws {
guard let requestedAnchor else {
throw NSFileProviderError(.syncAnchorExpired)
}
- _ = try await workingSetCoordinator(runtime: runtime).poll()
- guard let result = try await runtime.workingSetStateStore.workingSetChanges(
- domainIdentifier: runtime.configuration.domainIdentifier,
- from: requestedAnchor
- ) else {
+ let coordinator = workingSetCoordinator(runtime: runtime)
+ guard let result = try await KDriveWorkingSetChangeDelivery.changes(
+ domainIdentifier: runtime.configuration.domainIdentifier, from: requestedAnchor,
+ store: runtime.workingSetStateStore,
+ refresh: {
+ _ = try await coordinator.poll()
+ }) else {
throw NSFileProviderError(.syncAnchorExpired)
}
emit(result.changes, to: observer, rootFileID: runtime.configuration.rootFileID)
+ await recordWorkingSetMembers(result.changes.updatedItems, runtime: runtime)
FileProviderLog.enumeration.info("enumerateWorkingSetChanges success updated(\(result.changes.updatedItems.count, privacy: .public)) deleted(\(result.changes.deletedItemIDs.count, privacy: .public))")
+ await diagnosticSpan.complete(
+ statusClass: .success,
+ hasMore: false,
+ hasAnchor: true
+ )
observer.finishEnumeratingChanges(
upTo: FileProviderPageCodec.anchor(from: result.anchor),
moreComing: false
@@ -655,7 +776,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
private func enumerateStoredChanges(
for observer: NSFileProviderChangeObserver,
runtime: FileProviderRuntime,
- requestedAnchor: String
+ requestedAnchor: String,
+ diagnosticSpan: ProviderDiagnosticSpan
) async throws {
let pageToken = KDriveSnapshotPagingToken.isSnapshotToken(requestedAnchor) ? requestedAnchor : nil
do {
@@ -679,6 +801,11 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
itemPath: nil,
summary: "Synced \(page.changes.updatedItems.count) update(s) and \(page.changes.deletedItemIDs.count) delete(s)."
)
+ await diagnosticSpan.complete(
+ statusClass: .success,
+ hasMore: page.nextToken != nil,
+ hasAnchor: true
+ )
observer.finishEnumeratingChanges(
upTo: FileProviderPageCodec.anchor(from: nextAnchor),
moreComing: page.nextToken != nil
diff --git a/potassiumProviderFileProvider/FileProviderRuntime.swift b/potassiumProviderFileProvider/FileProviderRuntime.swift
index 87f8e89..94094eb 100644
--- a/potassiumProviderFileProvider/FileProviderRuntime.swift
+++ b/potassiumProviderFileProvider/FileProviderRuntime.swift
@@ -43,6 +43,29 @@ struct FileProviderRuntime: Sendable {
}
static func load(domain: NSFileProviderDomain) async throws -> FileProviderRuntime {
+ let eventStore = makeEventStore()
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .runtimeLoad,
+ recorder: eventStore as? any ProviderDiagnosticRecording
+ )
+ do {
+ let runtime = try await loadRuntime(
+ domain: domain,
+ eventStore: eventStore
+ )
+ await span.complete(statusClass: .success)
+ return runtime
+ } catch {
+ await span.fail(error: error)
+ throw error
+ }
+ }
+
+ private static func loadRuntime(
+ domain: NSFileProviderDomain,
+ eventStore: (any KDriveProviderEventStoring)?
+ ) async throws -> FileProviderRuntime {
FileProviderLog.runtime.debug("load runtime for domain(\(domain.identifier.rawValue, privacy: .public))")
let configuration = try await loadConfiguration(domain: domain)
let tokenStore = KeychainOAuthTokenStore(accessGroup: ProviderConstants.keychainAccessGroup)
@@ -63,7 +86,11 @@ struct FileProviderRuntime: Sendable {
}
let sqliteStore = try makeSQLiteStore()
- let remote = PotassiumKDriveService(bearerToken: token.accessToken)
+ let remote = PotassiumKDriveService(
+ bearerToken: token.accessToken,
+ diagnosticRecorder: eventStore as? any ProviderDiagnosticRecording,
+ diagnosticSource: .fileProviderExtension
+ )
let encryptedVault: (any EncryptedVaultProviding)?
if configuration.encryptionMode == .opaqueVaultV2 {
guard let vaultConfiguration = configuration.vault,
@@ -123,7 +150,7 @@ struct FileProviderRuntime: Sendable {
workingSetRemote: remote,
snapshotStore: sqliteStore,
workingSetStateStore: sqliteStore,
- eventStore: makeEventStore(),
+ eventStore: eventStore,
encryptedVault: encryptedVault
)
}
@@ -136,6 +163,10 @@ struct FileProviderRuntime: Sendable {
FileProviderLog.runtime.error("missing configuration for domain(\(domain.identifier.rawValue, privacy: .public)); returning notAuthenticated")
throw NSFileProviderError(.notAuthenticated)
}
+ guard configuration.isCompatible(with: .current) else {
+ FileProviderLog.runtime.error("domain purpose does not match this runtime profile; returning cannotSynchronize")
+ throw NSFileProviderError(.cannotSynchronize)
+ }
guard configuration.encryptionMode != .opaqueVaultV1 else {
FileProviderLog.runtime.error("unsupported experimental encrypted vault v1 for domain(\(domain.identifier.rawValue, privacy: .public)); returning cannotSynchronize")
throw NSFileProviderError(.cannotSynchronize)
@@ -178,9 +209,9 @@ struct FileProviderRuntime: Sendable {
static func makeEventStore() -> (any KDriveProviderEventStoring)? {
do {
- return try KDriveProviderEventSQLiteStore(appGroupIdentifier: ProviderConstants.appGroupIdentifier)
+ return try ProviderEventStoreFactory.makeDefault()
} catch {
- FileProviderLog.runtime.error("failed to open provider event store in app group: \(error.localizedDescription, privacy: .public)")
+ FileProviderLog.runtime.error("failed to open provider event store in app group")
return nil
}
}
@@ -229,11 +260,6 @@ enum FileProviderPageCodec {
}
}
-struct ProviderErrorMapping {
- let mappedError: Error
- let diagnostic: KDriveProviderActivityErrorDiagnostic
-}
-
func signalRecoverableProviderErrorsResolved(for domain: NSFileProviderDomain) async {
guard let manager = NSFileProviderManager(for: domain) else { return }
let errorCodes: [NSFileProviderError.Code] = [
@@ -257,280 +283,3 @@ func signalRecoverableProviderErrorsResolved(for domain: NSFileProviderDomain) a
}
}
}
-
-func providerErrorMapping(_ error: Error) -> ProviderErrorMapping {
- if let fileProviderError = error as? NSFileProviderError {
- let nsError = fileProviderError as NSError
- FileProviderLog.runtime.debug("preserve FileProvider error code(\(nsError.code, privacy: .public)): \(nsError.localizedDescription, privacy: .public)")
- return ProviderErrorMapping(
- mappedError: fileProviderError,
- diagnostic: providerDiagnostic(
- category: providerActivityErrorCategory(originalError: nsError),
- originalError: error,
- mappedError: fileProviderError
- )
- )
- }
-
- if let apiRejection = KDriveRemoteErrorClassifier.apiRejection(from: error) {
- let mappedError = fileProviderError(for: apiRejection.recovery)
- let mappedNSError = mappedError as NSError
- FileProviderLog.runtime.error("map API rejection HTTP \(apiRejection.statusCode, privacy: .public) to \(mappedNSError.domain, privacy: .public) code(\(mappedNSError.code, privacy: .public))")
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .api,
- originalError: error,
- mappedError: mappedError,
- diagnosticSummary: apiRejection.diagnosticSummary
- )
- )
- }
-
- if let mutationConflictError = error as? KDriveMutationConflictError {
- switch mutationConflictError {
- case .staleVersion:
- FileProviderLog.runtime.error("map stale mutation version to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
- let mappedError = staleMutationVersionError()
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .mutationConflict,
- originalError: error,
- mappedError: mappedError
- )
- )
- case .localContentConflict:
- let mappedError = NSFileProviderError(.localVersionConflictingWithServer)
- FileProviderLog.runtime.error("map fail-on-conflict upload to localVersionConflictingWithServer")
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .mutationConflict,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
- }
-
- if error is KDriveListingValidationError {
- FileProviderLog.runtime.error("map listing validation failure to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
- let mappedError = NSFileProviderError(.cannotSynchronize)
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .listing,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
-
- if let snapshotStoreError = error as? KDriveSnapshotStoreError,
- case .staleSnapshot = snapshotStoreError {
- FileProviderLog.runtime.error("map stale snapshot write to cannotSynchronize: \(error.localizedDescription, privacy: .public)")
- let mappedError = NSFileProviderError(.cannotSynchronize)
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .snapshot,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
-
- if error is VaultCryptoError ||
- error is VaultJournalError ||
- error is VaultLocalStoreError ||
- error is VaultProvisioningError {
- let mappedError = NSFileProviderError(.cannotSynchronize)
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .storage,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
-
- if let vaultError = error as? EncryptedVaultError {
- let mappedError: NSFileProviderError
- switch vaultError {
- case .missingKey:
- mappedError = NSFileProviderError(.notAuthenticated)
- case .itemNotFound:
- mappedError = NSFileProviderError(.noSuchItem)
- case .syncAnchorExpired:
- mappedError = NSFileProviderError(.syncAnchorExpired)
- default:
- mappedError = NSFileProviderError(.cannotSynchronize)
- }
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .storage,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
-
- let nsError = error as NSError
- if nsError.domain == NSURLErrorDomain {
- FileProviderLog.runtime.error("map URL error \(nsError.code, privacy: .public) to serverUnreachable: \(nsError.localizedDescription, privacy: .public)")
- let mappedError = NSFileProviderError(.serverUnreachable)
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: .network,
- originalError: error,
- mappedError: mappedError
- )
- )
- }
-
- if nsError.domain == NSCocoaErrorDomain || nsError.domain == NSFileProviderErrorDomain {
- FileProviderLog.runtime.error("preserve Cocoa/FileProvider error \(nsError.domain, privacy: .public) code(\(nsError.code, privacy: .public)): \(nsError.localizedDescription, privacy: .public)")
- return ProviderErrorMapping(
- mappedError: error,
- diagnostic: providerDiagnostic(
- category: providerActivityErrorCategory(originalError: nsError),
- originalError: error,
- mappedError: error
- )
- )
- }
-
- FileProviderLog.runtime.error("wrap unexpected error as XPC reply invalid: \(error.localizedDescription, privacy: .public)")
- let mappedError = NSError(
- domain: NSCocoaErrorDomain,
- code: NSXPCConnectionReplyInvalid,
- userInfo: [NSUnderlyingErrorKey: error]
- )
- return ProviderErrorMapping(
- mappedError: mappedError,
- diagnostic: providerDiagnostic(
- category: providerActivityErrorCategory(originalError: nsError),
- originalError: error,
- mappedError: mappedError
- )
- )
-}
-
-func providerError(_ error: Error) -> Error {
- providerErrorMapping(error).mappedError
-}
-
-func shouldRecordGenericFailure(for error: Error) -> Bool {
- if error is CancellationError { return false }
- if error is KDriveMutationConflictError { return false }
-
- let nsError = error as NSError
- return nsError.domain != NSCocoaErrorDomain || nsError.code != NSUserCancelledError
-}
-
-func providerActivityKindForRuntimeLoadFailure(_ error: Error) -> KDriveProviderActivityKind {
- let nsError = error as NSError
- if nsError.domain == NSFileProviderErrorDomain,
- nsError.code == NSFileProviderError.notAuthenticated.rawValue {
- return .authentication
- }
- if let apiRejection = KDriveRemoteErrorClassifier.apiRejection(from: error),
- apiRejection.recovery == .notAuthenticated {
- return .authentication
- }
- if error is KDriveOAuthError || error is KeychainTokenStoreError {
- return .authentication
- }
- return .runtimeLoading
-}
-
-private func providerDiagnostic(
- category: KDriveProviderActivityErrorCategory,
- originalError: Error,
- mappedError: Error,
- diagnosticSummary: String? = nil
-) -> KDriveProviderActivityErrorDiagnostic {
- let originalNSError = originalError as NSError
- let mappedNSError = mappedError as NSError
- let providerCode = mappedNSError.domain == NSFileProviderErrorDomain ? mappedNSError.code : nil
- let recoverySuggestion = mappedNSError.localizedRecoverySuggestion
- ?? (originalError as? LocalizedError)?.recoverySuggestion
-
- return KDriveProviderActivityErrorDiagnostic(
- errorCategory: category,
- providerErrorCode: providerCode,
- underlyingErrorDomain: originalNSError.domain,
- underlyingErrorCode: originalNSError.code,
- recoverySuggestion: recoverySuggestion,
- diagnosticSummary: diagnosticSummary ?? providerDiagnosticSummary(category: category)
- )
-}
-
-private func fileProviderError(for recovery: KDriveRemoteAPIRejectionRecovery) -> Error {
- switch recovery {
- case .notAuthenticated:
- return NSFileProviderError(.notAuthenticated)
- case .serverUnreachable:
- return NSFileProviderError(.serverUnreachable)
- case .insufficientQuota:
- return NSFileProviderError(.insufficientQuota)
- case .cannotSynchronize:
- return NSFileProviderError(.cannotSynchronize)
- }
-}
-
-private func providerActivityErrorCategory(originalError nsError: NSError) -> KDriveProviderActivityErrorCategory {
- if nsError.domain == NSURLErrorDomain {
- return .network
- }
- if nsError.domain == NSFileProviderErrorDomain {
- if nsError.code == NSFileProviderError.notAuthenticated.rawValue {
- return .authentication
- }
- return .fileProvider
- }
- if nsError.domain == NSCocoaErrorDomain {
- return .storage
- }
- return .unknown
-}
-
-private func providerDiagnosticSummary(category: KDriveProviderActivityErrorCategory) -> String {
- switch category {
- case .authentication:
- return "Authentication is unavailable or needs to be refreshed."
- case .network:
- return "A network request failed before the operation could complete."
- case .api:
- return "The remote API rejected the operation."
- case .fileProvider:
- return "File Provider returned a recoverable provider error."
- case .listing:
- return "The remote listing response could not be safely applied."
- case .snapshot:
- return "Local sync snapshot state could not be updated safely."
- case .storage:
- return "Local storage returned an error."
- case .validation:
- return "Input or remote state failed validation."
- case .mutationConflict:
- return "The remote item changed before the local mutation could be applied."
- case .unknown:
- return "An unexpected provider error occurred."
- }
-}
-
-private func staleMutationVersionError() -> Error {
- NSError(
- domain: NSFileProviderErrorDomain,
- code: NSFileProviderError.cannotSynchronize.rawValue,
- userInfo: [
- NSLocalizedDescriptionKey: "The item changed on the server before the local mutation could be applied.",
- NSLocalizedRecoverySuggestionErrorKey: "Refresh the folder and retry the change."
- ]
- )
-}
diff --git a/potassiumProviderFileProvider/PotassiumFileProviderExtension+Actions.swift b/potassiumProviderFileProvider/PotassiumFileProviderExtension+Actions.swift
index bae3bda..d7ae555 100644
--- a/potassiumProviderFileProvider/PotassiumFileProviderExtension+Actions.swift
+++ b/potassiumProviderFileProvider/PotassiumFileProviderExtension+Actions.swift
@@ -9,7 +9,12 @@ extension PotassiumFileProviderExtension: NSFileProviderCustomAction {
completionHandler: @escaping (Error?) -> Void
) -> Progress {
let progress = Progress(totalUnitCount: 1)
- let lifecycle = FileProviderOperationLifecycle(progress: progress) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: progress,
+ diagnosticOperation: Self.diagnosticOperation(for: actionIdentifier),
+ diagnosticItemIdentifier: itemIdentifiers.count == 1 ? itemIdentifiers.first?.rawValue : nil,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -83,6 +88,8 @@ extension PotassiumFileProviderExtension: NSFileProviderCustomAction {
throw NSFileProviderError(.noSuchItem)
}
+ let knownBefore = try await loadedRuntime.workingSetStateStore.workingSetSnapshot(
+ domainIdentifier: loadedRuntime.configuration.domainIdentifier)?.items.first { $0.id == fileID }
let execution = try await KDriveContextActionCoordinator(
driveID: loadedRuntime.configuration.driveID,
rootFileID: loadedRuntime.configuration.rootFileID,
@@ -90,6 +97,9 @@ extension PotassiumFileProviderExtension: NSFileProviderCustomAction {
actions: loadedRuntime.actions
).perform(action, fileID: fileID)
+ await self.publishKnownWorkingSetItem(execution.activityItem,
+ replacing: execution.activityItem.id == fileID ? knownBefore : nil, runtime: loadedRuntime)
+
let recordedIdentifier = action == .duplicate
? ProviderEventRecorder.itemIdentifier(for: execution.activityItem)
: selectedIdentifier.rawValue
@@ -128,7 +138,10 @@ extension PotassiumFileProviderExtension: NSFileProviderCustomAction {
itemPath: nil,
summary: "perform contextual action."
)
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(mappedError)
}
}
@@ -145,6 +158,24 @@ extension PotassiumFileProviderExtension: NSFileProviderCustomAction {
return activityKind(for: action)
}
+ private static func diagnosticOperation(
+ for actionIdentifier: NSFileProviderExtensionActionIdentifier
+ ) -> ProviderDiagnosticOperation {
+ guard let action = ProviderDirectContextAction(
+ rawValue: actionIdentifier.rawValue
+ ) else {
+ return .modifyItem
+ }
+ switch action {
+ case .addFavorite, .removeFavorite:
+ return .favoriteItem
+ case .duplicate:
+ return .duplicateItem
+ case .restoreFromTrash:
+ return .restoreTrashedItem
+ }
+ }
+
private static func activityKind(
for action: ProviderDirectContextAction
) -> KDriveProviderActivityKind {
diff --git a/potassiumProviderFileProvider/PotassiumFileProviderExtension+KnownFolders.swift b/potassiumProviderFileProvider/PotassiumFileProviderExtension+KnownFolders.swift
index ce572dc..c3597b6 100644
--- a/potassiumProviderFileProvider/PotassiumFileProviderExtension+KnownFolders.swift
+++ b/potassiumProviderFileProvider/PotassiumFileProviderExtension+KnownFolders.swift
@@ -17,6 +17,12 @@ extension PotassiumFileProviderExtension: NSFileProviderKnownFolderSupporting {
}
Task {
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .knownFolderLocations,
+ recorder: diagnosticRecorder
+ )
+ await span.withCorrelation {
do {
let runtime = try await FileProviderRuntime.load(domain: self.fileProviderDomain)
if let vault = runtime.encryptedVault {
@@ -47,6 +53,7 @@ extension PotassiumFileProviderExtension: NSFileProviderKnownFolderSupporting {
filename: "Documents"
)
}
+ await span.complete(statusClass: .success)
completionHandler(locations, nil)
return
}
@@ -93,6 +100,7 @@ extension PotassiumFileProviderExtension: NSFileProviderKnownFolderSupporting {
}
FileProviderLog.replicatedExtension.info("resolved known folders under kDrive parent item(\(parentFileID, privacy: .public)) for domain(\(self.fileProviderDomain.identifier.rawValue, privacy: .public))")
+ await span.complete(statusClass: .success)
completionHandler(locations, nil)
} catch {
let mappedError: Error
@@ -104,8 +112,10 @@ extension PotassiumFileProviderExtension: NSFileProviderKnownFolderSupporting {
mappedError = providerErrorMapping(error).mappedError
}
FileProviderLog.replicatedExtension.error("failed to resolve the kDrive known-folder location for domain(\(self.fileProviderDomain.identifier.rawValue, privacy: .public)): \(error.localizedDescription, privacy: .private)")
+ await span.fail(error: mappedError)
completionHandler(nil, mappedError)
}
+ }
}
}
diff --git a/potassiumProviderFileProvider/PotassiumFileProviderExtension+Thumbnailing.swift b/potassiumProviderFileProvider/PotassiumFileProviderExtension+Thumbnailing.swift
index b50030d..bed801b 100644
--- a/potassiumProviderFileProvider/PotassiumFileProviderExtension+Thumbnailing.swift
+++ b/potassiumProviderFileProvider/PotassiumFileProviderExtension+Thumbnailing.swift
@@ -18,8 +18,18 @@ extension PotassiumFileProviderExtension: NSFileProviderThumbnailing {
FileProviderLog.replicatedExtension.debug("fetchThumbnails(count:\(itemIdentifiers.count, privacy: .public) width:\(dimensions.width, privacy: .public) height:\(dimensions.height, privacy: .public)) domain(\(self.fileProviderDomain.identifier.rawValue, privacy: .public))")
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
+ let diagnosticSpanTask = Task {
+ await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .thumbnail,
+ optionShape: [.thumbnailDimensions, .cancellableTransfer],
+ recorder: diagnosticRecorder
+ )
+ }
let task = Task {
+ let span = await diagnosticSpanTask.value
+ await span.withCorrelation {
var runtime: FileProviderRuntime?
do {
let loadedRuntime = try await FileProviderRuntime.load(domain: self.fileProviderDomain)
@@ -35,12 +45,19 @@ extension PotassiumFileProviderExtension: NSFileProviderThumbnailing {
perThumbnailCompletionHandler: perThumbnailCompletionHandler
)
progress.completedUnitCount += 1
+ let denominator = max(progress.totalUnitCount, 1)
+ await span.progress(
+ fractionCompleted: Double(progress.completedUnitCount)
+ / Double(denominator)
+ )
}
try Task.checkCancellation()
FileProviderLog.replicatedExtension.info("fetched thumbnails count(\(itemIdentifiers.count, privacy: .public))")
+ await span.complete(statusClass: .success)
completionHandler(nil)
} catch is CancellationError {
+ await span.cancel()
completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
} catch {
let mappedError = await self.recordProviderFailure(
@@ -53,12 +70,18 @@ extension PotassiumFileProviderExtension: NSFileProviderThumbnailing {
summary: "fetch thumbnails."
)
FileProviderLog.replicatedExtension.error("fetchThumbnails failed: \(mappedError.localizedDescription, privacy: .public)")
+ await span.fail(error: mappedError)
completionHandler(mappedError)
}
+ }
}
progress.cancellationHandler = {
FileProviderLog.replicatedExtension.debug("cancel fetchThumbnails(count:\(itemIdentifiers.count, privacy: .public))")
task.cancel()
+ Task {
+ let span = await diagnosticSpanTask.value
+ await span.cancel()
+ }
}
return progress
diff --git a/potassiumProviderFileProvider/PotassiumFileProviderExtension+WorkingSetMutation.swift b/potassiumProviderFileProvider/PotassiumFileProviderExtension+WorkingSetMutation.swift
new file mode 100644
index 0000000..e7ba48a
--- /dev/null
+++ b/potassiumProviderFileProvider/PotassiumFileProviderExtension+WorkingSetMutation.swift
@@ -0,0 +1,23 @@
+import Foundation
+import PotassiumProviderCore
+
+extension PotassiumFileProviderExtension {
+ /// The remote mutation is already committed. Journal failure is diagnostic;
+ /// it must not turn this callback into a second remote mutation on retry.
+ func publishKnownWorkingSetItem(_ item: KDriveRemoteItem, replacing expected: KDriveRemoteItem?,
+ runtime: FileProviderRuntime) async {
+ // The configured root has a synthetic File Provider identity. Never
+ // publish it as a normal child under its server-side external parent.
+ guard item.id != runtime.configuration.rootFileID,
+ item.driveID == runtime.configuration.driveID else { return }
+ do {
+ _ = try await runtime.workingSetStateStore.publishKnownWorkingSetItem(item, replacing: expected,
+ domainIdentifier: runtime.configuration.domainIdentifier, recordedAt: Date())
+ } catch {
+ let mapping = providerErrorMapping(error)
+ await ProviderEventRecorder.recordFailure(kind: .changeSync, runtime: runtime,
+ itemIdentifier: String(item.id), itemName: nil, itemPath: nil,
+ summary: "Could not journal the confirmed mutation for working-set delivery.", diagnostic: mapping.diagnostic)
+ }
+ }
+}
diff --git a/potassiumProviderFileProvider/PotassiumFileProviderExtension.swift b/potassiumProviderFileProvider/PotassiumFileProviderExtension.swift
index 1d8f426..c80dda5 100644
--- a/potassiumProviderFileProvider/PotassiumFileProviderExtension.swift
+++ b/potassiumProviderFileProvider/PotassiumFileProviderExtension.swift
@@ -15,7 +15,11 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
let domain: NSFileProviderDomain
let manager: NSFileProviderManager
let temporaryDirectoryURL: URL
+ var diagnosticRecorder: (any ProviderDiagnosticRecording)? {
+ FileProviderRuntime.makeEventStore() as? any ProviderDiagnosticRecording
+ }
private var remotePollingTask: Task?
+ private let materializedWork = FileProviderBackgroundWork()
var fileProviderDomain: NSFileProviderDomain {
domain
@@ -26,64 +30,125 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
self.manager = NSFileProviderManager(for: domain)!
self.temporaryDirectoryURL = (try? manager.temporaryDirectoryURL()) ?? FileManager.default.temporaryDirectory
super.init()
+ if let diagnosticRecorder {
+ Task {
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .runtimeInitialize,
+ recorder: diagnosticRecorder
+ )
+ await span.complete(statusClass: .success)
+ }
+ }
startRemotePolling()
FileProviderLog.replicatedExtension.info("init replicated extension for domain(\(self.domain.identifier.rawValue, privacy: .public)) displayName(\(self.domain.displayName, privacy: .private)) temporaryDirectory(\(self.temporaryDirectoryURL.path, privacy: .private))")
}
public func invalidate() {
+ materializedWork.invalidate()
remotePollingTask?.cancel()
remotePollingTask = nil
+ if let diagnosticRecorder {
+ Task {
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .runtimeInvalidate,
+ recorder: diagnosticRecorder
+ )
+ await span.complete(statusClass: .success)
+ }
+ }
FileProviderLog.replicatedExtension.debug("invalidate replicated extension for domain(\(self.domain.identifier.rawValue, privacy: .public))")
}
public func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
FileProviderLog.replicatedExtension.debug("materialized items changed for domain(\(self.domain.identifier.rawValue, privacy: .public))")
- completionHandler()
-
- Task {
- do {
- let runtime = try await FileProviderRuntime.load(domain: domain)
- if let vault = runtime.encryptedVault {
- _ = try await vault.synchronize()
- await signalWorkingSet(runtime: runtime)
- return
- }
- let systemItems = try await MaterializedSetReader.read(using: manager)
- let materializedItems = systemItems.compactMap { item -> KDriveMaterializedItem? in
- let fileID: Int
- if item.itemIdentifier == .rootContainer {
- fileID = runtime.configuration.rootFileID
- } else {
- guard let parsed = try? KDriveItemIdentifier(rawValue: item.itemIdentifier.rawValue),
- let parsedFileID = parsed.fileID(rootFileID: runtime.configuration.rootFileID) else {
- return nil
+ Task { [diagnosticRecorder, materializedWork, weak self] in
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .materializedItemsChanged,
+ recorder: diagnosticRecorder
+ )
+ // Acknowledge once even if invalidation already rejected new work.
+ // The acknowledged callback must not retain the provider instance.
+ await span.complete(statusClass: .success)
+ completionHandler()
+ materializedWork.start { [weak self] in
+ guard let self else { return }
+ await span.withCorrelation {
+ do { try await self.refreshMaterializedItems() }
+ catch {
+ let errorClass = ProviderDiagnosticErrorClassifier.classify(error)
+ if errorClass != .cancellation {
+ FileProviderLog.replicatedExtension.error("refresh materialized items failed: class=\(errorClass.rawValue, privacy: .public) code=\((error as NSError).code, privacy: .public)")
}
- fileID = parsedFileID
}
- return KDriveMaterializedItem(
- fileID: fileID,
- isContainer: item.contentType?.conforms(to: .folder) == true
- )
}
- try await runtime.workingSetStateStore.replaceMaterializedItems(
- materializedItems,
- domainIdentifier: runtime.configuration.domainIdentifier
- )
- _ = try await pollWorkingSet(runtime: runtime, minimumInterval: 0)
- await signalWorkingSet(runtime: runtime)
- } catch {
- FileProviderLog.replicatedExtension.error("refresh materialized items failed: \(error.localizedDescription, privacy: .public)")
}
}
}
+ private func refreshMaterializedItems() async throws {
+ try Task.checkCancellation()
+ let runtime = try await FileProviderRuntime.load(domain: domain)
+ if let vault = runtime.encryptedVault {
+ _ = try await vault.synchronize()
+ await signalWorkingSet(runtime: runtime)
+ return
+ }
+ let systemItems = try await MaterializedSetReader.read(using: manager)
+ try Task.checkCancellation()
+ #if STABILITY
+ let previousMaterializedIDs = Set(try await runtime.workingSetStateStore.materializedItems(
+ domainIdentifier: runtime.configuration.domainIdentifier).map(\.fileID))
+ #endif
+ let materializedItems = systemItems.compactMap { item -> KDriveMaterializedItem? in
+ let fileID: Int
+ if item.itemIdentifier == .rootContainer {
+ fileID = runtime.configuration.rootFileID
+ } else {
+ guard let parsed = try? KDriveItemIdentifier(rawValue: item.itemIdentifier.rawValue),
+ let parsedFileID = parsed.fileID(rootFileID: runtime.configuration.rootFileID) else {
+ return nil
+ }
+ fileID = parsedFileID
+ }
+ return KDriveMaterializedItem(
+ fileID: fileID,
+ isContainer: item.contentType?.conforms(to: .folder) == true
+ )
+ }
+ try await runtime.workingSetStateStore.replaceMaterializedItems(
+ materializedItems,
+ domainIdentifier: runtime.configuration.domainIdentifier
+ )
+ #if STABILITY
+ // Attribute only identifiers actually added/removed by this
+ // system observation. A global materialization notification
+ // alone cannot prove eviction of the intended fixture.
+ for identifier in previousMaterializedIDs.symmetricDifference(Set(materializedItems.map(\.fileID))).sorted() {
+ let itemSpan = await ProviderDiagnosticSpan.start(itemIdentifier: String(identifier), source: .fileProviderExtension,
+ operation: .materializedItemsChanged, recorder: diagnosticRecorder)
+ await itemSpan.complete(statusClass: .success)
+ }
+ #endif
+ let outcome = try await pollWorkingSet(runtime: runtime, minimumInterval: 0,
+ coalescePendingMaterializationPolls: true)
+ if outcome.didPoll { await signalWorkingSet(runtime: runtime) }
+ }
+
public func item(
for identifier: NSFileProviderItemIdentifier,
request: NSFileProviderRequest,
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
) -> Progress {
FileProviderLog.replicatedExtension.debug("item(forIdentifier:\(identifier.rawValue, privacy: .public)) domain(\(self.domain.identifier.rawValue, privacy: .public)) @ domainVersion(\(request.logDomainVersion, privacy: .public))")
- let lifecycle = FileProviderOperationLifecycle(progress: .discreteOperation()) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: .discreteOperation(),
+ diagnosticOperation: .itemLookup,
+ diagnosticItemIdentifier: identifier.rawValue,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
FileProviderLog.replicatedExtension.debug("cancel item(forIdentifier:\(identifier.rawValue, privacy: .public))")
completionHandler(nil, NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -92,8 +157,12 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
do {
if identifier == .workingSet {
FileProviderLog.replicatedExtension.debug("working set is a virtual enumeration container; return noSuchItem for metadata lookup")
- await lifecycle.finish(markProgressComplete: false) {
- completionHandler(nil, NSFileProviderError(.noSuchItem))
+ let error = NSFileProviderError(.noSuchItem)
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: error
+ ) {
+ completionHandler(nil, error)
}
return
}
@@ -126,11 +195,27 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
throw NSFileProviderError(.noSuchItem)
}
- let item = try await loadedRuntime.remote.item(driveID: loadedRuntime.configuration.driveID, fileID: fileID)
+ let metadata: KDriveItemMetadataLookup.Result
+ do {
+ metadata = try await KDriveItemMetadataLookup.resolve(
+ driveID: loadedRuntime.configuration.driveID, fileID: fileID,
+ active: {
+ try await loadedRuntime.remote.item(driveID: loadedRuntime.configuration.driveID, fileID: fileID)
+ }, trashed: {
+ guard let actions = loadedRuntime.remote as? any KDriveContextActionProviding else {
+ throw KDriveItemMetadataLookupError.trashLookupUnavailable
+ }
+ return try await actions.trashedItem(driveID: loadedRuntime.configuration.driveID, fileID: fileID)
+ })
+ } catch KDriveItemMetadataLookupError.notFound {
+ throw NSFileProviderError(.noSuchItem)
+ }
+ let item = metadata.item
FileProviderLog.replicatedExtension.debug("resolved item identifier(\(identifier.rawValue, privacy: .public)) kDriveFileID(\(fileID, privacy: .public)) type(\(item.type ?? "unknown", privacy: .public))")
await signalRecoverableProviderErrorsResolved(for: self.domain)
await lifecycle.finish(markProgressComplete: true) {
- completionHandler(FileProviderItem(remoteItem: item, rootFileID: loadedRuntime.configuration.rootFileID), nil)
+ completionHandler(FileProviderItem(remoteItem: item, rootFileID: loadedRuntime.configuration.rootFileID,
+ isTrashed: metadata.isTrashed), nil)
}
} catch is CancellationError {
await lifecycle.cancel()
@@ -145,7 +230,10 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
summary: "resolve item metadata."
)
FileProviderLog.replicatedExtension.error("item(forIdentifier:\(identifier.rawValue, privacy: .public)) failed: \(mappedError.localizedDescription, privacy: .public)")
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(nil, mappedError)
}
}
@@ -163,7 +251,12 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
let progress = Progress.fileTransfer(operationKind: .downloading)
let domain = self.domain
let temporaryDirectoryURL = self.temporaryDirectoryURL
- let lifecycle = FileProviderOperationLifecycle(progress: progress) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: progress,
+ diagnosticOperation: .fetchContents,
+ diagnosticItemIdentifier: itemIdentifier.rawValue,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
FileProviderLog.replicatedExtension.debug("cancel fetchContents(for:\(itemIdentifier.rawValue, privacy: .public))")
completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -291,7 +384,10 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
summary: "fetch file contents."
)
FileProviderLog.replicatedExtension.error("fetchContents(for:\(itemIdentifier.rawValue, privacy: .public)) failed: \(mappedError.localizedDescription, privacy: .public)")
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(nil, nil, mappedError)
}
}
@@ -317,7 +413,16 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
if isDirectory == false {
progress.prepareForByteCount(url?.fileSize)
}
- let lifecycle = FileProviderOperationLifecycle(progress: progress) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: progress,
+ diagnosticOperation: .createItem,
+ diagnosticFieldShape: ProviderDiagnosticFieldClassifier.classify(
+ fields,
+ isTrashDestination: itemTemplate.parentItemIdentifier == .trashContainer
+ ),
+ diagnosticItemIdentifier: itemTemplate.parentItemIdentifier.rawValue,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
FileProviderLog.replicatedExtension.debug("cancel createItem(parentIdentifier:\(itemTemplate.parentItemIdentifier.rawValue, privacy: .public))")
completionHandler(nil, [], false, NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -395,27 +500,18 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
}
let coordinator = self.makeMutationCoordinator(runtime: loadedRuntime)
let parentID = try self.fileID(forParentIdentifier: itemTemplate.parentItemIdentifier, runtime: loadedRuntime)
- let createdItem: KDriveRemoteItem
-
- if isDirectory {
- createdItem = try await coordinator.createDirectory(
- parentID: parentID,
- name: itemTemplate.filename
- )
- } else {
- createdItem = try await Self.contentTransferLimiter.withPermit {
- let contents = try url.map { try Data(contentsOf: $0, options: .mappedIfSafe) } ?? Data()
- progress.prepareForByteCount(contents.count)
- FileProviderLog.replicatedExtension.debug("upload new file parentFileID(\(parentID, privacy: .public)) bytes(\(contents.count, privacy: .public))")
- return try await coordinator.createFile(
- parentID: parentID,
- fileName: itemTemplate.filename,
- contents: contents,
- lastModifiedAt: itemTemplate.contentModificationDate ?? nil,
- transferProgress: progress.attachTransfer
- )
- }
- }
+ let createdItem = try await KDriveCreationExecutor.execute(isDirectory: isDirectory, options: options,
+ createDirectory: {
+ try await coordinator.createDirectory(parentID: parentID, name: itemTemplate.filename)
+ }, createFile: {
+ try await Self.contentTransferLimiter.withPermit {
+ let contents = try url.map { try KDriveDirectUploadContentLoader.loadContents(at: $0) } ?? Data()
+ progress.prepareForByteCount(contents.count)
+ return try await coordinator.createFile(parentID: parentID, fileName: itemTemplate.filename,
+ contents: contents, lastModifiedAt: itemTemplate.contentModificationDate ?? nil,
+ transferProgress: progress.attachTransfer)
+ }
+ })
FileProviderLog.replicatedExtension.info("created \(kind, privacy: .public) item(\(createdItem.id, privacy: .public)) parentFileID(\(createdItem.parentID, privacy: .public)) driveID(\(loadedRuntime.configuration.driveID, privacy: .public))")
await ProviderEventRecorder.recordActivity(
@@ -450,7 +546,10 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
summary: "create \(kind)."
)
FileProviderLog.replicatedExtension.error("createItem(parentIdentifier:\(itemTemplate.parentItemIdentifier.rawValue, privacy: .public)) failed: \(mappedError.localizedDescription, privacy: .public)")
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(nil, [], false, mappedError)
}
}
@@ -477,7 +576,16 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
if changesContents {
progress.prepareForByteCount(newContents?.fileSize)
}
- let lifecycle = FileProviderOperationLifecycle(progress: progress) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: progress,
+ diagnosticOperation: .modifyItem,
+ diagnosticFieldShape: ProviderDiagnosticFieldClassifier.classify(
+ changedFields,
+ isTrashDestination: item.parentItemIdentifier == .trashContainer
+ ),
+ diagnosticItemIdentifier: item.itemIdentifier.rawValue,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
FileProviderLog.replicatedExtension.debug("cancel modifyItem(\(item.itemIdentifier.rawValue, privacy: .public))")
completionHandler(nil, [], false, NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -496,58 +604,38 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
throw NSFileProviderError(.cannotSynchronize)
}
let current = try await vault.item(vaultItemID)
- if changedFields.contains(.parentItemIdentifier),
- item.parentItemIdentifier == .trashContainer {
- try await vault.trash(
- itemID: vaultItemID,
- baseContentRevision: baseContentRevision,
- baseMetadataRevision: baseMetadataRevision
- )
- await self.signalEncryptedMutation(
- runtime: loadedRuntime,
- parentIDs: [current.parentID],
- includesTrash: true
- )
- await lifecycle.finish(markProgressComplete: true) {
- completionHandler(nil, [], false, nil)
- }
- return
- }
-
- let parentID = changedFields.contains(.parentItemIdentifier)
- ? try self.vaultParentIdentifier(item.parentItemIdentifier)
- : current.parentID
- let updated = try await Self.contentTransferLimiter.withPermit {
- try await vault.modify(
- itemID: vaultItemID,
- baseContentRevision: baseContentRevision,
- baseMetadataRevision: baseMetadataRevision,
- parentID: parentID,
- filename: changedFields.contains(.filename)
- ? item.filename
- : current.filename,
- favorite: current.isFavorite,
- plaintextURL: changesContents ? newContents : nil,
- modifiedAt: item.contentModificationDate.flatMap { $0 } ?? Date()
- )
- }
- await self.signalEncryptedMutation(
- runtime: loadedRuntime,
- parentIDs: [current.parentID, updated.parentID],
- includesTrash: false
- )
+ let requestsTrash = changedFields.contains(.parentItemIdentifier) && item.parentItemIdentifier == .trashContainer
+ let parentID = changedFields.contains(.parentItemIdentifier) && !requestsTrash
+ ? try self.vaultParentIdentifier(item.parentItemIdentifier) : current.parentID
+ let result = try await VaultModificationExecutor.execute(current: current, fields: changedFields,
+ requestsTrash: requestsTrash, hasContents: newContents != nil,
+ baseContentRevision: baseContentRevision, baseMetadataRevision: baseMetadataRevision,
+ modify: {
+ try await Self.contentTransferLimiter.withPermit {
+ try await vault.modify(itemID: vaultItemID,
+ baseContentRevision: baseContentRevision, baseMetadataRevision: baseMetadataRevision,
+ parentID: parentID,
+ filename: changedFields.contains(.filename) ? item.filename : current.filename,
+ favorite: current.isFavorite, plaintextURL: changesContents ? newContents : nil,
+ modifiedAt: item.contentModificationDate.flatMap { $0 } ?? Date())
+ }
+ }, trash: { content, metadata in
+ try await vault.trash(itemID: vaultItemID, baseContentRevision: content, baseMetadataRevision: metadata)
+ })
+ await self.signalEncryptedMutation(runtime: loadedRuntime,
+ parentIDs: [current.parentID, result.item?.parentID], includesTrash: result.trashed)
await ProviderEventRecorder.recordActivity(
- kind: .modify,
+ kind: result.trashed ? .trash : .modify,
runtime: loadedRuntime,
- itemIdentifier: updated.id.fileProviderIdentifier,
+ itemIdentifier: vaultItemID.fileProviderIdentifier,
itemName: nil,
itemPath: nil,
summary: "Modified an encrypted item."
)
await lifecycle.finish(markProgressComplete: true) {
completionHandler(
- FileProviderItem(vaultItem: updated),
- [],
+ result.item.map { FileProviderItem(vaultItem: $0) },
+ result.remainingFields,
false,
nil
)
@@ -566,177 +654,53 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
contentVersion: version.contentVersion,
metadataVersion: version.metadataVersion
)
- var affectedContainerIdentifiers: [NSFileProviderItemIdentifier] = []
- var remainingFields = changedFields
- var updatedItem: KDriveRemoteItem?
- let requestsTrash = changedFields.contains(.parentItemIdentifier)
- && item.parentItemIdentifier == .trashContainer
-
- // Apply location and name first. kDrive's move endpoint performs
- // collision-safe renaming, while rename retries with a unique name.
- if changedFields.contains(.parentItemIdentifier), requestsTrash == false {
- let parentID = try self.fileID(forParentIdentifier: item.parentItemIdentifier, runtime: loadedRuntime)
- updatedItem = try await coordinator.moveItem(
- fileID: fileID,
- baseMetadataVersion: version.metadataVersion,
- destinationParentID: parentID,
- name: changedFields.contains(.filename) ? item.filename : nil
- )
- remainingFields.remove(.parentItemIdentifier)
- remainingFields.remove(.filename)
- affectedContainerIdentifiers.append(contentsOf: self.containerIdentifiers(
- forFileIDs: [
- KDriveItemMetadataVersion(data: version.metadataVersion)?.parentID,
- parentID,
- updatedItem?.parentID,
- ],
- rootFileID: loadedRuntime.configuration.rootFileID
- ))
- } else if changedFields.contains(.filename) {
- updatedItem = try await coordinator.renameItem(
- fileID: fileID,
- baseMetadataVersion: version.metadataVersion,
- name: item.filename
- )
- remainingFields.remove(.filename)
- affectedContainerIdentifiers.append(contentsOf: self.containerIdentifiers(
- forFileIDs: [
- KDriveItemMetadataVersion(data: version.metadataVersion)?.parentID,
- updatedItem?.parentID,
- ],
- rootFileID: loadedRuntime.configuration.rootFileID
- ))
+ let requestsTrash = changedFields.contains(.parentItemIdentifier) && item.parentItemIdentifier == .trashContainer
+ let parentID = changedFields.contains(.parentItemIdentifier) && !requestsTrash
+ ? try self.fileID(forParentIdentifier: item.parentItemIdentifier, runtime: loadedRuntime) : nil
+ let executor = KDriveModificationExecutor(coordinator: coordinator) { identifier in
+ try await loadedRuntime.remote.item(driveID: loadedRuntime.configuration.driveID, fileID: identifier)
}
-
- if changedFields.contains(.contents) {
- guard let newContents else {
- throw NSFileProviderError(.cannotSynchronize)
- }
- do {
- let result = try await Self.contentTransferLimiter.withPermit {
- let data = try Data(contentsOf: newContents, options: .mappedIfSafe)
- progress.prepareForByteCount(data.count)
- return try await coordinator.replaceContents(
- itemIdentifier: item.itemIdentifier.rawValue,
- fileID: fileID,
- localFilename: item.filename,
- baseContentVersion: version.contentVersion,
- contents: data,
- lastModifiedAt: item.contentModificationDate ?? nil,
- failOnConflict: options.contains(.failOnConflict),
- transferProgress: progress.attachTransfer
- )
- }
- updatedItem = result.item
- remainingFields.remove(.contents)
- remainingFields.remove(.contentModificationDate)
- if case .conflictCopy(let conflictItem) = result {
- FileProviderLog.replicatedExtension.info("preserved stale content edit as conflict item(\(conflictItem.id, privacy: .public)) original(\(fileID, privacy: .public))")
+ let knownBefore = try await loadedRuntime.workingSetStateStore.workingSetSnapshot(
+ domainIdentifier: loadedRuntime.configuration.domainIdentifier)?.items.first { $0.id == fileID }
+ let result = try await executor.execute(fileID: fileID, filename: item.filename, baseVersion: baseVersion,
+ fields: changedFields, destinationParentID: parentID, requestsTrash: requestsTrash,
+ modificationDate: item.contentModificationDate ?? nil, hasContents: newContents != nil) {
+ guard let newContents else { throw NSFileProviderError(.cannotSynchronize) }
+ do {
+ return try await Self.contentTransferLimiter.withPermit {
+ let data = try KDriveDirectUploadContentLoader.loadContents(at: newContents)
+ progress.prepareForByteCount(data.count)
+ return try await coordinator.replaceContents(itemIdentifier: item.itemIdentifier.rawValue,
+ fileID: fileID, localFilename: item.filename, baseContentVersion: version.contentVersion,
+ contents: data, lastModifiedAt: item.contentModificationDate ?? nil,
+ failOnConflict: options.contains(.failOnConflict), transferProgress: progress.attachTransfer)
+ }
+ } catch let error as KDriveMutationConflictError {
+ await self.recordBlockedConflict(error, operation: .modify,
+ itemIdentifier: item.itemIdentifier.rawValue, itemName: item.filename,
+ runtime: loadedRuntime, summary: "Upload was blocked because fail-on-conflict was requested.")
+ throw error
}
- affectedContainerIdentifiers.append(contentsOf: self.containerIdentifiers(
- forFileIDs: [updatedItem?.parentID],
- rootFileID: loadedRuntime.configuration.rootFileID
- ))
- } catch let error as KDriveMutationConflictError {
- await self.recordBlockedConflict(
- error,
- operation: .modify,
- itemIdentifier: item.itemIdentifier.rawValue,
- itemName: item.filename,
- runtime: loadedRuntime,
- summary: "Upload was blocked because fail-on-conflict was requested."
- )
- throw error
}
- } else if changedFields.contains(.contentModificationDate),
- let modificationDate = item.contentModificationDate ?? nil {
- updatedItem = try await coordinator.updateModificationDate(
- fileID: fileID,
- date: modificationDate
- )
- remainingFields.remove(.contentModificationDate)
+ if !result.trashed, let updated = result.item,
+ !changedFields.intersection([.contents, .filename, .parentItemIdentifier, .contentModificationDate]).isEmpty {
+ await self.publishKnownWorkingSetItem(updated,
+ replacing: updated.id == fileID ? knownBefore : nil, runtime: loadedRuntime)
}
-
- // Trash runs last so a combined contents+trash request first
- // durably preserves the new bytes. If preservation created a
- // conflict copy, both items are moved to trash, not discarded.
- if requestsTrash {
- let originalItem = try await coordinator.trashItem(fileID: fileID, baseVersion: baseVersion)
- if let updatedItem, updatedItem.id != fileID {
- _ = try await coordinator.trashItem(
- fileID: updatedItem.id,
- baseVersion: KDriveItemBaseVersion(
- contentVersion: updatedItem.contentVersion,
- metadataVersion: updatedItem.metadataVersion
- )
- )
- }
- remainingFields.remove(.parentItemIdentifier)
- affectedContainerIdentifiers.append(contentsOf: self.containerIdentifiers(
- forFileIDs: [
- KDriveItemMetadataVersion(data: version.metadataVersion)?.parentID,
- originalItem.parentID,
- ],
- rootFileID: loadedRuntime.configuration.rootFileID
- ))
- affectedContainerIdentifiers.append(.trashContainer)
- await ProviderEventRecorder.recordActivity(
- kind: .trash,
- runtime: loadedRuntime,
- itemIdentifier: item.itemIdentifier.rawValue,
- itemName: originalItem.name,
- itemPath: originalItem.path,
- summary: "Applied pending item changes and moved the item to trash."
- )
- await self.invalidateCachedSnapshotsAndSignal(
- runtime: loadedRuntime,
- containerIdentifiers: affectedContainerIdentifiers
- )
- await signalRecoverableProviderErrorsResolved(for: self.domain)
- let completedFields = remainingFields
- await lifecycle.finish(markProgressComplete: true) {
- completionHandler(nil, completedFields, false, nil)
- }
- return
- }
-
- let resolvedItem: KDriveRemoteItem
- if let updatedItem {
- resolvedItem = updatedItem
- } else {
- resolvedItem = try await loadedRuntime.remote.item(
- driveID: loadedRuntime.configuration.driveID,
- fileID: fileID
- )
- }
-
- FileProviderLog.replicatedExtension.info("modified item(\(item.itemIdentifier.rawValue, privacy: .public)) kDriveFileID(\(fileID, privacy: .public)) remainingFields(\(String(describing: remainingFields), privacy: .public))")
- await ProviderEventRecorder.recordActivity(
- kind: .modify,
- runtime: loadedRuntime,
- itemIdentifier: ProviderEventRecorder.itemIdentifier(for: resolvedItem),
- itemName: resolvedItem.name,
- itemPath: resolvedItem.path,
- summary: "Modified item."
- )
- if resolvedItem.isDirectory {
- affectedContainerIdentifiers.append(NSFileProviderItemIdentifier(
- KDriveItemIdentifier.item(resolvedItem.id).rawValue
- ))
- }
- await self.invalidateCachedSnapshotsAndSignal(
- runtime: loadedRuntime,
- containerIdentifiers: affectedContainerIdentifiers
- )
+ var containers = self.containerIdentifiers(forFileIDs: result.affectedParentIDs.map(Optional.some),
+ rootFileID: loadedRuntime.configuration.rootFileID)
+ if result.trashed { containers.append(.trashContainer) }
+ await ProviderEventRecorder.recordActivity(kind: result.trashed ? .trash : .modify,
+ runtime: loadedRuntime, itemIdentifier: item.itemIdentifier.rawValue,
+ itemName: result.item?.name ?? item.filename, itemPath: result.item?.path,
+ summary: result.trashed ? "Applied pending item changes and moved the item to trash." : "Modified item.")
+ await self.invalidateCachedSnapshotsAndSignal(runtime: loadedRuntime, containerIdentifiers: containers)
await signalRecoverableProviderErrorsResolved(for: self.domain)
- let completedFields = remainingFields
- await lifecycle.finish(markProgressComplete: true) {
- completionHandler(
- FileProviderItem(remoteItem: resolvedItem, rootFileID: loadedRuntime.configuration.rootFileID),
- completedFields,
- false,
- nil
- )
+ await lifecycle.finish(markProgressComplete: true,
+ diagnosticItemMetadataAlias: StabilityDiagnosticIdentity.activeMetadataAlias(for: result.item)) {
+ completionHandler(result.item.map { FileProviderItem(remoteItem: $0,
+ rootFileID: loadedRuntime.configuration.rootFileID, isTrashed: result.trashed) },
+ result.remainingFields, false, nil)
}
} catch is CancellationError {
await lifecycle.cancel()
@@ -753,7 +717,10 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
shouldRecord: conflictFailureAlreadyRecorded == false
)
FileProviderLog.replicatedExtension.error("modifyItem(\(item.itemIdentifier.rawValue, privacy: .public)) failed: \(mappedError.localizedDescription, privacy: .public)")
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(nil, [], false, mappedError)
}
}
@@ -770,7 +737,12 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
) -> Progress {
let optionsDescription = String(describing: options)
FileProviderLog.replicatedExtension.debug("deleteItem(\(itemIdentifier.rawValue, privacy: .public)) options(\(optionsDescription, privacy: .public)) baseVersion(\(logVersionDescription(version), privacy: .public)) @ domainVersion(\(request.logDomainVersion, privacy: .public))")
- let lifecycle = FileProviderOperationLifecycle(progress: .discreteOperation()) {
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: .discreteOperation(),
+ diagnosticOperation: .deleteItem,
+ diagnosticItemIdentifier: itemIdentifier.rawValue,
+ diagnosticRecorder: diagnosticRecorder
+ ) {
FileProviderLog.replicatedExtension.debug("cancel deleteItem(\(itemIdentifier.rawValue, privacy: .public))")
completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError))
}
@@ -877,7 +849,10 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
summary: "delete item."
)
FileProviderLog.replicatedExtension.error("deleteItem(\(itemIdentifier.rawValue, privacy: .public)) failed: \(mappedError.localizedDescription, privacy: .public)")
- await lifecycle.finish(markProgressComplete: false) {
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: mappedError
+ ) {
completionHandler(mappedError)
}
}
@@ -1081,16 +1056,34 @@ public final class PotassiumFileProviderExtension: NSObject, NSFileProviderRepli
private func pollWorkingSet(
runtime: FileProviderRuntime,
- minimumInterval: TimeInterval = KDriveWorkingSetPollCoordinator.pollingInterval
+ minimumInterval: TimeInterval = KDriveWorkingSetPollCoordinator.pollingInterval,
+ coalescePendingMaterializationPolls: Bool = false
) async throws -> KDriveWorkingSetPollOutcome {
- try await KDriveWorkingSetPollCoordinator(
- domainIdentifier: runtime.configuration.domainIdentifier,
- driveID: runtime.configuration.driveID,
- rootFileID: runtime.configuration.rootFileID,
- remote: runtime.remote,
- workingSetRemote: runtime.workingSetRemote,
- stateStore: runtime.workingSetStateStore
- ).poll(minimumInterval: minimumInterval)
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .workingSetRefresh,
+ recorder: runtime.eventStore as? any ProviderDiagnosticRecording
+ )
+ do {
+ let outcome = try await span.withCorrelation {
+ try await KDriveWorkingSetPollCoordinator(
+ domainIdentifier: runtime.configuration.domainIdentifier,
+ driveID: runtime.configuration.driveID,
+ rootFileID: runtime.configuration.rootFileID,
+ remote: runtime.remote,
+ workingSetRemote: runtime.workingSetRemote,
+ stateStore: runtime.workingSetStateStore
+ ).poll(minimumInterval: minimumInterval, coalescePendingMaterializationPolls: coalescePendingMaterializationPolls) {
+ await span.checkpoint(errorClass: .concurrentSnapshot)
+ }
+ }
+ await span.complete(statusClass: .success)
+ return outcome
+ } catch {
+ if ProviderDiagnosticErrorClassifier.classify(error) == .cancellation { await span.cancel() }
+ else { await span.fail(error: error) }
+ throw error
+ }
}
private func signalWorkingSet(runtime: FileProviderRuntime) async {
diff --git a/potassiumProviderTests/ConflictCallbackLifecycleTests.swift b/potassiumProviderTests/ConflictCallbackLifecycleTests.swift
new file mode 100644
index 0000000..2f960f2
--- /dev/null
+++ b/potassiumProviderTests/ConflictCallbackLifecycleTests.swift
@@ -0,0 +1,97 @@
+import FileProvider
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+
+struct ConflictCallbackLifecycleTests {
+ @Test(.timeLimit(.minutes(1))) func gatePreservesReleaseAndCancellationOrdering() async throws {
+ let earlyRelease = ConflictTestGate()
+ await earlyRelease.release()
+ try await earlyRelease.arrive()
+ try await earlyRelease.waitUntilReached()
+ let cancelled = ConflictTestGate()
+ let worker = Task { try await cancelled.arrive() }
+ defer { worker.cancel() }
+ try await cancelled.waitUntilReached()
+ worker.cancel()
+ await cancelled.release()
+ await #expect(throws: CancellationError.self) { try await worker.value }
+ }
+
+ @Test(arguments: [401, 403, 408, 429, 507])
+ func productionErrorMappingIsRecoverableAndDoesNotLoseSafeStatus(status: Int) {
+ let result = providerErrorMapping(APIClientError.unacceptableStatusCode(status, body: "synthetic"))
+ let error = result.mappedError as NSError
+ let expected: NSFileProviderError.Code = switch status {
+ case 401: .notAuthenticated
+ case 408, 429: .serverUnreachable
+ case 507: .insufficientQuota
+ default: .cannotSynchronize
+ }
+ #expect(error.domain == NSFileProviderErrorDomain && error.code == expected.rawValue)
+ }
+
+ @Test func failOnConflictAndOfflineMapThroughProductionMapper() {
+ let item = ConflictTestRemote.item(3, name: "Synthetic.txt", parent: 1)
+ let conflict = KDriveMutationConflictError.localContentConflict(latestItem: item,
+ stagedURL: URL(filePath: "/synthetic/retained"))
+ let mapped = providerErrorMapping(conflict).mappedError as NSError
+ #expect(mapped.domain == NSFileProviderErrorDomain && mapped.code == NSFileProviderError.localVersionConflictingWithServer.rawValue)
+ #expect((providerErrorMapping(URLError(.notConnectedToInternet)).mappedError as NSError).code == NSFileProviderError.serverUnreachable.rawValue)
+ }
+
+ @Test(.timeLimit(.minutes(1))) func cancellingActualMutationSequencePreventsLateSuccessAndServerMutation() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ let base = try await remote.item(driveID: 7, fileID: 3)
+ try client.cache(base)
+ let gate = ConflictTestGate()
+ await remote.gateReplacement(gate)
+ let result = CallbackOutcomeRecorder()
+ let progress = Progress(totalUnitCount: 100)
+ let lifecycle = FileProviderOperationLifecycle(progress: progress) { result.record("cancelled") }
+ let executor = KDriveModificationExecutor(coordinator: client.coordinator) { try await remote.item(driveID: 7, fileID: $0) }
+ lifecycle.start { lifecycle in
+ defer { result.finishWorker() }
+ do {
+ _ = try await executor.execute(fileID: 3, filename: base.name,
+ baseVersion: KDriveItemBaseVersion(contentVersion: base.contentVersion, metadataVersion: base.metadataVersion),
+ fields: [.contents, .parentItemIdentifier], destinationParentID: nil, requestsTrash: true,
+ modificationDate: nil, hasContents: true) { try await client.edit(Data("local".utf8)) }
+ await lifecycle.finish(markProgressComplete: true) { result.record("success") }
+ } catch is CancellationError { await lifecycle.cancel() }
+ catch { await lifecycle.finish(markProgressComplete: false) { result.record("failure") } }
+ }
+ try await gate.waitUntilReached()
+ await lifecycle.cancel()
+ await gate.release()
+ await lifecycle.finish(markProgressComplete: true) { result.record("late-success") }
+ #expect(result.values == ["cancelled"])
+ #expect(progress.completedUnitCount == 0)
+ await result.waitForWorker()
+ #expect(result.workerFinished)
+ #expect(await remote.operations() == [])
+ #expect(await remote.snapshot().trash.isEmpty)
+ let staged = try FileManager.default.contentsOfDirectory(at: client.directory.appendingPathComponent("staging"), includingPropertiesForKeys: nil)
+ #expect(try staged.map { try Data(contentsOf: $0) } == [Data("local".utf8)])
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == Data("base".utf8))
+ }
+}
+
+private final class CallbackOutcomeRecorder: @unchecked Sendable {
+ private let lock = NSLock()
+ private var outcomes: [String] = []
+ private var finished = false
+ private let workerTerminal = AsyncStream.makeStream()
+ func finishWorker() {
+ lock.withLock { finished = true }
+ workerTerminal.continuation.finish()
+ }
+ func waitForWorker() async { for await _ in workerTerminal.stream { } }
+ var workerFinished: Bool { lock.withLock { finished } }
+ func record(_ value: String) { lock.withLock { outcomes.append(value) } }
+ var values: [String] { lock.withLock { outcomes } }
+}
diff --git a/potassiumProviderTests/ConflictDeletionTests.swift b/potassiumProviderTests/ConflictDeletionTests.swift
new file mode 100644
index 0000000..2000431
--- /dev/null
+++ b/potassiumProviderTests/ConflictDeletionTests.swift
@@ -0,0 +1,35 @@
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+
+struct ConflictDeletionTests {
+ @Test func permanentDeleteUsesAuthoritativeTrashMetadataAndRepeatedAbsenceIsObservable() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let base = try await remote.item(driveID: 7, fileID: 3)
+ try await remote.trashItem(driveID: 7, fileID: 3)
+ let coordinator = KDriveMutationCoordinator(configuration: ProviderDomainConfiguration(domainIdentifier: "synthetic",
+ displayName: "Synthetic", driveID: 7, driveName: "Synthetic", rootFileID: 1), remote: remote,
+ trashedItemLookup: { id in
+ guard let item = await remote.snapshot().items[id], await remote.snapshot().trash.contains(id) else {
+ throw APIClientError.unacceptableStatusCode(404, body: "")
+ }
+ return item
+ })
+ // An active-item request would fail. Successful deletion therefore has
+ // to use the injected authoritative Trash lookup, not active metadata.
+ await remote.hideFromActiveLookup(3)
+ let version = KDriveItemBaseVersion(contentVersion: base.contentVersion, metadataVersion: base.metadataVersion)
+ await #expect(throws: APIClientError.self) { try await remote.item(driveID: 7, fileID: 3) }
+ let deleted = try await coordinator.deleteTrashedItem(fileID: 3, baseVersion: version)
+ #expect(deleted.id == 3)
+ #expect(await remote.snapshot().items[3] == nil)
+ do {
+ _ = try await coordinator.deleteTrashedItem(fileID: 3, baseVersion: version)
+ Issue.record("Expected authoritative absence on a repeated deletion")
+ } catch { #expect(KDriveRemoteErrorClassifier.isNotFound(error)) }
+ #expect(await remote.operations().filter { $0 == "delete" }.count == 1)
+ }
+}
diff --git a/potassiumProviderTests/ConflictMatrixTests.swift b/potassiumProviderTests/ConflictMatrixTests.swift
new file mode 100644
index 0000000..3577650
--- /dev/null
+++ b/potassiumProviderTests/ConflictMatrixTests.swift
@@ -0,0 +1,330 @@
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+/// Stable test IDs are also reproduction selectors. Expected outcomes are
+/// specified independently of the coordinator's implementation.
+enum ConflictCase: String, CaseIterable, Sendable {
+ case staleEdit = "plaintext.content.stale"
+ case missingVersion = "plaintext.content.missing-version"
+ case conditionalRace = "plaintext.content.after-preflight"
+ case failOnConflict = "plaintext.content.fail-on-conflict"
+ case renameRename = "plaintext.metadata.rename-rename"
+ case moveMove = "plaintext.metadata.move-move"
+ case editRename = "plaintext.metadata.edit-rename"
+ case editMove = "plaintext.metadata.edit-move"
+ case moveRemoteRename = "plaintext.metadata.move-preserves-rename"
+ case nameCollision = "plaintext.identity.name-collision"
+ case caseCollision = "plaintext.identity.case-collision"
+ case unicodeCollision = "plaintext.identity.unicode-collision"
+ case trashEdit = "plaintext.trash.concurrent-edit"
+ case staleDeletion = "plaintext.delete.stale"
+ case lostCreateResponse = "plaintext.retry.create-response-lost"
+ case lostConflictResponse = "plaintext.retry.conflict-response-lost"
+ case lostReplacementResponse = "plaintext.retry.replacement-response-lost"
+ case lostDirectoryResponse = "plaintext.retry.directory-response-lost"
+ case sameNameReplacement = "plaintext.identity.same-name-replacement"
+ case repeatedMetadata = "plaintext.retry.metadata-delivery"
+ case mayAlreadyExistFile = "plaintext.callback.may-already-exist-file"
+ case mayAlreadyExistDirectory = "plaintext.callback.may-already-exist-directory"
+ case movedParent = "plaintext.folder.parent-moved"
+ case removedParent = "plaintext.folder.parent-removed"
+
+ case vaultContent = "vault.content.concurrent"
+ case vaultMetadata = "vault.metadata.concurrent"
+ case vaultCycle = "vault.folder.reciprocal-moves"
+ case vaultDeleteChild = "vault.folder.delete-child-create"
+ case vaultTrashProvenance = "vault.trash.independent-descendant"
+ case vaultNameAllocation = "vault.identity.name-collision"
+ case vaultStalePurge = "vault.delete.stale-purge"
+ case vaultHistoricalRestore = "vault.version.restore-aba"
+ case vaultOmittedHistory = "vault.history.omission"
+ case vaultOfflineReference = "vault.history.offline-reference"
+
+ enum Engine: String { case plaintext, vault }
+ var engine: Engine { rawValue.hasPrefix("vault.") ? .vault : .plaintext }
+ static var callbackCases: [Self] { [.mayAlreadyExistFile, .mayAlreadyExistDirectory] }
+ static var plaintextCases: [Self] { allCases.filter { $0.engine == .plaintext && !callbackCases.contains($0) } }
+ static var vaultCases: [Self] { allCases.filter { $0.engine == .vault } }
+ var ordering: String {
+ switch self {
+ case .conditionalRace: "competing client commits after local preflight"
+ case .mayAlreadyExistFile, .mayAlreadyExistDirectory: "existing server identity, then duplicate create delivery through production callback routing"
+ case .lostCreateResponse, .lostConflictResponse, .lostReplacementResponse, .lostDirectoryResponse: "server commits, response is lost, client restarts and retries"
+ default: engine == .vault ? "causal transactions delivered in different orders" : "both clients cache base; remote intent precedes local intent"
+ }
+ }
+ var competingOperations: [String] {
+ switch self {
+ case .staleEdit, .missingVersion, .conditionalRace, .failOnConflict, .vaultContent: ["edit base on client A", "edit same base on client B"]
+ case .renameRename: ["rename A", "rename B"]
+ case .moveMove: ["move to A", "move to B"]
+ case .editRename: ["edit contents", "rename same identity"]
+ case .editMove, .moveRemoteRename: ["edit independent field", "move same identity"]
+ case .nameCollision, .caseCollision, .unicodeCollision, .vaultNameAllocation: ["create sibling", "claim equivalent name"]
+ case .trashEdit, .staleDeletion, .vaultStalePurge: ["edit contents", "trash or delete stale identity"]
+ case .lostCreateResponse, .lostConflictResponse, .lostReplacementResponse, .lostDirectoryResponse: ["commit mutation", "restart and replay unacknowledged mutation"]
+ case .mayAlreadyExistFile, .mayAlreadyExistDirectory: ["server item already exists", "create callback with reconciliation hint", "repeat callback"]
+ case .sameNameReplacement: ["remove original identity", "create another identity at the same path", "edit original cached identity"]
+ case .repeatedMetadata: ["move and rename", "repeat same callback intent"]
+ case .movedParent, .removedParent: ["move or remove parent", "create child by parent identity"]
+ case .vaultMetadata: ["rename A", "rename B"]
+ case .vaultCycle: ["move A under B", "move B under A"]
+ case .vaultDeleteChild: ["delete parent", "create child"]
+ case .vaultTrashProvenance: ["trash descendant independently", "trash and restore ancestor"]
+ case .vaultHistoricalRestore: ["restore historical content", "edit with old revision"]
+ case .vaultOmittedHistory: ["persist causal history", "omit a remote journal object"]
+ case .vaultOfflineReference: ["retain offline reference", "run maintenance"]
+ }
+ }
+ var unresolvedFinding: String? {
+ switch self {
+ case .lostReplacementResponse, .lostDirectoryResponse, .mayAlreadyExistDirectory: "CR-009"
+ case .staleDeletion: "CR-013"
+ default: nil
+ }
+ }
+ var recoveryAssertion: String { "Re-read preserved identities and contents after resolution; retain staged bytes when rejected." }
+ var expectedResolution: String {
+ switch self {
+ case .staleEdit, .missingVersion, .conditionalRace: "preserve both byte streams"
+ case .failOnConflict: "reject and retain staged local bytes"
+ case .renameRename, .moveMove: "apply local metadata intent to the same identity"
+ case .editRename, .editMove, .moveRemoteRename: "preserve independent remote metadata"
+ case .nameCollision, .caseCollision, .unicodeCollision: "retain both identities with distinct names"
+ case .trashEdit: "preserve remote bytes in reversible trash"
+ case .staleDeletion: "reject deletion and preserve changed item"
+ case .lostCreateResponse, .lostConflictResponse: "retry against retained state without a second upload effect"
+ case .lostReplacementResponse: "preserve bytes in original and redundant copy; ambiguous success remains CR-009"
+ case .lostDirectoryResponse: "preserve original and create second directory; reconciliation remains CR-009"
+ case .sameNameReplacement: "reject missing original identity; preserve replacement and staged local bytes"
+ case .repeatedMetadata: "return same identity without repeating remote effects"
+ case .mayAlreadyExistFile: "preserve existing bytes; file replay returns the same created identity under the service token model"
+ case .mayAlreadyExistDirectory: "preserve both directories; do not infer identity from a matching name; CR-009 remains"
+ case .movedParent: "address the parent by stable identity"
+ case .removedParent: "reject create and retain staged bytes"
+ case .vaultContent: "canonical winner plus stable copies of competing contents"
+ case .vaultMetadata: "canonical metadata winner with explicit conflict"
+ case .vaultCycle: "preserve an acyclic parent graph in every replay order"
+ case .vaultDeleteChild: "preserve the folder and concurrent child"
+ case .vaultTrashProvenance: "leave independently trashed descendants in trash"
+ case .vaultNameAllocation: "allocate distinct normalized sibling names"
+ case .vaultStalePurge: "preserve the changed identity"
+ case .vaultHistoricalRestore: "publish a fresh revision; reject stale ABA write"
+ case .vaultOmittedHistory: "reject rollback without filling omitted history from cache"
+ case .vaultOfflineReference: "never delete ciphertext that an offline device may reference"
+ }
+ }
+}
+
+struct ConflictMatrixTests {
+ @Test(.timeLimit(.minutes(1)), arguments: ConflictCase.plaintextCases)
+ func twoPersistentClientsResolve(_ scenario: ConflictCase) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let first = ConflictTestClient(directory: directory.appendingPathComponent("first"), remote: remote)
+ let second = ConflictTestClient(directory: directory.appendingPathComponent("second"), remote: remote)
+ let base = try await remote.item(driveID: 7, fileID: 3)
+ try first.cache(base); try second.cache(base)
+ let local = Data("local changed contents".utf8), other = Data("remote changed contents".utf8)
+ var resultID = 3
+ switch scenario {
+ case .staleEdit, .missingVersion, .failOnConflict:
+ _ = try await second.edit(other)
+ if scenario == .failOnConflict {
+ await #expect(throws: KDriveMutationConflictError.self) { try await first.edit(local, failOnConflict: true) }
+ let files = try FileManager.default.contentsOfDirectory(at: first.directory.appendingPathComponent("staging"), includingPropertiesForKeys: nil)
+ #expect(try files.map { try Data(contentsOf: $0) } == [local])
+ #expect(await remote.snapshot().items.count == 3)
+ } else {
+ let result = try await first.edit(local, version: scenario == .missingVersion ? Data() : nil)
+ resultID = result.item.id
+ #expect(resultID != base.id)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: resultID) == local)
+ }
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == other)
+ case .conditionalRace:
+ let gate = ConflictTestGate()
+ await remote.gateReplacement(gate)
+ let edit = Task { try await first.edit(local) }
+ defer { edit.cancel() }
+ try await gate.waitUntilReached()
+ _ = try await second.edit(other)
+ await gate.release()
+ let result = try await edit.value
+ resultID = result.item.id
+ #expect(resultID != 3)
+ #expect(await remote.operations() == ["replace", "replace", "upload"])
+ #expect(try await remote.downloadFile(driveID: 7, fileID: resultID) == local)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == other)
+ case .renameRename:
+ _ = try await second.coordinator.renameItem(fileID: 3, baseMetadataVersion: base.metadataVersion, name: "Remote.txt")
+ let result = try await first.coordinator.renameItem(fileID: 3, baseMetadataVersion: base.metadataVersion, name: "Local.txt")
+ #expect(result.id == 3 && result.name == "Local.txt" && result.parentID == 1)
+ case .moveMove:
+ let folder = try await remote.createDirectory(driveID: 7, parentID: 1, name: "Third")
+ _ = try await second.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: folder.id, name: nil)
+ let result = try await first.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: 2, name: nil)
+ #expect(result.id == 3 && result.parentID == 2 && result.name == base.name)
+ case .editRename, .moveRemoteRename:
+ _ = try await second.coordinator.renameItem(fileID: 3, baseMetadataVersion: base.metadataVersion, name: "Remote.txt")
+ let result: KDriveRemoteItem
+ if scenario == .editRename { result = try await first.edit(local).item }
+ else { result = try await first.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: 2, name: nil) }
+ #expect(result.id == 3 && result.name == "Remote.txt")
+ #expect(result.parentID == (scenario == .editRename ? 1 : 2))
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == (scenario == .editRename ? local : Data("base".utf8)))
+ case .editMove:
+ _ = try await second.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: 2, name: nil)
+ let result = try await first.edit(local).item
+ #expect(result.id == 3 && result.parentID == 2 && result.name == base.name)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == local)
+ case .nameCollision, .caseCollision, .unicodeCollision:
+ let existingName = scenario == .unicodeCollision ? "caf\u{00E9}.txt" : "Local.txt"
+ let desired = scenario == .caseCollision ? "LOCAL.txt" : scenario == .unicodeCollision ? "cafe\u{0301}.txt" : existingName
+ let sibling = try await second.coordinator.createFile(parentID: 1, fileName: existingName, contents: other, lastModifiedAt: nil)
+ let result = try await first.coordinator.renameItem(fileID: 3, baseMetadataVersion: base.metadataVersion, name: desired)
+ #expect(result.id == 3 && result.parentID == 1)
+ #expect(result.name.precomposedStringWithCanonicalMapping.lowercased() != existingName.precomposedStringWithCanonicalMapping.lowercased())
+ #expect(try await remote.downloadFile(driveID: 7, fileID: sibling.id) == other)
+ case .trashEdit, .staleDeletion:
+ _ = try await second.edit(other)
+ let version = KDriveItemBaseVersion(contentVersion: base.contentVersion, metadataVersion: base.metadataVersion)
+ if scenario == .trashEdit {
+ _ = try await first.coordinator.trashItem(fileID: 3, baseVersion: version)
+ #expect(await remote.snapshot().trash == [3])
+ } else {
+ await #expect(throws: KDriveMutationConflictError.self) { try await first.coordinator.deleteTrashedItem(fileID: 3, baseVersion: version) }
+ #expect(!(await remote.operations()).contains("delete"))
+ }
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == other)
+ case .lostCreateResponse, .lostConflictResponse:
+ if scenario == .lostConflictResponse { _ = try await second.edit(other) }
+ await remote.inject(.responseLost)
+ await #expect(throws: URLError.self) {
+ if scenario == .lostCreateResponse { _ = try await first.coordinator.createFile(parentID: 1, fileName: "New.txt", contents: local, lastModifiedAt: nil) }
+ else { _ = try await first.edit(local) }
+ }
+ let committed = await remote.snapshot()
+ let restartedRemote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let restarted = ConflictTestClient(directory: first.directory, remote: restartedRemote)
+ #expect(try restarted.base() == base)
+ let result: KDriveRemoteItem
+ if scenario == .lostCreateResponse { result = try await restarted.coordinator.createFile(parentID: 1, fileName: "New.txt", contents: local, lastModifiedAt: nil) }
+ else { result = try await restarted.edit(local).item }
+ #expect(await restartedRemote.snapshot().items == committed.items)
+ #expect(try await restartedRemote.downloadFile(driveID: 7, fileID: result.id) == local)
+ #expect(try FileManager.default.contentsOfDirectory(atPath: first.directory.appendingPathComponent("staging").path).isEmpty)
+ case .lostReplacementResponse, .lostDirectoryResponse:
+ await remote.inject(.responseLost)
+ await #expect(throws: URLError.self) {
+ if scenario == .lostReplacementResponse { _ = try await first.edit(local) }
+ else { _ = try await first.coordinator.createDirectory(parentID: 1, name: "Unacknowledged") }
+ }
+ let committed = await remote.snapshot()
+ let restartedRemote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let restarted = ConflictTestClient(directory: first.directory, remote: restartedRemote)
+ let retried: KDriveRemoteItem
+ if scenario == .lostReplacementResponse {
+ retried = try await restarted.edit(local).item
+ #expect(retried.id != 3)
+ #expect(try await restartedRemote.downloadFile(driveID: 7, fileID: 3) == local)
+ #expect(try await restartedRemote.downloadFile(driveID: 7, fileID: retried.id) == local)
+ } else {
+ retried = try await restarted.coordinator.createDirectory(parentID: 1, name: "Unacknowledged")
+ #expect(retried.isDirectory && retried.name != "Unacknowledged" && retried.parentID == 1)
+ let child = try await restarted.coordinator.createFile(parentID: retried.id, fileName: "Usable.txt", contents: local, lastModifiedAt: nil)
+ #expect(try await restartedRemote.downloadFile(driveID: 7, fileID: child.id) == local)
+ }
+ #expect(committed.items[retried.id] == nil)
+ for (id, item) in committed.items { #expect(await restartedRemote.snapshot().items[id] == item) }
+ #expect(scenario.unresolvedFinding == "CR-009")
+ case .sameNameReplacement:
+ try await remote.trashItem(driveID: 7, fileID: 3)
+ try await remote.deleteTrashedItem(driveID: 7, fileID: 3)
+ let replacement = try await second.coordinator.createFile(parentID: 1, fileName: base.name, contents: other, lastModifiedAt: nil)
+ resultID = replacement.id
+ #expect(replacement.id != base.id && replacement.name == base.name)
+ await remote.clearOperations()
+ await #expect(throws: (any Error).self) { try await first.edit(local) }
+ #expect(await remote.operations().isEmpty)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: replacement.id) == other)
+ let files = try FileManager.default.contentsOfDirectory(at: first.directory.appendingPathComponent("staging"), includingPropertiesForKeys: nil)
+ #expect(try files.map { try Data(contentsOf: $0) } == [local])
+ case .repeatedMetadata:
+ let applied = try await first.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: 2, name: "Applied.txt")
+ await remote.clearOperations()
+ let replayed = try await second.coordinator.moveItem(fileID: 3, baseMetadataVersion: base.metadataVersion, destinationParentID: 2, name: "Applied.txt")
+ #expect(replayed == applied && applied.id == base.id)
+ #expect(await remote.operations().isEmpty)
+ case .movedParent:
+ let folder = try await remote.createDirectory(driveID: 7, parentID: 1, name: "Moving Parent")
+ _ = try await second.coordinator.moveItem(fileID: folder.id, baseMetadataVersion: folder.metadataVersion, destinationParentID: 2, name: nil)
+ let result = try await first.coordinator.createFile(parentID: folder.id, fileName: "Child.txt", contents: local, lastModifiedAt: nil)
+ resultID = result.id
+ #expect(result.parentID == folder.id)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: result.id) == local)
+ case .removedParent:
+ try await remote.trashItem(driveID: 7, fileID: 2)
+ await #expect(throws: (any Error).self) { try await first.coordinator.createFile(parentID: 2, fileName: "Child.txt", contents: local, lastModifiedAt: nil) }
+ let staged = try FileManager.default.contentsOfDirectory(at: first.directory.appendingPathComponent("staging"), includingPropertiesForKeys: nil)
+ #expect(try staged.map { try Data(contentsOf: $0) } == [local])
+ default:
+ Issue.record("A vault case was routed through the plaintext engine")
+ }
+ // Independent persisted base must never silently advance on the other client.
+ #expect(try first.base() == base && second.base() == base)
+ let final = try await remote.item(driveID: 7, fileID: resultID)
+ #expect(final.driveID == 7 && final.etag != nil)
+ _ = try await remote.downloadFile(driveID: 7, fileID: resultID)
+ }
+
+ @Test(arguments: ConflictCase.callbackCases)
+ func creationCallbackPolicyMatrix(_ scenario: ConflictCase) async throws {
+ switch scenario {
+ case .mayAlreadyExistFile:
+ try await CreationCallbackTests().reconciliationHintPreservesExistingBytesAndReplaysFileIdentity(mayAlreadyExist: true)
+ case .mayAlreadyExistDirectory:
+ try await CreationCallbackTests().repeatedDirectoryHintRetainsTheDocumentedReconciliationLimitation()
+ default: Issue.record("Unexpected create callback catalog entry")
+ }
+ }
+
+ @Test(arguments: ConflictCase.vaultCases)
+ func vaultPolicyMatrix(_ scenario: ConflictCase) async throws {
+ switch scenario {
+ case .vaultContent: try VaultJournalTests().concurrentContentEditsConvergeForEveryReplayOrder()
+ case .vaultMetadata: try VaultConflictReplayTests().competingMetadataUsesCanonicalWinnerInEveryPermutation()
+ case .vaultCycle: try VaultJournalTests().concurrentDirectoryMovesCannotCreateAParentCycle()
+ case .vaultDeleteChild: try VaultJournalTests().staleDeleteLosesToEditAndFolderDeleteLosesToChild()
+ case .vaultTrashProvenance: try VaultJournalTests().restoringFolderPreservesIndependentlyTrashedDescendant()
+ case .vaultNameAllocation: try VaultJournalTests().siblingConflictAllocatorSkipsExistingGeneratedName()
+ case .vaultStalePurge: try VaultConflictReplayTests().stalePurgePreservesEditedIdentity()
+ case .vaultHistoricalRestore: try await VaultProvisioningTests().restoringVersionPublishesFreshRevisionAndRejectsABAStaleWrite()
+ case .vaultOmittedHistory: try await VaultProvisioningTests().returningDeviceRejectsOmittedRemoteJournalObject()
+ case .vaultOfflineReference: try await VaultProvisioningTests().maintenanceNeverDeletesCiphertextThatAnOfflineDeviceMayReference()
+ default: Issue.record("A plaintext case was routed through the vault engine")
+ }
+ }
+
+ @Test(arguments: [ConflictTestRemote.Fault.offline, .status(401), .status(403), .status(429), .status(507), .cancelled])
+ func interruptedEditCanBeRecoveredAfterRestart(_ fault: ConflictTestRemote.Fault) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ try client.cache(await remote.item(driveID: 7, fileID: 3))
+ let local = Data("retained change".utf8)
+ await remote.inject(fault)
+ await #expect(throws: (any Error).self) { try await client.edit(local) }
+ let files = try FileManager.default.contentsOfDirectory(at: client.directory.appendingPathComponent("staging"), includingPropertiesForKeys: nil)
+ #expect(try files.map { try Data(contentsOf: $0) } == [local])
+ #expect(await remote.operations().isEmpty)
+ let restarted = ConflictTestClient(directory: client.directory, remote: remote)
+ let result = try await restarted.edit(local).item
+ #expect(result.id == 3)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: 3) == local)
+ }
+}
diff --git a/potassiumProviderTests/ConflictTestRemote.swift b/potassiumProviderTests/ConflictTestRemote.swift
new file mode 100644
index 0000000..d338c63
--- /dev/null
+++ b/potassiumProviderTests/ConflictTestRemote.swift
@@ -0,0 +1,221 @@
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+
+/// A stateful service double, not a queue of canned successful responses. Each
+/// request validates the current identity, parent and conditional version.
+actor ConflictTestRemote: KDriveFileProviding {
+ struct State: Codable {
+ var items: [Int: KDriveRemoteItem] = [:]
+ var bytes: [Int: Data] = [:]
+ var trash: Set = []
+ var tokens: [String: Int] = [:]
+ var revision = 1
+ var nextID = 100
+ }
+ enum Fault: Sendable { case status(Int), offline, cancelled, responseLost }
+ private var state: State
+ private let url: URL
+ private var fault: Fault?
+ private var calls: [String] = []
+ private var replaceGate: ConflictTestGate?
+ private var hiddenFromActiveLookup: Set = []
+ func hideFromActiveLookup(_ id: Int) { hiddenFromActiveLookup.insert(id) }
+
+ init(directory: URL) throws {
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ url = directory.appendingPathComponent("server.json")
+ if FileManager.default.fileExists(atPath: url.path) {
+ state = try JSONDecoder().decode(State.self, from: Data(contentsOf: url))
+ } else {
+ state = State()
+ state.items[1] = Self.item(1, name: "Root", parent: 0, directory: true)
+ state.items[2] = Self.item(2, name: "Other", parent: 1, directory: true)
+ state.items[3] = Self.item(3, name: "Original.txt", parent: 1)
+ state.bytes[3] = Data("base".utf8)
+ try JSONEncoder().encode(state).write(to: url, options: .atomic)
+ }
+ }
+ static func item(_ id: Int, name: String, parent: Int, directory: Bool = false,
+ revision: Int = 1, size: Int = 4) -> KDriveRemoteItem {
+ KDriveRemoteItem(id: id, name: name, type: directory ? "dir" : "file", status: "ok",
+ driveID: 7, parentID: parent, path: nil, size: directory ? nil : size,
+ mimeType: directory ? nil : "text/plain", createdAt: Date(timeIntervalSince1970: 1),
+ modifiedAt: Date(timeIntervalSince1970: Double(revision)),
+ updatedAt: Date(timeIntervalSince1970: Double(revision)), etag: "revision-\(revision)")
+ }
+ func snapshot() -> State { state }
+ func operations() -> [String] { calls }
+ func inject(_ fault: Fault) { self.fault = fault }
+ func gateReplacement(_ gate: ConflictTestGate) { replaceGate = gate }
+ func clearOperations() { calls = [] }
+ private func save() throws { try JSONEncoder().encode(state).write(to: url, options: .atomic) }
+ private func failBeforeRequest() throws {
+ guard let fault else { return }
+ if case .responseLost = fault { return }
+ self.fault = nil
+ switch fault {
+ case .status(let code): throw APIClientError.unacceptableStatusCode(code, body: "")
+ case .offline: throw URLError(.notConnectedToInternet)
+ case .cancelled: throw CancellationError()
+ case .responseLost: break
+ }
+ }
+ private func afterCommit() throws {
+ try save()
+ if case .responseLost = fault { fault = nil; throw URLError(.networkConnectionLost) }
+ }
+ private func existing(_ id: Int) throws -> KDriveRemoteItem {
+ guard let item = state.items[id] else { throw APIClientError.unacceptableStatusCode(404, body: "") }
+ return item
+ }
+ private func parent(_ id: Int, child: Int? = nil) throws {
+ guard try existing(id).isDirectory, !state.trash.contains(id) else {
+ throw APIClientError.unacceptableStatusCode(404, body: "")
+ }
+ var ancestor = id, seen = Set()
+ while ancestor != 0 {
+ guard ancestor != child, seen.insert(ancestor).inserted else {
+ throw APIClientError.unacceptableStatusCode(409, body: "")
+ }
+ ancestor = try existing(ancestor).parentID
+ }
+ }
+ private func allocated(_ name: String, parent: Int, excluding: Int? = nil) -> String {
+ let names = Set(state.items.values.filter { $0.parentID == parent && $0.id != excluding && !state.trash.contains($0.id) }
+ .map { $0.name.precomposedStringWithCanonicalMapping.lowercased() })
+ var candidate = name, suffix = 2
+ while names.contains(candidate.precomposedStringWithCanonicalMapping.lowercased()) {
+ candidate = "\((name as NSString).deletingPathExtension) \(suffix).\((name as NSString).pathExtension)"
+ suffix += 1
+ }
+ return candidate
+ }
+ func item(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
+ try failBeforeRequest()
+ if hiddenFromActiveLookup.contains(fileID) { throw APIClientError.unacceptableStatusCode(404, body: "") }
+ return try existing(fileID)
+ }
+ func listDrives() async throws -> [KDriveDriveSummary] { [] }
+ func listDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) async throws -> KDriveItemPage {
+ KDriveItemPage(items: state.items.values.filter { $0.parentID == folderID && !state.trash.contains($0.id) }, nextCursor: nil, hasMore: false)
+ }
+ func listAdvancedDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) async throws -> KDriveAdvancedItemPage {
+ let page = try await listDirectory(driveID: driveID, folderID: folderID, cursor: cursor, limit: limit)
+ return KDriveAdvancedItemPage(items: page.items, actions: [], actionItems: [], nextCursor: nil, hasMore: false)
+ }
+ func listTrash(driveID: Int, cursor: String?, limit: Int) async throws -> KDriveItemPage {
+ KDriveItemPage(items: state.items.values.filter { state.trash.contains($0.id) }, nextCursor: nil, hasMore: false)
+ }
+ func downloadFile(driveID: Int, fileID: Int) async throws -> Data {
+ try failBeforeRequest(); _ = try existing(fileID)
+ return state.bytes[fileID] ?? Data()
+ }
+ func thumbnail(driveID: Int, fileID: Int, width: Int?, height: Int?) async throws -> Data { Data() }
+ func uploadFile(driveID: Int, parentID: Int, fileName: String, contents: Data, lastModifiedAt: Date?,
+ conflictStrategy: KDriveUploadConflictStrategy, clientToken: String?, contentHash: String?) async throws -> KDriveRemoteItem {
+ try failBeforeRequest(); calls.append("upload")
+ if let clientToken, let id = state.tokens[clientToken] { return try existing(id) }
+ try parent(parentID)
+ let name = allocated(fileName, parent: parentID)
+ if conflictStrategy == .error, name != fileName { throw APIClientError.unacceptableStatusCode(409, body: "") }
+ state.nextID += 1; state.revision += 1
+ let item = Self.item(state.nextID, name: name, parent: parentID, revision: state.revision, size: contents.count)
+ state.items[item.id] = item; state.bytes[item.id] = contents
+ if let clientToken { state.tokens[clientToken] = item.id }
+ try afterCommit(); return item
+ }
+ func replaceFile(driveID: Int, fileID: Int, expectedETag: String, clientToken: String, contentHash: String,
+ contents: Data, lastModifiedAt: Date?) async throws -> KDriveRemoteItem {
+ if let gate = replaceGate { replaceGate = nil; try await gate.arrive() }
+ try failBeforeRequest(); calls.append("replace")
+ let current = try existing(fileID)
+ guard current.etag == expectedETag else { throw APIClientError.unacceptableStatusCode(412, body: "") }
+ state.revision += 1
+ let updated = Self.item(fileID, name: current.name, parent: current.parentID, revision: state.revision, size: contents.count)
+ state.items[fileID] = updated; state.bytes[fileID] = contents
+ try afterCommit(); return updated
+ }
+ func createDirectory(driveID: Int, parentID: Int, name: String) async throws -> KDriveRemoteItem {
+ try parent(parentID); calls.append("mkdir")
+ guard allocated(name, parent: parentID) == name else { throw APIClientError.unacceptableStatusCode(409, body: "") }
+ state.nextID += 1
+ let item = Self.item(state.nextID, name: name, parent: parentID, directory: true)
+ state.items[item.id] = item; try afterCommit(); return item
+ }
+ func renameItem(driveID: Int, fileID: Int, name: String) async throws {
+ try failBeforeRequest(); calls.append("rename")
+ let item = try existing(fileID)
+ guard allocated(name, parent: item.parentID, excluding: fileID) == name else { throw APIClientError.unacceptableStatusCode(409, body: "") }
+ state.items[fileID] = Self.item(fileID, name: name, parent: item.parentID, directory: item.isDirectory,
+ revision: Int(item.modifiedAt.timeIntervalSince1970), size: item.size ?? 0)
+ try afterCommit()
+ }
+ func moveItem(driveID: Int, fileID: Int, destinationParentID: Int, name: String?) async throws {
+ try failBeforeRequest(); calls.append("move"); try parent(destinationParentID, child: fileID)
+ let item = try existing(fileID)
+ state.items[fileID] = Self.item(fileID, name: allocated(name ?? item.name, parent: destinationParentID, excluding: fileID),
+ parent: destinationParentID, directory: item.isDirectory, revision: Int(item.modifiedAt.timeIntervalSince1970), size: item.size ?? 0)
+ try afterCommit()
+ }
+ func updateModificationDate(driveID: Int, fileID: Int, date: Date) async throws { calls.append("date") }
+ func trashItem(driveID: Int, fileID: Int) async throws {
+ try failBeforeRequest(); _ = try existing(fileID); calls.append("trash")
+ state.trash.insert(fileID); try afterCommit()
+ }
+ func deleteTrashedItem(driveID: Int, fileID: Int) async throws {
+ try failBeforeRequest(); calls.append("delete")
+ state.items[fileID] = nil; state.bytes[fileID] = nil; state.trash.remove(fileID); try afterCommit()
+ }
+}
+
+actor ConflictTestGate {
+ private let arrival = AsyncStream.makeStream()
+ private let releaseSignal = AsyncStream.makeStream()
+ func arrive() async throws {
+ arrival.continuation.finish()
+ for await _ in releaseSignal.stream { }
+ // Cancellation and release can race. A released gate must never turn
+ // a cancelled request into a committed synthetic mutation.
+ try Task.checkCancellation()
+ }
+ func waitUntilReached() async throws {
+ for await _ in arrival.stream { }
+ try Task.checkCancellation()
+ }
+ func release() { releaseSignal.continuation.finish() }
+}
+
+struct ConflictTestClient: Sendable {
+ let directory: URL
+ let remote: ConflictTestRemote
+ var coordinator: KDriveMutationCoordinator {
+ KDriveMutationCoordinator(configuration: ProviderDomainConfiguration(domainIdentifier: directory.lastPathComponent,
+ displayName: "Synthetic", driveID: 7, driveName: "Synthetic", rootFileID: 1),
+ remote: remote, conflictStager: ConflictTestStager(directory: directory.appendingPathComponent("staging")),
+ conflictDeviceName: { "Synthetic" }, conflictDate: { Date(timeIntervalSince1970: 1) },
+ conflictTimeZone: { TimeZone(secondsFromGMT: 0)! })
+ }
+ func cache(_ item: KDriveRemoteItem) throws {
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ try JSONEncoder().encode(item).write(to: directory.appendingPathComponent("base.json"), options: .atomic)
+ }
+ func base() throws -> KDriveRemoteItem {
+ try JSONDecoder().decode(KDriveRemoteItem.self, from: Data(contentsOf: directory.appendingPathComponent("base.json")))
+ }
+ func edit(_ bytes: Data, filename: String? = nil, version: Data? = nil, failOnConflict: Bool = false) async throws -> KDriveContentMutationResult {
+ let base = try base()
+ return try await coordinator.replaceContents(itemIdentifier: String(base.id), fileID: base.id,
+ localFilename: filename ?? base.name, baseContentVersion: version ?? base.contentVersion,
+ contents: bytes, lastModifiedAt: nil, failOnConflict: failOnConflict)
+ }
+}
+struct ConflictTestStager: KDriveConflictContentStaging {
+ let directory: URL
+ func stageConflictContents(_ contents: Data, itemIdentifier: String) async throws -> URL {
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ let file = directory.appendingPathComponent(KDriveMutationIdentity.clientToken([itemIdentifier, KDriveMutationIdentity.contentHash(contents)]))
+ try contents.write(to: file, options: .atomic); return file
+ }
+ func removeStagedConflictContents(at url: URL) async { try? FileManager.default.removeItem(at: url) }
+}
diff --git a/potassiumProviderTests/CreationCallbackTests.swift b/potassiumProviderTests/CreationCallbackTests.swift
new file mode 100644
index 0000000..299d593
--- /dev/null
+++ b/potassiumProviderTests/CreationCallbackTests.swift
@@ -0,0 +1,54 @@
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct CreationCallbackTests {
+ @Test(arguments: [false, true])
+ func reconciliationHintPreservesExistingBytesAndReplaysFileIdentity(mayAlreadyExist: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ let original = try await remote.item(driveID: 7, fileID: 3)
+ let local = Data("local create".utf8)
+ let options: NSFileProviderCreateItemOptions = mayAlreadyExist ? [.mayAlreadyExist] : []
+ func deliver(_ client: ConflictTestClient) async throws -> KDriveRemoteItem {
+ try await KDriveCreationExecutor.execute(isDirectory: false, options: options,
+ createDirectory: { Issue.record("File callback routed as directory"); throw CancellationError() },
+ createFile: { try await client.coordinator.createFile(parentID: 1, fileName: original.name, contents: local, lastModifiedAt: nil) })
+ }
+ let created = try await deliver(client)
+ let restarted = ConflictTestClient(directory: client.directory, remote: remote)
+ let replay = try await deliver(restarted)
+ #expect(created == replay && created.id != original.id && created.name != original.name)
+ #expect(created.parentID == original.parentID && created.etag != nil)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: created.id) == local)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: original.id) == Data("base".utf8))
+ #expect(await remote.snapshot().items.count == 4)
+ }
+
+ @Test func repeatedDirectoryHintRetainsTheDocumentedReconciliationLimitation() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ func deliver() async throws -> KDriveRemoteItem {
+ try await KDriveCreationExecutor.execute(isDirectory: true, options: [.mayAlreadyExist],
+ createDirectory: { try await client.coordinator.createDirectory(parentID: 1, name: "Repeated") },
+ createFile: { Issue.record("Directory callback tried uploading file bytes"); throw CancellationError() })
+ }
+ let original = try await deliver(), replay = try await deliver()
+ #expect(original.id != replay.id && original.name != replay.name)
+ #expect(original.isDirectory && replay.isDirectory && original.parentID == replay.parentID)
+ #expect(try await remote.item(driveID: 7, fileID: original.id) == original)
+ for folder in [original, replay] {
+ let data = Data("usable child".utf8)
+ let child = try await client.coordinator.createFile(parentID: folder.id, fileName: "Child.txt", contents: data, lastModifiedAt: nil)
+ #expect(child.parentID == folder.id)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: child.id) == data)
+ }
+ // This verifies current conservative policy. It intentionally does not
+ // claim no-duplicate-effect acceptance or resolve CR-009.
+ }
+}
diff --git a/potassiumProviderTests/FileProviderBackgroundWorkTests.swift b/potassiumProviderTests/FileProviderBackgroundWorkTests.swift
new file mode 100644
index 0000000..ba90eeb
--- /dev/null
+++ b/potassiumProviderTests/FileProviderBackgroundWorkTests.swift
@@ -0,0 +1,83 @@
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+struct FileProviderBackgroundWorkTests {
+ @Test func invalidationCancelsAcknowledgedWorkBeforeAnotherRequest() async throws {
+ let scope = FileProviderBackgroundWork(), work = BackgroundRefreshProbe()
+ defer { scope.invalidate() }
+ let accepted = scope.start { await work.run() }
+ #expect(accepted)
+ try await wait { await work.started == 1 }
+ scope.invalidate()
+ scope.invalidate() // Repeated invalidation cannot duplicate a terminal.
+ try await wait { await work.cancelled == 1 }
+ await work.release()
+ #expect(await work.requests == 1)
+ #expect(await work.completed == 0)
+ #expect(await work.cancelled == 1)
+ #expect(scope.registeredTaskCount == 0)
+ let acceptedAfterInvalidation = scope.start { await work.run() }
+ #expect(!acceptedAfterInvalidation)
+ #expect(await work.started == 1)
+ }
+
+ @Test func replacingOneInstanceDoesNotCancelAnotherInstancesWork() async throws {
+ let original = FileProviderBackgroundWork(), replacement = FileProviderBackgroundWork()
+ defer { original.invalidate(); replacement.invalidate() }
+ let oldWork = BackgroundRefreshProbe(), newWork = BackgroundRefreshProbe()
+ original.start { await oldWork.run() }
+ replacement.start { await newWork.run() }
+ try await wait {
+ let oldStarted = await oldWork.started, newStarted = await newWork.started
+ return oldStarted == 1 && newStarted == 1
+ }
+ original.invalidate()
+ try await wait { await oldWork.cancelled == 1 }
+ #expect(await newWork.cancelled == 0)
+ await newWork.release()
+ try await wait { await newWork.completed == 1 }
+ #expect(await newWork.requests == 2)
+ replacement.invalidate()
+ #expect(await newWork.cancelled == 0)
+ }
+
+ @Test func immediateCompletionsCannotRemainRegistered() async throws {
+ let scope = FileProviderBackgroundWork()
+ defer { scope.invalidate() }
+ for _ in 0..<100 {
+ let accepted = scope.start {}
+ #expect(accepted)
+ }
+ try await wait { scope.registeredTaskCount == 0 }
+ scope.invalidate()
+ let acceptedAfterInvalidation = scope.start {}
+ #expect(!acceptedAfterInvalidation)
+ }
+
+ private func wait(_ condition: () async -> Bool) async throws {
+ let deadline = ContinuousClock.now.advanced(by: .seconds(3))
+ while !(await condition()) {
+ guard ContinuousClock.now < deadline else { throw StabilityDeadlineError.expired }
+ try await Task.sleep(for: .milliseconds(5))
+ }
+ }
+}
+
+private actor BackgroundRefreshProbe {
+ var started = 0, requests = 0, completed = 0, cancelled = 0
+ private var released = false
+ func release() { released = true }
+ func run() async {
+ started += 1
+ do {
+ try Task.checkCancellation()
+ requests += 1
+ while !released { try await Task.sleep(for: .milliseconds(5)) }
+ try Task.checkCancellation()
+ requests += 1
+ completed += 1
+ } catch is CancellationError { cancelled += 1 }
+ catch { Issue.record("Unexpected synthetic refresh error") }
+ }
+}
diff --git a/potassiumProviderTests/FileProviderOperationLifecycleTests.swift b/potassiumProviderTests/FileProviderOperationLifecycleTests.swift
index d5de460..280bb8c 100644
--- a/potassiumProviderTests/FileProviderOperationLifecycleTests.swift
+++ b/potassiumProviderTests/FileProviderOperationLifecycleTests.swift
@@ -83,6 +83,118 @@ struct FileProviderOperationLifecycleTests {
operation.cancel()
#expect(recorder.values == ["cancelled"])
}
+
+ @Test func diagnosticLifecycleRecordsOneTerminalOutcome() async throws {
+ let sink = LifecycleDiagnosticSink()
+ let progress = Progress(totalUnitCount: 1)
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: progress,
+ diagnosticOperation: .modifyItem,
+ diagnosticFieldShape: [.contents, .filename],
+ diagnosticRecorder: sink
+ ) {}
+
+ let privateFailure = NSError(
+ domain: "private-canary-9C42",
+ code: 91
+ )
+ await lifecycle.finish(
+ markProgressComplete: false,
+ diagnosticError: privateFailure
+ ) {}
+ await lifecycle.cancel()
+
+ let events = try await sink.waitForEvents(count: 2)
+ #expect(events.map(\.phase) == [.started, .failed])
+ #expect(Set(events.map(\.correlationID)).count == 1)
+ #expect(events.last?.errorClass == .unknown)
+ #expect(events.last?.fieldShape == [.contents, .filename])
+ }
+
+ @Test func successfulCallbackRecordsReturnedMetadataOnce() async throws {
+ let sink = LifecycleDiagnosticSink()
+ let lifecycle = FileProviderOperationLifecycle(progress: Progress(totalUnitCount: 1),
+ diagnosticOperation: .modifyItem, diagnosticRecorder: sink) {}
+ let alias = UUID()
+ await lifecycle.finish(markProgressComplete: true, diagnosticItemMetadataAlias: alias) {}
+ await lifecycle.finish(markProgressComplete: true, diagnosticItemMetadataAlias: UUID()) {}
+ let events = try await sink.waitForEvents(count: 2)
+ #expect(events.map(\.phase) == [.started, .completed])
+ #expect(events.first?.itemMetadataAlias == nil)
+ #expect(events.last?.itemMetadataAlias == alias)
+ }
+
+ @Test func cancellationWhileDiagnosticStartIsSuspendedNeverLaunchesWork() async {
+ let completion = CompletionRecorder()
+ let sink = SuspendingLifecycleDiagnosticSink()
+ let lifecycle = FileProviderOperationLifecycle(
+ progress: Progress(totalUnitCount: 1),
+ diagnosticOperation: .deleteItem,
+ diagnosticRecorder: sink
+ ) {
+ completion.record("cancelled")
+ }
+
+ lifecycle.start { _ in
+ completion.record("operation-started")
+ }
+ await sink.waitUntilEntered()
+ let cancellation = Task {
+ await lifecycle.cancel()
+ }
+ await Task.yield()
+ await sink.release()
+ await cancellation.value
+ for _ in 0..<100 {
+ await Task.yield()
+ }
+
+ #expect(completion.values == ["cancelled"])
+ }
+}
+
+private actor LifecycleDiagnosticSink: ProviderDiagnosticRecording {
+ private var events: [ProviderDiagnosticEvent] = []
+
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) {
+ events.append(event)
+ }
+
+ func waitForEvents(count: Int) async throws -> [ProviderDiagnosticEvent] {
+ for _ in 0..<200 where events.count < count {
+ await Task.yield()
+ }
+ guard events.count >= count else {
+ throw LifecycleDiagnosticSinkError.timedOut
+ }
+ return events
+ }
+}
+
+private enum LifecycleDiagnosticSinkError: Error {
+ case timedOut
+}
+
+private actor SuspendingLifecycleDiagnosticSink: ProviderDiagnosticRecording {
+ private var isBlocked = true
+ private var hasEntered = false
+
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) async {
+ hasEntered = true
+ while isBlocked {
+ await Task.yield()
+ }
+ }
+
+ func waitUntilEntered() async {
+ while hasEntered == false {
+ await Task.yield()
+ }
+ }
+
+ func release() {
+ isBlocked = false
+ }
}
private final class CompletionRecorder: @unchecked Sendable {
diff --git a/potassiumProviderTests/FinderActionConfirmationTests.swift b/potassiumProviderTests/FinderActionConfirmationTests.swift
new file mode 100644
index 0000000..165f076
--- /dev/null
+++ b/potassiumProviderTests/FinderActionConfirmationTests.swift
@@ -0,0 +1,46 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+struct FinderActionConfirmationTests {
+ @Test(arguments: FinderActionConfirmation.allCases)
+ func exactPromptAndRecoveryMessageBindOneDialog(_ action: FinderActionConfirmation) {
+ let dialog = FinderActionConfirmationObservation(texts: [action.prompt + " " + action.message],
+ enabledButtonTitles: ["Cancel", action.buttonTitle])
+ #expect(action.uniqueMatchIndex(in: [dialog]) == 0)
+ #expect(action.uniqueMatchIndex(in: [dialog, dialog]) == nil)
+ }
+
+ @Test(arguments: FinderActionConfirmation.allCases)
+ func underlyingFormOrIncompleteConfirmationCannotAuthorize(_ action: FinderActionConfirmation) {
+ for texts in [[], [action.prompt], [action.message], ["Confirm another operation"]] {
+ let dialog = FinderActionConfirmationObservation(texts: texts,
+ enabledButtonTitles: ["Cancel", action.buttonTitle])
+ #expect(action.uniqueMatchIndex(in: [dialog]) == nil)
+ }
+ for buttons in [["Cancel"], [action.buttonTitle], ["Cancel", action.buttonTitle, action.buttonTitle]] {
+ let dialog = FinderActionConfirmationObservation(texts: [action.prompt, action.message],
+ enabledButtonTitles: buttons)
+ #expect(action.uniqueMatchIndex(in: [dialog]) == nil)
+ }
+ }
+
+ @Test func aDifferentActionCannotMatch() {
+ let restore = FinderActionConfirmation.restoreVersion
+ let dialog = FinderActionConfirmationObservation(texts: [restore.prompt, restore.message],
+ enabledButtonTitles: ["Cancel", restore.buttonTitle])
+ #expect(FinderActionConfirmation.disableShareLink.uniqueMatchIndex(in: [dialog]) == nil)
+ }
+
+ @Test func onlyTheCompletedActionResultCanConfirmAnUncertainPress() {
+ #expect(FinderActionConfirmation.disableShareLink.hasSuccessfulResult(in: ["Disabled share link."]))
+ #expect(!FinderActionConfirmation.disableShareLink.hasSuccessfulResult(in: ["Disable Link", "Could not disable share link."]))
+ let restore = FinderActionConfirmation.restoreVersion
+ #expect(restore.hasSuccessfulResult(in: ["Restored synthetic copy.txt as a new copy."]))
+ for text in ["Restore as Copy", "The current file will not be overwritten.",
+ "Restored as a new copy.", "Could not restore synthetic copy.txt.", "Disabled share link."] {
+ #expect(!restore.hasSuccessfulResult(in: [text]))
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderActionPanelTargetTests.swift b/potassiumProviderTests/FinderActionPanelTargetTests.swift
new file mode 100644
index 0000000..862df23
--- /dev/null
+++ b/potassiumProviderTests/FinderActionPanelTargetTests.swift
@@ -0,0 +1,57 @@
+#if os(macOS) && STABILITY
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+struct FinderActionPanelTargetTests {
+ let expected = URL(fileURLWithPath: "/tmp/Chosen.app/Contents/PlugIns/Actions.appex/Contents/MacOS/Actions")
+ let alias = "provider.stability.action." + UUID().uuidString
+
+ @Test func attestsBothPhysicalExecutableAndCode() {
+ #expect(FinderActionPanelTarget.isExpectedProcess(executableURL: expected, expectedURL: expected, codeHash: "new", expectedCodeHash: "new"))
+ #expect(!FinderActionPanelTarget.isExpectedProcess(executableURL: expected, expectedURL: expected, codeHash: "old", expectedCodeHash: "new"))
+ #expect(!FinderActionPanelTarget.isExpectedProcess(executableURL: URL(fileURLWithPath: "/tmp/Older.app/Actions"), expectedURL: expected, codeHash: "new", expectedCodeHash: "new"))
+ #expect(!FinderActionPanelTarget.isExpectedProcess(executableURL: nil, expectedURL: expected, codeHash: "new", expectedCodeHash: "new"))
+ #expect(!FinderActionPanelTarget.isExpectedProcess(executableURL: expected, expectedURL: expected, codeHash: nil, expectedCodeHash: nil))
+ }
+
+ @Test func mainWindowOnlyDiscoveryStillRequiresExactAliasAndDeduplicatesListedWindow() {
+ #expect(FinderActionPanelTarget.candidates(listed: [Int](), main: 7, equal: ==) == [7])
+ #expect(FinderActionPanelTarget.candidates(listed: [7], main: 7, equal: ==) == [7])
+ #expect(FinderActionPanelTarget.candidates(listed: [8], main: 7, equal: ==) == [8, 7])
+ #expect(FinderActionPanelTarget.candidates(listed: [Int](), main: nil, equal: ==).isEmpty)
+ let discovered = FinderActionPanelTarget.candidates(listed: [[String]](), main: ["unrelated"], equal: ==)
+ #expect(FinderActionPanelTarget.index(alias: alias, windowIdentifiers: discovered) == nil)
+ }
+
+ @Test func requiresOnePanelWithTheExactFixtureAlias() {
+ #expect(FinderActionPanelTarget.index(alias: alias, windowIdentifiers: [["unrelated"], [alias]]) == 1)
+ #expect(FinderActionPanelTarget.index(alias: alias, windowIdentifiers: [[alias], [alias]]) == nil)
+ #expect(FinderActionPanelTarget.index(alias: alias, windowIdentifiers: [["Action Unavailable"], ["provider.stability.action.unbound"]]) == nil)
+ #expect(FinderActionPanelTarget.index(alias: "provider.stability.action.unbound", windowIdentifiers: [["provider.stability.action.unbound"]]) == nil)
+ }
+
+ @Test func twoHostsOfTheSameRemotePanelResolveToTheLastAttestedWindow() {
+ #expect(FinderActionPanelTarget.windowIndex(alias: alias, panels: [[7], [7]],
+ identifier: { _ in alias }, equal: ==) == 1)
+ #expect(FinderActionPanelTarget.windowIndex(alias: alias, panels: [[7], []],
+ identifier: { _ in alias }, equal: ==) == 0)
+ }
+
+ @Test func detachedMainWindowCannotKeepADismissedPanelBound() {
+ #expect(FinderActionPanelTarget.windowIndex(alias: alias, panels: [[], [7]],
+ identifier: { _ in alias }, equal: ==) == nil)
+ }
+
+ @Test func distinctPanelsWithTheSameAliasRemainAmbiguousAcrossOrWithinWindows() {
+ for panels in [[[7], [8]], [[7, 8]], [[7], [7, 8]]] {
+ #expect(FinderActionPanelTarget.windowIndex(alias: alias, panels: panels,
+ identifier: { _ in alias }, equal: ==) == nil)
+ }
+ #expect(FinderActionPanelTarget.windowIndex(alias: alias, panels: [[7]],
+ identifier: { _ in "unrelated" }, equal: ==) == nil)
+ #expect(FinderActionPanelTarget.windowIndex(alias: "provider.stability.action.unbound", panels: [[7]],
+ identifier: { _ in "provider.stability.action.unbound" }, equal: ==) == nil)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderContextMenuTargetTests.swift b/potassiumProviderTests/FinderContextMenuTargetTests.swift
new file mode 100644
index 0000000..1932979
--- /dev/null
+++ b/potassiumProviderTests/FinderContextMenuTargetTests.swift
@@ -0,0 +1,39 @@
+#if os(macOS) && STABILITY
+import Foundation
+import CoreGraphics
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderContextMenuTargetTests {
+ let window = CGRect(x: 100, y: 100, width: 600, height: 400)
+ let field = CGRect(x: 150, y: 150, width: 100, height: 20)
+
+ @Test func exactDisplayNameAllowsSecondaryClickWithoutAnAXMenuAction() {
+ // This is the field state observed live: a label and bounds, with no
+ // advertised AXShowMenu. URL/domain/selection binding precedes this input.
+ #expect(FinderContextMenuTarget.point(displayedName: "Generated", windowBounds: window,
+ fields: [.init(name: "Generated", bounds: field)]) == CGPoint(x: 200, y: 160))
+ }
+
+ @Test func missingAmbiguousOrDifferentNamesCannotBroadenTheSelection() {
+ let candidates: [[FinderContextMenuTarget.Field]] = [[], [.init(name: nil, bounds: field)],
+ [.init(name: "Generated.txt", bounds: field)],
+ [.init(name: "Generated", bounds: field), .init(name: "Generated", bounds: nil)]]
+ for fields in candidates {
+ #expect(FinderContextMenuTarget.point(displayedName: "Generated", windowBounds: window, fields: fields) == nil)
+ }
+ #expect(FinderContextMenuTarget.point(displayedName: "", windowBounds: window,
+ fields: [.init(name: "", bounds: field)]) == nil)
+ }
+
+ @Test func missingOrOutOfWindowGeometryCannotProduceAClick() {
+ let candidates: [CGRect?] = [nil, .zero, CGRect(x: 150, y: 150, width: 1, height: 20),
+ CGRect(x: 99, y: 150, width: 100, height: 20), CGRect(x: 690, y: 150, width: 100, height: 20)]
+ for bounds in candidates {
+ #expect(FinderContextMenuTarget.point(displayedName: "Generated", windowBounds: window,
+ fields: [.init(name: "Generated", bounds: bounds)]) == nil)
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderCopyCancellationTargetTests.swift b/potassiumProviderTests/FinderCopyCancellationTargetTests.swift
new file mode 100644
index 0000000..9dd5240
--- /dev/null
+++ b/potassiumProviderTests/FinderCopyCancellationTargetTests.swift
@@ -0,0 +1,22 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+struct FinderCopyCancellationTargetTests {
+ @Test func copyPreparationBindsBothExactQuotedNames() {
+ #expect(FinderCopyCancellationTarget.matches(sourceName: "transfer.dat", destinationName: "probe-unique",
+ labels: ["Preparing to copy “transfer.dat”", "Preparing to copy to “probe-unique”"]))
+ }
+
+ @Test(arguments: [
+ ["Preparing to copy “other.dat”", "Preparing to copy to “probe-unique”"],
+ ["Preparing to copy “transfer.dat”", "Preparing to copy to “other-probe”"],
+ ["Preparing to copy “transfer.dat.backup”", "Preparing to copy to “probe-unique”"],
+ ["Preparing to copy “transfer.dat”", "Preparing to copy to “probe-unique-other”"],
+ ["Copying files"], []
+ ])
+ func unrelatedOrIncompleteProgressIsNeverATarget(_ labels: [String]) {
+ #expect(!FinderCopyCancellationTarget.matches(sourceName: "transfer.dat", destinationName: "probe-unique", labels: labels))
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderDeletionDialogTests.swift b/potassiumProviderTests/FinderDeletionDialogTests.swift
new file mode 100644
index 0000000..d796fa3
--- /dev/null
+++ b/potassiumProviderTests/FinderDeletionDialogTests.swift
@@ -0,0 +1,55 @@
+#if os(macOS) && STABILITY
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderDeletionDialogTests {
+ private let selected = URL(filePath: "/synthetic/.Trash/fixture 12.34.56.txt")
+
+ @Test(arguments: ["fixture 12.34.56", "fixture 12.34.56.txt", "café ‘draft’"])
+ func usesTheObservedDisplayNameWithoutGuessingAnExtension(_ displayName: String) {
+ let expected = FinderDeletionDialogExpectation(selectedURL: selected, displayName: displayName)
+ let dialog = observation(name: displayName)
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: [dialog]) == 0)
+ }
+
+ @Test func hiddenExtensionIsNotMatchedAgainstTheURLFilename() {
+ let displayed = "fixture 12.34.56"
+ let dialog = observation(name: displayed)
+ let wrong = FinderDeletionDialogExpectation(selectedURL: selected, displayName: selected.lastPathComponent)
+ #expect(wrong.uniqueMatchIndex(selection: selected, dialogs: [dialog]) == nil)
+ }
+
+ @Test func changedSelectionOrEmptyDisplayNameCannotAuthorizeDeletion() {
+ let expected = FinderDeletionDialogExpectation(selectedURL: selected, displayName: "fixture")
+ #expect(expected.uniqueMatchIndex(selection: selected.deletingLastPathComponent().appendingPathComponent("other.txt"),
+ dialogs: [observation(name: "fixture")]) == nil)
+ #expect(FinderDeletionDialogExpectation(selectedURL: selected, displayName: "")
+ .uniqueMatchIndex(selection: selected, dialogs: [observation(name: "")]) == nil)
+ }
+
+ @Test(arguments: ["other fixture", "fixture copy", "fixture.txt", "fixture and another item"])
+ func anotherQuotedNameCannotMatchBySubstring(_ name: String) {
+ let expected = FinderDeletionDialogExpectation(selectedURL: selected, displayName: "fixture")
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: [observation(name: name)]) == nil)
+ }
+
+ @Test func onlyOneExactScopedDialogCanBeConfirmed() {
+ let expected = FinderDeletionDialogExpectation(selectedURL: selected, displayName: "fixture")
+ let matching = observation(name: "fixture"), unrelated = observation(name: "another")
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: [unrelated, matching]) == 1)
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: [matching, matching]) == nil)
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: []) == nil)
+ for buttons in [["Delete"], ["Cancel", "Empty Trash"], ["Cancel", "Delete", "Delete"]] {
+ let missingControl = FinderDeletionDialogObservation(texts: matching.texts, buttonTitles: buttons)
+ #expect(expected.uniqueMatchIndex(selection: selected, dialogs: [missingControl]) == nil)
+ }
+ }
+
+ private func observation(name: String) -> FinderDeletionDialogObservation {
+ FinderDeletionDialogObservation(texts: ["Are you sure you want to delete “\(name)”? This item will be deleted immediately. You can’t undo this action."],
+ buttonTitles: ["Delete", "Cancel"])
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderHydrationSequenceTests.swift b/potassiumProviderTests/FinderHydrationSequenceTests.swift
new file mode 100644
index 0000000..ec77f80
--- /dev/null
+++ b/potassiumProviderTests/FinderHydrationSequenceTests.swift
@@ -0,0 +1,55 @@
+#if os(macOS) && STABILITY
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderHydrationSequenceTests {
+ @Test func inspectedFixtureReleasesItsPresenterWithoutClosingUnrelatedDocuments() async throws {
+ let ui = HydrationDocumentProbe()
+ let fixture = URL(filePath: "/synthetic/run/remote-seed.txt")
+ var inspected = false
+ try await FinderHydrationSequence.execute(using: ui, url: fixture) {
+ #expect(ui.presentedFixture == fixture)
+ inspected = true
+ }
+ #expect(inspected)
+ #expect(ui.presentedFixture == nil)
+ #expect(ui.unrelatedDocumentIsOpen)
+ try ui.requestEviction() // An open generated presenter rejects eviction.
+ #expect(ui.evicted)
+ }
+
+ @Test func failedPresenterReleaseCannotCompleteHydration() async {
+ let ui = HydrationDocumentProbe(failClose: true)
+ var inspected = false
+ await #expect(throws: HydrationDocumentProbe.Failure.closeFailed) {
+ try await FinderHydrationSequence.execute(using: ui, url: URL(filePath: "/synthetic/run/remote-seed.txt")) {
+ inspected = true
+ }
+ }
+ #expect(inspected && ui.presentedFixture != nil)
+ #expect(throws: HydrationDocumentProbe.Failure.resourceBusy) { try ui.requestEviction() }
+ #expect(!ui.evicted && ui.unrelatedDocumentIsOpen)
+ }
+}
+
+@MainActor
+private final class HydrationDocumentProbe: FinderDocumentUIDriving {
+ enum Failure: Error { case closeFailed, resourceBusy }
+ let failClose: Bool
+ private(set) var presentedFixture: URL?
+ private(set) var evicted = false
+ let unrelatedDocumentIsOpen = true
+ init(failClose: Bool = false) { self.failClose = failClose }
+ func edit(_ url: URL, contents: String?) async throws { presentedFixture = url }
+ func closeOwnedEditorDocuments() async throws {
+ if failClose { throw Failure.closeFailed }
+ presentedFixture = nil
+ }
+ func requestEviction() throws {
+ guard presentedFixture == nil else { throw Failure.resourceBusy }
+ evicted = true
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderMenuCommandSequenceTests.swift b/potassiumProviderTests/FinderMenuCommandSequenceTests.swift
new file mode 100644
index 0000000..fdefbf3
--- /dev/null
+++ b/potassiumProviderTests/FinderMenuCommandSequenceTests.swift
@@ -0,0 +1,35 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderMenuCommandSequenceTests {
+ enum UnexpectedWait: Error { case transferCompletion }
+
+ @Test func downloadReturnsAfterDispatchAndDismissalWithoutWaitingForTransferCompletion() async throws {
+ var transitions: [String] = []
+ try await FinderMenuCommandSequence.perform(command: "Download Now",
+ press: { throw UnexpectedWait.transferCompletion }, click: { transitions.append("click") },
+ waitForDismissal: { transitions.append("dismissed") },
+ waitForResult: { throw UnexpectedWait.transferCompletion })
+ #expect(transitions == ["click", "dismissed"])
+ }
+
+ @Test(arguments: ["Remove Download", "Delete Immediately…", "Share kDrive Link…", "Version History…"])
+ func otherCommandsRetainResultObservation(_ command: String) async throws {
+ var transitions: [String] = []
+ try await FinderMenuCommandSequence.perform(command: command,
+ press: { transitions.append("press") }, click: { Issue.record("Unexpected click dispatch") },
+ waitForDismissal: { transitions.append("dismissed") }, waitForResult: { transitions.append("result") })
+ #expect(transitions == ["press", "dismissed", "result"])
+ }
+
+ @Test func failedDispatchCannotBecomeAnObservedAction() async {
+ await #expect(throws: UnexpectedWait.self) {
+ try await FinderMenuCommandSequence.perform(command: "Download Now",
+ press: { Issue.record("Unexpected AX dispatch") }, click: { throw UnexpectedWait.transferCompletion },
+ waitForDismissal: { Issue.record("Nothing was dispatched") }, waitForResult: { Issue.record("Nothing was dispatched") })
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderMenuWindowRefreshTests.swift b/potassiumProviderTests/FinderMenuWindowRefreshTests.swift
new file mode 100644
index 0000000..47e675f
--- /dev/null
+++ b/potassiumProviderTests/FinderMenuWindowRefreshTests.swift
@@ -0,0 +1,28 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+struct FinderMenuWindowRefreshTests {
+ @Test func oneMissingCommandRefreshDoesNotBecomeAnUnboundedRetry() {
+ var policy = FinderMenuWindowRefresh()
+ let early = policy.claim(command: "Download Now", matchingCommands: 0, elapsed: .seconds(1))
+ let first = policy.claim(command: "Download Now", matchingCommands: 0, elapsed: .seconds(2))
+ let repeated = policy.claim(command: "Download Now", matchingCommands: 0, elapsed: .seconds(60))
+ #expect(!early && first && !repeated)
+ }
+
+ @Test(arguments: ["Delete Immediately…", "Empty Trash", "Open", "Unknown"])
+ func otherCommandsNeverReplaceTheWindow(_ command: String) {
+ var policy = FinderMenuWindowRefresh()
+ let claimed = policy.claim(command: command, matchingCommands: 0, elapsed: .seconds(60))
+ #expect(!claimed)
+ }
+
+ @Test(arguments: [1, 2])
+ func presentOrAmbiguousCommandsDoNotRefresh(_ count: Int) {
+ var policy = FinderMenuWindowRefresh()
+ let claimed = policy.claim(command: "Restore from kDrive Trash", matchingCommands: count, elapsed: .seconds(60))
+ #expect(!claimed)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderNavigationTests.swift b/potassiumProviderTests/FinderNavigationTests.swift
new file mode 100644
index 0000000..5902b73
--- /dev/null
+++ b/potassiumProviderTests/FinderNavigationTests.swift
@@ -0,0 +1,173 @@
+#if os(macOS) && STABILITY
+import Foundation
+import CoreGraphics
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+@Suite("Finder UI transitions")
+struct FinderNavigationTests {
+ @Test func nameEditorRequiresExactSelectionLabelAndOwnedWindowGeometry() {
+ let window = CGRect(x: 100, y: 100, width: 600, height: 400)
+ let field = CGRect(x: 250, y: 200, width: 150, height: 20)
+ #expect(FinderNameEditorObservation.isConfined(value: "New Folder", expected: "New Folder", editorBounds: field,
+ windowBounds: window, sameProcess: true, ownedWindowIsFront: true))
+ #expect(!FinderNameEditorObservation.isConfined(value: "Other", expected: "New Folder", editorBounds: field,
+ windowBounds: window, sameProcess: true, ownedWindowIsFront: true))
+ #expect(!FinderNameEditorObservation.isConfined(value: "New Folder", expected: "New Folder", editorBounds: field.offsetBy(dx: 900, dy: 0),
+ windowBounds: window, sameProcess: true, ownedWindowIsFront: true))
+ #expect(!FinderNameEditorObservation.isConfined(value: "New Folder", expected: "New Folder", editorBounds: field,
+ windowBounds: window, sameProcess: false, ownedWindowIsFront: true))
+ #expect(!FinderNameEditorObservation.isConfined(value: "New Folder", expected: "New Folder", editorBounds: field,
+ windowBounds: window, sameProcess: true, ownedWindowIsFront: false))
+ }
+ @Test func unrelatedBusyAlertsAreNotTreatedAsTheGeneratedEvictionResult() {
+ #expect(FinderAlertObservation.isResourceBusyEviction(labels: ["Unable to Remove Download", "Resource busy", "OK"]))
+ #expect(!FinderAlertObservation.isResourceBusyEviction(labels: ["Unable to Delete", "Resource busy", "OK"]))
+ #expect(!FinderAlertObservation.isResourceBusyEviction(labels: ["Unable to Remove Download", "Permission denied", "OK"]))
+ }
+ @Test func windowCleanupClosesOnlyTheCreatedID() throws {
+ let process = FinderProcessIdentity(pid: 12, launchedAt: Date(timeIntervalSince1970: 100))
+ let ownership = FinderWindowOwnership(windowID: 42, process: process)
+ var closed: [Int32] = []
+ ownership.close(currentProcess: process) { closed.append($0) }
+ #expect(closed == [42])
+ }
+
+ @Test func windowCleanupRejectsRelaunchedFinderEvenWhenPIDAndWindowIDAreReused() {
+ let original = FinderProcessIdentity(pid: 12, launchedAt: Date(timeIntervalSince1970: 100))
+ let replacement = FinderProcessIdentity(pid: 12, launchedAt: Date(timeIntervalSince1970: 200))
+ let ownership = FinderWindowOwnership(windowID: 42, process: original)
+ var closed = false
+ ownership.close(currentProcess: replacement) { _ in closed = true }
+ ownership.close(currentProcess: nil) { _ in closed = true }
+ #expect(!closed)
+ }
+
+ @Test func windowCleanupFailureRemainsObservable() {
+ let process = FinderProcessIdentity(pid: 12, launchedAt: Date(timeIntervalSince1970: 100))
+ let ownership = FinderWindowOwnership(windowID: 42, process: process)
+ #expect(throws: FinderUIError.automationFailed) {
+ try ownership.close(currentProcess: process) { _ in throw FinderUIError.automationFailed }
+ }
+ }
+ @Test func localURLIdentityAcceptsCanonicalSpellingButRejectsOtherTargets() {
+ let expected = URL(filePath: "/synthetic/café.txt")
+ #expect(FinderUIURLIdentity.matches(URL(string: "file://localhost/synthetic/caf%C3%A9.txt"), expected))
+ #expect(FinderUIURLIdentity.matches(URL(filePath: "/synthetic/cafe\u{301}.txt"), expected))
+ #expect(!FinderUIURLIdentity.matches(URL(filePath: "/synthetic/other.txt"), expected))
+ #expect(!FinderUIURLIdentity.matches(URL(string: "file://different-host/synthetic/caf%C3%A9.txt"), expected))
+ #expect(!FinderUIURLIdentity.matches(nil, expected))
+ }
+
+ @Test func unavailableRowCannotTriggerSelection() async {
+ var assigned = false
+ await #expect(throws: FinderUIError.timedOut) {
+ try await FinderSelectionSequence.execute(waitUntilVisible: { throw FinderUIError.timedOut },
+ assignSelection: { assigned = true }, waitUntilSelected: {})
+ }
+ #expect(!assigned)
+ }
+
+ @Test func ignoredSelectionCannotCompleteTheSelectionSequence() async {
+ await #expect(throws: FinderUIError.selectionMismatch) {
+ try await FinderSelectionSequence.execute(waitUntilVisible: {}, assignSelection: {},
+ waitUntilSelected: { throw FinderUIError.selectionMismatch })
+ }
+ }
+ @Test func hiddenExtensionUsesExactDisplayedNameAndRejectsAmbiguousRows() {
+ #expect(FinderUINameObservation.hasUniqueMatch(displayedName: "remote-change", rowNames: ["remote-change"]))
+ #expect(!FinderUINameObservation.hasUniqueMatch(displayedName: "remote-change.txt", rowNames: ["remote-change"]))
+ #expect(!FinderUINameObservation.hasUniqueMatch(displayedName: "remote-change", rowNames: ["remote-change", "remote-change"]))
+ #expect(!FinderUINameObservation.hasUniqueMatch(displayedName: "", rowNames: [""]))
+ }
+ @Test func navigationVerifiesHistoryAndParentInOrder() async throws {
+ let ui = NavigationFake()
+ let root = URL(filePath: "/synthetic"), nested = URL(filePath: "/synthetic/a")
+ let deep = URL(filePath: "/synthetic/a/b"), sibling = URL(filePath: "/synthetic/c")
+ try await FinderNavigationSequence.execute(using: ui, root: root, nested: nested, deep: deep, sibling: sibling)
+ #expect(ui.calls == [.navigate(root), .navigate(nested), .navigate(deep), .back(nested), .forward(deep), .parent(nested), .navigate(sibling)])
+ }
+ @Test func unexpectedWindowStopsNavigation() async {
+ let ui = NavigationFake(); ui.rejectHistory = true
+ let root = URL(filePath: "/synthetic")
+ await #expect(throws: FinderUIError.windowMismatch) {
+ try await FinderNavigationSequence.execute(using: ui, root: root, nested: root, deep: root, sibling: root)
+ }
+ #expect(ui.calls.count == 4)
+ }
+ @Test func fixtureChildrenResolveOnlyAfterTheirVerifiedParentIsOpened() async throws {
+ let ui = NavigationFake()
+ let root = URL(filePath: "/synthetic"), nested = URL(filePath: "/synthetic/a")
+ let deep = URL(filePath: "/synthetic/a/b"), sibling = URL(filePath: "/synthetic/c")
+ let seed = deep.appendingPathComponent("seed.txt")
+ let result = try await FinderFixtureNavigation.resolve(using: ui) { target in
+ let parent: URL?, result: URL
+ switch target {
+ case .root: (parent, result) = (nil, root)
+ case .nested: (parent, result) = (root, nested)
+ case .deep: (parent, result) = (nested, deep)
+ case .sibling: (parent, result) = (root, sibling)
+ case .seed: (parent, result) = (deep, seed)
+ }
+ if let parent, ui.calls.last != .navigate(parent) { throw FinderUIError.timedOut }
+ return result
+ }
+ #expect(result.root == root && result.nested == nested && result.deep == deep && result.sibling == sibling && result.seed == seed)
+ }
+ @Test func conflictPreparationDoesNotDependOnDeepNavigationOrSeedHydration() async throws {
+ let ui = NavigationFake(), root = URL(filePath: "/synthetic")
+ var targets: [FinderFixtureNavigation.Target] = []
+ let result = try await FinderFixtureNavigation.resolveConflictRoot(using: ui) { target in
+ targets.append(target)
+ guard target == .root else { throw FinderUIError.timedOut }
+ return root
+ }
+ #expect(result == root && targets == [.root] && ui.calls == [.navigate(root)])
+ }
+ @Test func conflictPreparationStillRequiresAValidRootBinding() async {
+ let ui = NavigationFake()
+ await #expect(throws: FinderUIError.selectionMismatch) {
+ try await FinderFixtureNavigation.resolveConflictRoot(using: ui) { _ in throw FinderUIError.selectionMismatch }
+ }
+ #expect(ui.calls.isEmpty)
+ }
+ @Test func failedFixtureBindingCannotNavigateOrResolveDescendants() async {
+ let ui = NavigationFake(), root = URL(filePath: "/synthetic")
+ var attempts = 0
+ await #expect(throws: FinderUIError.selectionMismatch) {
+ try await FinderFixtureNavigation.resolve(using: ui) { target in
+ attempts += 1
+ if target == .nested { throw FinderUIError.selectionMismatch }
+ return root
+ }
+ }
+ #expect(attempts == 2 && ui.calls == [.navigate(root)])
+ }
+ @Test func missingMilestoneEvidenceStopsBeforeFurtherNavigation() async {
+ let ui = NavigationFake(), root = URL(filePath: "/synthetic")
+ var observed: [Int] = []
+ await #expect(throws: FinderUIError.screenshotUnavailable) {
+ try await FinderNavigationSequence.execute(using: ui, root: root, nested: root, deep: root, sibling: root) { index, _ in
+ observed.append(index)
+ if index == 2 { throw FinderUIError.screenshotUnavailable }
+ }
+ }
+ #expect(observed == [0, 1, 2])
+ #expect(ui.calls.count == 3)
+ }
+}
+
+@MainActor
+private final class NavigationFake: FinderUINavigating {
+ enum Call: Equatable { case navigate(URL), back(URL), forward(URL), parent(URL) }
+ var calls: [Call] = []
+ var rejectHistory = false
+ func navigate(to url: URL) async throws { calls.append(.navigate(url)) }
+ func navigateHistory(back: Bool, expectedURL: URL) async throws {
+ calls.append(back ? .back(expectedURL) : .forward(expectedURL))
+ if rejectHistory { throw FinderUIError.windowMismatch }
+ }
+ func navigateParent(expectedURL: URL) async throws { calls.append(.parent(expectedURL)) }
+}
+#endif
diff --git a/potassiumProviderTests/FinderPointerClickTests.swift b/potassiumProviderTests/FinderPointerClickTests.swift
new file mode 100644
index 0000000..c4ba2ac
--- /dev/null
+++ b/potassiumProviderTests/FinderPointerClickTests.swift
@@ -0,0 +1,72 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderPointerClickTests {
+ @Test(arguments: [CGMouseButton.left, .right])
+ func movesBeforeRevalidationAndUsesOneUnmodifiedClick(_ button: CGMouseButton) async throws {
+ var events: [CGEvent] = [], observations: [Int] = []
+ let point = CGPoint(x: 20, y: 30)
+ let clicked = try await FinderPointerClick.perform(at: point, button: button,
+ mayClick: { observations.append(events.count); return true },
+ canPostEvents: { true }, post: { events.append($0) }, settle: {})
+ #expect(clicked)
+ #expect(observations == [0, 1])
+ #expect(events.map(\.type) == [.mouseMoved, button == .right ? .rightMouseDown : .leftMouseDown,
+ button == .right ? .rightMouseUp : .leftMouseUp])
+ #expect(events.allSatisfy { $0.flags.isEmpty && $0.location == point })
+ #expect(events.dropFirst().allSatisfy { $0.getIntegerValueField(.mouseEventClickState) == 1 })
+ }
+
+ @Test func deniedInputPermissionCannotPostEventsOrAskForConsent() async {
+ await #expect(throws: FinderUIError.permissionRequired) {
+ _ = try await FinderPointerClick.perform(at: .zero, button: .right,
+ mayClick: { Issue.record("Target lookup is unnecessary when posting is denied"); return true },
+ canPostEvents: { false }, post: { _ in Issue.record("Denied event was posted") },
+ settle: { Issue.record("Denied event must fail immediately") })
+ }
+ }
+
+ @Test func completedTransferOrChangedTargetAfterHoverPreventsButtonDown() async throws {
+ var events: [CGEventType] = []
+ let clicked = try await FinderPointerClick.perform(at: .zero, button: .left,
+ mayClick: { events.isEmpty }, canPostEvents: { true }, post: { events.append($0.type) }, settle: {})
+ #expect(!clicked)
+ #expect(events == [.mouseMoved])
+ }
+
+ @Test(arguments: [0, 1])
+ func obstructedHitBeforeOrAfterHoverNeverPostsButtonDown(afterMoves: Int) async {
+ var events: [CGEventType] = []
+ await #expect(throws: FinderUIError.pointerTargetObstructed) {
+ _ = try await FinderPointerClick.perform(at: .zero, button: .right,
+ mayClick: {
+ if events.count == afterMoves { throw FinderUIError.pointerTargetObstructed }
+ return true
+ }, canPostEvents: { true }, post: { events.append($0.type) }, settle: {})
+ }
+ #expect(events == (afterMoves == 0 ? [] : [.mouseMoved]))
+ }
+
+ @Test(arguments: [(600, 90), (20, 20), (0, 0), (-1, 0)])
+ func transferBudgetDoesNotExtendOrdinaryUIWait(seconds: Int, expected: Int) {
+ let now = ContinuousClock.now
+ let deadline = FinderUIObservationDeadline.make(remaining: .seconds(seconds), now: now)
+ #expect(deadline.remaining(now: now) == .seconds(expected))
+ #expect(deadline.remaining(now: now.advanced(by: .seconds(91))) == .zero)
+ }
+
+ @Test func cancellationAlwaysReleasesAnAlreadyPressedButton() async {
+ var events: [CGEventType] = []
+ await #expect(throws: CancellationError.self) {
+ _ = try await FinderPointerClick.perform(at: .zero, button: .right,
+ mayClick: { true }, canPostEvents: { true }, post: { events.append($0.type) },
+ settle: { if events.contains(.rightMouseDown) { throw CancellationError() } })
+ }
+ #expect(events == [.mouseMoved, .rightMouseDown, .rightMouseUp])
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderPointerTargetTests.swift b/potassiumProviderTests/FinderPointerTargetTests.swift
new file mode 100644
index 0000000..a92b5cf
--- /dev/null
+++ b/potassiumProviderTests/FinderPointerTargetTests.swift
@@ -0,0 +1,43 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+struct FinderPointerTargetTests {
+ @Test(arguments: [1, 2])
+ func exactScopeAndItsDescendantReceiveInput(hit: Int) throws {
+ try FinderPointerTarget.verify(processIdentifier: 42, scope: 2, hitTest: { hit },
+ owner: { _ in 42 }, parent: { $0 == 1 ? 2 : nil }, equal: ==)
+ }
+
+ @Test func anotherAppIsRejectedWithoutInspectingItsUI() {
+ #expect(throws: FinderUIError.pointerTargetObstructed) {
+ try FinderPointerTarget.verify(processIdentifier: 42, scope: 2, hitTest: { 1 },
+ owner: { _ in 43 }, parent: { _ in Issue.record("Other apps must not be traversed"); return nil }, equal: ==)
+ }
+ #expect(FinderUIError.pointerTargetObstructed.isEnvironmental)
+ #expect(!FinderUIError.controlUnavailable.isEnvironmental)
+ }
+
+ @Test func anotherFinderWindowAndMissingHitCannotAuthorizeAClick() {
+ for hit in [nil, 3] as [Int?] {
+ #expect(throws: FinderUIError.pointerTargetObstructed) {
+ try FinderPointerTarget.verify(processIdentifier: 42, scope: 2, hitTest: { hit },
+ owner: { _ in 42 }, parent: { _ in nil }, equal: ==)
+ }
+ }
+ }
+
+ @Test func cyclicOrCrossProcessAncestryFailsClosed() {
+ var visits = 0
+ #expect(throws: FinderUIError.pointerTargetObstructed) {
+ try FinderPointerTarget.verify(processIdentifier: 42, scope: 2, hitTest: { 1 },
+ owner: { _ in 42 }, parent: { value in visits += 1; return value }, equal: ==)
+ }
+ #expect(visits == 32)
+ #expect(throws: FinderUIError.pointerTargetObstructed) {
+ try FinderPointerTarget.verify(processIdentifier: 42, scope: 2, hitTest: { 1 },
+ owner: { $0 == 1 ? 42 : 43 }, parent: { _ in 2 }, equal: ==)
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderPopupMenuDiscoveryTests.swift b/potassiumProviderTests/FinderPopupMenuDiscoveryTests.swift
new file mode 100644
index 0000000..98e4963
--- /dev/null
+++ b/potassiumProviderTests/FinderPopupMenuDiscoveryTests.swift
@@ -0,0 +1,30 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderPopupMenuDiscoveryTests {
+ @Test func expandedSubmenuDoesNotHideItsRootOrExposeMenuBarCommands() {
+ // App -> popup -> Open With -> submenu; app also has a menu bar.
+ let children = [0: [1, 4], 1: [2], 2: [3], 4: [5], 5: [6]]
+ let menus = Set([1, 3, 6])
+ let roots = FinderPopupMenuDiscovery.roots(from: 0,
+ role: { $0 == 4 ? .menuBar : menus.contains($0) ? .menu : .other },
+ children: { children[$0] ?? [] }, visible: { _ in true })
+ #expect(roots == [1])
+ }
+
+ @Test func twoIndependentPopupsRemainAmbiguous() {
+ let roots = FinderPopupMenuDiscovery.roots(from: 0, role: { $0 == 0 ? .other : .menu },
+ children: { $0 == 0 ? [1, 2] : [] }, visible: { _ in true })
+ #expect(roots.count == 2)
+ }
+
+ @Test func hiddenMenuAndTraversalExhaustionFailClosed() {
+ #expect(FinderPopupMenuDiscovery.roots(from: 0, role: { _ in .menu },
+ children: { _ in [1] }, visible: { _ in false }).isEmpty)
+ #expect(FinderPopupMenuDiscovery.roots(from: 0, limit: 2, role: { _ in .other },
+ children: { _ in [0] }, visible: { _ in true }).isEmpty)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderReplicatedRefreshTests.swift b/potassiumProviderTests/FinderReplicatedRefreshTests.swift
new file mode 100644
index 0000000..90066ac
--- /dev/null
+++ b/potassiumProviderTests/FinderReplicatedRefreshTests.swift
@@ -0,0 +1,46 @@
+#if os(macOS) && STABILITY
+import FileProvider
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderReplicatedRefreshTests {
+ @Test func changedFoldersRequestOneWorkingSetEnumeration() async throws {
+ let generated = NSFileProviderItemIdentifier("synthetic-folder")
+ let batches: [[NSFileProviderItemIdentifier]] = [[.rootContainer], [generated], [.rootContainer, generated, generated], [.workingSet]]
+ for batch in batches {
+ var requested: [NSFileProviderItemIdentifier] = []
+ try await FinderReplicatedRefresh.signal(changedContainers: batch) { requested.append($0) }
+ #expect(requested == [.workingSet])
+ }
+ }
+
+ @Test func emptyBatchDoesNotWakeTheProvider() async throws {
+ var requests = 0
+ try await FinderReplicatedRefresh.signal(changedContainers: []) { _ in requests += 1 }
+ #expect(requests == 0)
+ }
+
+ @Test func nativeRefreshTargetDoesNotExpandScenarioSubjectsToTheWholeDomain() async throws {
+ let generated = NSFileProviderItemIdentifier("synthetic-folder")
+ var subjects: [NSFileProviderItemIdentifier] = [], requests: [NSFileProviderItemIdentifier] = []
+ try await FinderReplicatedRefresh.signal(changedContainers: [generated], recordSubject: { subjects.append($0) }) {
+ requests.append($0)
+ }
+ #expect(subjects == [generated] && requests == [.workingSet])
+ subjects = []
+ // The working-set scenario may explicitly select that subject.
+ try await FinderReplicatedRefresh.signal(changedContainers: [.workingSet], recordSubject: { subjects.append($0) }) { _ in }
+ #expect(subjects == [.workingSet])
+ }
+
+ @Test func failedSignalRemainsAFailure() async throws {
+ await #expect(throws: URLError.self) {
+ try await FinderReplicatedRefresh.signal(changedContainers: [.rootContainer]) { _ in
+ throw URLError(.cannotConnectToHost)
+ }
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderRestoreObservationTests.swift b/potassiumProviderTests/FinderRestoreObservationTests.swift
new file mode 100644
index 0000000..823198e
--- /dev/null
+++ b/potassiumProviderTests/FinderRestoreObservationTests.swift
@@ -0,0 +1,112 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+@Suite("Finder Restore observation")
+struct FinderRestoreObservationTests {
+ @Test func staleTrashLocationOrWrongNameCannotProveVisibleRestoration() {
+ let parent = URL(filePath: "/synthetic/run/Created Folder", directoryHint: .isDirectory)
+ #expect(FinderRestoreObservation.matchesVisibleDestination(parent.appendingPathComponent("restored.txt"), parent: parent, name: "restored.txt"))
+ #expect(!FinderRestoreObservation.matchesVisibleDestination(URL(filePath: "/synthetic/.Trash/restored.txt"), parent: parent, name: "restored.txt"))
+ #expect(!FinderRestoreObservation.matchesVisibleDestination(parent.appendingPathComponent("different.txt"), parent: parent, name: "restored.txt"))
+ }
+
+ @Test func delayedMoveLocationCannotPassUntilParentAndNameBothMatch() async throws {
+ let parent = URL(filePath: "/synthetic/run/Sibling", directoryHint: .isDirectory)
+ let previous = URL(filePath: "/synthetic/run/conflict.txt")
+ let final = parent.appendingPathComponent("conflict.txt")
+ let pending = try await FinderRestoreObservation.observeVisibleDestination(parent: parent, name: "conflict.txt") { previous }
+ #expect(pending == nil)
+ let wrongName = try await FinderRestoreObservation.observeVisibleDestination(parent: parent, name: "conflict.txt") {
+ parent.appendingPathComponent("another.txt")
+ }
+ #expect(wrongName == nil)
+ let resolved = try await FinderRestoreObservation.observeVisibleDestination(parent: parent, name: "conflict.txt") { final }
+ #expect(resolved == final)
+ await #expect(throws: CancellationError.self) {
+ try await FinderRestoreObservation.observeVisibleDestination(parent: parent, name: "conflict.txt") { throw CancellationError() }
+ }
+ }
+
+ @Test func providerParentIdentityHandlesURLAliasesAndRejectsStaleParents() async throws {
+ let candidate = URL(filePath: "/synthetic/mounted-alias/Sibling/conflict.txt")
+ var checked: URL?
+ let ready = try await FinderRestoreObservation.observeVisibleDestination(name: "conflict.txt", readVisible: { candidate }) { parent in
+ checked = parent
+ return FinderStabilityTargetBinding.matches(expectedFileID: 42, expectedDomainIdentifier: "synthetic-domain",
+ actualItemIdentifier: "42", actualDomainIdentifier: "synthetic-domain")
+ }
+ #expect(ready == candidate && checked == candidate.deletingLastPathComponent())
+ for (identifier, domain) in [("41", "synthetic-domain"), ("42", "different-domain")] {
+ let pending = try await FinderRestoreObservation.observeVisibleDestination(name: "conflict.txt", readVisible: { candidate }) { _ in
+ FinderStabilityTargetBinding.matches(expectedFileID: 42, expectedDomainIdentifier: "synthetic-domain",
+ actualItemIdentifier: identifier, actualDomainIdentifier: domain)
+ }
+ #expect(pending == nil)
+ }
+ let wrongName = try await FinderRestoreObservation.observeVisibleDestination(name: "different.txt", readVisible: { candidate }) { _ in true }
+ #expect(wrongName == nil)
+ }
+
+ @Test(arguments: [401, 403, 404, 429, 500])
+ func visibleDestinationDoesNotSuppressRemoteLookupFailures(_ status: Int) async {
+ let candidate = URL(filePath: "/synthetic/run/Sibling/conflict.txt")
+ await #expect(throws: APIClientError.self) {
+ try await FinderRestoreObservation.observeVisibleDestination(name: "conflict.txt", readVisible: {
+ throw APIClientError.unacceptableStatusCode(status, body: "synthetic")
+ }, matchesParent: { _ in true })
+ }
+ await #expect(throws: APIClientError.self) {
+ try await FinderRestoreObservation.observeVisibleDestination(name: "conflict.txt", readVisible: { candidate }) { _ in
+ throw APIClientError.unacceptableStatusCode(status, body: "synthetic")
+ }
+ }
+ }
+
+ private func item(id: Int = 42, drive: Int = 7, parent: Int = 3) -> KDriveRemoteItem {
+ KDriveRemoteItem(id: id, name: "Synthetic.txt", type: "file", status: "ok", driveID: drive,
+ parentID: parent, path: nil, size: 4, mimeType: "text/plain", createdAt: nil,
+ modifiedAt: Date(timeIntervalSince1970: 10), updatedAt: Date(timeIntervalSince1970: 10))
+ }
+
+ @Test func missingCallbackCannotReleaseVerificationEvenWhenTheFileExists() async throws {
+ var read = false
+ let result = try await FinderRestoreObservation.observe(callbackCompleted: false, expected: item()) {
+ read = true; return item()
+ }
+ #expect(result == nil)
+ #expect(!read)
+ }
+
+ @Test func active404RemainsPendingUntilTheRestoredIdentityAppears() async throws {
+ let expected = item()
+ let pending = try await FinderRestoreObservation.observe(callbackCompleted: true, expected: expected) {
+ throw APIClientError.unacceptableStatusCode(404, body: "synthetic")
+ }
+ #expect(pending == nil)
+ let ready = try await FinderRestoreObservation.observe(callbackCompleted: true, expected: expected) { expected }
+ #expect(ready == expected)
+ }
+
+ @Test(arguments: [401, 403, 429, 500])
+ func operationalErrorsRemainFailures(_ status: Int) async {
+ await #expect(throws: APIClientError.self) {
+ try await FinderRestoreObservation.observe(callbackCompleted: true, expected: item()) {
+ throw APIClientError.unacceptableStatusCode(status, body: "synthetic")
+ }
+ }
+ }
+
+ @Test func differentIdentityDriveOrDestinationCannotPass() async {
+ for wrong in [item(id: 43), item(drive: 8), item(parent: 4)] {
+ await #expect(throws: FinderLiveError.unsafeTarget) {
+ try await FinderRestoreObservation.observe(callbackCompleted: true, expected: item()) { wrong }
+ }
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderStabilityCommandTests.swift b/potassiumProviderTests/FinderStabilityCommandTests.swift
new file mode 100644
index 0000000..1bf6827
--- /dev/null
+++ b/potassiumProviderTests/FinderStabilityCommandTests.swift
@@ -0,0 +1,651 @@
+#if os(macOS) && STABILITY
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+@Suite("Finder Stability command")
+struct FinderStabilityCommandTests {
+ @Test func permanentDeletionIsExplicitAndRunScoped() async throws {
+ let prefix = ["app", "--finder-stability"]
+ #expect(try FinderStabilityArgumentParser.parse(arguments: prefix + ["run", "--yes-live"]) ==
+ .execute(.init(mode: .run, requestPermissions: false, includePermanentDeletion: false)))
+ #expect(try FinderStabilityArgumentParser.parse(arguments: prefix + ["run", "--yes-live", "--include-permanent-deletion"]) ==
+ .execute(.init(mode: .run, requestPermissions: false, includePermanentDeletion: true)))
+ for args in [["preflight"], ["conflicts", "--yes-live"], ["provision", "--yes-live"], ["watch"],
+ ["recover", "--yes-recover"], ["run", "--yes-live", "--include-permanent-deletion"]] {
+ #expect(throws: FinderStabilityArgumentError.unknownOption) {
+ try FinderStabilityArgumentParser.parse(arguments: prefix + args + ["--include-permanent-deletion"])
+ }
+ }
+ let executor = FinderStabilityCommandExecutorFake(preflightResult: .ready)
+ for include in [false, true] {
+ let code = await FinderStabilityCommandLine.run(arguments: prefix + ["run", "--yes-live"] +
+ (include ? ["--include-permanent-deletion"] : []), executor: executor)
+ #expect(code == (include ? 0 : 4))
+ }
+ #expect(executor.deletionSelections == [false, true])
+ #expect(FinderStabilityCommandResult.completedWithDeferredDeletion.exitCode == 4)
+ }
+
+ @Test func deferralContinuesRemainingScenariosWithoutInvokingDeletion() {
+ let selection = FinderStabilityScenarioSelection()
+ var invoked: [StabilityFinderScenario] = []
+ var skipped: [StabilityFinderScenario] = []
+ for scenario in StabilityFinderScenario.allCases {
+ if let reason = selection.skipReason(for: scenario, afterFailure: false) {
+ #expect(reason == .permanentDeletionNotSelected)
+ skipped.append(scenario)
+ } else { invoked.append(scenario) }
+ }
+ #expect(skipped == [.permanentDeletion])
+ #expect(invoked.count == 15)
+ #expect(Array(invoked.suffix(4)) == [.concurrentRemotePreserveBoth, .cancellationAndProgress,
+ .workingSetRefresh, .supportedContextualActions])
+ let full = FinderStabilityScenarioSelection(includePermanentDeletion: true)
+ #expect(StabilityFinderScenario.allCases.allSatisfy { full.skipReason(for: $0, afterFailure: false) == nil })
+ #expect(selection.skipReason(for: .permanentDeletion, afterFailure: true) == .earlierStepFailure)
+ #expect(selection.skipReason(for: .concurrentRemotePreserveBoth, afterFailure: true) == nil)
+ let conflict = FinderStabilityScenarioSelection(conflictCase: .contentAfterPreflight)
+ #expect(conflict.skipReason(for: .permanentDeletion, afterFailure: false) == .notSelectedForConflictProfile)
+ #expect(conflict.skipReason(for: .concurrentRemotePreserveBoth, afterFailure: false) == nil)
+ }
+
+ @Test func independentScenariosContinueAfterFailureWithoutBroadeningConflictSelection() {
+ let selection = FinderStabilityScenarioSelection()
+ #expect(StabilityFinderScenario.allCases.filter { selection.skipReason(for: $0, afterFailure: true) == nil } ==
+ [.concurrentRemotePreserveBoth, .cancellationAndProgress, .workingSetRefresh, .supportedContextualActions])
+ let conflict = FinderStabilityScenarioSelection(conflictCase: .contentAfterPreflight)
+ #expect(conflict.skipReason(for: .workingSetRefresh, afterFailure: true) == .notSelectedForConflictProfile)
+ #expect(conflict.skipReason(for: .supportedContextualActions, afterFailure: true) == .notSelectedForConflictProfile)
+ }
+
+ @Test func conflictProfileRequiresLiveOptInAndExactCaseSelection() throws {
+ #expect(throws: FinderStabilityArgumentError.liveConfirmationRequired) {
+ try FinderStabilityArgumentParser.parse(arguments: ["app", "--finder-stability", "conflicts"])
+ }
+ let parsed = try FinderStabilityArgumentParser.parse(arguments: ["app", "--finder-stability", "conflicts", "--yes-live", "--case", "edit-move"])
+ #expect(parsed == .execute(FinderStabilityCommandOptions(mode: .conflicts, requestPermissions: false, conflictCase: .editMove)))
+ for arguments in [
+ ["run", "--yes-live", "--case", "edit-move"],
+ ["conflicts", "--yes-live", "--case", "unknown"],
+ ["conflicts", "--yes-live", "--case"],
+ ["conflicts", "--yes-live", "--case", "edit-move", "--case", "edit-move"]
+ ] {
+ #expect(throws: FinderStabilityArgumentError.unknownOption) {
+ try FinderStabilityArgumentParser.parse(arguments: ["app", "--finder-stability"] + arguments)
+ }
+ }
+ }
+
+ @Test func launchStateSelectionIsClosedAndRunScoped() throws {
+ for mode in [StabilityExtensionLaunchMode.fresh, .running] {
+ let parsed = try FinderStabilityArgumentParser.parse(arguments:
+ ["app", "--finder-stability", "conflicts", "--yes-live", "--extension-state", mode.rawValue])
+ #expect(parsed == .execute(.init(mode: .conflicts, requestPermissions: false, extensionLaunchMode: mode)))
+ }
+ let fullRun = try FinderStabilityArgumentParser.parse(arguments: ["app", "--finder-stability", "run", "--yes-live", "--extension-state", "fresh"])
+ #expect(fullRun == .execute(.init(mode: .run, requestPermissions: false, extensionLaunchMode: .fresh)))
+ for arguments in [
+ ["preflight", "--extension-state", "fresh"],
+ ["conflicts", "--yes-live", "--extension-state", "unknown"],
+ ["conflicts", "--yes-live", "--extension-state"],
+ ["conflicts", "--yes-live", "--extension-state", "fresh", "--extension-state", "running"]
+ ] {
+ #expect(throws: FinderStabilityArgumentError.unknownOption) {
+ try FinderStabilityArgumentParser.parse(arguments: ["app", "--finder-stability"] + arguments)
+ }
+ }
+ }
+
+ @Test func recorderStartsBeforePreflightCanEmitItsFirstCallback() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: directory)
+ let callback = ProviderDiagnosticEvent(spanID: UUID(), processInstanceID: UUID(),
+ processCodeHash: String(repeating: "a", count: 40), correlationID: UUID(),
+ source: .fileProviderExtension, operation: .runtimeInitialize, phase: .started)
+ let accounts = FinderPreflightAccountStore(accounts: [], beforeLoad: {
+ let run = try #require(await coordinator.activeRun())
+ let store = try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+ try await store.recordDiagnostic(callback)
+ throw FinderPreflightProbeError.stopAfterFirstCallback
+ })
+ let remote = FinderPreflightRemote(root: finderPreflightItem(id: 42, parentID: 1, type: "dir"),
+ marker: finderPreflightItem(id: 43, parentID: 42, type: "file"), markerData: Data())
+ let loader = FinderStabilityContextLoader(accountStore: accounts,
+ domainStore: FinderPreflightDomainStore(domains: []), tokenStore: InMemoryOAuthTokenStore(),
+ registrar: FinderPreflightRegistrar(domainIdentifier: "synthetic", rootURL: directory),
+ remoteFactory: { _, _ in remote },
+ visibleDomainResolver: { _ in throw FinderPreflightProbeError.stopAfterFirstCallback })
+ let executor = SystemFinderStabilityCommandExecutor(contextLoader: loader,
+ permissionChecker: FinderPreflightPermissionProbe(), scenarioRunner: FinderPreflightScenarioProbe(),
+ runCoordinatorProvider: { coordinator }, statusWriter: { _, _ in }, registrationChecker: {})
+ #expect(await executor.run(requestPermissions: false) == .rejected)
+ let run = try #require(await coordinator.activeRun())
+ #expect(try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL).map(\.id) == [callback.id])
+ #expect(!FileManager.default.fileExists(atPath: run.finderReportURL.path))
+ #expect(!FileManager.default.fileExists(atPath: run.directoryURL.appendingPathComponent("summary.json").path))
+ }
+
+ @Test func parserRequiresExplicitLiveConfirmationOnlyForRun() throws {
+ #expect(try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "preflight",
+ ]) == .execute(FinderStabilityCommandOptions(
+ mode: .preflight,
+ requestPermissions: false
+ )))
+ #expect(try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "run", "--yes-live", "--request-permissions",
+ ]) == .execute(FinderStabilityCommandOptions(
+ mode: .run,
+ requestPermissions: true
+ )))
+ #expect(throws: FinderStabilityArgumentError.liveConfirmationRequired) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "run",
+ ])
+ }
+ #expect(throws: FinderStabilityArgumentError.liveConfirmationNotAllowedForPreflight) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "preflight", "--yes-live",
+ ])
+ }
+ #expect(try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "recover", "--yes-recover",
+ ]) == .execute(FinderStabilityCommandOptions(
+ mode: .recover,
+ requestPermissions: false
+ )))
+ #expect(throws: FinderStabilityArgumentError.recoveryConfirmationRequired) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "recover",
+ ])
+ }
+ #expect(throws: FinderStabilityArgumentError.recoveryConfirmationNotAllowed) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "run", "--yes-live", "--yes-recover",
+ ])
+ }
+ #expect(throws: FinderStabilityArgumentError.permissionRequestNotAllowedForRecovery) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "recover", "--yes-recover", "--request-permissions",
+ ])
+ }
+ }
+
+ @Test func parserRejectsUnknownDuplicateAndCredentialShapedArguments() {
+ #expect(throws: FinderStabilityArgumentError.duplicateMode) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "preflight", "run",
+ ])
+ }
+ for argument in ["--access-token", "--credential-file", "--account-id", "--root-path"] {
+ #expect(throws: FinderStabilityArgumentError.unknownOption) {
+ try FinderStabilityArgumentParser.parse(arguments: [
+ "app", "--finder-stability", "preflight", argument,
+ ])
+ }
+ }
+ }
+
+ @Test func commandMapsConsentCheckpointWithoutReportingFailure() async {
+ let executor = FinderStabilityCommandExecutorFake(preflightResult: .checkpoint)
+ let code = await FinderStabilityCommandLine.run(
+ arguments: ["app", "--finder-stability", "preflight"],
+ executor: executor
+ )
+
+ #expect(code == 3)
+ #expect(executor.preflightRequests == [false])
+ #expect(executor.runRequests.isEmpty)
+ }
+
+ @Test func recoveryCommandUsesOnlyTheLocalRecoveryPath() async {
+ let executor = FinderStabilityCommandExecutorFake(preflightResult: .ready)
+ let code = await FinderStabilityCommandLine.run(
+ arguments: ["app", "--finder-stability", "recover", "--yes-recover"],
+ executor: executor
+ )
+
+ #expect(code == 0)
+ #expect(executor.recoveryRequestCount == 1)
+ #expect(executor.preflightRequests.isEmpty)
+ #expect(executor.runRequests.isEmpty)
+ }
+
+ @Test func consoleDescriptionsContainOnlyClosedStatusText() {
+ #expect(FinderStabilityCommandResult.ready.safeConsoleDescription == "finder stability preflight: ready")
+ #expect(FinderStabilityCommandResult.checkpoint.exitCode == 3)
+ #expect(FinderStabilityCommandResult.failed.exitCode == 1)
+ #expect(FinderStabilityCommandResult.recovered.exitCode == 0)
+ #expect(FinderStabilityCommandResult.recovered.safeConsoleDescription
+ == "finder stability recovery: stale run preserved and released")
+ }
+
+ @Test func permissionAndConsentStatesBecomeCheckpointsRatherThanFailures() {
+ let recordedAt = Date(timeIntervalSince1970: 1_800_000_000)
+ let results = FinderStabilityPreflightEvaluator.results(
+ permissions: FinderStabilityPermissionSnapshot(
+ accessibilityOutcome: .checkpoint(.accessibilityConsentRequired),
+ automationOutcome: .checkpoint(.finderAutomationConsentRequired)
+ ),
+ hasFileProviderRegistration: true,
+ hasVerifiedFileProviderConsent: false,
+ labSafetyAllowed: true,
+ recordedAt: recordedAt
+ )
+
+ #expect(FinderStabilityPreflightEvaluator.commandResult(for: results) == .checkpoint)
+ #expect(results.map(\.recordedAt).allSatisfy { $0 == recordedAt })
+ #expect(results.first { $0.check == .fileProviderConsent }?.outcome
+ == .checkpoint(.fileProviderConsentRequired))
+ }
+
+ @Test func registrationAndLabSafetyFailuresRejectTheCommand() {
+ let results = FinderStabilityPreflightEvaluator.results(
+ permissions: FinderStabilityPermissionSnapshot(
+ accessibilityOutcome: .passed,
+ automationOutcome: .passed
+ ),
+ hasFileProviderRegistration: false,
+ hasVerifiedFileProviderConsent: true,
+ labSafetyAllowed: false,
+ recordedAt: Date(timeIntervalSince1970: 1_800_000_000)
+ )
+
+ #expect(FinderStabilityPreflightEvaluator.commandResult(for: results) == .rejected)
+ #expect(results.first { $0.check == .fileProviderRegistration }?.outcome
+ == .failed(.fileProviderNotRegistered))
+ #expect(results.first { $0.check == .stabilityLabSafety }?.outcome
+ == .failed(.stabilityLabRejected))
+ }
+
+ @Test func commandPreflightLoaderUsesInjectedDomainKeychainAndRemoteEvidence() async throws {
+ let marker = StabilityLabOwnershipMarker(
+ identifier: UUID(),
+ driveID: 7,
+ rootFileID: 42,
+ createdAt: Date(timeIntervalSince1970: 1_800_000_000)
+ )
+ let domain = ProviderDomainConfiguration(
+ domainIdentifier: "finder-preflight-domain",
+ accountIdentifier: "finder-preflight-account",
+ displayName: "Synthetic Lab",
+ driveID: 7,
+ driveName: "Synthetic Drive",
+ rootFileID: 42,
+ purpose: .stabilityLab,
+ stabilityLab: ProviderStabilityLabConfiguration(
+ driveRootFileID: ProviderConstants.defaultRootFileID,
+ markerFileID: 43,
+ ownershipMarker: marker
+ )
+ )
+ let account = ProviderAccount(
+ accountIdentifier: domain.accountIdentifier,
+ displayName: "Synthetic Account",
+ authenticationKind: .manualAccessToken
+ )
+ let tokenStore = InMemoryOAuthTokenStore()
+ let privateCanary = UUID().uuidString
+ await tokenStore.saveToken(
+ KDriveOAuthToken(
+ accessToken: privateCanary,
+ tokenType: "Synthetic",
+ refreshToken: nil,
+ scope: nil,
+ idToken: nil,
+ expiresAt: nil
+ ),
+ accountIdentifier: account.accountIdentifier
+ )
+ let remote = FinderPreflightRemote(
+ root: finderPreflightItem(id: 42, parentID: 1, type: "dir"),
+ marker: finderPreflightItem(id: 43, parentID: 42, type: "file"),
+ markerData: try JSONEncoder().encode(marker)
+ )
+ let registrar = FinderPreflightRegistrar(
+ domainIdentifier: domain.domainIdentifier,
+ rootURL: URL(fileURLWithPath: "/tmp/synthetic-finder-preflight-root")
+ )
+ let factory = FinderPreflightRemoteFactory(remote: remote)
+ let domainStore = FinderPreflightDomainStore(domains: [domain])
+ let visibleBindingResolver = FinderVisibleBindingResolverFake(binding:
+ FinderStabilityVisibleItemBinding(
+ itemIdentifier: NSFileProviderItemIdentifier.rootContainer.rawValue,
+ domainIdentifier: domain.domainIdentifier
+ )
+ )
+ let loader = FinderStabilityContextLoader(
+ accountStore: FinderPreflightAccountStore(accounts: [account]),
+ domainStore: domainStore,
+ tokenStore: tokenStore,
+ registrar: registrar,
+ remoteFactory: { _, _ in factory.makeRemote() },
+ visibleDomainResolver: { url in
+ visibleBindingResolver.resolve(url)
+ }
+ )
+
+ let context = try await loader.loadPreflight()
+
+ #expect(context.hasVerifiedFileProviderConsent)
+ #expect(factory.makeCallCount == 1)
+ #expect(await remote.readCallCount == 4)
+
+ await domainStore.save(ProviderDomainConfiguration(
+ domainIdentifier: "ordinary-race-domain",
+ displayName: "Synthetic Ordinary",
+ driveID: 8,
+ driveName: "Synthetic Drive"
+ ))
+ await #expect(throws: FinderStabilityContextError.fileProviderNotRegistered) {
+ try await context.verifySafety()
+ }
+ #expect(await remote.readCallCount == 4)
+
+ await domainStore.remove(domainIdentifier: "ordinary-race-domain")
+ visibleBindingResolver.binding = FinderStabilityVisibleItemBinding(
+ itemIdentifier: "rebound-root",
+ domainIdentifier: domain.domainIdentifier
+ )
+ await #expect(throws: FinderStabilityContextError.fileProviderNotRegistered) {
+ try await context.verifySafety()
+ }
+ #expect(await remote.readCallCount == 8)
+ }
+
+ @Test func targetBindingRejectsItemOrDomainDrift() {
+ #expect(FinderStabilityTargetBinding.matches(
+ expectedFileID: 42,
+ expectedDomainIdentifier: "expected-domain",
+ actualItemIdentifier: "42",
+ actualDomainIdentifier: "expected-domain"
+ ))
+ #expect(FinderStabilityTargetBinding.matches(
+ expectedFileID: 42,
+ expectedDomainIdentifier: "expected-domain",
+ actualItemIdentifier: "43",
+ actualDomainIdentifier: "expected-domain"
+ ) == false)
+ #expect(FinderStabilityTargetBinding.matches(
+ expectedFileID: 42,
+ expectedDomainIdentifier: "expected-domain",
+ actualItemIdentifier: "42",
+ actualDomainIdentifier: "other-domain"
+ ) == false)
+ }
+
+ @Test func targetBindingReturnsTheSingleValidatedResolution() async throws {
+ var resolutionCount = 0
+ let resolvedIdentifier = try await FinderStabilityTargetBinding.resolve(
+ expectedFileID: 42,
+ expectedDomainIdentifier: "expected-domain",
+ using: {
+ resolutionCount += 1
+ if resolutionCount == 1 {
+ return ("42", "expected-domain")
+ }
+ return ("43", "other-domain")
+ }
+ )
+
+ #expect(resolvedIdentifier == "42")
+ #expect(resolutionCount == 1)
+ }
+
+ @Test func scenarioGateRevalidatesAfterBaselineImmediatelyBeforeExecution() async throws {
+ var order: [String] = []
+ let values = try await FinderStabilityScenarioGate.run(
+ baseline: {
+ order.append("baseline")
+ return 1
+ },
+ verifySafety: {
+ order.append("safety")
+ },
+ execute: {
+ order.append("execute")
+ return 2
+ }
+ )
+
+ #expect(order == ["baseline", "safety", "execute"])
+ #expect(values.0 == 1)
+ #expect(values.1 == 2)
+ }
+
+ @Test func stabilityMacBuildAloneCarriesFinderAutomationEntitlement() throws {
+ let repositoryRoot = URL(fileURLWithPath: #filePath)
+ .deletingLastPathComponent()
+ .deletingLastPathComponent()
+ let projectText = try String(contentsOf:
+ repositoryRoot
+ .appendingPathComponent("potassiumProvider.xcodeproj")
+ .appendingPathComponent("project.pbxproj"),
+ encoding: .utf8
+ )
+ #expect(projectText.contains(
+ "\"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]\" = "
+ + "Config/potassiumProviderStability.entitlements;"
+ ))
+
+ let stabilityData = try Data(contentsOf: repositoryRoot
+ .appendingPathComponent("Config/potassiumProviderStability.entitlements"))
+ let ordinaryData = try Data(contentsOf: repositoryRoot
+ .appendingPathComponent("Config/potassiumProvider.entitlements"))
+ let stability = try #require(PropertyListSerialization.propertyList(
+ from: stabilityData,
+ format: nil
+ ) as? [String: Any])
+ let ordinary = try #require(PropertyListSerialization.propertyList(
+ from: ordinaryData,
+ format: nil
+ ) as? [String: Any])
+
+ #expect(stability["com.apple.security.automation.apple-events"] as? Bool == true)
+ #expect(stability["com.apple.security.temporary-exception.apple-events"] == nil)
+ #expect(projectText.contains("\"ENABLE_APP_SANDBOX[sdk=macosx*]\" = NO;"))
+ #expect(ordinary["com.apple.security.automation.apple-events"] == nil)
+ #expect(ordinary["com.apple.security.temporary-exception.apple-events"] == nil)
+ }
+}
+
+@MainActor
+private final class FinderVisibleBindingResolverFake {
+ var binding: FinderStabilityVisibleItemBinding
+
+ init(binding: FinderStabilityVisibleItemBinding) {
+ self.binding = binding
+ }
+
+ func resolve(_ url: URL) -> FinderStabilityVisibleItemBinding {
+ binding
+ }
+}
+
+@MainActor
+private final class FinderStabilityCommandExecutorFake: FinderStabilityCommandExecuting {
+ let preflightResult: FinderStabilityCommandResult
+ var preflightRequests: [Bool] = []
+ var runRequests: [Bool] = []
+ var deletionSelections: [Bool] = []
+ var recoveryRequestCount = 0
+
+ init(preflightResult: FinderStabilityCommandResult) {
+ self.preflightResult = preflightResult
+ }
+
+ func preflight(requestPermissions: Bool) async -> FinderStabilityCommandResult {
+ preflightRequests.append(requestPermissions)
+ return preflightResult
+ }
+
+ func run(requestPermissions: Bool) async -> FinderStabilityCommandResult {
+ runRequests.append(requestPermissions)
+ return .completed
+ }
+
+ func run(requestPermissions: Bool, extensionLaunchMode: StabilityExtensionLaunchMode?, includePermanentDeletion: Bool) async -> FinderStabilityCommandResult {
+ runRequests.append(requestPermissions)
+ deletionSelections.append(includePermanentDeletion)
+ return includePermanentDeletion ? .completed : .completedWithDeferredDeletion
+ }
+
+ func recoverStaleRun() async -> FinderStabilityCommandResult {
+ recoveryRequestCount += 1
+ return .recovered
+ }
+}
+
+private actor FinderPreflightAccountStore: ProviderAccountStoring {
+ var accounts: [ProviderAccount]
+ let beforeLoad: (@Sendable () async throws -> Void)?
+
+ init(accounts: [ProviderAccount], beforeLoad: (@Sendable () async throws -> Void)? = nil) {
+ self.accounts = accounts; self.beforeLoad = beforeLoad
+ }
+
+ func allAccounts() async throws -> [ProviderAccount] { try await beforeLoad?(); return accounts }
+ func account(accountIdentifier: String) -> ProviderAccount? {
+ accounts.first { $0.accountIdentifier == accountIdentifier }
+ }
+ func save(_ account: ProviderAccount) { accounts.append(account) }
+ func remove(accountIdentifier: String) {
+ accounts.removeAll { $0.accountIdentifier == accountIdentifier }
+ }
+}
+
+private enum FinderPreflightProbeError: Error { case stopAfterFirstCallback }
+
+private struct FinderPreflightPermissionProbe: FinderStabilityPermissionChecking {
+ nonisolated func check(requestPermissions: Bool) -> FinderStabilityPermissionSnapshot {
+ FinderStabilityPermissionSnapshot(accessibilityOutcome: .passed, automationOutcome: .passed)
+ }
+}
+
+@MainActor
+private struct FinderPreflightScenarioProbe: FinderStabilityScenarioRunning {
+ func run(context: FinderStabilityLiveContext) async -> FinderStabilityScenarioExecution {
+ Issue.record("A failed preflight must not start Finder scenarios")
+ return .skippingAll(reason: .preflightFailure, startedAt: Date())
+ }
+}
+
+private actor FinderPreflightDomainStore: DomainConfigurationStoring {
+ var domains: [ProviderDomainConfiguration]
+
+ init(domains: [ProviderDomainConfiguration]) { self.domains = domains }
+
+ func allConfigurations() -> [ProviderDomainConfiguration] { domains }
+ func configuration(domainIdentifier: String) -> ProviderDomainConfiguration? {
+ domains.first { $0.domainIdentifier == domainIdentifier }
+ }
+ func save(_ configuration: ProviderDomainConfiguration) { domains.append(configuration) }
+ func remove(domainIdentifier: String) {
+ domains.removeAll { $0.domainIdentifier == domainIdentifier }
+ }
+}
+
+@MainActor
+private struct FinderPreflightRegistrar: ProviderDomainRegistering {
+ let domainIdentifier: String
+ let rootURL: URL
+
+ func addDomain(for configuration: ProviderDomainConfiguration) async throws {}
+ func removeDomain(for configuration: ProviderDomainConfiguration) async throws {}
+ func registeredDomainIdentifiers() async throws -> Set { [domainIdentifier] }
+ func userVisibleRootURL(for configuration: ProviderDomainConfiguration) async throws -> URL {
+ rootURL
+ }
+}
+
+@MainActor
+private final class FinderPreflightRemoteFactory {
+ let remote: FinderPreflightRemote
+ private(set) var makeCallCount = 0
+
+ init(remote: FinderPreflightRemote) { self.remote = remote }
+
+ func makeRemote() -> any KDriveFileProviding {
+ makeCallCount += 1
+ return remote
+ }
+}
+
+private actor FinderPreflightRemote: KDriveFileProviding {
+ let root: KDriveRemoteItem
+ let marker: KDriveRemoteItem
+ let markerData: Data
+ private(set) var readCallCount = 0
+
+ init(root: KDriveRemoteItem, marker: KDriveRemoteItem, markerData: Data) {
+ self.root = root
+ self.marker = marker
+ self.markerData = markerData
+ }
+
+ func listDrives() -> [KDriveDriveSummary] {
+ readCallCount += 1
+ return [KDriveDriveSummary(
+ id: 7,
+ name: "Synthetic Drive",
+ accountID: 0,
+ role: "admin",
+ status: "active",
+ isInMaintenance: false
+ )]
+ }
+
+ func item(driveID: Int, fileID: Int) throws -> KDriveRemoteItem {
+ readCallCount += 1
+ if fileID == root.id { return root }
+ if fileID == marker.id { return marker }
+ throw FinderPreflightRemoteError.unexpectedCall
+ }
+
+ func downloadFile(driveID: Int, fileID: Int) throws -> Data {
+ readCallCount += 1
+ guard fileID == marker.id else { throw FinderPreflightRemoteError.unexpectedCall }
+ return markerData
+ }
+
+ func listDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) throws -> KDriveItemPage { throw FinderPreflightRemoteError.unexpectedCall }
+ func listAdvancedDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) throws -> KDriveAdvancedItemPage { throw FinderPreflightRemoteError.unexpectedCall }
+ func listTrash(driveID: Int, cursor: String?, limit: Int) throws -> KDriveItemPage { throw FinderPreflightRemoteError.unexpectedCall }
+ func thumbnail(driveID: Int, fileID: Int, width: Int?, height: Int?) throws -> Data { throw FinderPreflightRemoteError.unexpectedCall }
+ func uploadFile(driveID: Int, parentID: Int, fileName: String, contents: Data, lastModifiedAt: Date?, conflictStrategy: KDriveUploadConflictStrategy, clientToken: String?, contentHash: String?) throws -> KDriveRemoteItem { throw FinderPreflightRemoteError.unexpectedCall }
+ func replaceFile(driveID: Int, fileID: Int, expectedETag: String, clientToken: String, contentHash: String, contents: Data, lastModifiedAt: Date?) throws -> KDriveRemoteItem { throw FinderPreflightRemoteError.unexpectedCall }
+ func createDirectory(driveID: Int, parentID: Int, name: String) throws -> KDriveRemoteItem { throw FinderPreflightRemoteError.unexpectedCall }
+ func renameItem(driveID: Int, fileID: Int, name: String) throws { throw FinderPreflightRemoteError.unexpectedCall }
+ func moveItem(driveID: Int, fileID: Int, destinationParentID: Int, name: String?) throws { throw FinderPreflightRemoteError.unexpectedCall }
+ func updateModificationDate(driveID: Int, fileID: Int, date: Date) throws { throw FinderPreflightRemoteError.unexpectedCall }
+ func trashItem(driveID: Int, fileID: Int) throws { throw FinderPreflightRemoteError.unexpectedCall }
+ func deleteTrashedItem(driveID: Int, fileID: Int) throws { throw FinderPreflightRemoteError.unexpectedCall }
+}
+
+private enum FinderPreflightRemoteError: Error { case unexpectedCall }
+
+private func finderPreflightItem(id: Int, parentID: Int, type: String) -> KDriveRemoteItem {
+ let date = Date(timeIntervalSince1970: 1_800_000_000)
+ return KDriveRemoteItem(
+ id: id,
+ name: "Synthetic Item",
+ type: type,
+ status: "active",
+ driveID: 7,
+ parentID: parentID,
+ path: nil,
+ size: type == "file" ? 10 : nil,
+ mimeType: type == "file" ? "application/json" : nil,
+ createdAt: date,
+ modifiedAt: date,
+ revisedAt: date,
+ updatedAt: date,
+ etag: "synthetic-etag"
+ )
+}
+#endif
diff --git a/potassiumProviderTests/FinderTextEditSequenceTests.swift b/potassiumProviderTests/FinderTextEditSequenceTests.swift
new file mode 100644
index 0000000..c294ae0
--- /dev/null
+++ b/potassiumProviderTests/FinderTextEditSequenceTests.swift
@@ -0,0 +1,49 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+@Suite("Native TextEdit command sequencing")
+struct FinderTextEditSequenceTests {
+ @Test func lostPasteCannotReachSave() async {
+ var commands: [FinderTextEditAction] = []
+ var checkedSave = false
+ await #expect(throws: FinderUIError.selectionMismatch) {
+ try await FinderTextEditSequence.execute(menu: { commands.append($0) }, verifyText: {
+ throw FinderUIError.selectionMismatch
+ }, verifySave: { checkedSave = true })
+ }
+ #expect(commands == [.selectAll, .paste])
+ #expect(!checkedSave)
+ }
+
+ @Test func rejectedSelectionCannotPasteIntoAnotherDocument() async {
+ var commands: [FinderTextEditAction] = []
+ await #expect(throws: FinderUIError.windowMismatch) {
+ try await FinderTextEditSequence.execute(menu: {
+ commands.append($0)
+ throw FinderUIError.windowMismatch
+ }, verifyText: { Issue.record("Text verification must not follow rejected selection") },
+ verifySave: { Issue.record("Save must not follow rejected selection") })
+ }
+ #expect(commands == [.selectAll])
+ }
+
+ @Test(arguments: [false, true])
+ func completionRequiresSaveAcknowledgement(saveFails: Bool) async throws {
+ var commands: [FinderTextEditAction] = []
+ var textVerified = false, completed = false
+ do {
+ try await FinderTextEditSequence.execute(menu: {
+ if $0 == .save { #expect(textVerified) }
+ commands.append($0)
+ }, verifyText: { textVerified = true }, verifySave: {
+ if saveFails { throw FinderUIError.timedOut }
+ })
+ completed = true
+ } catch FinderUIError.timedOut { #expect(saveFails) }
+ #expect(completed == !saveFails)
+ #expect(commands == [.selectAll, .paste, .save])
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderToolbarActionTargetTests.swift b/potassiumProviderTests/FinderToolbarActionTargetTests.swift
new file mode 100644
index 0000000..3da003d
--- /dev/null
+++ b/potassiumProviderTests/FinderToolbarActionTargetTests.swift
@@ -0,0 +1,39 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+import Testing
+@testable import potassiumProvider
+
+struct FinderToolbarActionTargetTests {
+ let window = CGRect(x: 0, y: 0, width: 500, height: 400)
+ let toolbar = CGRect(x: 0, y: 0, width: 500, height: 60)
+ let button = CGRect(x: 300, y: 10, width: 30, height: 30)
+
+ @Test(arguments: ["Restore from kDrive Trash", "Add to kDrive Favorites", "Remove from kDrive Favorites",
+ "Duplicate on kDrive", "Share kDrive Link…", "Version History…", "Delete Immediately…"])
+ func providerAndSelectedDeletionCommandsKeepTheirItemContextMenu(_ command: String) {
+ #expect(!FinderToolbarActionTarget.supports(command: command))
+ }
+
+ @Test(arguments: ["Remove Download", "Download Now"])
+ func verifiedBuiltInDownloadsUseTheToolbar(_ command: String) {
+ #expect(FinderToolbarActionTarget.supports(command: command))
+ }
+
+ @Test func choosesTheExactEnabledActionsControlWithinTheBoundToolbar() {
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar, buttons: [
+ .init(description: "Group", enabled: true, bounds: button),
+ .init(description: "Action", enabled: true, bounds: button)]) == 1)
+ }
+
+ @Test func missingAmbiguousDisabledOrUnconfinedControlsFailClosed() {
+ let valid = FinderToolbarActionTarget.Button(description: "Action", enabled: true, bounds: button)
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar, buttons: []) == nil)
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar, buttons: [valid, valid]) == nil)
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar, buttons: [
+ .init(description: "Action", enabled: false, bounds: button)]) == nil)
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar, buttons: [
+ .init(description: "Action", enabled: true, bounds: CGRect(x: 300, y: 80, width: 30, height: 30))]) == nil)
+ #expect(FinderToolbarActionTarget.index(window: window, toolbar: toolbar.offsetBy(dx: 500, dy: 0), buttons: [valid]) == nil)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderTransferCancellationTargetTests.swift b/potassiumProviderTests/FinderTransferCancellationTargetTests.swift
new file mode 100644
index 0000000..6cf1d06
--- /dev/null
+++ b/potassiumProviderTests/FinderTransferCancellationTargetTests.swift
@@ -0,0 +1,35 @@
+#if os(macOS) && STABILITY
+import CoreGraphics
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+struct FinderTransferCancellationTargetTests {
+ let window = CGRect(x: 100, y: 100, width: 600, height: 400)
+ let row = CGRect(x: 300, y: 180, width: 350, height: 24)
+ let ring = CGRect(x: 500, y: 182, width: 20, height: 20)
+
+ @Test func onlyTheActiveIndicatorInTheBoundRowCanBeClicked() {
+ #expect(FinderTransferCancellationTarget.point(window: window, row: row,
+ indicators: [.init(fraction: 0.4, bounds: ring)]) == CGPoint(x: 510, y: 192))
+ #expect(FinderTransferCancellationTarget.point(window: window, row: row, indicators: []) == nil)
+ #expect(FinderTransferCancellationTarget.point(window: window, row: row,
+ indicators: [.init(fraction: 0.4, bounds: ring), .init(fraction: 0.5, bounds: ring)]) == nil)
+ }
+
+ @Test(arguments: [nil, 0, 1, -1, 2, .nan, .infinity] as [Double?])
+ func missingOrTerminalProgressNeverBecomesACancelTarget(fraction: Double?) {
+ #expect(FinderTransferCancellationTarget.point(window: window, row: row,
+ indicators: [.init(fraction: fraction, bounds: ring)]) == nil)
+ }
+
+ @Test func missingOrUnconfinedGeometryNeverBecomesACancelTarget() {
+ for bounds in [nil, .zero, CGRect(x: 500, y: 205, width: 20, height: 20)] as [CGRect?] {
+ #expect(FinderTransferCancellationTarget.point(window: window, row: row,
+ indicators: [.init(fraction: 0.4, bounds: bounds)]) == nil)
+ }
+ #expect(FinderTransferCancellationTarget.point(window: .zero, row: row,
+ indicators: [.init(fraction: 0.4, bounds: ring)]) == nil)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderTransferObservationTests.swift b/potassiumProviderTests/FinderTransferObservationTests.swift
new file mode 100644
index 0000000..33f25df
--- /dev/null
+++ b/potassiumProviderTests/FinderTransferObservationTests.swift
@@ -0,0 +1,63 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderTransferObservationTests {
+ let subject = UUID(), correlation = UUID(), process = UUID(), fetch = UUID(), download = UUID()
+ let hash = String(repeating: "a", count: 40)
+
+ @Test func cancellationRequiresIntermediateProgressFromTheActualFetchAttempt() throws {
+ let observation = observer()
+ try observation.ingest([event(.started)])
+ #expect(!observation.canCancel)
+ try observation.ingest([event(.progress, operation: .downloadFile, progress: 0)])
+ #expect(!observation.canCancel)
+ try observation.ingest([event(.progress, operation: .downloadFile, progress: 30)])
+ #expect(observation.canCancel)
+ try observation.ingest([event(.cancelled)])
+ #expect(observation.fetchCancelled)
+ #expect(!observation.canCancel)
+ #expect(!observation.fetchCompleted)
+ #expect(throws: StabilityLiveEvidenceError.contradictoryTerminal) { try observation.ingest([event(.completed)]) }
+ }
+
+ @Test func completedTransferStopsControlSearchWithoutCountingAsCancellation() throws {
+ let observation = observer()
+ try observation.ingest([event(.started), event(.progress, operation: .downloadFile, progress: 40),
+ event(.completed, operation: .downloadFile)])
+ #expect(observation.downloadFinished)
+ #expect(!observation.canCancel)
+ #expect(!observation.fetchCancelled)
+ try observation.ingest([event(.completed)])
+ #expect(observation.fetchCompleted)
+ }
+
+ @Test(arguments: ["subject", "correlation", "build", "process", "parent", "source", "no-start", "terminal-only"])
+ func unrelatedOrTerminalProgressCannotTriggerCancellation(mismatch: String) throws {
+ let observation = observer()
+ if mismatch != "no-start" { try observation.ingest([event(.started)]) }
+ let progress = ProviderDiagnosticEvent(spanID: download, parentSpanID: mismatch == "parent" ? UUID() : fetch,
+ subjectAlias: mismatch == "subject" ? UUID() : subject,
+ processInstanceID: mismatch == "process" ? UUID() : process,
+ processCodeHash: mismatch == "build" ? String(repeating: "b", count: 40) : hash,
+ correlationID: mismatch == "correlation" ? UUID() : correlation,
+ source: mismatch == "source" ? .app : .fileProviderExtension, operation: .downloadFile, phase: .progress,
+ progressPercentBucket: mismatch == "terminal-only" ? 100 : 30)
+ try observation.ingest([progress])
+ #expect(!observation.canCancel)
+ #expect(!observation.fetchCancelled)
+ }
+
+ private func observer() -> FinderTransferObservation { .init(subject: subject, correlation: correlation, codeHash: hash) }
+ private func event(_ phase: ProviderDiagnosticPhase, operation: ProviderDiagnosticOperation = .fetchContents,
+ progress: Int? = nil) -> ProviderDiagnosticEvent {
+ ProviderDiagnosticEvent(spanID: operation == .fetchContents ? fetch : download,
+ parentSpanID: operation == .fetchContents ? nil : fetch, subjectAlias: subject,
+ processInstanceID: process, processCodeHash: hash, correlationID: correlation,
+ source: .fileProviderExtension, operation: operation, phase: phase, progressPercentBucket: progress)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/FinderTrashedItemSequenceTests.swift b/potassiumProviderTests/FinderTrashedItemSequenceTests.swift
new file mode 100644
index 0000000..462b16b
--- /dev/null
+++ b/potassiumProviderTests/FinderTrashedItemSequenceTests.swift
@@ -0,0 +1,42 @@
+#if os(macOS) && STABILITY
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct FinderTrashedItemSequenceTests {
+ @Test func targetIsReboundAfterNavigationBeforeAction() async throws {
+ var state = "bound-before-navigation"
+ try await FinderTrashedItemSequence.execute(reveal: {
+ state = "navigation-complete"
+ }, revalidate: {
+ #expect(state == "navigation-complete")
+ state = "fresh-binding"
+ }, action: {
+ #expect(state == "fresh-binding")
+ state = "action-invoked"
+ })
+ #expect(state == "action-invoked")
+ }
+
+ @Test func identityReplacementDuringNavigationPreventsAction() async {
+ var currentIdentity = 42
+ var invoked = false
+ await #expect(throws: FinderLiveError.unsafeTarget) {
+ try await FinderTrashedItemSequence.execute(reveal: { currentIdentity = 43 },
+ revalidate: {
+ guard currentIdentity == 42 else { throw FinderLiveError.unsafeTarget }
+ }, action: { invoked = true })
+ }
+ #expect(!invoked)
+ }
+
+ @Test func unavailableParentCannotProceedToBindingOrAction() async {
+ var rebound = false, invoked = false
+ await #expect(throws: FinderUIError.windowMismatch) {
+ try await FinderTrashedItemSequence.execute(reveal: { throw FinderUIError.windowMismatch },
+ revalidate: { rebound = true }, action: { invoked = true })
+ }
+ #expect(!rebound && !invoked)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/KDriveAPIEvidenceTests.swift b/potassiumProviderTests/KDriveAPIEvidenceTests.swift
new file mode 100644
index 0000000..f6ab128
--- /dev/null
+++ b/potassiumProviderTests/KDriveAPIEvidenceTests.swift
@@ -0,0 +1,508 @@
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+
+@Suite("Version-pinned kDrive API evidence", .serialized)
+struct KDriveAPIEvidenceTests {
+ @Test func partialActivitiesRequestsOnlySupportedFileExpansion() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ #expect(try await service.listPartialActivities(driveID: 11, fileIDs: [22], since: Date(timeIntervalSince1970: 100)).isEmpty)
+ let captured = try #require(await KDriveAPIEvidenceURLProtocol.recordedRequests().first)
+ let url = try #require(captured.request.url)
+ #expect(url.path == "/3/drive/11/files/listing/partial")
+ #expect(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems == [URLQueryItem(name: "with", value: "file")])
+ let body = try #require(captured.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ let files = try #require(json["files"] as? [[String: Int]])
+ #expect(files == [["id": 22, "from_date": 100]])
+ }
+ @Test("Share-link access supports inherit")
+ func shareLinkAccessSupportsInherit() async throws {
+ #expect(KDriveShareLinkConfiguration.Access.allCases.contains(.inherit))
+
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ let summary = try #require(try await service.shareLink(driveID: 11, fileID: 22))
+
+ #expect(summary.configuration.access == .inherit)
+ }
+
+ @Test("Unknown share-link access fails closed")
+ func unknownShareLinkAccessFailsClosed() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "unrecognized")
+ defer { session.invalidateAndCancel() }
+
+ await #expect(throws: KDriveContextActionError.unsupportedShareLinkAccess) {
+ _ = try await service.shareLink(driveID: 11, fileID: 22)
+ }
+ }
+
+ @Test("Share-link update explicitly clears expiration and preserves inherit access")
+ func shareLinkUpdateEncodesExplicitNullExpiration() async throws {
+ await KDriveAPIEvidenceURLProtocol.reset(returningShareLinkRight: "inherit", expiration: 1700000400)
+ let session = evidenceSession()
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(
+ bearerToken: "",
+ apiBaseURL: evidenceBaseURL,
+ session: session
+ )
+ let configuration = KDriveShareLinkConfiguration(
+ access: .inherit,
+ validUntil: nil,
+ allowsDownload: false
+ )
+
+ let summary = try await service.updateShareLink(
+ driveID: 11,
+ fileID: 22,
+ configuration: configuration
+ )
+
+ #expect(summary.configuration.access == .inherit)
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let requestURL = try #require(update.request.url)
+ #expect(requestURL.path == "/2/drive/11/files/22/link")
+ let body = try #require(update.body)
+ let json = try #require(
+ try JSONSerialization.jsonObject(with: body) as? [String: Any]
+ )
+ #expect(json["right"] == nil)
+ #expect(json["valid_until"] is NSNull)
+ #expect(json["can_download"] as? Bool == false)
+ }
+
+ @Test("Unchanged absent expiration does not invoke a plan-gated setting")
+ func shareLinkUpdateOmitsAlreadyAbsentExpiration() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .inherit, allowsComments: true))
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ #expect(requests.map { $0.request.httpMethod } == ["GET", "PUT", "GET"])
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let body = try #require(update.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ #expect(!json.keys.contains("valid_until"))
+ #expect(json["can_comment"] as? Bool == true)
+ #expect(Set(json.keys) == ["can_comment"])
+ }
+
+ @Test("An explicit expiration patch does not resend unchanged settings")
+ func shareLinkUpdateSetsExpiration() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .inherit, validUntil: Date(timeIntervalSince1970: 1700000900)))
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ #expect(requests.map { $0.request.httpMethod } == ["GET", "PUT", "GET"])
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let body = try #require(update.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ #expect(json["valid_until"] as? Int == 1700000900)
+ #expect(Set(json.keys) == ["valid_until", "can_comment"])
+ }
+
+ @Test("Unchanged comments remain explicit when restricting downloads")
+ func downloadRestrictionPreservesIndependentComments() async throws {
+ await KDriveAPIEvidenceURLProtocol.reset(returningShareLinkRight: "inherit", allowsComments: true)
+ let session = evidenceSession()
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(bearerToken: "", apiBaseURL: evidenceBaseURL, session: session)
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .inherit, allowsDownload: false, allowsComments: true))
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let body = try #require(update.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ #expect(json["can_comment"] as? Bool == true)
+ #expect(json["can_download"] as? Bool == false)
+ #expect(json["can_edit"] == nil)
+ #expect(Set(json.keys) == ["can_download", "can_comment"])
+ }
+
+ @Test("Saving unchanged settings does not issue an empty update")
+ func unchangedShareLinkDoesNotMutate() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22, configuration: .init(access: .inherit))
+ #expect(await KDriveAPIEvidenceURLProtocol.recordedRequests().map { $0.request.httpMethod } == ["GET"])
+ }
+
+ @Test("A changed access policy and password remain explicit patch fields")
+ func changedShareAccessRemainsExplicit() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "inherit")
+ defer { session.invalidateAndCancel() }
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .password, password: "synthetic-only"))
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let body = try #require(update.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ #expect(Set(json.keys) == ["right", "password", "can_comment"])
+ #expect(json["right"] as? String == "password")
+ #expect(json["password"] as? String == "synthetic-only")
+ }
+
+ @Test("Password rotation is not mistaken for an unchanged reported configuration")
+ func unchangedPasswordAccessStillSendsANewPassword() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "password")
+ defer { session.invalidateAndCancel() }
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .password, password: "synthetic-rotation"))
+ let requests = await KDriveAPIEvidenceURLProtocol.recordedRequests()
+ let update = try #require(requests.first { $0.request.httpMethod == "PUT" })
+ let body = try #require(update.body)
+ let json = try #require(try JSONSerialization.jsonObject(with: body) as? [String: Any])
+ #expect(Set(json.keys) == ["password", "can_comment"])
+ #expect(json["password"] as? String == "synthetic-rotation")
+ }
+
+ @Test("An unrecognized current share configuration prevents an update")
+ func failedExpirationPreflightDoesNotMutate() async throws {
+ let (service, session) = await makeService(returningShareLinkRight: "unrecognized")
+ defer { session.invalidateAndCancel() }
+ await #expect(throws: KDriveContextActionError.unsupportedShareLinkAccess) {
+ _ = try await service.updateShareLink(driveID: 11, fileID: 22,
+ configuration: .init(access: .inherit, allowsComments: true))
+ }
+ #expect(await KDriveAPIEvidenceURLProtocol.recordedRequests().map { $0.request.httpMethod } == ["GET"])
+ }
+
+ @Test("Duplicate request carries an explicit caller-selected name")
+ func duplicateRequestEncodesExplicitName() async throws {
+ await KDriveAPIEvidenceURLProtocol.reset(returningShareLinkRight: "inherit")
+ let session = evidenceSession()
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(
+ bearerToken: "",
+ apiBaseURL: evidenceBaseURL,
+ session: session
+ )
+
+ let duplicate = try await service.duplicateItem(
+ driveID: 11,
+ fileID: 22,
+ name: "Evidence copy.txt"
+ )
+
+ #expect(duplicate.name == "Evidence copy.txt")
+ let captured = try #require(
+ await KDriveAPIEvidenceURLProtocol.recordedRequests().first
+ )
+ let requestURL = try #require(captured.request.url)
+ #expect(captured.request.httpMethod == "POST")
+ #expect(requestURL.path == "/3/drive/11/files/22/duplicate")
+ let body = try #require(captured.body)
+ let json = try #require(
+ try JSONSerialization.jsonObject(with: body) as? [String: Any]
+ )
+ #expect(json["name"] as? String == "Evidence copy.txt")
+ #expect(json.isEmpty == false)
+ }
+
+ @Test("Direct upload byte-count validation enforces the documented boundary")
+ func directUploadByteCountBoundary() throws {
+ let maximum = PotassiumKDriveService.directUploadMaximumByteCount
+
+ #expect(maximum == 1_000_000_000)
+ #expect(
+ KDriveDirectUploadError.requiresUploadSession(maximumByteCount: maximum).recovery
+ == .cannotSynchronize
+ )
+ #expect(
+ KDriveDirectUploadError.requiresUploadSession(maximumByteCount: maximum)
+ .diagnosticCategory == .validation
+ )
+ #expect(
+ KDriveDirectUploadError.requiresUploadSession(maximumByteCount: maximum)
+ .diagnosticSummary
+ == "The direct-upload size limit requires a session-backed transfer."
+ )
+ #expect(
+ KDriveDirectUploadError.requiresUploadSession(maximumByteCount: maximum)
+ .recoverySuggestion
+ == "Keep the local content and retry after session-backed uploads are available."
+ )
+ #expect(
+ ProviderDiagnosticErrorClassifier.classify(
+ KDriveDirectUploadError.requiresUploadSession(maximumByteCount: maximum)
+ ) == .validation
+ )
+ #expect(throws: Never.self) {
+ try PotassiumKDriveService.validateDirectUploadByteCount(maximum)
+ }
+ #expect(throws: KDriveDirectUploadError.requiresUploadSession(
+ maximumByteCount: maximum
+ )) {
+ try PotassiumKDriveService.validateDirectUploadByteCount(maximum + 1)
+ }
+ #expect(throws: KDriveDirectUploadError.fileSizeUnavailable) {
+ try PotassiumKDriveService.validateDirectUploadByteCount(-1)
+ }
+ #expect(KDriveDirectUploadError.fileSizeUnavailable.recovery == .cannotSynchronize)
+ #expect(KDriveDirectUploadError.fileSizeUnavailable.diagnosticCategory == .validation)
+ #expect(
+ KDriveDirectUploadError.fileSizeUnavailable.diagnosticSummary
+ == "The callback file size could not be verified before direct upload."
+ )
+ #expect(
+ KDriveDirectUploadError.fileSizeUnavailable.recoverySuggestion
+ == "Keep the callback source available and retry once its size can be verified."
+ )
+ }
+
+ @Test("Oversized callback files are rejected before content loading")
+ func oversizedCallbackFileIsRejectedBeforeLoading() throws {
+ let temporaryDirectory = FileManager.default.temporaryDirectory
+ .appendingPathComponent(UUID().uuidString, isDirectory: true)
+ try FileManager.default.createDirectory(
+ at: temporaryDirectory,
+ withIntermediateDirectories: true
+ )
+ defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
+
+ let fileURL = temporaryDirectory.appendingPathComponent("oversized.bin")
+ #expect(FileManager.default.createFile(atPath: fileURL.path, contents: Data()))
+ let handle = try FileHandle(forWritingTo: fileURL)
+ try handle.truncate(
+ atOffset: UInt64(PotassiumKDriveService.directUploadMaximumByteCount + 1)
+ )
+ try handle.close()
+
+ #expect(throws: KDriveDirectUploadError.requiresUploadSession(
+ maximumByteCount: PotassiumKDriveService.directUploadMaximumByteCount
+ )) {
+ _ = try KDriveDirectUploadContentLoader.loadContents(at: fileURL)
+ }
+ }
+
+ @Test("Retryable HTTP rejections retain only a safe integer retry delay")
+ func retryableHTTPRejectionsParseSafeRetryDelay() throws {
+ let timeout = try #require(KDriveRemoteErrorClassifier.apiRejection(
+ from: APIClientError.unacceptableStatusCode(
+ 408,
+ body: "timeout-detail",
+ metadata: APIResponseMetadata(retryAfter: "3")
+ )
+ ))
+ let throttled = try #require(KDriveRemoteErrorClassifier.apiRejection(
+ from: APIClientError.unacceptableStatusCode(
+ 429,
+ body: "request quota reached",
+ metadata: APIResponseMetadata(retryAfter: " 17 ")
+ )
+ ))
+
+ #expect(timeout.recovery == .serverUnreachable)
+ #expect(timeout.retryAfterSeconds == 3)
+ #expect(throttled.recovery == .serverUnreachable)
+ #expect(throttled.retryAfterSeconds == 17)
+ #expect(throttled.diagnosticSummary == "The remote API rejected the operation. HTTP 429.")
+ #expect(throttled.diagnosticSummary.contains("request quota reached") == false)
+ #expect(throttled.diagnosticSummary.contains("17") == false)
+ #expect(throttled.responseBodyPreview() == "request quota reached")
+ }
+
+ @Test("Non-integer Retry-After values are ignored")
+ func nonIntegerRetryAfterValuesAreIgnored() throws {
+ let malformed = try #require(KDriveRemoteErrorClassifier.apiRejection(
+ from: APIClientError.unacceptableStatusCode(
+ 429,
+ body: "retry-later",
+ metadata: APIResponseMetadata(retryAfter: "later")
+ )
+ ))
+ let httpDate = try #require(KDriveRemoteErrorClassifier.apiRejection(
+ from: APIClientError.unacceptableStatusCode(
+ 429,
+ body: "retry-at-date",
+ metadata: APIResponseMetadata(retryAfter: "Wed, 21 Oct 2015 07:28:00 GMT")
+ )
+ ))
+
+ #expect(malformed.retryAfterSeconds == nil)
+ #expect(httpDate.retryAfterSeconds == nil)
+ #expect(malformed.recovery == .serverUnreachable)
+ #expect(httpDate.recovery == .serverUnreachable)
+ #expect(malformed.diagnosticSummary.contains(malformed.responseBody) == false)
+ #expect(httpDate.diagnosticSummary.contains(httpDate.responseBody) == false)
+ }
+
+ private var evidenceBaseURL: URL {
+ URL(string: "https://evidence.invalid")!
+ }
+
+ private func evidenceSession() -> URLSession {
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.protocolClasses = [KDriveAPIEvidenceURLProtocol.self]
+ return URLSession(configuration: configuration)
+ }
+
+ private func makeService(
+ returningShareLinkRight right: String
+ ) async -> (PotassiumKDriveService, URLSession) {
+ await KDriveAPIEvidenceURLProtocol.reset(returningShareLinkRight: right)
+ let session = evidenceSession()
+ let service = PotassiumKDriveService(
+ bearerToken: "",
+ apiBaseURL: evidenceBaseURL,
+ session: session
+ )
+ return (service, session)
+ }
+}
+
+private struct KDriveAPIEvidenceCapturedRequest: Sendable {
+ let request: URLRequest
+ let body: Data?
+}
+
+private actor KDriveAPIEvidenceTransportState {
+ private var shareLinkRight = "inherit"
+ private var expiration: Int?
+ private var allowsComments = false
+ private var requests: [KDriveAPIEvidenceCapturedRequest] = []
+
+ func reset(returningShareLinkRight right: String, expiration: Int?, allowsComments: Bool) {
+ shareLinkRight = right
+ self.expiration = expiration
+ self.allowsComments = allowsComments
+ requests.removeAll()
+ }
+
+ func record(_ request: KDriveAPIEvidenceCapturedRequest) {
+ requests.append(request)
+ }
+
+ func recordedRequests() -> [KDriveAPIEvidenceCapturedRequest] {
+ requests
+ }
+
+ func responseBody(for request: URLRequest) -> Data {
+ if request.url?.path.hasSuffix("/listing/partial") == true {
+ return Data("{\"result\":\"success\",\"data\":[]}".utf8)
+ }
+ if request.httpMethod == "PUT" {
+ return Data(#"{"result":"success","data":true}"#.utf8)
+ }
+
+ if request.url?.path.hasSuffix("/duplicate") == true {
+ return Data(
+ """
+ {
+ "result": "success",
+ "data": {
+ "id": 23,
+ "name": "Evidence copy.txt",
+ "type": "file",
+ "status": "active",
+ "visibility": "is_private_space",
+ "drive_id": 11,
+ "parent_id": 2,
+ "path": null,
+ "depth": 2,
+ "created_at": 1700000000,
+ "last_modified_at": 1700000000,
+ "updated_at": 1700000000,
+ "size": 0,
+ "mime_type": "text/plain",
+ "is_favorite": false
+ }
+ }
+ """.utf8
+ )
+ }
+
+ let syntheticShareLink = ["https:", "", "share.invalid", "link"].joined(separator: "/")
+ return Data(
+ """
+ {
+ "result": "success",
+ "data": {
+ "url": "\(syntheticShareLink)",
+ "file_id": 22,
+ "right": "\(shareLinkRight)",
+ "valid_until": \(expiration.map(String.init) ?? "null"),
+ "created_by": 1,
+ "created_at": 1700000000,
+ "updated_at": 1700000001,
+ "capabilities": {
+ "can_edit": false,
+ "can_see_stats": false,
+ "can_see_info": true,
+ "can_download": true,
+ "can_comment": \(allowsComments),
+ "can_request_access": false
+ },
+ "access_blocked": false,
+ "views": null
+ }
+ }
+ """.utf8
+ )
+ }
+}
+
+private final class KDriveAPIEvidenceURLProtocol: URLProtocol {
+ private static let state = KDriveAPIEvidenceTransportState()
+
+ static func reset(returningShareLinkRight right: String, expiration: Int? = nil, allowsComments: Bool = false) async {
+ await state.reset(returningShareLinkRight: right, expiration: expiration, allowsComments: allowsComments)
+ }
+
+ static func recordedRequests() async -> [KDriveAPIEvidenceCapturedRequest] {
+ await state.recordedRequests()
+ }
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ let request = request
+ let captured = KDriveAPIEvidenceCapturedRequest(
+ request: request,
+ body: request.httpBody ?? Self.readBodyStream(from: request)
+ )
+ Task {
+ await Self.state.record(captured)
+ let data = await Self.state.responseBody(for: request)
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 200,
+ httpVersion: "HTTP/1.1",
+ headerFields: ["Content-Type": "application/json"]
+ )!
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: data)
+ client?.urlProtocolDidFinishLoading(self)
+ }
+ }
+
+ override func stopLoading() {}
+
+ private static func readBodyStream(from request: URLRequest) -> Data? {
+ guard let stream = request.httpBodyStream else {
+ return nil
+ }
+
+ stream.open()
+ defer { stream.close() }
+ var data = Data()
+ var buffer = [UInt8](repeating: 0, count: 4096)
+ while stream.hasBytesAvailable {
+ let bytesRead = stream.read(&buffer, maxLength: buffer.count)
+ guard bytesRead > 0 else { break }
+ data.append(buffer, count: bytesRead)
+ }
+ return data
+ }
+}
diff --git a/potassiumProviderTests/KDriveContextActionTests.swift b/potassiumProviderTests/KDriveContextActionTests.swift
index 4a6571f..d7b0379 100644
--- a/potassiumProviderTests/KDriveContextActionTests.swift
+++ b/potassiumProviderTests/KDriveContextActionTests.swift
@@ -83,10 +83,11 @@ struct KDriveContextActionTests {
}
@Test func duplicateRefetchesAuthoritativeResultAndInvalidatesItsParent() async throws {
+ let source = item(id: 42, parentID: 10, name: "Document.txt")
let response = item(id: 99, parentID: 10, name: "Copy (pending)")
let authoritative = item(id: 99, parentID: 12, name: "Copy")
let remote = ContextActionRemoteMock(
- metadataResponses: [99: [authoritative]],
+ metadataResponses: [42: [source], 99: [authoritative]],
duplicateResult: response
)
let coordinator = KDriveContextActionCoordinator(
@@ -101,11 +102,18 @@ struct KDriveContextActionTests {
#expect(execution.activityItem == authoritative)
#expect(execution.affectedParentIDs == [12])
#expect(await remote.calls() == [
- .duplicate(fileID: 42),
+ .item(fileID: 42),
+ .duplicate(fileID: 42, name: "Document copy.txt"),
.item(fileID: 99),
])
}
+ @Test func duplicateNamePolicyPreservesExtensionsAndHandlesDirectories() {
+ #expect(KDriveDuplicateNamePolicy.duplicateName(for: "Report.pdf") == "Report copy.pdf")
+ #expect(KDriveDuplicateNamePolicy.duplicateName(for: "Archive") == "Archive copy")
+ #expect(KDriveDuplicateNamePolicy.duplicateName(for: ".settings") == ".settings copy")
+ }
+
@Test func restoreUsesOriginalParentWhenItStillExists() async throws {
let trashed = item(id: 42, parentID: 25)
let remote = ContextActionRemoteMock(
@@ -297,7 +305,7 @@ private actor ContextActionRemoteMock: KDriveItemMetadataProviding, KDriveContex
enum Call: Equatable {
case item(fileID: Int)
case setFavorite(fileID: Int, isFavorite: Bool)
- case duplicate(fileID: Int)
+ case duplicate(fileID: Int, name: String)
case trashedItem(fileID: Int)
case existingFileIDs([Int])
case restore(fileID: Int, destinationParentID: Int)
@@ -342,8 +350,8 @@ private actor ContextActionRemoteMock: KDriveItemMetadataProviding, KDriveContex
recordedCalls.append(.setFavorite(fileID: fileID, isFavorite: isFavorite))
}
- func duplicateItem(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
- recordedCalls.append(.duplicate(fileID: fileID))
+ func duplicateItem(driveID: Int, fileID: Int, name: String) async throws -> KDriveRemoteItem {
+ recordedCalls.append(.duplicate(fileID: fileID, name: name))
guard let duplicateResult else {
throw ContextActionMockError.missingFixture
}
diff --git a/potassiumProviderTests/KDriveItemMetadataLookupTests.swift b/potassiumProviderTests/KDriveItemMetadataLookupTests.swift
new file mode 100644
index 0000000..4f35666
--- /dev/null
+++ b/potassiumProviderTests/KDriveItemMetadataLookupTests.swift
@@ -0,0 +1,122 @@
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+
+@Suite("Active and trashed item metadata")
+struct KDriveItemMetadataLookupTests {
+ private static func item(id: Int = 42, driveID: Int = 7) -> KDriveRemoteItem {
+ KDriveRemoteItem(id: id, name: "Synthetic.txt", type: "file", status: "ok",
+ driveID: driveID, parentID: 3, path: nil, size: 4, mimeType: "text/plain",
+ createdAt: nil, modifiedAt: Date(timeIntervalSince1970: 10), updatedAt: Date(timeIntervalSince1970: 10))
+ }
+
+ private static func rejection(_ status: Int) -> APIClientError {
+ .unacceptableStatusCode(status, body: "synthetic")
+ }
+
+ @Test func activeMetadataDoesNotConsultTrash() async throws {
+ let calls = MetadataLookupCalls()
+ let result = try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { await calls.record("active"); return Self.item() },
+ trashed: { await calls.record("trash"); return Self.item() })
+ #expect(result.item == Self.item())
+ #expect(!result.isTrashed)
+ #expect(await calls.values == ["active"])
+ }
+
+ @Test func activeNotFoundResolvesTheExactTrashIdentity() async throws {
+ let calls = MetadataLookupCalls()
+ let result = try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { await calls.record("active"); throw Self.rejection(404) },
+ trashed: { await calls.record("trash"); return Self.item() })
+ #expect(result.item == Self.item())
+ #expect(result.isTrashed)
+ #expect(await calls.values == ["active", "trash"])
+ }
+
+ @Test func absenceRequiresBothIdentityEndpoints() async {
+ let calls = MetadataLookupCalls()
+ await #expect(throws: KDriveItemMetadataLookupError.notFound) {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { await calls.record("active"); throw Self.rejection(404) },
+ trashed: { await calls.record("trash"); throw Self.rejection(404) })
+ }
+ #expect(await calls.values == ["active", "trash"])
+ }
+
+ @Test(arguments: [401, 403, 429, 500])
+ func activeFailuresDoNotConsultTrash(_ status: Int) async {
+ let calls = MetadataLookupCalls()
+ do {
+ _ = try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { throw Self.rejection(status) },
+ trashed: { await calls.record("trash"); return Self.item() })
+ Issue.record("Expected the original active metadata error")
+ } catch {
+ #expect(KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode == status)
+ }
+ #expect(await calls.values.isEmpty)
+ }
+
+ @Test(arguments: [401, 403, 429, 500])
+ func trashFailuresDoNotEstablishAbsence(_ status: Int) async {
+ do {
+ _ = try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { throw Self.rejection(404) }, trashed: { throw Self.rejection(status) })
+ Issue.record("Expected the original Trash metadata error")
+ } catch {
+ #expect(KDriveRemoteErrorClassifier.apiRejection(from: error)?.statusCode == status)
+ }
+ }
+
+ @Test func unavailableTrashLookupDoesNotEstablishAbsence() async {
+ await #expect(throws: KDriveItemMetadataLookupError.trashLookupUnavailable) {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { throw Self.rejection(404) },
+ trashed: { throw KDriveItemMetadataLookupError.trashLookupUnavailable })
+ }
+ }
+
+ @Test(arguments: [false, true], [false, true])
+ func mismatchedIdentityIsRejected(inTrash: Bool, wrongDrive: Bool) async {
+ let wrong = Self.item(id: wrongDrive ? 42 : 43, driveID: wrongDrive ? 8 : 7)
+ await #expect(throws: KDriveItemMetadataLookupError.identityMismatch) {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { if inTrash { throw Self.rejection(404) }; return wrong }, trashed: { wrong })
+ }
+ }
+
+ @Test func cancellationBetweenLookupsPreventsTrashRequest() async {
+ let calls = MetadataLookupCalls()
+ let task = Task {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: {
+ withUnsafeCurrentTask { $0?.cancel() }
+ throw Self.rejection(404)
+ }, trashed: { await calls.record("trash"); return Self.item() })
+ }
+ await #expect(throws: CancellationError.self) { try await task.value }
+ #expect(await calls.values.isEmpty)
+ }
+
+ @Test func transportAndCancellationErrorsArePreserved() async {
+ let calls = MetadataLookupCalls()
+ await #expect(throws: URLError(.notConnectedToInternet)) {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { throw URLError(.notConnectedToInternet) },
+ trashed: { await calls.record("trash"); return Self.item() })
+ }
+ await #expect(throws: CancellationError.self) {
+ try await KDriveItemMetadataLookup.resolve(driveID: 7, fileID: 42,
+ active: { throw CancellationError() },
+ trashed: { await calls.record("trash"); return Self.item() })
+ }
+ #expect(await calls.values.isEmpty)
+ }
+}
+
+private actor MetadataLookupCalls {
+ private(set) var values: [String] = []
+ func record(_ value: String) { values.append(value) }
+}
diff --git a/potassiumProviderTests/KDriveMutationCoordinatorTests.swift b/potassiumProviderTests/KDriveMutationCoordinatorTests.swift
index 6004f06..d19ce31 100644
--- a/potassiumProviderTests/KDriveMutationCoordinatorTests.swift
+++ b/potassiumProviderTests/KDriveMutationCoordinatorTests.swift
@@ -5,6 +5,29 @@ import PotassiumProviderCore
@Suite(.serialized)
struct KDriveMutationCoordinatorTests {
+ @Test(arguments: ["dir", "directory"])
+ func directoryTimestampResolvesToServerValueWithoutFileOnlyMutation(_ type: String) async throws {
+ let current = makeItem(id: Self.fileID, name: "Folder", type: type, mimeType: nil)
+ let remote = RecordingKDriveFileProvider(itemResults: [Self.fileID: [current]])
+ let result = try await makeCoordinator(remote: remote).updateModificationDate(
+ fileID: Self.fileID, date: Date(timeIntervalSince1970: 900))
+ #expect(result == current)
+ #expect(await remote.calls() == [.item(driveID: Self.driveID, fileID: Self.fileID)])
+ }
+
+ @Test func fileTimestampMutationIsAppliedAndRefetched() async throws {
+ let current = makeItem(id: Self.fileID, name: "File.txt")
+ let date = Date(timeIntervalSince1970: 900)
+ let updated = makeItem(id: Self.fileID, name: "File.txt", modifiedAt: date)
+ let remote = RecordingKDriveFileProvider(itemResults: [Self.fileID: [current, updated]])
+ let result = try await makeCoordinator(remote: remote).updateModificationDate(fileID: Self.fileID, date: date)
+ #expect(result == updated)
+ #expect(await remote.calls() == [
+ .item(driveID: Self.driveID, fileID: Self.fileID),
+ .updateModificationDate(driveID: Self.driveID, fileID: Self.fileID, date: date),
+ .item(driveID: Self.driveID, fileID: Self.fileID)
+ ])
+ }
@Test func fileCreateUsesCollisionSafeIdempotentUpload() async throws {
let createdItem = makeItem(id: 101, name: "New.txt")
let remote = RecordingKDriveFileProvider(uploadResult: createdItem)
diff --git a/potassiumProviderTests/KDriveTransferProgressTests.swift b/potassiumProviderTests/KDriveTransferProgressTests.swift
new file mode 100644
index 0000000..6e49758
--- /dev/null
+++ b/potassiumProviderTests/KDriveTransferProgressTests.swift
@@ -0,0 +1,55 @@
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+struct KDriveTransferProgressTests {
+ @Test(.timeLimit(.minutes(1)))
+ func nestedTransferProgressIsRecordedBeforeCompletion() async throws {
+ let events = AsyncStream.makeStream()
+ let sink = TransferProgressSink(continuation: events.continuation)
+ let span = await ProviderDiagnosticSpan.start(source: .fileProviderExtension,
+ operation: .downloadFile, recorder: sink)
+ let parent = Progress(totalUnitCount: 100)
+ let child = Progress(totalUnitCount: 1_000)
+ parent.addChild(child, withPendingUnitCount: 100)
+ child.completedUnitCount = 400
+ #expect(parent.completedUnitCount == 0)
+ let tracker = PotassiumKDriveService.trackProgress(parent, with: span)
+ defer { tracker.cancel(); events.continuation.finish() }
+ var iterator = events.stream.makeAsyncIterator()
+ while let event = await iterator.next() {
+ if event.phase == .progress {
+ #expect(event.progressPercentBucket == 40)
+ break
+ }
+ }
+ try Task.checkCancellation()
+ child.completedUnitCount = 900
+ while let event = await iterator.next() {
+ if event.phase == .progress {
+ #expect(event.progressPercentBucket == 90)
+ break
+ }
+ }
+ try Task.checkCancellation()
+ #expect(parent.completedUnitCount == 0)
+ tracker.cancel()
+ await tracker.value
+ await span.cancel()
+ child.completedUnitCount = 1_000
+ await span.progress(fractionCompleted: parent.fractionCompleted)
+ let recorded = await sink.events
+ #expect(recorded.filter { $0.phase == .progress }.compactMap(\.progressPercentBucket) == [40, 90])
+ #expect(recorded.filter { [.completed, .cancelled, .failed].contains($0.phase) }.map(\.phase) == [.cancelled])
+ }
+}
+
+private actor TransferProgressSink: ProviderDiagnosticRecording {
+ let continuation: AsyncStream.Continuation
+ private(set) var events: [ProviderDiagnosticEvent] = []
+ init(continuation: AsyncStream.Continuation) { self.continuation = continuation }
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) async throws {
+ events.append(event)
+ continuation.yield(event)
+ }
+}
diff --git a/potassiumProviderTests/MacAppPresenceTests.swift b/potassiumProviderTests/MacAppPresenceTests.swift
index d7372b5..8086bb5 100644
--- a/potassiumProviderTests/MacAppPresenceTests.swift
+++ b/potassiumProviderTests/MacAppPresenceTests.swift
@@ -93,7 +93,7 @@ struct MacAppPresenceTests {
let projectFile = try String(contentsOf: projectFileURL, encoding: .utf8)
let readWriteSetting = "ENABLE_USER_SELECTED_FILES = readwrite;"
- #expect(projectFile.components(separatedBy: readWriteSetting).count - 1 == 2)
+ #expect(projectFile.components(separatedBy: readWriteSetting).count - 1 == 3)
#expect(projectFile.contains("ENABLE_USER_SELECTED_FILES = readonly;") == false)
}
}
diff --git a/potassiumProviderTests/ModificationCallbackTests.swift b/potassiumProviderTests/ModificationCallbackTests.swift
new file mode 100644
index 0000000..71d7ce4
--- /dev/null
+++ b/potassiumProviderTests/ModificationCallbackTests.swift
@@ -0,0 +1,154 @@
+import FileProvider
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct ModificationCallbackTests {
+ struct Shape: Sendable {
+ let fields: NSFileProviderItemFields
+ let trash: Bool
+ let operations: [String]
+ }
+ static let shapes: [Shape] = [
+ Shape(fields: [.contents, .filename], trash: false, operations: ["rename", "replace"]),
+ Shape(fields: [.contents, .parentItemIdentifier], trash: false, operations: ["move", "replace"]),
+ Shape(fields: [.contents, .filename, .parentItemIdentifier], trash: false, operations: ["move", "replace"]),
+ Shape(fields: [.contents, .parentItemIdentifier], trash: true, operations: ["replace", "trash"]),
+ Shape(fields: [.parentItemIdentifier], trash: true, operations: ["trash"]),
+ Shape(fields: [.filename, .parentItemIdentifier], trash: true, operations: ["rename", "trash"]),
+ Shape(fields: [.contents, .filename, .parentItemIdentifier], trash: true, operations: ["rename", "replace", "trash"]),
+ Shape(fields: [.filename, .tagData], trash: false, operations: ["rename"]),
+ Shape(fields: [.contents, .filename, .tagData], trash: false, operations: ["rename", "replace"]),
+ Shape(fields: [.tagData], trash: false, operations: [])
+ ]
+
+ @Test(arguments: shapes, [false, true])
+ func productionPlaintextSequence(_ shape: Shape, remoteChanged: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory.appendingPathComponent("server"))
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ let base = try await remote.item(driveID: 7, fileID: 3)
+ try client.cache(base)
+ let other = Data("remote".utf8), local = Data("local".utf8)
+ if remoteChanged {
+ _ = try await remote.replaceFile(driveID: 7, fileID: 3, expectedETag: base.etag!, clientToken: "other",
+ contentHash: KDriveMutationIdentity.contentHash(other), contents: other, lastModifiedAt: nil)
+ }
+ await remote.clearOperations()
+ let executor = KDriveModificationExecutor(coordinator: client.coordinator) { try await remote.item(driveID: 7, fileID: $0) }
+ let requestedName = shape.fields.contains(.filename) ? "Renamed.txt" : base.name
+ let result = try await executor.execute(fileID: 3, filename: requestedName,
+ baseVersion: KDriveItemBaseVersion(contentVersion: base.contentVersion, metadataVersion: base.metadataVersion),
+ fields: shape.fields, destinationParentID: shape.trash ? nil : 2, requestsTrash: shape.trash,
+ modificationDate: nil, hasContents: true) { try await client.edit(local, filename: requestedName) }
+ let uploads = remoteChanged && shape.fields.contains(.contents)
+ var expected = shape.operations.map { $0 == "replace" && uploads ? "upload" : $0 }
+ if shape.trash && uploads { expected.append("trash") }
+ #expect(await remote.operations() == expected)
+ #expect(result.remainingFields == shape.fields.intersection(.tagData))
+ #expect(result.trashed == shape.trash)
+ let state = await remote.snapshot()
+ let expectedParent = shape.fields.contains(.parentItemIdentifier) && !shape.trash ? 2 : 1
+ #expect(state.items[3]?.parentID == expectedParent)
+ #expect(state.items[3]?.name == (shape.fields.contains(.filename) ? "Renamed.txt" : base.name))
+ if shape.fields.contains(.contents) {
+ let localIDs = state.bytes.filter { $0.value == local }.map(\.key)
+ #expect(localIDs.count == 1)
+ let localID = try #require(localIDs.first)
+ #expect(state.items[localID]?.parentID == expectedParent)
+ #expect(state.items[localID]?.contentVersion != base.contentVersion)
+ let expectedName = remoteChanged ? KDriveConflictFilename.filename(for: requestedName,
+ deviceName: "Synthetic", date: Date(timeIntervalSince1970: 1), timeZone: TimeZone(secondsFromGMT: 0)!) : requestedName
+ #expect(state.items[localID]?.name == expectedName)
+ #expect(try await remote.downloadFile(driveID: 7, fileID: localID) == local)
+ #expect(result.item?.id == localID)
+ if remoteChanged {
+ #expect(localID != 3 && state.bytes[3] == other)
+ if shape.trash { #expect(state.trash == [3, localID]) }
+ } else { #expect(localID == 3) }
+ }
+ let item = try #require(result.item)
+ #expect(state.items[item.id] == item)
+ #expect(item.parentID == expectedParent)
+ if shape.trash { #expect(state.trash.contains(item.id) && state.trash.contains(3)) }
+ }
+
+ @Test func malformedCombinedCallbackDoesNotRenameBeforeFailing() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let remote = try ConflictTestRemote(directory: directory)
+ let client = ConflictTestClient(directory: directory.appendingPathComponent("client"), remote: remote)
+ let base = try await remote.item(driveID: 7, fileID: 3)
+ let executor = KDriveModificationExecutor(coordinator: client.coordinator) { try await remote.item(driveID: 7, fileID: $0) }
+ await #expect(throws: (any Error).self) {
+ try await executor.execute(fileID: 3, filename: "Incorrect.txt",
+ baseVersion: KDriveItemBaseVersion(contentVersion: base.contentVersion, metadataVersion: base.metadataVersion),
+ fields: [.contents, .filename], destinationParentID: nil, requestsTrash: false,
+ modificationDate: nil, hasContents: false) { throw CancellationError() }
+ }
+ #expect(await remote.operations().isEmpty)
+ #expect(try await remote.item(driveID: 7, fileID: 3) == base)
+ }
+
+ @Test(arguments: [false, true])
+ func vaultCombinedTrashUsesCommittedRevisionsAndPreservesUnsupportedFields(failUpload: Bool) async throws {
+ let current = vaultCallbackItem(revision: 1)
+ let updated = vaultCallbackItem(revision: 2)
+ let calls = VaultCallbackProbe()
+ do {
+ let result = try await VaultModificationExecutor.execute(current: current,
+ fields: [.contents, .filename, .parentItemIdentifier, .tagData], requestsTrash: true,
+ hasContents: true, baseContentRevision: current.contentRevision, baseMetadataRevision: current.metadataRevision,
+ modify: {
+ await calls.record("modify")
+ if failUpload { throw URLError(.notConnectedToInternet) }
+ return updated
+ }, trash: { content, metadata in
+ #expect(content == updated.contentRevision && metadata == updated.metadataRevision)
+ await calls.record("trash")
+ })
+ #expect(!failUpload)
+ #expect(result.item == nil && result.trashed && result.remainingFields == [.tagData])
+ #expect(await calls.values() == ["modify", "trash"])
+ } catch {
+ #expect(failUpload)
+ #expect(await calls.values() == ["modify"])
+ }
+ }
+
+ @Test func vaultUnsupportedFieldsAndDateAreNotFalselyAcknowledged() async throws {
+ let current = vaultCallbackItem(revision: 1)
+ let fields: NSFileProviderItemFields = [.tagData, .contentModificationDate]
+ let result = try await VaultModificationExecutor.execute(current: current, fields: fields,
+ requestsTrash: false, hasContents: false, baseContentRevision: current.contentRevision,
+ baseMetadataRevision: current.metadataRevision,
+ modify: { Issue.record("Unsupported fields must not perform a mutation"); return current },
+ trash: { _, _ in Issue.record("Unexpected trash") })
+ #expect(result.item == current && result.remainingFields == fields)
+ }
+
+ @Test func vaultMissingContentsCannotTrashOrModify() async throws {
+ let item = vaultCallbackItem(revision: 1)
+ await #expect(throws: (any Error).self) {
+ try await VaultModificationExecutor.execute(current: item, fields: [.contents, .parentItemIdentifier],
+ requestsTrash: true, hasContents: false, baseContentRevision: item.contentRevision,
+ baseMetadataRevision: item.metadataRevision,
+ modify: { Issue.record("Missing contents mutated metadata"); return item },
+ trash: { _, _ in Issue.record("Missing contents were discarded by trash") })
+ }
+ }
+}
+
+private actor VaultCallbackProbe {
+ var calls: [String] = []
+ func record(_ call: String) { calls.append(call) }
+ func values() -> [String] { calls }
+}
+private func vaultCallbackItem(revision: UInt8) -> VaultItem {
+ VaultItem(id: VaultItemIdentifier(rawValue: UUID(uuidString: "AAAAAAAA-0000-0000-0000-000000000003")!),
+ parentID: nil, filename: "Synthetic.txt", isDirectory: false,
+ createdAt: Date(timeIntervalSince1970: 1), modifiedAt: Date(timeIntervalSince1970: Double(revision)),
+ plaintextSize: 1, contentRevision: VaultRevision(data: Data(repeating: revision, count: 32))!,
+ metadataRevision: VaultRevision(data: Data(repeating: revision &+ 10, count: 32))!)
+}
diff --git a/potassiumProviderTests/ProviderActionItemResolverTests.swift b/potassiumProviderTests/ProviderActionItemResolverTests.swift
new file mode 100644
index 0000000..349920c
--- /dev/null
+++ b/potassiumProviderTests/ProviderActionItemResolverTests.swift
@@ -0,0 +1,100 @@
+import FileProvider
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@Suite("Action selection identity")
+struct ProviderActionItemResolverTests {
+ private let url = URL(fileURLWithPath: "/tmp/generated-action.txt")
+
+ private func resolver(item: String = "42", domain: String = "expected") -> ProviderActionItemResolver {
+ let url = url
+ return .init(visibleURL: { input, completion in
+ #expect(input == "__fp/fs/docID(123)" || input == "42")
+ completion(.success(url))
+ }, identifier: { resolvedURL, completion in
+ #expect(resolvedURL == url)
+ completion(.success(.init(itemIdentifier: item, domainIdentifier: domain)))
+ })
+ }
+
+ @Test(arguments: ["__fp/fs/docID(123)", "42"])
+ func resolvesBothOpaqueAndCanonicalSelectionsThroughExpectedDomain(_ selection: String) async throws {
+ #expect(try await resolver().resolve(selection, domainIdentifier: "expected", engine: .legacyPlaintext) == "42")
+ }
+
+ @Test func rejectsCrossDomainSelectionEvenWhenNumericIDMatches() async {
+ await #expect(throws: ProviderActionItemResolutionError.domainMismatch) {
+ try await resolver(domain: "different").resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext)
+ }
+ }
+
+ @Test(arguments: ["__fp/fs/docID(123)", "", "NSFileProviderTrashContainerItemIdentifier", "NSFileProviderWorkingSetContainerItemIdentifier"])
+ func unresolvedAndVirtualContainerResultsCannotLoadActions(_ item: String) async {
+ await #expect(throws: ProviderActionItemResolutionError.invalidIdentifier) {
+ try await resolver(item: item).resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext)
+ }
+ }
+
+ @Test func rejectsSameDomainReplacementOfACanonicalSelection() async {
+ await #expect(throws: ProviderActionItemResolutionError.invalidIdentifier) {
+ try await resolver(item: "43").resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext)
+ }
+ }
+
+ @Test func enginesStayIsolatedAndVaultIdentifiersRemainStable() async throws {
+ let vault = VaultItemIdentifier().fileProviderIdentifier
+ #expect(try await resolver(item: vault).resolve("__fp/fs/docID(123)", domainIdentifier: "expected", engine: .opaqueVaultV2) == vault)
+ #expect(throws: ProviderActionItemResolutionError.invalidIdentifier) {
+ try ProviderActionItemResolver.validate(vault, engine: .legacyPlaintext)
+ }
+ #expect(throws: ProviderActionItemResolutionError.invalidIdentifier) {
+ try ProviderActionItemResolver.validate("42", engine: .opaqueVaultV2)
+ }
+ #expect(throws: ProviderActionItemResolutionError.invalidIdentifier) {
+ try ProviderActionItemResolver.validate(vault, engine: .opaqueVaultV1)
+ }
+ #expect(try ProviderActionItemResolver.validate("42", engine: .legacyPlaintext) == "42")
+ }
+
+ @Test func failuresDoNotExposeSystemPathsOrIdentifiersOrPerformReverseLookup() async {
+ let resolver = ProviderActionItemResolver(visibleURL: { _, completion in
+ completion(.failure(NSError(domain: "test", code: 1, userInfo: [NSLocalizedDescriptionKey: "private path"])))
+ }, identifier: { _, _ in Issue.record("Reverse lookup must not run after failure") })
+ await #expect(throws: ProviderActionItemResolutionError.unavailable) {
+ try await resolver.resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext)
+ }
+ #expect(!ProviderActionItemResolutionError.unavailable.localizedDescription.contains("private path"))
+ }
+
+ @Test func rejectsNonFileURLs() async {
+ let resolver = ProviderActionItemResolver(visibleURL: { _, completion in
+ completion(.success(URL(string: "https://example.invalid")!))
+ }, identifier: { _, _ in Issue.record("Non-file URLs must not be resolved") })
+ await #expect(throws: ProviderActionItemResolutionError.unavailable) {
+ try await resolver.resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext)
+ }
+ }
+
+ @Test func boundsMissingSystemCallback() async {
+ let resolver = ProviderActionItemResolver(visibleURL: { _, _ in }, identifier: { _, _ in Issue.record("Must not resolve after timeout") })
+ await #expect(throws: ProviderActionItemResolutionError.timedOut) {
+ try await resolver.resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext, timeout: .milliseconds(10))
+ }
+ }
+
+ @Test func cancellationDuringReverseLookupIgnoresLateCompletion() async throws {
+ let registered = AsyncStream>.makeStream()
+ let url = url
+ let resolver = ProviderActionItemResolver(visibleURL: { _, completion in completion(.success(url)) }, identifier: { _, completion in
+ registered.continuation.yield(completion)
+ })
+ let task = Task { try await resolver.resolve("42", domainIdentifier: "expected", engine: .legacyPlaintext) }
+ var iterator = registered.stream.makeAsyncIterator()
+ let completion = try #require(await iterator.next())
+ task.cancel()
+ await #expect(throws: CancellationError.self) { try await task.value }
+ completion(.success(.init(itemIdentifier: "42", domainIdentifier: "expected")))
+ registered.continuation.finish()
+ }
+}
diff --git a/potassiumProviderTests/ProviderDiagnosticSpanTests.swift b/potassiumProviderTests/ProviderDiagnosticSpanTests.swift
new file mode 100644
index 0000000..867fb3d
--- /dev/null
+++ b/potassiumProviderTests/ProviderDiagnosticSpanTests.swift
@@ -0,0 +1,246 @@
+import Darwin
+import FileProvider
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+@preconcurrency import SQLite
+
+@Suite("Provider diagnostic spans")
+struct ProviderDiagnosticSpanTests {
+ @Test(arguments: [false, true])
+ func mappedSQLiteFailuresRetainOnlySafeCodeAndCategory(extended: Bool) async throws {
+ let sink = InMemoryDiagnosticSink(), canary = UUID().uuidString
+ let code: Int32 = extended ? 517 : 5
+ let original: SQLite.Result = extended ? .extendedError(message: canary, extendedCode: code, statement: nil) :
+ .error(message: canary, code: code, statement: nil)
+ let mapped = providerErrorMapping(original).mappedError
+ let span = await ProviderDiagnosticSpan.start(source: .fileProviderExtension, operation: .currentSyncAnchor, recorder: sink)
+ await span.fail(error: mapped)
+ let events = await sink.snapshot()
+ #expect(events.last?.errorClass == .storage)
+ #expect(events.last?.errorCode == Int(code))
+ #expect(!String(decoding: try JSONEncoder().encode(events), as: UTF8.self).contains(canary))
+ }
+ @Test func snapshotRaceClassificationOmitsPrivateIdentifiers() async throws {
+ let sink = InMemoryDiagnosticSink()
+ let span = await ProviderDiagnosticSpan.start(source: .fileProviderExtension, operation: .workingSetRefresh, recorder: sink)
+ await span.fail(error: KDriveSnapshotStoreError.staleSnapshot(domainIdentifier: "private-domain-sentinel", containerIdentifier: "private-container-sentinel"))
+ let events = await sink.snapshot()
+ #expect(events.last?.errorClass == .concurrentSnapshot)
+ let json = String(decoding: try JSONEncoder().encode(events), as: UTF8.self)
+ #expect(!json.contains("private-domain-sentinel"))
+ #expect(!json.contains("private-container-sentinel"))
+ }
+ @Test func startAndOnlyOneTerminalEventAreRecordedUnderRaces() async {
+ let sink = InMemoryDiagnosticSink()
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .modifyItem,
+ fieldShape: [.contents, .filename],
+ routeTemplate: .upload,
+ optionShape: [.conditionalETag],
+ recorder: sink
+ )
+
+ await withTaskGroup(of: Void.self) { group in
+ for index in 0..<30 {
+ group.addTask {
+ switch index % 3 {
+ case 0:
+ await span.complete(statusClass: .success)
+ case 1:
+ await span.fail(error: URLError(.timedOut))
+ default:
+ await span.cancel()
+ }
+ }
+ }
+ }
+
+ let events = await sink.snapshot()
+ #expect(events.filter { $0.phase == .started }.count == 1)
+ #expect(events.filter { [.completed, .failed, .cancelled].contains($0.phase) }.count == 1)
+ #expect(Set(events.map(\.correlationID)) == [span.correlationID])
+ #expect(Set(events.compactMap(\.spanID)) == [span.spanID])
+ }
+
+ @Test func recorderFailuresNeverChangeCallerFlow() async {
+ let sink = InMemoryDiagnosticSink(alwaysThrows: true)
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .fetchContents,
+ recorder: sink
+ )
+
+ await span.progress(fractionCompleted: 0.5)
+ await span.checkpoint(hasCursor: false, hasMore: true, hasAnchor: true)
+ await span.fail(error: URLError(.networkConnectionLost))
+
+ #expect(await sink.attemptCount() == 4)
+ }
+
+ @Test func nestedSpanInheritsTaskLocalCorrelation() async {
+ let callback = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .createItem
+ )
+
+ let nested = await callback.withCorrelation {
+ await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .uploadFile,
+ routeTemplate: .upload
+ )
+ }
+ let unrelated = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .uploadFile
+ )
+
+ #expect(nested.correlationID == callback.correlationID)
+ #expect(unrelated.correlationID != callback.correlationID)
+ #expect(nested.spanID != callback.spanID)
+ }
+
+ @Test func progressAndDurationAreBounded() async {
+ let sink = InMemoryDiagnosticSink()
+ let span = await ProviderDiagnosticSpan.start(
+ source: .finderRunner,
+ operation: .finderScenario,
+ recorder: sink
+ )
+
+ await span.progress(fractionCompleted: -10)
+ await span.progress(fractionCompleted: 0.01)
+ await span.progress(fractionCompleted: 0.349)
+ await span.progress(fractionCompleted: 0.399)
+ await span.progress(fractionCompleted: 10)
+ await span.progress(fractionCompleted: .infinity)
+ await span.complete()
+
+ let events = await sink.snapshot()
+ let progress = events.filter { $0.phase == .progress }
+ #expect(progress.map(\.progressPercentBucket) == [0, 30, 100, 0])
+ #expect(events.compactMap(\.durationMilliseconds).allSatisfy {
+ (0...ProviderDiagnosticSpan.maximumDurationMilliseconds).contains($0)
+ })
+ }
+
+ @Test func errorClassificationUsesOnlyClosedCategories() async throws {
+ #expect(ProviderDiagnosticErrorClassifier.classify(CancellationError()) == .cancellation)
+ #expect(ProviderDiagnosticErrorClassifier.classify(URLError(.notConnectedToInternet)) == .network)
+ #expect(ProviderDiagnosticErrorClassifier.classify(URLError(.userAuthenticationRequired)) == .authentication)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSError(domain: NSPOSIXErrorDomain, code: Int(ENOSPC))
+ ) == .storage)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ APIClientError.unacceptableStatusCode(429, body: "private-canary-429")
+ ) == .network)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ APIClientError.unacceptableStatusCode(507, body: "private-canary-507")
+ ) == .quota)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.notAuthenticated)
+ ) == .authentication)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.serverUnreachable)
+ ) == .network)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.insufficientQuota)
+ ) == .quota)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.cannotSynchronize)
+ ) == .synchronization)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.noSuchItem)
+ ) == .notFound)
+ #expect(ProviderDiagnosticErrorClassifier.classify(
+ NSFileProviderError(.syncAnchorExpired)
+ ) == .invalidCursor)
+
+ let privateCanary = "private-canary-7F01D30C/opaque"
+ let sink = InMemoryDiagnosticSink()
+ let span = await ProviderDiagnosticSpan.start(
+ source: .fileProviderExtension,
+ operation: .deleteItem,
+ fieldShape: [.filename],
+ optionShape: [.stableFileID],
+ recorder: sink
+ )
+ await span.fail(error: NSError(
+ domain: privateCanary,
+ code: 998,
+ userInfo: [NSLocalizedDescriptionKey: privateCanary]
+ ))
+
+ let data = try JSONEncoder().encode(await sink.snapshot())
+ let encoded = try #require(String(data: data, encoding: .utf8))
+ #expect(encoded.contains(privateCanary) == false)
+ #expect(encoded.contains("unknown"))
+ }
+
+ @Test func fileProviderFieldClassificationCoversEverySDKFieldAndTrash() {
+ let allFields: NSFileProviderItemFields = [
+ .contents,
+ .filename,
+ .parentItemIdentifier,
+ .lastUsedDate,
+ .tagData,
+ .favoriteRank,
+ .creationDate,
+ .contentModificationDate,
+ .fileSystemFlags,
+ .extendedAttributes,
+ .typeAndCreator,
+ ]
+
+ #expect(ProviderDiagnosticFieldClassifier.classify(allFields) == [
+ .contents,
+ .filename,
+ .parent,
+ .lastUsedDate,
+ .tagData,
+ .favoriteRank,
+ .creationDate,
+ .contentModificationDate,
+ .fileSystemFlags,
+ .extendedAttributes,
+ .typeAndCreator,
+ ])
+ #expect(ProviderDiagnosticFieldClassifier.classify(
+ .parentItemIdentifier,
+ isTrashDestination: true
+ ) == [.parent, .trash])
+ }
+}
+
+private actor InMemoryDiagnosticSink: ProviderDiagnosticRecording {
+ private let alwaysThrows: Bool
+ private var events: [ProviderDiagnosticEvent] = []
+ private var attempts = 0
+
+ init(alwaysThrows: Bool = false) {
+ self.alwaysThrows = alwaysThrows
+ }
+
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) throws {
+ attempts += 1
+ if alwaysThrows {
+ throw InMemoryDiagnosticSinkError.unavailable
+ }
+ events.append(event)
+ }
+
+ func snapshot() -> [ProviderDiagnosticEvent] {
+ events
+ }
+
+ func attemptCount() -> Int {
+ attempts
+ }
+}
+
+private enum InMemoryDiagnosticSinkError: Error {
+ case unavailable
+}
diff --git a/potassiumProviderTests/ProviderDiagnosticValidationFieldTests.swift b/potassiumProviderTests/ProviderDiagnosticValidationFieldTests.swift
new file mode 100644
index 0000000..ff1ed87
--- /dev/null
+++ b/potassiumProviderTests/ProviderDiagnosticValidationFieldTests.swift
@@ -0,0 +1,23 @@
+import Foundation
+import PotassiumChannelCore
+import PotassiumProviderCore
+import Testing
+
+struct ProviderDiagnosticValidationFieldTests {
+ @Test func onlyKnownFieldClassesSurviveValidationParsing() throws {
+ let error = APIClientError.unacceptableStatusCode(422,
+ body: "{\"error\":{\"errors\":{\"with\":[\"PRIVATE_CANARY\"],\"PRIVATE_FIELD\":\"PRIVATE_VALUE\",\"actions\":[]}}}",
+ metadata: APIResponseMetadata())
+ let fields = ProviderDiagnosticValidationField.classify(error)
+ #expect(fields == [.actions, .includedResources])
+ let encoded = String(decoding: try JSONEncoder().encode(fields), as: UTF8.self)
+ #expect(!encoded.contains("PRIVATE"))
+ }
+
+ @Test func unknownAndMalformedResponsesProduceNoFields() {
+ for body in ["with PRIVATE_CANARY", "{\"error\":{\"message\":\"with\"}}", "{\"PRIVATE_FIELD\":1}"] {
+ #expect(ProviderDiagnosticValidationField.classify(
+ APIClientError.unacceptableStatusCode(422, body: body, metadata: APIResponseMetadata())) == nil)
+ }
+ }
+}
diff --git a/potassiumProviderTests/ProviderSetupViewTests.swift b/potassiumProviderTests/ProviderSetupViewTests.swift
index 552a305..2e72561 100644
--- a/potassiumProviderTests/ProviderSetupViewTests.swift
+++ b/potassiumProviderTests/ProviderSetupViewTests.swift
@@ -128,6 +128,7 @@ struct ProviderSetupViewTests {
#expect(model.accounts == [account])
}
+ #if !STABILITY
@Test func duplicateAddIsGuardedAndActionStateClears() async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
@@ -174,6 +175,44 @@ struct ProviderSetupViewTests {
#expect(model.activeDriveAction(for: key) == nil)
#expect(model.isConfigured(accountIdentifier: account.accountIdentifier, driveID: drive.id))
}
+ #endif
+
+ #if STABILITY
+ @Test func ordinaryDomainAdditionIsRejectedByTheStabilityProfile() async throws {
+ let directory = FileManager.default.temporaryDirectory
+ .appendingPathComponent(UUID().uuidString, isDirectory: true)
+ defer { try? FileManager.default.removeItem(at: directory) }
+
+ let account = ProviderAccount(
+ accountIdentifier: "account-a",
+ displayName: "Account",
+ authenticationKind: .oauth
+ )
+ let drive = makeDrive(id: 10, name: "Projects", role: "admin")
+ let domainStore = DomainConfigurationFileStore(
+ directoryURL: directory.appendingPathComponent("Domains", isDirectory: true)
+ )
+ let registrar = StabilityProfileDomainRegistrar()
+ let model = PotassiumProviderAppModel(
+ accountStore: ProviderAccountFileStore(
+ directoryURL: directory.appendingPathComponent("Accounts", isDirectory: true)
+ ),
+ domainStore: domainStore,
+ tokenStore: InMemoryOAuthTokenStore(),
+ oauthAuthenticator: SetupTestOAuthAuthenticator(),
+ domainRegistrar: registrar,
+ automaticallyReloadStoredState: false,
+ initialAccounts: [account],
+ initialDrivesByAccountIdentifier: [account.accountIdentifier: [drive]]
+ )
+
+ await model.addDomain(accountIdentifier: account.accountIdentifier, drive: drive)
+
+ #expect(model.errorMessage == "The Stability build registers only a verified Stability Lab root. Use the Stability Lab tab.")
+ #expect(registrar.addCallCount == 0)
+ #expect(try await domainStore.allConfigurations().isEmpty)
+ }
+ #endif
@Test func explicitRenamePersistsOnlyTheSubmittedNormalizedName() async throws {
let directory = FileManager.default.temporaryDirectory
@@ -290,6 +329,19 @@ private struct SetupTestDomainRegistrar: ProviderDomainRegistering {
func removeDomain(for configuration: ProviderDomainConfiguration) async throws {}
}
+#if STABILITY
+@MainActor
+private final class StabilityProfileDomainRegistrar: ProviderDomainRegistering {
+ private(set) var addCallCount = 0
+
+ func addDomain(for configuration: ProviderDomainConfiguration) async throws {
+ addCallCount += 1
+ }
+
+ func removeDomain(for configuration: ProviderDomainConfiguration) async throws {}
+}
+#endif
+
private actor BlockingSetupAccountStore: ProviderAccountStoring {
private var accounts: [ProviderAccount]
private var hasStartedAllAccounts = false
diff --git a/potassiumProviderTests/ShareLinkSettingsVerificationTests.swift b/potassiumProviderTests/ShareLinkSettingsVerificationTests.swift
new file mode 100644
index 0000000..b29d8fe
--- /dev/null
+++ b/potassiumProviderTests/ShareLinkSettingsVerificationTests.swift
@@ -0,0 +1,43 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct ShareLinkSettingsVerificationTests {
+ enum Field: CaseIterable {
+ case access, expiration, downloads, comments, editing, accessRequests, information, statistics
+ }
+
+ @Test(arguments: Field.allCases)
+ func acknowledgedButUnappliedSettingIsRejected(_ field: Field) {
+ let requested = KDriveShareLinkConfiguration(access: .inherit,
+ validUntil: Date(timeIntervalSince1970: 1_700_000_000), allowsComments: true)
+ var returned = requested
+ switch field {
+ case .access: returned.access = .public
+ case .expiration: returned.validUntil = nil
+ case .downloads: returned.allowsDownload.toggle()
+ case .comments: returned.allowsComments.toggle()
+ case .editing: returned.allowsEditing.toggle()
+ case .accessRequests: returned.allowsAccessRequests.toggle()
+ case .information: returned.showsFileInformation.toggle()
+ case .statistics: returned.showsStatistics.toggle()
+ }
+ #expect(!requested.hasSameReportedSettings(as: returned))
+ }
+
+ @Test func unreportedPasswordAndSubsecondExpirationDoNotProduceFalseRejection() {
+ let requested = KDriveShareLinkConfiguration(access: .password, password: "synthetic-only",
+ validUntil: Date(timeIntervalSince1970: 1_700_000_000.75))
+ var returned = requested
+ returned.password = nil
+ returned.validUntil = Date(timeIntervalSince1970: 1_700_000_000)
+ #expect(requested.hasSameReportedSettings(as: returned))
+ returned.validUntil = returned.validUntil?.addingTimeInterval(1)
+ #expect(!requested.hasSameReportedSettings(as: returned))
+ }
+
+ @Test func unchangedAbsentExpirationAndSuccessfulDownloadRestrictionMatch() {
+ let requested = KDriveShareLinkConfiguration(access: .inherit, allowsDownload: false)
+ #expect(requested.hasSameReportedSettings(as: requested))
+ }
+}
diff --git a/potassiumProviderTests/SnapshotInitializationContentionTests.swift b/potassiumProviderTests/SnapshotInitializationContentionTests.swift
new file mode 100644
index 0000000..3a34602
--- /dev/null
+++ b/potassiumProviderTests/SnapshotInitializationContentionTests.swift
@@ -0,0 +1,50 @@
+import Foundation
+import PotassiumProviderCore
+@preconcurrency import SQLite
+import Synchronization
+import Testing
+
+struct SnapshotInitializationContentionTests {
+ @Test(.timeLimit(.minutes(1))) func openingSnapshotStoreWaitsForTemporaryWALLockAndPreservesData() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let url = directory.appendingPathComponent("Snapshots.sqlite3")
+ // SQLite's synchronous busy wait must not occupy the executor needed to
+ // release its test lock. Dedicated queues keep that contention real even
+ // when the simulator's cooperative pool has only one available thread.
+ let (store, waited): (KDriveSnapshotSQLiteStore, Duration) = try await withCheckedThrowingContinuation { continuation in
+ DispatchQueue(label: "snapshot-contention-open").async {
+ do {
+ let lock = try SnapshotInitializationLock(url: url)
+ defer { lock.release() }
+ DispatchQueue(label: "snapshot-contention-release").asyncAfter(deadline: .now() + .milliseconds(200)) {
+ lock.release()
+ }
+ let start = ContinuousClock.now
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: url)
+ continuation.resume(returning: (store, start.duration(to: .now)))
+ } catch { continuation.resume(throwing: error) }
+ }
+ }
+ #expect(waited >= .milliseconds(100))
+ let snapshot = KDriveSnapshot(anchor: "synthetic-anchor", items: [])
+ try await store.save(snapshot, domainIdentifier: "synthetic", containerIdentifier: "root")
+ #expect(try await store.snapshot(domainIdentifier: "synthetic", containerIdentifier: "root") == snapshot)
+ let verifier = try Connection(url.path)
+ #expect(try verifier.scalar("SELECT value FROM retained_fixture") as? String == "preserved")
+ }
+}
+
+private final class SnapshotInitializationLock: Sendable {
+ private let database: Mutex
+ init(url: URL) throws {
+ let database = try Connection(url.path)
+ try database.execute("PRAGMA locking_mode=EXCLUSIVE")
+ try database.execute("PRAGMA journal_mode=WAL")
+ try database.execute("CREATE TABLE retained_fixture(value TEXT)")
+ try database.run("INSERT INTO retained_fixture VALUES (?)", "preserved")
+ self.database = Mutex(database)
+ }
+ func release() { database.withLock { $0 = nil } }
+}
diff --git a/potassiumProviderTests/SnapshotWriteContentionTests.swift b/potassiumProviderTests/SnapshotWriteContentionTests.swift
new file mode 100644
index 0000000..6e45d2c
--- /dev/null
+++ b/potassiumProviderTests/SnapshotWriteContentionTests.swift
@@ -0,0 +1,75 @@
+import Foundation
+import PotassiumProviderCore
+@preconcurrency import SQLite
+import Synchronization
+import Testing
+
+struct SnapshotWriteContentionTests {
+ enum Mutation: CaseIterable, Sendable {
+ case snapshotSave, pollClaim, pollCommit, mutationPublication
+ }
+
+ @Test(.timeLimit(.minutes(1)), arguments: Mutation.allCases)
+ func temporaryWriterDoesNotFailReadThenWriteTransaction(mutation: Mutation) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let url = directory.appendingPathComponent("Snapshots.sqlite3")
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: url)
+ let base = ConflictTestRemote.item(3, name: "Original.txt", parent: 1)
+ let changed = ConflictTestRemote.item(3, name: "Renamed.txt", parent: 1)
+ let time = Date(timeIntervalSince1970: 100)
+ let initial = KDriveSnapshot(anchor: "initial", items: [base])
+ try await store.save(initial, domainIdentifier: "synthetic", containerIdentifier: "1")
+ let workingSet = try await store.commitWorkingSetPoll(domainIdentifier: "synthetic",
+ containerSnapshotUpdates: [], items: [base],
+ changes: .init(updatedItems: [base], deletedItemIDs: []), completedAt: time)
+
+ // WAL readers can proceed while this independent connection owns the
+ // write lock. Upgrading a deferred read transaction then fails at once,
+ // even with busy_timeout. A write reservation must precede those reads.
+ let writer = try SnapshotTemporaryWriter(url: url)
+ defer { writer.release() }
+ // Release on a dedicated queue: SQLite's synchronous busy handler must
+ // not block the cooperative executor needed to release its own fixture.
+ DispatchQueue(label: "snapshot-write-contention-release").asyncAfter(deadline: .now() + .milliseconds(500)) {
+ writer.release()
+ }
+ let next = time.addingTimeInterval(100)
+ let updated = KDriveSnapshot(anchor: "updated", items: [changed])
+ switch mutation {
+ case .snapshotSave:
+ try await store.save(updated, domainIdentifier: "synthetic", containerIdentifier: "1",
+ condition: .matching(anchor: initial.anchor, serverCursor: nil))
+ #expect(try await store.snapshot(domainIdentifier: "synthetic", containerIdentifier: "1") == updated)
+ case .pollClaim:
+ #expect(try await store.claimWorkingSetPoll(domainIdentifier: "synthetic", now: next, minimumInterval: 60))
+ #expect(try await store.claimWorkingSetPoll(domainIdentifier: "synthetic", now: next, minimumInterval: 60) == false)
+ case .pollCommit:
+ let result = try await store.commitWorkingSetPoll(domainIdentifier: "synthetic",
+ containerSnapshotUpdates: [.init(containerIdentifier: "1", snapshot: updated,
+ condition: .matching(anchor: initial.anchor, serverCursor: nil))],
+ items: [changed], changes: .init(updatedItems: [changed], deletedItemIDs: []),
+ completedAt: next, condition: .matchingAnchor(workingSet.anchor))
+ #expect(result.items == [changed] && result.anchor != workingSet.anchor)
+ #expect(try await store.snapshot(domainIdentifier: "synthetic", containerIdentifier: "1") == updated)
+ case .mutationPublication:
+ #expect(try await store.publishKnownWorkingSetItem(changed, replacing: base,
+ domainIdentifier: "synthetic", recordedAt: next))
+ #expect(try await store.workingSetChanges(domainIdentifier: "synthetic", from: workingSet.anchor)?.changes.updatedItems == [changed])
+ }
+ let expectedPollTime = mutation == .pollCommit ? next : time
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "synthetic") == expectedPollTime)
+ }
+}
+
+private final class SnapshotTemporaryWriter: Sendable {
+ private let database: Mutex
+
+ init(url: URL) throws {
+ let database = try Connection(url.path)
+ try database.execute("BEGIN IMMEDIATE")
+ self.database = Mutex(database)
+ }
+
+ func release() { database.withLock { $0 = nil } }
+}
diff --git a/potassiumProviderTests/StabilityActionPanelIdentityTests.swift b/potassiumProviderTests/StabilityActionPanelIdentityTests.swift
new file mode 100644
index 0000000..78f3ff0
--- /dev/null
+++ b/potassiumProviderTests/StabilityActionPanelIdentityTests.swift
@@ -0,0 +1,46 @@
+#if STABILITY
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@MainActor
+struct StabilityActionPanelIdentityTests {
+ @Test func mainActorCallerDoesNotRunDiskLookupOnTheUIThread() async throws {
+ let alias = UUID()
+ let result = try await StabilityActionPanelIdentity.resolve(for: "42") { identifier in
+ #expect(!Thread.isMainThread)
+ #expect(identifier == "42")
+ return alias
+ }
+ #expect(result == alias)
+ }
+
+ @Test func missingActiveRunDoesNotInventAnAlias() async throws {
+ #expect(try await StabilityActionPanelIdentity.resolve(for: "42", lookup: { _ in nil }) == nil)
+ }
+
+ @Test func blockedLookupExpiresWithoutWaitingForTheDiskReply() async {
+ // Hold the synchronous lookup until the waiter has actually expired.
+ // Relative sleeps raced under the full parallel CI test load.
+ let release = DispatchSemaphore(value: 0)
+ defer { release.signal() }
+ await #expect(throws: StabilityDeadlineError.expired) {
+ try await StabilityActionPanelIdentity.resolve(for: "42", timeout: .milliseconds(10)) { _ in
+ release.wait()
+ return UUID()
+ }
+ }
+ }
+
+ @Test func cancelledRequestDoesNotStartDiskLookup() async {
+ let task = Task {
+ withUnsafeCurrentTask { $0?.cancel() }
+ return try await StabilityActionPanelIdentity.resolve(for: "42") { _ in
+ Issue.record("Cancelled lookup must not access the shared store")
+ return UUID()
+ }
+ }
+ await #expect(throws: CancellationError.self) { try await task.value }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityActionRegistrationTests.swift b/potassiumProviderTests/StabilityActionRegistrationTests.swift
new file mode 100644
index 0000000..7c9feb6
--- /dev/null
+++ b/potassiumProviderTests/StabilityActionRegistrationTests.swift
@@ -0,0 +1,28 @@
+#if os(macOS) && STABILITY
+import Foundation
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct StabilityActionRegistrationTests {
+ let identifier = "test.provider.Actions"
+ let expected = URL(fileURLWithPath: "/tmp/Chosen App.app/Contents/PlugIns/Actions.appex")
+ func line(path: String) -> String { " test.provider.Actions(1.0)\t00000000-0000-0000-0000-000000000001\t2026-09-11 10:00:00 +0000\t" + path }
+
+ @Test func exactUniqueRegistrationPasses() throws {
+ try StabilityActionRegistration.validate(listing: line(path: expected.path) + "\n (1 plug-in)\n", identifier: identifier, expectedURL: expected)
+ }
+
+ @Test func duplicatesWrongPathsAndMalformedDiscoveryFailClosed() {
+ let correct = line(path: expected.path)
+ for listing in ["", " (0 plug-ins)", line(path: "/Applications/Older.app/Contents/PlugIns/Actions.appex"),
+ correct + "\n" + line(path: "/Applications/Older.app/Contents/PlugIns/Actions.appex"),
+ correct + "\n" + correct, "test.provider.Actions(1.0) " + expected.path,
+ correct.replacingOccurrences(of: identifier, with: "different.provider.Actions")] {
+ #expect(throws: StabilityActionRegistrationError.self) {
+ try StabilityActionRegistration.validate(listing: listing, identifier: identifier, expectedURL: expected)
+ }
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityAppGroupProvisioningTests.swift b/potassiumProviderTests/StabilityAppGroupProvisioningTests.swift
new file mode 100644
index 0000000..e8809aa
--- /dev/null
+++ b/potassiumProviderTests/StabilityAppGroupProvisioningTests.swift
@@ -0,0 +1,59 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+struct StabilityAppGroupProvisioningTests {
+ private let bundleIdentifier = "test.provider.Actions"
+ private let now = Date(timeIntervalSince1970: 1_000)
+ private var entitlements: [String: Any] { [
+ "com.apple.application-identifier": "TESTTEAM.test.provider.Actions",
+ "com.apple.developer.team-identifier": "TESTTEAM",
+ "com.apple.security.application-groups": [ProviderConstants.appGroupIdentifier],
+ ] }
+ private func profile(_ grants: [String: Any]) -> [String: Any] {
+ ["Entitlements": grants, "ExpirationDate": now.addingTimeInterval(60)]
+ }
+
+ @Test func explicitIdentityWithAuthorizedGroupPasses() throws {
+ try StabilityAppGroupProvisioning.validate(entitlements: entitlements, profile: profile(entitlements), bundleIdentifier: bundleIdentifier, now: now)
+ }
+
+ @Test(arguments: ["missing", "different", "wildcard"])
+ func claimedGroupWithoutExactProfileAuthorizationFails(kind: String) {
+ var grants = entitlements
+ grants["com.apple.security.application-groups"] = switch kind {
+ case "missing": nil
+ case "different": ["group.test.unrelated"]
+ default: ["TESTTEAM.*"]
+ }
+ #expect(throws: StabilityAppGroupProvisioningError.unauthorizedGroup) {
+ try StabilityAppGroupProvisioning.validate(entitlements: entitlements, profile: profile(grants), bundleIdentifier: bundleIdentifier, now: now)
+ }
+ }
+
+ @Test(arguments: ["TESTTEAM.*", "TESTTEAM.test.other", "$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)"])
+ func wildcardWrongOrUnexpandedApplicationIdentityFails(identity: String) {
+ var grants = entitlements
+ grants["com.apple.application-identifier"] = identity
+ #expect(throws: StabilityAppGroupProvisioningError.invalidApplicationIdentity) {
+ try StabilityAppGroupProvisioning.validate(entitlements: entitlements, profile: profile(grants), bundleIdentifier: bundleIdentifier, now: now)
+ }
+ }
+
+ @Test func expiredMalformedAndUnsignedGroupClaimsFail() {
+ #expect(throws: StabilityAppGroupProvisioningError.expiredProfile) {
+ try StabilityAppGroupProvisioning.validate(entitlements: entitlements, profile: profile(entitlements), bundleIdentifier: bundleIdentifier, now: now.addingTimeInterval(60))
+ }
+ #expect(throws: StabilityAppGroupProvisioningError.unreadableProfile) {
+ try StabilityAppGroupProvisioning.validate(entitlements: entitlements, profile: [:], bundleIdentifier: bundleIdentifier, now: now)
+ }
+ var missing = entitlements
+ missing.removeValue(forKey: "com.apple.security.application-groups")
+ #expect(throws: StabilityAppGroupProvisioningError.unauthorizedGroup) {
+ try StabilityAppGroupProvisioning.validate(entitlements: missing, profile: profile(entitlements), bundleIdentifier: bundleIdentifier, now: now)
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityConflictBarrierTests.swift b/potassiumProviderTests/StabilityConflictBarrierTests.swift
new file mode 100644
index 0000000..f79afc9
--- /dev/null
+++ b/potassiumProviderTests/StabilityConflictBarrierTests.swift
@@ -0,0 +1,134 @@
+#if STABILITY
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@Suite("Live conflict scheduling barrier")
+struct StabilityConflictBarrierTests {
+ private func makeRun() throws -> StabilityRunHandle {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
+ return StabilityRunHandle(runID: UUID(), directoryURL: directory)
+ }
+
+ @Test func oldAttemptCannotReleaseNextCaseAndWrongPointCannotArrive() async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ let old = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ try StabilityConflictBarrier.release(old, run: run, activeRunID: run.runID)
+ let next = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation,
+ caseID: .renameRename, activeRunID: run.runID)
+ #expect(old.attemptID != next.attemptID)
+ #expect(throws: CancellationError.self) { try StabilityConflictBarrier.release(old, run: run, activeRunID: run.runID) }
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: correlation,
+ point: .afterContentPreflight, activeRun: { run })
+ #expect(!StabilityConflictBarrier.reached(next, run: run))
+ #expect(throws: CancellationError.self) {
+ try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ }
+ try StabilityConflictBarrier.release(next, run: run, activeRunID: run.runID)
+ }
+
+ @Test(arguments: [false, true])
+ func competingMutationProofRequiresTheHeldAttempt(cancel: Bool) async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ let ticket = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation,
+ activeRunID: run.runID)
+ #expect(!StabilityConflictBarrier.competingMutationVerified(ticket, run: run))
+ #expect(throws: CancellationError.self) {
+ try StabilityConflictBarrier.recordVerifiedCompetingMutation(ticket, run: run,
+ itemIdentifier: "fixture", metadataAlias: UUID(), activeRunID: run.runID)
+ }
+ let task = Task { try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture",
+ correlationID: correlation, activeRun: { run }) }
+ defer { task.cancel() }
+ let end = ContinuousClock.now.advanced(by: .seconds(2))
+ while !StabilityConflictBarrier.reached(ticket, run: run), ContinuousClock.now < end {
+ try await Task.sleep(for: .milliseconds(5))
+ }
+ #expect(throws: CancellationError.self) {
+ try StabilityConflictBarrier.recordVerifiedCompetingMutation(ticket, run: run,
+ itemIdentifier: "unrelated", metadataAlias: UUID(), activeRunID: run.runID)
+ }
+ try StabilityConflictBarrier.recordVerifiedCompetingMutation(ticket, run: run,
+ itemIdentifier: "fixture", metadataAlias: UUID(), activeRunID: run.runID)
+ #expect(StabilityConflictBarrier.competingMutationVerified(ticket, run: run))
+ if cancel {
+ task.cancel()
+ await #expect(throws: CancellationError.self) { try await task.value }
+ #expect(!StabilityConflictBarrier.competingMutationVerified(ticket, run: run))
+ } else {
+ try StabilityConflictBarrier.release(ticket, run: run, activeRunID: run.runID)
+ try await task.value
+ #expect(throws: CancellationError.self) {
+ try StabilityConflictBarrier.recordVerifiedCompetingMutation(ticket, run: run,
+ itemIdentifier: "fixture", metadataAlias: UUID(), activeRunID: run.runID)
+ }
+ let next = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation,
+ activeRunID: run.runID)
+ #expect(!StabilityConflictBarrier.competingMutationVerified(next, run: run))
+ }
+ }
+
+ @Test func expiredAttemptDoesNotBlockTheNextCase() async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ let first = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ await #expect(throws: CancellationError.self) {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: correlation,
+ budget: .milliseconds(5), activeRun: { run })
+ }
+ await #expect(throws: CancellationError.self) {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: correlation, activeRun: { run })
+ }
+ let second = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ #expect(first.attemptID != second.attemptID)
+ #expect(!StabilityConflictBarrier.reached(second, run: run))
+ }
+
+ @Test func unrelatedItemsAndCorrelationsNeverReachTheBarrier() async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "other", correlationID: correlation, activeRun: { run })
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: UUID(), activeRun: { run })
+ #expect(!StabilityConflictBarrier.reached(run: run))
+ #expect(throws: CancellationError.self) {
+ try StabilityConflictBarrier.release(run: run, activeRunID: UUID())
+ }
+ }
+
+ @Test(arguments: [false, true])
+ func reachedBarrierCanBeReleasedOrCancelled(cancel: Bool) async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ let task = Task { try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: correlation, activeRun: { run }) }
+ defer { task.cancel() }
+ let deadline = ContinuousClock.now.advanced(by: .seconds(2))
+ while !StabilityConflictBarrier.reached(run: run), ContinuousClock.now < deadline {
+ try await Task.sleep(for: .milliseconds(5))
+ }
+ #expect(StabilityConflictBarrier.reached(run: run))
+ if cancel {
+ task.cancel()
+ await #expect(throws: CancellationError.self) { try await task.value }
+ } else {
+ try StabilityConflictBarrier.release(run: run, activeRunID: run.runID)
+ try await task.value
+ }
+ }
+
+ @Test func anUnreleasedBarrierExpiresWithoutProceeding() async throws {
+ let run = try makeRun(), correlation = UUID()
+ defer { try? FileManager.default.removeItem(at: run.directoryURL) }
+ try StabilityConflictBarrier.arm(run: run, itemIdentifier: "fixture", correlationID: correlation, activeRunID: run.runID)
+ await #expect(throws: CancellationError.self) {
+ try await StabilityConflictBarrier.arriveIfArmed(itemIdentifier: "fixture", correlationID: correlation,
+ budget: .milliseconds(20), activeRun: { run })
+ }
+ #expect(StabilityConflictBarrier.reached(run: run))
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityConflictProfileTests.swift b/potassiumProviderTests/StabilityConflictProfileTests.swift
new file mode 100644
index 0000000..e7b1c40
--- /dev/null
+++ b/potassiumProviderTests/StabilityConflictProfileTests.swift
@@ -0,0 +1,54 @@
+#if STABILITY
+import Foundation
+@testable import PotassiumProviderCore
+import Testing
+
+struct StabilityConflictProfileTests {
+ @Test func selectedCaseCannotBecomeFullFinderAcceptanceAndNeedsItsOwnGate() throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let run = StabilityRunHandle(runID: UUID(), directoryURL: directory)
+ let correlation = UUID(), selected = StabilityLiveConflictCase.contentAfterPreflight
+ let ticket = try StabilityConflictBarrier.arm(run: run, itemIdentifier: "synthetic", correlationID: correlation,
+ caseID: selected, activeRunID: run.runID)
+ let profile = StabilityConflictProfile(runID: run.runID, selectedCase: selected)
+ let time = Date(timeIntervalSince1970: 1_800_000_000)
+ let proof = StabilityLiveStepEvidence(subjects: [ticket.subject], observedUIActions: 3,
+ expectedExtensionCodeHash: String(repeating: "a", count: 40), conflictBarrierReached: true)
+ let steps = StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ let chosen = scenario == selected.scenario
+ return StabilityFinderStepResult(sequenceNumber: UInt16(index + 1), scenario: scenario,
+ correlationID: chosen ? correlation : UUID(), startedAt: time, finishedAt: time,
+ outcome: chosen ? .passed : .skipped(.notSelectedForConflictProfile),
+ assertions: StabilityFinderAssertionClass.allCases.map { StabilityFinderAssertionResult(assertionClass: $0,
+ outcome: chosen ? .passed : .notEvaluated(.stepSkipped)) }, liveEvidence: chosen ? proof : nil)
+ }
+ let report = try StabilityFinderRunReport(schemaVersion: StabilityFinderRunReport.liveSchemaVersion,
+ correlationID: UUID(), startedAt: time, finishedAt: time,
+ preflightResults: StabilityFinderPreflightCheck.allCases.map { .init(check: $0, outcome: .passed, recordedAt: time) }, stepResults: steps)
+ let callback = ProviderDiagnosticEvent(subjectAlias: ticket.subject, correlationID: correlation,
+ source: .fileProviderExtension, operation: .modifyItem, phase: .completed)
+ try profile.validate(report: report, ticket: ticket, reached: true, released: true, competingMutationVerified: true, diagnostics: [callback])
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try profile.validate(report: report, ticket: ticket, reached: true, released: true, diagnostics: [callback])
+ }
+ #expect(report.stepSummary.passed == 1 && report.stepSummary.skipped == 15)
+ for (reached, released) in [(false, true), (true, false), (false, false)] {
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try profile.validate(report: report, ticket: ticket, reached: reached, released: released, competingMutationVerified: true, diagnostics: [callback])
+ }
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try profile.validate(report: report, ticket: nil, reached: true, released: true, competingMutationVerified: true, diagnostics: [callback])
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try profile.validate(report: report, ticket: ticket, reached: true, released: true, competingMutationVerified: true, diagnostics: [])
+ }
+ let wrongRun = StabilityConflictProfile(runID: UUID(), selectedCase: selected)
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try wrongRun.validate(report: report, ticket: ticket, reached: true, released: true, competingMutationVerified: true, diagnostics: [callback])
+ }
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityDeadlineTests.swift b/potassiumProviderTests/StabilityDeadlineTests.swift
new file mode 100644
index 0000000..afff158
--- /dev/null
+++ b/potassiumProviderTests/StabilityDeadlineTests.swift
@@ -0,0 +1,55 @@
+import Foundation
+import Testing
+import PotassiumProviderCore
+
+@Suite("Stability deadlines")
+struct StabilityDeadlineTests {
+ @Test func operatorPausePreservesBudgetAndRepeatedResumeDoesNotExtendIt() {
+ let start = ContinuousClock.now
+ var deadline = StabilityDeadline(budget: .seconds(90), now: start)
+ deadline.pause(now: start.advanced(by: .seconds(10)))
+ deadline.pause(now: start.advanced(by: .seconds(20)))
+ #expect(deadline.remaining(now: start.advanced(by: .seconds(800))) == .seconds(80))
+ deadline.resume(now: start.advanced(by: .seconds(800)))
+ deadline.resume(now: start.advanced(by: .seconds(801)))
+ #expect(deadline.remaining(now: start.advanced(by: .seconds(801))) == .seconds(79))
+ #expect(deadline.remaining(now: start.advanced(by: .seconds(900))) == .zero)
+ }
+
+ @Test func callbackTimeoutAndLateCompletionAreTerminalOnce() async {
+ let waiter = StabilityCallbackWaiter()
+ await #expect(throws: StabilityDeadlineError.expired) {
+ try await waiter.wait(timeout: .milliseconds(10)) { completion in
+ Task {
+ try? await Task.sleep(for: .milliseconds(30))
+ completion(.success(1))
+ completion(.success(2))
+ }
+ }
+ }
+ }
+
+ @Test func callbackSuccessCancelsTimerAndDuplicateCompletionIsIgnored() async throws {
+ let waiter = StabilityCallbackWaiter()
+ let result = try await waiter.wait(timeout: .seconds(1)) { completion in
+ completion(.success(42))
+ completion(.success(43))
+ }
+ #expect([42, 43].contains(result))
+ }
+
+ @Test func concurrentWaitCannotReplaceTheFirstContinuation() async throws {
+ let waiter = StabilityCallbackWaiter()
+ let registered = AsyncStream.makeStream()
+ let first = Task {
+ try await waiter.wait(timeout: .seconds(5)) { _ in registered.continuation.yield(()) }
+ }
+ for await _ in registered.stream { break }
+ await #expect(throws: StabilityDeadlineError.waiterAlreadyInUse) {
+ try await waiter.wait { _ in Issue.record("Second callback must not be registered") }
+ }
+ first.cancel()
+ await #expect(throws: CancellationError.self) { try await first.value }
+ registered.continuation.finish()
+ }
+}
diff --git a/potassiumProviderTests/StabilityDiagnosticSettlementTests.swift b/potassiumProviderTests/StabilityDiagnosticSettlementTests.swift
new file mode 100644
index 0000000..6be77ac
--- /dev/null
+++ b/potassiumProviderTests/StabilityDiagnosticSettlementTests.swift
@@ -0,0 +1,59 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct StabilityDiagnosticSettlementTests {
+ private func event(_ phase: ProviderDiagnosticPhase, span: UUID) -> ProviderDiagnosticEvent {
+ ProviderDiagnosticEvent(spanID: span, correlationID: span, source: .fileProviderExtension, operation: .modifyItem, phase: phase)
+ }
+
+ @Test func pendingWorkAndNewEventsRestartTheQuietPeriod() {
+ var fence = StabilityDiagnosticSettlement()
+ let now = ContinuousClock.now, id = UUID(), second = UUID()
+ let start = event(.started, span: id), end = event(.completed, span: id)
+ func check(_ events: [ProviderDiagnosticEvent], at instant: ContinuousClock.Instant, expecting expected: Bool = false) {
+ let settled = fence.observe(events, at: instant)
+ #expect(settled == expected)
+ }
+ check([start], at: now)
+ check([start], at: now.advanced(by: .seconds(20)))
+ check([start, end], at: now.advanced(by: .seconds(21)))
+ check([start, end], at: now.advanced(by: .milliseconds(21_500)))
+ let otherStart = event(.started, span: second), otherEnd = event(.completed, span: second)
+ check([start, end, otherStart], at: now.advanced(by: .seconds(22)))
+ let complete = [start, end, otherStart, otherEnd]
+ check(complete, at: now.advanced(by: .seconds(23)))
+ check(complete, at: now.advanced(by: .seconds(24)), expecting: true)
+ }
+
+ @Test func missingStartsAndContradictoryTerminalsNeverSettle() {
+ let id = UUID(), now = ContinuousClock.now
+ let start = event(.started, span: id), end = event(.completed, span: id)
+ for events in [[end], [start, end, end], [start, start, end]] {
+ var fence = StabilityDiagnosticSettlement()
+ let initial = fence.observe(events, at: now)
+ let later = fence.observe(events, at: now.advanced(by: .seconds(100)))
+ #expect(!initial && !later)
+ }
+ }
+
+ @Test func deliveredMembersDoNotSettleAnOlderCallbackBeforeItsTerminal() {
+ let oldStep = UUID(), callback = UUID(), member = UUID(), metadata = UUID(), now = ContinuousClock.now
+ let start = ProviderDiagnosticEvent(spanID: callback, correlationID: oldStep,
+ source: .fileProviderExtension, operation: .enumerateChanges, phase: .started)
+ let delivered = [ProviderDiagnosticPhase.started, .completed].map {
+ ProviderDiagnosticEvent(spanID: member, parentSpanID: callback, itemMetadataAlias: $0 == .completed ? metadata : nil,
+ correlationID: oldStep, source: .fileProviderExtension, operation: .workingSetRefresh, phase: $0)
+ }
+ let end = ProviderDiagnosticEvent(spanID: callback, correlationID: oldStep,
+ source: .fileProviderExtension, operation: .enumerateChanges, phase: .completed)
+ var fence = StabilityDiagnosticSettlement()
+ let initiallySettled = fence.observe([start] + delivered, at: now)
+ let settledWithoutParent = fence.observe([start] + delivered, at: now.advanced(by: .seconds(30)))
+ let settled = [start] + delivered + [end]
+ let beforeQuietPeriod = fence.observe(settled, at: now.advanced(by: .seconds(31)))
+ let afterQuietPeriod = fence.observe(settled, at: now.advanced(by: .seconds(32)))
+ #expect(!initiallySettled && !settledWithoutParent && !beforeQuietPeriod && afterQuietPeriod)
+ #expect(settled.allSatisfy { $0.correlationID == oldStep })
+ }
+}
diff --git a/potassiumProviderTests/StabilityDiagnosticTailTests.swift b/potassiumProviderTests/StabilityDiagnosticTailTests.swift
new file mode 100644
index 0000000..b0a1a1b
--- /dev/null
+++ b/potassiumProviderTests/StabilityDiagnosticTailTests.swift
@@ -0,0 +1,96 @@
+#if os(macOS) && STABILITY
+import Darwin
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@MainActor
+struct StabilityDiagnosticTailTests {
+ @Test func registrationExcludesHistoryAndRetainsImmediateAppend() async throws {
+ let (directory, run, store) = try await fixture()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let first = event(), second = event()
+ try await store.recordDiagnostic(first)
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ try await store.recordDiagnostic(second)
+ #expect(try tail.readAvailable().map(\.id) == [second.id])
+ #expect(try tail.readAvailable().isEmpty)
+ #expect(try StabilityRunCoordinator.readDiagnosticEvents(from: run.eventsURL).map(\.id) == [first.id, second.id])
+ }
+
+ @Test func incompleteRecordIsRetainedUntilNewline() async throws {
+ let (directory, run, store) = try await fixture()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let sample = event()
+ try await store.recordDiagnostic(sample)
+ let record = try Data(contentsOf: run.eventsURL)
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ try append(Data(record.dropLast()), to: run.eventsURL)
+ #expect(try tail.readAvailable().isEmpty)
+ try append(Data([0x0A]), to: run.eventsURL)
+ #expect(try tail.readAvailable().map(\.id) == [sample.id])
+ #expect(try tail.readAvailable().isEmpty)
+ }
+
+ @Test(arguments: ["replace", "truncate", "rewrite", "symlink", "health"])
+ func evidenceChangesFailClosed(change: String) async throws {
+ let (directory, run, store) = try await fixture()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ try await store.recordDiagnostic(event())
+ let original = try Data(contentsOf: run.eventsURL)
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ switch change {
+ case "replace": try original.write(to: run.eventsURL, options: .atomic)
+ case "truncate":
+ let handle = try FileHandle(forWritingTo: run.eventsURL)
+ try handle.truncate(atOffset: 0); try handle.close()
+ case "rewrite":
+ let handle = try FileHandle(forWritingTo: run.eventsURL)
+ try handle.write(contentsOf: Data(repeating: 0x20, count: original.count)); try handle.close()
+ case "symlink":
+ let other = run.directoryURL.appendingPathComponent("other.jsonl")
+ try original.write(to: other)
+ try FileManager.default.removeItem(at: run.eventsURL)
+ try FileManager.default.createSymbolicLink(at: run.eventsURL, withDestinationURL: other)
+ default: try Data().write(to: run.directoryURL.appendingPathComponent("diagnostic-health.failed"))
+ }
+ #expect(throws: StabilityDiagnosticTailError.self) { try tail.readAvailable() }
+ }
+
+ @Test func busyWriterDoesNotBlockOrConsumeAnEvent() async throws {
+ let (directory, run, store) = try await fixture()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ let fd = Darwin.open(run.eventsURL.path, O_RDWR)
+ defer { Darwin.close(fd) }
+ #expect(flock(fd, LOCK_EX) == 0)
+ #expect(try tail.readAvailable().isEmpty)
+ #expect(flock(fd, LOCK_UN) == 0)
+ let sample = event()
+ try await store.recordDiagnostic(sample)
+ #expect(try tail.readAvailable().map(\.id) == [sample.id])
+ }
+
+ @Test func malformedCompleteRecordIsNotIgnored() async throws {
+ let (directory, run, _) = try await fixture()
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let tail = try StabilityDiagnosticTail(eventsURL: run.eventsURL)
+ try append(Data("{malformed}\n".utf8), to: run.eventsURL)
+ #expect(throws: ProviderDiagnosticStoreError.corruptRecord(line: 1)) { try tail.readAvailable() }
+ }
+
+ private func event() -> ProviderDiagnosticEvent {
+ ProviderDiagnosticEvent(correlationID: UUID(), source: .fileProviderExtension, operation: .fetchContents, phase: .started)
+ }
+ private func append(_ bytes: Data, to url: URL) throws {
+ let handle = try FileHandle(forWritingTo: url)
+ try handle.seekToEnd(); try handle.write(contentsOf: bytes); try handle.close()
+ }
+ private func fixture() async throws -> (URL, StabilityRunHandle, KDriveProviderEventJSONLStore) {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: directory)
+ let run = try await coordinator.startRun(buildRevision: nil)
+ return (directory, run, try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL))
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityDiagnosticsTests.swift b/potassiumProviderTests/StabilityDiagnosticsTests.swift
new file mode 100644
index 0000000..2c6510e
--- /dev/null
+++ b/potassiumProviderTests/StabilityDiagnosticsTests.swift
@@ -0,0 +1,496 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+@Suite("Stability diagnostics")
+struct StabilityDiagnosticsTests {
+ @Test func compilationConditionSelectsTheExpectedRuntimeProfile() {
+ #if STABILITY
+ #expect(ProviderRuntimeProfile.current == .stability)
+ #else
+ #expect(ProviderRuntimeProfile.current == .standard)
+ #endif
+ }
+
+ @Test func jsonlStoreReplaysSanitizedActivityAndConflictEvents() async throws {
+ let (_, run, store) = try await makeRun()
+ let secret = "PRIVATE_CANARY_7f01d30c_customer_name"
+
+ try await store.recordActivity(KDriveProviderActivityEvent(
+ domainIdentifier: "account-domain-42",
+ driveID: 9001,
+ kind: .modify,
+ outcome: .failure,
+ severity: .error,
+ itemIdentifier: "remote-item-77",
+ itemName: secret,
+ itemPath: "/customer/private/file.txt",
+ summary: "Failed \(secret)",
+ diagnostic: KDriveProviderActivityErrorDiagnostic(
+ errorCategory: .api,
+ recoverySuggestion: secret,
+ diagnosticSummary: secret
+ ),
+ correlationID: UUID().uuidString,
+ networkOperation: "replaceFile",
+ remoteRequestID: "private-request-id"
+ ))
+ try await store.saveConflict(KDriveConflictEvent(
+ domainIdentifier: "account-domain-42",
+ driveID: 9001,
+ operation: .conflict,
+ originalItemIdentifier: "remote-item-77",
+ originalItemName: secret,
+ originalItemPath: "/customer/private/file.txt",
+ resolutionState: .blockedRetryable,
+ automaticallyResolved: false,
+ resolutionKind: .retainedStagedUploadAfterFailure,
+ resolutionSummary: secret,
+ stagedUploadRelativePath: "ConflictStaging/private-file"
+ ))
+
+ let rawData = try Data(contentsOf: run.eventsURL)
+ let raw = try #require(String(data: rawData, encoding: .utf8))
+ for privateValue in [
+ secret, "account-domain-42", "9001", "remote-item-77",
+ "/customer/private/file.txt", "private-request-id", "ConflictStaging/private-file",
+ ] {
+ #expect(raw.contains(privateValue) == false)
+ }
+
+ let activity = try await store.recentActivity(domainIdentifier: "account-domain-42", limit: 10)
+ let conflicts = try await store.recentConflicts(domainIdentifier: "account-domain-42", limit: 10)
+ #expect(activity.count == 1)
+ #expect(activity.first?.itemIdentifier == nil)
+ #expect(activity.first?.summary == "Modify failed.")
+ #expect(conflicts.count == 1)
+ #expect(conflicts.first?.originalItemName == nil)
+ }
+
+ @Test func jsonlStoreToleratesOnlyAnInterruptedTrailingRecord() async throws {
+ let (_, run, store) = try await makeRun()
+ try await store.recordActivity(activity(id: UUID(), occurredAt: Date(timeIntervalSince1970: 1)))
+
+ let handle = try FileHandle(forWritingTo: run.eventsURL)
+ try handle.seekToEnd()
+ try handle.write(contentsOf: Data("{\"schemaVersion\":1".utf8))
+ try handle.close()
+
+ #expect(try await store.recentActivity(domainIdentifier: "domain", limit: 10).count == 1)
+ try await store.recordActivity(activity(id: UUID(), occurredAt: Date(timeIntervalSince1970: 2)))
+ #expect(try await store.recentActivity(domainIdentifier: "domain", limit: 10).count == 2)
+
+ try Data("not-json\n".utf8).write(to: run.eventsURL, options: .atomic)
+ await #expect(throws: ProviderDiagnosticStoreError.self) {
+ _ = try await store.recentActivity(domainIdentifier: nil, limit: 10)
+ }
+ }
+
+ @Test func independentStoresSerializeConcurrentAppends() async throws {
+ let (_, run, first) = try await makeRun()
+ let second = try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+
+ try await withThrowingTaskGroup(of: Void.self) { group in
+ for index in 0..<100 {
+ group.addTask {
+ let store = index.isMultiple(of: 2) ? first : second
+ try await store.recordActivity(self.activity(
+ id: UUID(),
+ occurredAt: Date(timeIntervalSince1970: TimeInterval(index))
+ ))
+ }
+ }
+ try await group.waitForAll()
+ }
+
+ #expect(try await first.recentActivity(domainIdentifier: "domain", limit: 200).count == 100)
+ }
+
+ @Test func runCoordinatorWritesImmutableManifestAndPrunesOnlyCompletedBundles() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let first = try await coordinator.startRun(buildRevision: "abc123")
+ #expect(FileManager.default.fileExists(atPath: first.manifestURL.path))
+ #expect(try await coordinator.activeRun()?.runID == first.runID)
+ _ = try await coordinator.finishRun(
+ runID: first.runID,
+ summary: StabilityRunSummary(assertionCount: 2, failedAssertionCount: 0, checkpointCount: 1)
+ )
+ #expect(try await coordinator.activeRun() == nil)
+
+ for index in 0..<4 {
+ let run = try await coordinator.startRun(buildRevision: "revision-\(index)")
+ _ = try await coordinator.finishRun(
+ runID: run.runID,
+ summary: StabilityRunSummary(assertionCount: index, failedAssertionCount: 0, checkpointCount: 0)
+ )
+ }
+ let active = try await coordinator.startRun(buildRevision: "active")
+ let removed = try await coordinator.pruneCompletedRuns(maximumRunCount: 2, maximumTotalBytes: .max)
+ #expect(removed.count == 3)
+ #expect(FileManager.default.fileExists(atPath: active.directoryURL.path))
+ }
+
+ @Test func inactiveRunLeaseRejectsAnActiveRunAndAllowsSafeResetWorkAfterFinish() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let run = try await coordinator.startRun(buildRevision: nil)
+
+ await #expect(throws: ProviderDiagnosticStoreError.runAlreadyActive) {
+ _ = try await coordinator.withInactiveRunLease { true }
+ }
+
+ _ = try await coordinator.finishRun(
+ runID: run.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ #expect(try await coordinator.withInactiveRunLease { true })
+ }
+
+ @Test func inactiveRunLeaseBlocksAnIndependentRunStartUntilRelease() async throws {
+ let root = try temporaryDirectory()
+ let resetCoordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let runCoordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let leaseGate = StabilityLeaseTestGate()
+ let startState = StabilityRunStartTestState()
+
+ let leaseTask = Task {
+ try await resetCoordinator.withInactiveRunLease {
+ await leaseGate.hold()
+ return true
+ }
+ }
+ await leaseGate.waitUntilHeld()
+
+ let startTask = Task {
+ await startState.markAttempted()
+ let run = try await runCoordinator.startRun(buildRevision: "after-reset")
+ await startState.markCompleted()
+ return run
+ }
+ await startState.waitUntilAttempted()
+ for _ in 0..<20 {
+ await Task.yield()
+ }
+ #expect(await startState.isCompleted == false)
+
+ await leaseGate.release()
+ #expect(try await leaseTask.value)
+ let run = try await startTask.value
+ #expect(await startState.isCompleted)
+ #expect(try await runCoordinator.activeRun()?.runID == run.runID)
+ }
+
+ @Test func independentCoordinatorsSelectOneRunAndByteRetentionKeepsIncompleteRuns() async throws {
+ let root = try temporaryDirectory()
+ let first = StabilityRunCoordinator(rootDirectoryURL: root)
+ let second = StabilityRunCoordinator(rootDirectoryURL: root)
+ async let firstRun = first.startRun(buildRevision: "first")
+ async let secondRun = second.startRun(buildRevision: "second")
+ let selectedRuns = try await [firstRun, secondRun]
+ #expect(Set(selectedRuns.map(\.runID)).count == 1)
+
+ let active = try #require(selectedRuns.first)
+ _ = try await first.finishRun(
+ runID: active.runID,
+ summary: StabilityRunSummary(assertionCount: 0, failedAssertionCount: 0, checkpointCount: 0)
+ )
+ let incomplete = try await second.startRun(buildRevision: "incomplete")
+ let removed = try await first.pruneCompletedRuns(maximumRunCount: 20, maximumTotalBytes: 0)
+ #expect(removed == [active.runID])
+ #expect(FileManager.default.fileExists(atPath: incomplete.directoryURL.path))
+ }
+
+ @Test func diagnosticEventEncodingHasNoFreeFormPrivateFields() throws {
+ let event = ProviderDiagnosticEvent(
+ correlationID: UUID(),
+ source: .fileProviderExtension,
+ operation: .modifyItem,
+ phase: .completed,
+ fieldShape: [.contents, .filename, .parent],
+ routeTemplate: .upload,
+ optionShape: [.conditionalETag, .stableFileID, .clientToken, .contentHash],
+ statusClass: .success,
+ durationMilliseconds: 42,
+ progressPercentBucket: 100,
+ hasCursor: true,
+ hasMore: false,
+ hasAnchor: true
+ )
+ let data = try JSONEncoder().encode(event)
+ let text = try #require(String(data: data, encoding: .utf8))
+ #expect(text.contains("modifyItem"))
+ #expect(text.contains("authorization") == false)
+ #expect(text.contains("url") == false)
+ }
+
+ @Test func factoryChangesOnlyTheEventStoreForAStartedStabilityRun() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let run = try await coordinator.startRun(buildRevision: nil)
+ let standardDatabaseURL = root.appendingPathComponent("Snapshots.sqlite3")
+
+ let standardCandidate = try ProviderEventStoreFactory.make(
+ profile: .standard,
+ standardDatabaseURL: standardDatabaseURL,
+ stabilityRootDirectoryURL: root
+ )
+ let stabilityCandidate = try ProviderEventStoreFactory.make(
+ profile: .stability,
+ standardDatabaseURL: standardDatabaseURL,
+ stabilityRootDirectoryURL: root
+ )
+ let standard = try #require(standardCandidate)
+ let stability = try #require(stabilityCandidate)
+
+ #expect(standard is KDriveProviderEventSQLiteStore)
+ #expect(stability is KDriveProviderEventJSONLStore)
+ #expect(FileManager.default.fileExists(atPath: run.eventsURL.path))
+ #expect(FileManager.default.fileExists(atPath: standardDatabaseURL.path))
+ }
+
+ @Test func jsonlProtocolsPreserveClearStatisticsPagingAndExportSemantics() async throws {
+ let (_, _, store) = try await makeRun()
+ try await store.recordActivity(activity(id: UUID(), occurredAt: Date(timeIntervalSince1970: 1)))
+ try await store.recordActivity(KDriveProviderActivityEvent(
+ occurredAt: Date(timeIntervalSince1970: 2),
+ domainIdentifier: "domain",
+ driveID: 1,
+ kind: .modify,
+ outcome: .failure,
+ severity: .error,
+ itemIdentifier: "PRIVATE_ITEM_CANARY",
+ itemName: nil,
+ itemPath: nil,
+ summary: "PRIVATE_SUMMARY_CANARY"
+ ))
+ try await store.saveConflict(conflict(state: .unresolved, date: 3))
+ try await store.saveConflict(conflict(state: .automaticallyResolved, date: 4))
+
+ let statisticCandidates = try await store.eventStatistics(domainIdentifiers: ["domain"])
+ let statistics = try #require(statisticCandidates.first)
+ #expect(statistics.unresolvedConflictCount == 1)
+ #expect(statistics.resolvedConflictCount == 1)
+ #expect(statistics.recentSuccessCount == 1)
+ #expect(statistics.recentFailureCount == 1)
+ let page = try await store.timelinePage(filter: .errorsAndConflicts, before: nil, limit: 2)
+ #expect(page.entries.count == 2)
+ #expect(page.hasMore)
+ let emptyPage = try await store.timelinePage(filter: .allActivity, before: nil, limit: 0)
+ #expect(emptyPage.entries.isEmpty)
+ #expect(emptyPage.hasMore == false)
+
+ let export = try await store.supportLogData(domainIdentifier: "domain")
+ let exportText = try #require(String(data: export, encoding: .utf8))
+ #expect(exportText.contains("PRIVATE_ITEM_CANARY") == false)
+ #expect(exportText.contains("PRIVATE_SUMMARY_CANARY") == false)
+
+ try await store.removeActivityAndResolvedConflicts(domainIdentifier: "domain")
+ #expect(try await store.recentActivity(domainIdentifier: "domain", limit: 10).isEmpty)
+ let remaining = try await store.recentConflicts(domainIdentifier: "domain", limit: 10)
+ #expect(remaining.count == 1)
+ #expect(remaining.first?.resolutionState == .unresolved)
+ try await store.removeEvents(domainIdentifier: "domain")
+ #expect(try await store.recentConflicts(domainIdentifier: "domain", limit: 10).isEmpty)
+ }
+
+ @Test(.timeLimit(.minutes(1))) func eventObservationSeesAnAppendFromAnotherStore() async throws {
+ let (_, run, store) = try await makeRun()
+ let writer = try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+ let changes = await store.eventChanges(pollInterval: 0.02)
+ // No startup sleep: a returned subscription must already cover writes.
+ try await writer.recordActivity(activity(id: UUID(), occurredAt: Date()))
+ var iterator = changes.makeAsyncIterator()
+ #expect(await iterator.next() != nil)
+ }
+
+ @Test func eventCapacityRejectsBeforeCorruptingReplayableEvidence() async throws {
+ let (_, run, _) = try await makeRun()
+ let store = try KDriveProviderEventJSONLStore(
+ runDirectoryURL: run.directoryURL,
+ maximumEventBytes: 700
+ )
+ try await store.recordActivity(activity(id: UUID(), occurredAt: Date()))
+ await #expect(throws: ProviderDiagnosticStoreError.self) {
+ for _ in 0..<20 {
+ try await store.recordActivity(self.activity(id: UUID(), occurredAt: Date()))
+ }
+ }
+ #expect(try await store.recentActivity(domainIdentifier: "domain", limit: 100).isEmpty == false)
+ #expect(FileManager.default.fileExists(atPath: run.directoryURL.appendingPathComponent("diagnostic-health.failed").path))
+ }
+
+ #if os(macOS)
+ @Test func parentAndSubprocessWritersProduceOnlyCompleteRecords() async throws {
+ let (_, run, store) = try await makeRun()
+ for _ in 0..<50 {
+ try await store.recordActivity(activity(id: UUID(), occurredAt: Date()))
+ }
+ let template = try Data(contentsOf: run.eventsURL)
+ let templateURL = try temporaryDirectory().appendingPathComponent("sanitized-template.jsonl")
+ try template.write(to: templateURL)
+ let eventHandle = try FileHandle(forWritingTo: run.eventsURL)
+ try eventHandle.truncate(atOffset: 0)
+ try eventHandle.close()
+
+ let process = Process()
+ let output = Pipe()
+ let errors = Pipe()
+ process.executableURL = URL(
+ fileURLWithPath: "/Applications/Xcode.app/Contents/Developer/usr/bin/python3"
+ )
+ process.arguments = [
+ "-c",
+ "import fcntl,os,sys,time; lines=open(sys.argv[2],'rb').readlines(); f=open(sys.argv[1],'ab',buffering=0); sys.stdout.write('1'); sys.stdout.flush(); [(fcntl.flock(f,fcntl.LOCK_EX),f.write(line),f.flush(),os.fsync(f.fileno()),fcntl.flock(f,fcntl.LOCK_UN),time.sleep(0.001)) for line in lines]",
+ run.eventsURL.path,
+ templateURL.path,
+ ]
+ process.standardOutput = output
+ process.standardError = errors
+ try process.run()
+ let readyData = try output.fileHandleForReading.read(upToCount: 1)
+ guard let ready = readyData else {
+ process.waitUntilExit()
+ let errorData = try errors.fileHandleForReading.readToEnd() ?? Data()
+ Issue.record("Subprocess writer exited before ready (status \(process.terminationStatus)): \(String(decoding: errorData, as: UTF8.self))")
+ return
+ }
+ #expect(ready == Data("1".utf8))
+
+ try await withThrowingTaskGroup(of: Void.self) { group in
+ for _ in 0..<50 {
+ group.addTask {
+ try await store.recordActivity(self.activity(id: UUID(), occurredAt: Date()))
+ }
+ }
+ try await group.waitForAll()
+ }
+ process.waitUntilExit()
+ #expect(process.terminationStatus == 0)
+ #expect(try await store.recentActivity(domainIdentifier: "domain", limit: 200).count == 100)
+ }
+ #endif
+
+ @Test func completedRunRejectsCachedWriterAndSymlinkedEventFile() async throws {
+ let (coordinator, run, store) = try await makeRun()
+ _ = try await coordinator.finishRun(
+ runID: run.runID,
+ summary: StabilityRunSummary(assertionCount: 0, failedAssertionCount: 0, checkpointCount: 0)
+ )
+ await #expect(throws: ProviderDiagnosticStoreError.self) {
+ try await store.recordActivity(self.activity(id: UUID(), occurredAt: Date()))
+ }
+
+ let (_, unsafeRun, _) = try await makeRun()
+ try FileManager.default.removeItem(at: unsafeRun.eventsURL)
+ let outside = try temporaryDirectory().appendingPathComponent("outside.jsonl")
+ _ = FileManager.default.createFile(atPath: outside.path, contents: nil)
+ try FileManager.default.createSymbolicLink(at: unsafeRun.eventsURL, withDestinationURL: outside)
+ #expect(throws: ProviderDiagnosticStoreError.self) {
+ _ = try KDriveProviderEventJSONLStore(runDirectoryURL: unsafeRun.directoryURL)
+ }
+ }
+
+ private func activity(id: UUID, occurredAt: Date) -> KDriveProviderActivityEvent {
+ KDriveProviderActivityEvent(
+ id: id,
+ occurredAt: occurredAt,
+ domainIdentifier: "domain",
+ driveID: 1,
+ kind: .enumeration,
+ itemIdentifier: nil,
+ itemName: nil,
+ itemPath: nil,
+ summary: "Enumerated."
+ )
+ }
+
+ private func conflict(state: KDriveConflictResolutionState, date: TimeInterval) -> KDriveConflictEvent {
+ KDriveConflictEvent(
+ detectedAt: Date(timeIntervalSince1970: date),
+ domainIdentifier: "domain",
+ driveID: 1,
+ operation: .conflict,
+ originalItemIdentifier: nil,
+ originalItemName: nil,
+ originalItemPath: nil,
+ resolutionState: state,
+ automaticallyResolved: state == .automaticallyResolved,
+ resolutionKind: nil,
+ resolutionSummary: "PRIVATE_CONFLICT_CANARY"
+ )
+ }
+
+ private func makeRun() async throws -> (
+ StabilityRunCoordinator,
+ StabilityRunHandle,
+ KDriveProviderEventJSONLStore
+ ) {
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: try temporaryDirectory())
+ let run = try await coordinator.startRun(buildRevision: nil)
+ return (coordinator, run, try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL))
+ }
+
+ private func temporaryDirectory() throws -> URL {
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("StabilityDiagnosticsTests-\(UUID().uuidString)", isDirectory: true)
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ return url
+ }
+}
+
+private actor StabilityLeaseTestGate {
+ private var isHeld = false
+ private var heldWaiters: [CheckedContinuation] = []
+ private var releaseWaiter: CheckedContinuation?
+
+ func hold() async {
+ isHeld = true
+ heldWaiters.forEach { $0.resume() }
+ heldWaiters.removeAll()
+ await withCheckedContinuation { continuation in
+ releaseWaiter = continuation
+ }
+ }
+
+ func waitUntilHeld() async {
+ guard isHeld == false else { return }
+ await withCheckedContinuation { continuation in
+ heldWaiters.append(continuation)
+ }
+ }
+
+ func release() {
+ releaseWaiter?.resume()
+ releaseWaiter = nil
+ }
+}
+
+private actor StabilityRunStartTestState {
+ private var attempted = false
+ private var completed = false
+ private var attemptedWaiters: [CheckedContinuation] = []
+
+ var isCompleted: Bool { completed }
+
+ func markAttempted() {
+ attempted = true
+ attemptedWaiters.forEach { $0.resume() }
+ attemptedWaiters.removeAll()
+ }
+
+ func waitUntilAttempted() async {
+ guard attempted == false else { return }
+ await withCheckedContinuation { continuation in
+ attemptedWaiters.append(continuation)
+ }
+ }
+
+ func markCompleted() {
+ completed = true
+ }
+}
diff --git a/potassiumProviderTests/StabilityExtensionLaunchEvidenceTests.swift b/potassiumProviderTests/StabilityExtensionLaunchEvidenceTests.swift
new file mode 100644
index 0000000..ab671e2
--- /dev/null
+++ b/potassiumProviderTests/StabilityExtensionLaunchEvidenceTests.swift
@@ -0,0 +1,149 @@
+#if STABILITY
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct StabilityExtensionLaunchEvidenceTests {
+ private let runID = UUID(), process = UUID(), correlation = UUID()
+ private let hash = String(repeating: "a", count: 40)
+ private func time(_ value: Double) -> Date { Date(timeIntervalSince1970: value) }
+ private func event(_ operation: ProviderDiagnosticOperation, _ phase: ProviderDiagnosticPhase,
+ _ at: Double, process other: UUID? = nil, span: UUID = UUID()) -> ProviderDiagnosticEvent {
+ ProviderDiagnosticEvent(occurredAt: time(at), spanID: span, processInstanceID: other ?? process,
+ processCodeHash: hash, correlationID: correlation, source: .fileProviderExtension,
+ operation: operation, phase: phase)
+ }
+ private func lifecycle(_ operation: ProviderDiagnosticOperation, at: Double) -> [ProviderDiagnosticEvent] {
+ let span = UUID()
+ return [event(operation, .started, at, span: span), event(operation, .completed, at + 1, span: span)]
+ }
+ private func report(passing: Bool = false) throws -> StabilityFinderRunReport {
+ try StabilityFinderRunReport(schemaVersion: StabilityFinderRunReport.liveSchemaVersion,
+ correlationID: correlation, startedAt: time(10), finishedAt: time(30),
+ preflightResults: StabilityFinderPreflightCheck.allCases.map { .init(check: $0, outcome: .passed, recordedAt: time(10)) },
+ stepResults: StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ .init(sequenceNumber: UInt16(index + 1), scenario: scenario, correlationID: UUID(),
+ startedAt: time(20), finishedAt: time(20), outcome: passing && index == 0 ? .passed : .skipped(.notSelectedForConflictProfile),
+ assertions: StabilityFinderAssertionClass.allCases.map { .init(assertionClass: $0, outcome: passing && index == 0 ? .passed : .notEvaluated(.stepSkipped)) })
+ })
+ }
+ private func proof(_ mode: StabilityExtensionLaunchMode, born: Double) -> StabilityExtensionLaunchEvidence {
+ .init(runID: runID, mode: mode, recordingStartedAt: time(10), preparedAt: time(15), processStartedAt: time(born),
+ processInstanceID: process, expectedCodeHash: hash)
+ }
+
+ @Test func freshRequiresKernelBirthAndInitializationBeforeMutation() throws {
+ let events = lifecycle(.runtimeInitialize, at: 16) + [event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)]
+ try proof(.fresh, born: 16).validate(runID: runID, report: report(), diagnostics: events)
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.fresh, born: 9).validate(runID: runID, report: report(), diagnostics: events)
+ }
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.fresh, born: 16).validate(runID: runID, report: report(),
+ diagnostics: events.filter { $0.operation != .runtimeInitialize })
+ }
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.fresh, born: 16).validate(runID: runID, report: report(),
+ diagnostics: [event(.modifyItem, .started, 16)] + lifecycle(.runtimeInitialize, at: 17))
+ }
+ }
+ @Test func warmRequiresEarlierCallbackAndAnUnchangedProcess() throws {
+ let events = [event(.itemLookup, .completed, 12), event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)]
+ try proof(.running, born: 9).validate(runID: runID, report: report(), diagnostics: events)
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.running, born: 16).validate(runID: runID, report: report(), diagnostics: events)
+ }
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.running, born: 9).validate(runID: runID, report: report(), diagnostics: Array(events.dropFirst()))
+ }
+ for unexpected in [event(.itemLookup, .completed, 11, process: UUID()),
+ event(.modifyItem, .completed, 22, process: UUID())] {
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try proof(.running, born: 9).validate(runID: runID, report: report(), diagnostics: events + [unexpected])
+ }
+ }
+ }
+ @Test func kernelTimesKeepSubsecondPrecisionAcrossDiagnosticEncoding() throws {
+ let original = StabilityExtensionLaunchEvidence(runID: runID, mode: .running,
+ recordingStartedAt: time(10.8), preparedAt: time(15), processStartedAt: time(10.2),
+ processInstanceID: process, expectedCodeHash: hash)
+ let encoder = JSONEncoder(); encoder.dateEncodingStrategy = .iso8601
+ let decoder = JSONDecoder(); decoder.dateDecodingStrategy = .iso8601
+ let decoded = try decoder.decode(StabilityExtensionLaunchEvidence.self, from: encoder.encode(original))
+ #expect(decoded == original && decoded.processStartedAt < decoded.recordingStartedAt)
+ try decoded.validate(runID: runID, report: report(), diagnostics:
+ [event(.itemLookup, .completed, 12), event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)])
+ }
+
+ @Test func warmProcessSupportsBalancedReplicatedInstanceLifecycles() throws {
+ let events = [event(.itemLookup, .completed, 12)] +
+ lifecycle(.runtimeInvalidate, at: 13) + lifecycle(.runtimeInitialize, at: 16) +
+ [event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)] +
+ lifecycle(.runtimeInvalidate, at: 22)
+ try proof(.running, born: 9).validate(runID: runID, report: report(), diagnostics: events)
+ }
+
+ @Test func freshProcessMayDiscardAReplicatedInstanceAfterCompletedWork() throws {
+ let events = lifecycle(.runtimeInitialize, at: 16) +
+ [event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)] +
+ lifecycle(.runtimeInvalidate, at: 22)
+ try proof(.fresh, born: 16).validate(runID: runID, report: report(), diagnostics: events)
+ }
+
+ @Test(arguments: [ProviderDiagnosticPhase.failed, .cancelled])
+ func failedInstanceLifecycleCannotPass(_ terminal: ProviderDiagnosticPhase) throws {
+ let span = UUID()
+ let events = [event(.itemLookup, .completed, 12), event(.runtimeInitialize, .started, 16, span: span),
+ event(.runtimeInitialize, terminal, 17, span: span)]
+ #expect(throws: StabilityLiveEvidenceError.unexpectedFailure) {
+ try proof(.running, born: 9).validate(runID: runID, report: report(), diagnostics: events)
+ }
+ }
+
+ @Test func incompleteDuplicatedOrMisorderedInstanceLifecycleCannotPass() throws {
+ let pair = lifecycle(.runtimeInvalidate, at: 22)
+ let span = UUID()
+ for broken in [[pair[0]], [pair[1]], pair + [pair[0]], pair + [pair[1]],
+ [event(.runtimeInitialize, .started, 23, span: span), event(.runtimeInitialize, .completed, 22, span: span)],
+ [event(.runtimeInitialize, .started, 22, span: span), event(.runtimeInvalidate, .completed, 23, span: span)]] {
+ #expect(throws: StabilityLiveEvidenceError.pendingOperations) {
+ try proof(.running, born: 9).validate(runID: runID, report: report(),
+ diagnostics: [event(.itemLookup, .completed, 12)] + broken)
+ }
+ }
+ }
+
+ @Test func historicalProofKeepsItsOriginalLifecycleInterpretation() throws {
+ let modern = proof(.running, born: 9)
+ #expect(modern.schemaVersion == 2)
+ let encoded = try JSONEncoder().encode(modern)
+ let decoded = try JSONSerialization.jsonObject(with: encoded)
+ var object = try #require(decoded as? [String: Any])
+ object["schemaVersion"] = 1
+ let legacy = try JSONDecoder().decode(StabilityExtensionLaunchEvidence.self, from: JSONSerialization.data(withJSONObject: object))
+ let earlierWork = [event(.itemLookup, .completed, 12), event(.modifyItem, .completed, 21)]
+ try legacy.validate(runID: runID, report: report(), diagnostics: earlierWork)
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try legacy.validate(runID: runID, report: report(), diagnostics: earlierWork + lifecycle(.runtimeInitialize, at: 16))
+ }
+ }
+
+ @Test func originalRunCannotCertifyRequestedLifecycleWithoutMatchingEvidence() throws {
+ let request = StabilityExtensionLaunchRequest(runID: runID, mode: .fresh)
+ let events = lifecycle(.runtimeInitialize, at: 16) + [event(.modifyItem, .started, 20), event(.modifyItem, .completed, 21)]
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try request.validate(evidence: nil, report: report(passing: true), diagnostics: events)
+ }
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try request.validate(evidence: proof(.running, born: 9), report: report(passing: true), diagnostics: events)
+ }
+ try request.validate(evidence: proof(.fresh, born: 16), report: report(passing: true), diagnostics: events)
+ }
+
+ @Test func historicalProfileRemainsReadableWithoutLaunchCertification() throws {
+ let json = Data("{\"schemaVersion\":1,\"runID\":\"\(runID.uuidString)\",\"selectedCase\":\"content-after-preflight\"}".utf8)
+ let old = try JSONDecoder().decode(StabilityConflictProfile.self, from: json)
+ #expect(old.schemaVersion == 1 && old.extensionLaunchMode == nil)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityFinderEvidenceTests.swift b/potassiumProviderTests/StabilityFinderEvidenceTests.swift
new file mode 100644
index 0000000..8faed01
--- /dev/null
+++ b/potassiumProviderTests/StabilityFinderEvidenceTests.swift
@@ -0,0 +1,473 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+@Suite("Stability Finder evidence")
+struct StabilityFinderEvidenceTests {
+ @Test(arguments: [StabilityFinderRunReport.liveSchemaVersion, StabilityFinderRunReport.selectiveSchemaVersion])
+ func newerReportsCannotFallBackToHistoricalTelemetryRules(version: UInt16) async throws {
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: try temporaryDirectory())
+ let owned = try await coordinator.startOwnedRun(buildRevision: nil)
+ let historical = try passingReport()
+ let report = try StabilityFinderRunReport(schemaVersion: version, correlationID: historical.correlationID,
+ startedAt: historical.startedAt, finishedAt: historical.finishedAt,
+ preflightResults: historical.preflightResults, stepResults: historical.stepResults)
+ // Historical diagnostics lack item/build/UI evidence. Neither newer
+ // schema may accept them, even if all old operation mappings match.
+ try await recordPassingDiagnostics(report, in: owned.run)
+ await #expect(throws: StabilityLiveEvidenceError.self) {
+ try await coordinator.writeFinderEvidence(ownedRun: owned, report: report,
+ observations: passingObservations(for: report))
+ }
+ #expect(!FileManager.default.fileExists(atPath: owned.run.finderReportURL.path))
+ #expect(!FileManager.default.fileExists(atPath: owned.run.summaryURL.path))
+ }
+
+ @Test func finderRunOwnershipAndStepCorrelationAreExclusive() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let ordinary = try await coordinator.startRun(buildRevision: nil)
+
+ await #expect(throws: ProviderDiagnosticStoreError.runAlreadyActive) {
+ _ = try await coordinator.startOwnedRun(buildRevision: nil)
+ }
+ _ = try await coordinator.finishRun(
+ runID: ordinary.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+
+ let owned = try await coordinator.startOwnedRun(buildRevision: nil)
+ await #expect(throws: ProviderDiagnosticStoreError.runOwnedByFinderRunner(owned.run.runID)) {
+ _ = try await coordinator.finishRun(
+ runID: owned.run.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ }
+
+ let correlationID = UUID()
+ try await coordinator.beginFinderStep(ownedRun: owned, correlationID: correlationID)
+ #expect(try StabilityRunLocator.activeFinderStepCorrelation(rootDirectoryURL: root) == correlationID)
+ await #expect(throws: ProviderDiagnosticStoreError.finderStepAlreadyActive) {
+ try await coordinator.beginFinderStep(
+ ownedRun: owned,
+ correlationID: UUID()
+ )
+ }
+ await #expect(throws: ProviderDiagnosticStoreError.finderStepCorrelationMismatch) {
+ try await coordinator.endFinderStep(
+ ownedRun: owned,
+ correlationID: UUID()
+ )
+ }
+ try await coordinator.endFinderStep(ownedRun: owned, correlationID: correlationID)
+ #expect(try StabilityRunLocator.activeFinderStepCorrelation(rootDirectoryURL: root) == nil)
+
+ let report = try passingReport()
+ try await recordPassingDiagnostics(report, in: owned.run)
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: passingObservations(for: report)
+ )
+ await #expect(throws: ProviderDiagnosticStoreError.finderSummaryMismatch) {
+ _ = try await coordinator.finishOwnedRun(
+ owned,
+ summary: StabilityRunSummary(
+ assertionCount: 32,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ }
+ #expect(FileManager.default.fileExists(atPath: owned.run.summaryURL.path) == false)
+ _ = try await coordinator.finishOwnedRun(
+ owned,
+ summary: StabilityRunSummary(
+ assertionCount: 32,
+ failedAssertionCount: 0,
+ checkpointCount: 4
+ )
+ )
+ #expect(try await coordinator.activeRun() == nil)
+ }
+
+ @Test func failedEvidenceAssemblyNeverSealsAndCanBeRetried() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let owned = try await coordinator.startOwnedRun(buildRevision: nil)
+ let report = try passingReport()
+ let observations = passingObservations(for: report)
+ try await recordPassingDiagnostics(report, in: owned.run)
+
+ try FileManager.default.removeItem(at: owned.run.observationsURL)
+ try FileManager.default.createDirectory(
+ at: owned.run.observationsURL,
+ withIntermediateDirectories: false
+ )
+ await #expect(throws: ProviderDiagnosticStoreError.self) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: observations
+ )
+ }
+ #expect(FileManager.default.fileExists(atPath: owned.run.finderReportURL.path) == false)
+ #expect(FileManager.default.fileExists(atPath: owned.run.summaryURL.path) == false)
+ #expect(try Data(contentsOf: owned.run.assertionsURL).isEmpty == false)
+
+ try FileManager.default.removeItem(at: owned.run.observationsURL)
+ try Data().write(to: owned.run.observationsURL, options: .withoutOverwriting)
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: observations
+ )
+ #expect(FileManager.default.fileExists(atPath: owned.run.finderReportURL.path))
+ }
+
+ @Test func deadFinderOwnerCanBeExplicitlyAbandonedWithoutSealingEvidence() async throws {
+ let root = try temporaryDirectory()
+ let owner = StabilityRunCoordinator(
+ rootDirectoryURL: root,
+ processIdentifier: Int32.max
+ )
+ let owned = try await owner.startOwnedRun(buildRevision: nil)
+ try await owner.beginFinderStep(ownedRun: owned, correlationID: UUID())
+
+ let recovery = StabilityRunCoordinator(rootDirectoryURL: root)
+ let abandoned = try await recovery.abandonStaleOwnedRun()
+ #expect(abandoned.runID == owned.run.runID)
+ #expect(FileManager.default.fileExists(atPath: abandoned.finderAbandonedURL.path))
+ #expect(try await recovery.activeRun() == nil)
+ #expect(try StabilityRunLocator.activeFinderStepCorrelation(rootDirectoryURL: root) == nil)
+ await #expect(throws: ProviderDiagnosticStoreError.runAbandonedByFinderRunner(
+ owned.run.runID
+ )) {
+ _ = try await recovery.finishRun(
+ runID: owned.run.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ }
+
+ let next = try await recovery.startRun(buildRevision: nil)
+ _ = try await recovery.finishRun(
+ runID: next.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ }
+
+ @Test func liveFinderOwnerCannotBeAbandoned() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ _ = try await coordinator.startOwnedRun(buildRevision: nil)
+
+ await #expect(throws: ProviderDiagnosticStoreError.finderOwnerProcessStillRunning) {
+ _ = try await coordinator.abandonStaleOwnedRun()
+ }
+ #expect(try await coordinator.activeRun() != nil)
+ }
+
+ @Test func ordinaryRunIsNotEligibleForStaleFinderRecovery() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let ordinary = try await coordinator.startRun(buildRevision: nil)
+
+ await #expect(throws: ProviderDiagnosticStoreError.noStaleFinderRun) {
+ _ = try await coordinator.abandonStaleOwnedRun()
+ }
+ _ = try await coordinator.finishRun(
+ runID: ordinary.runID,
+ summary: StabilityRunSummary(
+ assertionCount: 0,
+ failedAssertionCount: 0,
+ checkpointCount: 0
+ )
+ )
+ }
+
+ @Test func rejectedCandidateRetainsEvidenceWithoutCreatingAcceptanceMarkers() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let owner = try await coordinator.startOwnedRun(buildRevision: "synthetic")
+ let report = try passingReport()
+ let rejection = StabilityFinderEvidenceRejection(report: report,
+ observations: passingObservations(for: report), error: StabilityLiveEvidenceError.pendingOperations)
+ try await coordinator.recordFinderEvidenceRejection(rejection, ownedRun: owner)
+ let url = owner.run.directoryURL.appendingPathComponent("finder-evidence-rejected.json")
+ let decoded = try JSONDecoder.stability.decode(StabilityFinderEvidenceRejection.self, from: Data(contentsOf: url))
+ #expect(decoded.schemaVersion == 1)
+ #expect(!decoded.eligibleForAcceptance)
+ #expect(decoded.evidenceError == .pendingOperations)
+ #expect(decoded.report == report)
+ #expect(!FileManager.default.fileExists(atPath: owner.run.finderReportURL.path))
+ #expect(!FileManager.default.fileExists(atPath: owner.run.summaryURL.path))
+ await #expect(throws: (any Error).self) {
+ try await coordinator.recordFinderEvidenceRejection(rejection, ownedRun: owner)
+ }
+ #expect(try JSONDecoder.stability.decode(StabilityFinderEvidenceRejection.self, from: Data(contentsOf: url)).report == report)
+ }
+
+ @Test func coordinatorWritesClosedImmutableFinderEvidence() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let ownedRun = try await coordinator.startOwnedRun(buildRevision: "abc123")
+ let handle = ownedRun.run
+ let report = try passingReport()
+ let observations = passingObservations(for: report)
+ try await recordPassingDiagnostics(report, in: handle)
+
+ try await coordinator.writeFinderEvidence(
+ ownedRun: ownedRun,
+ report: report,
+ observations: observations
+ )
+
+ let decodedReport = try JSONDecoder.stability.decode(
+ StabilityFinderRunReport.self,
+ from: Data(contentsOf: handle.finderReportURL)
+ )
+ #expect(decodedReport == report)
+ #expect(try Data(contentsOf: handle.assertionsURL).split(separator: 0x0A).count == StabilityFinderPreflightCheck.allCases.count + StabilityFinderScenario.allCases.count)
+ #expect(try Data(contentsOf: handle.observationsURL).split(separator: 0x0A).count == 32)
+
+ for url in [handle.finderReportURL, handle.assertionsURL, handle.observationsURL] {
+ let text = try #require(String(data: Data(contentsOf: url), encoding: .utf8))
+ for prohibited in [
+ "\"name\":", "\"path\":", "\"url\":",
+ "\"account\":", "\"accountIdentifier\":", "\"driveID\":",
+ "\"fileID\":", "\"remoteID\":", "\"requestBody\":",
+ "\"responseBody\":", "\"authorization\":", "\"shareLink\":",
+ ] {
+ #expect(text.localizedCaseInsensitiveContains(prohibited) == false)
+ }
+ }
+
+ await #expect(throws: ProviderDiagnosticStoreError.finderEvidenceAlreadyFinalized(handle.runID)) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: ownedRun,
+ report: report,
+ observations: observations
+ )
+ }
+ }
+
+ @Test func passedStepsRequireUniqueCorrelatedBaselineAndPostconditions() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let first = try await coordinator.startOwnedRun(buildRevision: nil)
+ let report = try passingReport()
+ var missing = passingObservations(for: report)
+ missing.removeFirst()
+
+ await #expect(throws: StabilityFinderEvidenceValidationError.missingPassedObservation(
+ scenario: .enumerationAndChangeAnchors,
+ phase: .baseline
+ )) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: first,
+ report: report,
+ observations: missing
+ )
+ }
+
+ var duplicate = passingObservations(for: report)
+ duplicate.append(duplicate[0])
+ await #expect(throws: StabilityFinderEvidenceValidationError.duplicateObservation(
+ scenario: .enumerationAndChangeAnchors,
+ phase: .baseline
+ )) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: first,
+ report: report,
+ observations: duplicate
+ )
+ }
+ }
+
+ @Test func passedStepsRequireScenarioSpecificCorrelatedTerminalDiagnostics() async throws {
+ let root = try temporaryDirectory()
+ let coordinator = StabilityRunCoordinator(rootDirectoryURL: root)
+ let owned = try await coordinator.startOwnedRun(buildRevision: nil)
+ let report = try passingReport()
+
+ await #expect(throws: StabilityFinderEvidenceValidationError.missingDiagnosticEvidence(
+ .enumerationAndChangeAnchors
+ )) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: passingObservations(for: report)
+ )
+ }
+
+ let store = try KDriveProviderEventJSONLStore(runDirectoryURL: owned.run.directoryURL)
+ let firstStep = try #require(report.stepResults.first)
+ try await store.recordDiagnostic(ProviderDiagnosticEvent(
+ correlationID: firstStep.correlationID,
+ source: .finderRunner,
+ operation: .enumerateItems,
+ phase: .completed
+ ))
+ await #expect(throws: StabilityFinderEvidenceValidationError.missingDiagnosticEvidence(
+ .enumerationAndChangeAnchors
+ )) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: passingObservations(for: report)
+ )
+ }
+
+ try await store.recordDiagnostic(ProviderDiagnosticEvent(
+ correlationID: firstStep.correlationID,
+ source: .fileProviderExtension,
+ operation: .enumerateItems,
+ phase: .completed
+ ))
+ await #expect(throws: StabilityFinderEvidenceValidationError.missingDiagnosticEvidence(
+ .enumerationAndChangeAnchors
+ )) {
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: passingObservations(for: report)
+ )
+ }
+
+ try await recordPassingDiagnostics(report, in: owned.run)
+ try await coordinator.writeFinderEvidence(
+ ownedRun: owned,
+ report: report,
+ observations: passingObservations(for: report)
+ )
+ }
+
+ private func passingReport() throws -> StabilityFinderRunReport {
+ let startedAt = Date(timeIntervalSince1970: 1_800_000_000)
+ let preflight = StabilityFinderPreflightCheck.allCases.map {
+ StabilityFinderPreflightResult(check: $0, outcome: .passed, recordedAt: startedAt)
+ }
+ let steps = StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ let isCheckpoint = scenario.diagnosticRequirement == nil
+ return StabilityFinderStepResult(
+ sequenceNumber: UInt16(index + 1),
+ scenario: scenario,
+ correlationID: correlationID(index),
+ startedAt: startedAt.addingTimeInterval(TimeInterval(index + 1)),
+ finishedAt: startedAt.addingTimeInterval(TimeInterval(index + 2)),
+ outcome: isCheckpoint
+ ? .checkpoint(checkpointReason(for: scenario))
+ : .passed,
+ assertions: StabilityFinderAssertionClass.allCases.map {
+ StabilityFinderAssertionResult(
+ assertionClass: $0,
+ outcome: isCheckpoint ? .notEvaluated(.checkpointReached) : .passed
+ )
+ }
+ )
+ }
+ return try StabilityFinderRunReport(
+ correlationID: UUID(uuidString: "30000000-0000-0000-0000-000000000001")!,
+ startedAt: startedAt,
+ finishedAt: startedAt.addingTimeInterval(30),
+ preflightResults: preflight,
+ stepResults: steps
+ )
+ }
+
+ private func passingObservations(
+ for report: StabilityFinderRunReport
+ ) -> [StabilityFinderAPIObservation] {
+ report.stepResults.flatMap { step in
+ StabilityFinderAPIObservationPhase.allCasesForEvidence.map { phase in
+ StabilityFinderAPIObservation(
+ scenario: step.scenario,
+ correlationID: step.correlationID,
+ phase: phase,
+ outcome: .passed,
+ recordedAt: step.finishedAt,
+ hasMore: false,
+ itemCount: 2
+ )
+ }
+ }
+ }
+
+ private func correlationID(_ index: Int) -> UUID {
+ UUID(uuidString: String(format: "40000000-0000-0000-0000-%012d", index + 1))!
+ }
+
+ private func checkpointReason(
+ for scenario: StabilityFinderScenario
+ ) -> StabilityFinderCheckpointReason {
+ switch scenario {
+ case .restore:
+ .scopedRestoreWorkflowRequired
+ case .permanentDeletion:
+ .scopedPermanentDeletionWorkflowRequired
+ case .cancellationAndProgress:
+ .finderCancellationRequired
+ case .supportedContextualActions:
+ .variableContextualUI
+ default:
+ .variableContextualUI
+ }
+ }
+
+ private func recordPassingDiagnostics(
+ _ report: StabilityFinderRunReport,
+ in run: StabilityRunHandle
+ ) async throws {
+ let store = try KDriveProviderEventJSONLStore(runDirectoryURL: run.directoryURL)
+ for step in report.stepResults where step.outcome == .passed {
+ let requirement = try #require(step.scenario.diagnosticRequirement)
+ for operationGroup in requirement.operationGroups {
+ let operation = try #require(operationGroup.sorted {
+ $0.rawValue < $1.rawValue
+ }.first)
+ try await store.recordDiagnostic(ProviderDiagnosticEvent(
+ correlationID: step.correlationID,
+ source: requirement.source,
+ operation: operation,
+ phase: .completed,
+ statusClass: .success
+ ))
+ }
+ }
+ }
+
+ private func temporaryDirectory() throws -> URL {
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("StabilityFinderEvidenceTests-\(UUID().uuidString)", isDirectory: true)
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ return url
+ }
+}
+
+private extension StabilityFinderAPIObservationPhase {
+ static var allCasesForEvidence: [Self] { [.baseline, .postcondition] }
+}
+
+private extension JSONDecoder {
+ static var stability: JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+ return decoder
+ }
+}
diff --git a/potassiumProviderTests/StabilityFinderRunModelTests.swift b/potassiumProviderTests/StabilityFinderRunModelTests.swift
new file mode 100644
index 0000000..25a4d73
--- /dev/null
+++ b/potassiumProviderTests/StabilityFinderRunModelTests.swift
@@ -0,0 +1,490 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+@Suite("Stability Finder run model")
+struct StabilityFinderRunModelTests {
+ @Test func deferredDeletionIsVersionedAndNeverCountsAsPassed() throws {
+ var steps = passingSteps()
+ steps[11] = step(sequenceNumber: 12, scenario: .permanentDeletion,
+ outcome: .skipped(.permanentDeletionNotSelected), assertionOutcome: .notEvaluated(.stepSkipped))
+ let report = try StabilityFinderRunReport(schemaVersion: StabilityFinderRunReport.selectiveSchemaVersion,
+ correlationID: UUID(), startedAt: timestamp(0), finishedAt: timestamp(20),
+ preflightResults: passingPreflight(), stepResults: steps)
+ #expect(report.hasOnlyDeferredPermanentDeletion)
+ #expect(report.stepSummary.passed == 15 && report.stepSummary.skipped == 1)
+ #expect(try JSONDecoder().decode(StabilityFinderRunReport.self, from: JSONEncoder().encode(report)) == report)
+ for version in [StabilityFinderRunReport.currentSchemaVersion, StabilityFinderRunReport.liveSchemaVersion] {
+ #expect(throws: StabilityFinderRunValidationError.invalidStepDeferral(.permanentDeletion)) {
+ try StabilityFinderRunReport(schemaVersion: version, correlationID: UUID(),
+ startedAt: timestamp(0), finishedAt: timestamp(20), preflightResults: passingPreflight(), stepResults: steps)
+ }
+ let historical = try StabilityFinderRunReport(schemaVersion: version, correlationID: UUID(),
+ startedAt: timestamp(0), finishedAt: timestamp(20), preflightResults: passingPreflight(), stepResults: passingSteps())
+ #expect(try JSONDecoder().decode(StabilityFinderRunReport.self, from: JSONEncoder().encode(historical)) == historical)
+ #expect(!historical.hasOnlyDeferredPermanentDeletion)
+ }
+ steps[12] = step(sequenceNumber: 13, scenario: .concurrentRemotePreserveBoth,
+ outcome: .failed(.operationFailed), assertionOutcome: .notEvaluated(.operationDidNotReachAssertion))
+ let failed = try StabilityFinderRunReport(schemaVersion: StabilityFinderRunReport.selectiveSchemaVersion,
+ correlationID: UUID(), startedAt: timestamp(0), finishedAt: timestamp(20), preflightResults: passingPreflight(), stepResults: steps)
+ #expect(!failed.hasOnlyDeferredPermanentDeletion)
+ steps[12] = step(sequenceNumber: 13, scenario: .concurrentRemotePreserveBoth,
+ outcome: .skipped(.permanentDeletionNotSelected), assertionOutcome: .notEvaluated(.stepSkipped))
+ #expect(throws: StabilityFinderRunValidationError.invalidStepDeferral(.concurrentRemotePreserveBoth)) {
+ try StabilityFinderRunReport(schemaVersion: StabilityFinderRunReport.selectiveSchemaVersion,
+ correlationID: UUID(), startedAt: timestamp(0), finishedAt: timestamp(20), preflightResults: passingPreflight(), stepResults: steps)
+ }
+ }
+
+ @Test func standardSequenceCoversEveryPlannedScenario() {
+ #expect(StabilityFinderScenario.allCases == [
+ .enumerationAndChangeAnchors,
+ .hydrate,
+ .evict,
+ .download,
+ .fileCreate,
+ .directoryCreate,
+ .editAndUpload,
+ .rename,
+ .move,
+ .trash,
+ .restore,
+ .permanentDeletion,
+ .concurrentRemotePreserveBoth,
+ .cancellationAndProgress,
+ .workingSetRefresh,
+ .supportedContextualActions,
+ ])
+ }
+
+ @Test func completedRunProducesImmutableOutcomeCounts() throws {
+ let report = try makeReport()
+
+ #expect(report.preflightSummary.passed == StabilityFinderPreflightCheck.allCases.count)
+ #expect(report.preflightSummary.failed == 0)
+ #expect(report.preflightSummary.checkpointed == 0)
+ #expect(report.preflightSummary.skipped == 0)
+ #expect(report.stepSummary.passed == StabilityFinderScenario.allCases.count)
+ #expect(report.stepSummary.failed == 0)
+ #expect(report.stepSummary.checkpointed == 0)
+ #expect(report.stepSummary.skipped == 0)
+ #expect(report.durationMilliseconds == 20_000)
+ }
+
+ @Test func checkpointsRemainDistinctFromFailuresAndSkips() throws {
+ var preflight = passingPreflight()
+ preflight[0] = StabilityFinderPreflightResult(
+ check: .accessibilityPermission,
+ outcome: .checkpoint(.accessibilityConsentRequired),
+ recordedAt: timestamp(0)
+ )
+ var steps = passingSteps()
+ steps[0] = step(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ outcome: .skipped(.preflightCheckpoint),
+ assertionOutcome: .notEvaluated(.stepSkipped)
+ )
+ steps[1] = step(
+ sequenceNumber: 2,
+ scenario: .hydrate,
+ outcome: .failed(.assertionFailed),
+ assertionOutcome: .failed(.stateMismatch)
+ )
+ steps[15] = step(
+ sequenceNumber: 16,
+ scenario: .supportedContextualActions,
+ outcome: .checkpoint(.variableContextualUI),
+ assertionOutcome: .notEvaluated(.checkpointReached)
+ )
+
+ let report = try makeReport(preflight: preflight, steps: steps)
+
+ #expect(report.preflightSummary.checkpointed == 1)
+ #expect(report.preflightSummary.failed == 0)
+ #expect(report.stepSummary.checkpointed == 1)
+ #expect(report.stepSummary.skipped == 1)
+ #expect(report.stepSummary.failed == 1)
+ }
+
+ @Test func checkpointReasonsAreRestrictedToTheirScenario() throws {
+ var invalidCreateCheckpoint = passingSteps()
+ invalidCreateCheckpoint[4] = step(
+ sequenceNumber: 5,
+ scenario: .fileCreate,
+ outcome: .checkpoint(.scopedPermanentDeletionWorkflowRequired),
+ assertionOutcome: .notEvaluated(.checkpointReached)
+ )
+ #expect(throws: StabilityFinderRunValidationError.invalidStepCheckpoint(
+ scenario: .fileCreate,
+ reason: .scopedPermanentDeletionWorkflowRequired
+ )) {
+ try makeReport(steps: invalidCreateCheckpoint)
+ }
+
+ var invalidRestoreCheckpoint = passingSteps()
+ invalidRestoreCheckpoint[10] = step(
+ sequenceNumber: 11,
+ scenario: .restore,
+ outcome: .checkpoint(.variableContextualUI),
+ assertionOutcome: .notEvaluated(.checkpointReached)
+ )
+ #expect(throws: StabilityFinderRunValidationError.invalidStepCheckpoint(
+ scenario: .restore,
+ reason: .variableContextualUI
+ )) {
+ try makeReport(steps: invalidRestoreCheckpoint)
+ }
+
+ var validRestoreCheckpoint = passingSteps()
+ validRestoreCheckpoint[10] = step(
+ sequenceNumber: 11,
+ scenario: .restore,
+ outcome: .checkpoint(.scopedRestoreWorkflowRequired),
+ assertionOutcome: .notEvaluated(.checkpointReached)
+ )
+ let validReport = try makeReport(steps: validRestoreCheckpoint)
+ #expect(validReport.stepSummary.checkpointed == 1)
+
+ let encoded = try JSONEncoder().encode(validReport)
+ let encodedText = try #require(String(data: encoded, encoding: .utf8))
+ let tamperedText = encodedText.replacingOccurrences(
+ of: "scopedRestoreWorkflowRequired",
+ with: "variableContextualUI"
+ )
+ #expect(tamperedText != encodedText)
+ #expect(throws: StabilityFinderRunValidationError.invalidStepCheckpoint(
+ scenario: .restore,
+ reason: .variableContextualUI
+ )) {
+ try JSONDecoder().decode(
+ StabilityFinderRunReport.self,
+ from: Data(tamperedText.utf8)
+ )
+ }
+ }
+
+ @Test func preflightAllowsTypedPermissionQueryFailuresButRejectsMisplacedConsentCheckpoints() throws {
+ var failedConsent = passingPreflight()
+ failedConsent[0] = StabilityFinderPreflightResult(
+ check: .accessibilityPermission,
+ outcome: .failed(.permissionStateUnavailable),
+ recordedAt: timestamp(0)
+ )
+ #expect(try makeReport(preflight: failedConsent).preflightResults[0].outcome == .failed(.permissionStateUnavailable))
+
+ var misplacedCheckpoint = passingPreflight()
+ misplacedCheckpoint[2] = StabilityFinderPreflightResult(
+ check: .fileProviderRegistration,
+ outcome: .checkpoint(.accessibilityConsentRequired),
+ recordedAt: timestamp(0)
+ )
+ #expect(throws: StabilityFinderRunValidationError.invalidPreflightOutcome(
+ check: .fileProviderRegistration
+ )) {
+ try makeReport(preflight: misplacedCheckpoint)
+ }
+ }
+
+ @Test func duplicateAndMissingTerminalStepResultsAreRejected() {
+ var duplicate = passingSteps()
+ duplicate[1] = step(
+ sequenceNumber: 2,
+ scenario: .enumerationAndChangeAnchors
+ )
+ #expect(throws: StabilityFinderRunValidationError.duplicateStepResult(
+ .enumerationAndChangeAnchors
+ )) {
+ try makeReport(steps: duplicate)
+ }
+
+ let missing = Array(passingSteps().dropLast())
+ #expect(throws: StabilityFinderRunValidationError.missingStepResult(
+ .supportedContextualActions
+ )) {
+ try makeReport(steps: missing)
+ }
+ }
+
+ @Test func outOfSequenceResultsAndDuplicateCorrelationsAreRejected() {
+ var outOfSequence = passingSteps()
+ outOfSequence.swapAt(0, 1)
+ #expect(throws: StabilityFinderRunValidationError.stepOutOfSequence(
+ expected: .enumerationAndChangeAnchors,
+ actual: .hydrate
+ )) {
+ try makeReport(steps: outOfSequence)
+ }
+
+ var duplicateCorrelation = passingSteps()
+ duplicateCorrelation[1] = step(
+ sequenceNumber: 2,
+ scenario: .hydrate,
+ correlationID: duplicateCorrelation[0].correlationID
+ )
+ #expect(throws: StabilityFinderRunValidationError.duplicateCorrelationID(
+ duplicateCorrelation[0].correlationID
+ )) {
+ try makeReport(steps: duplicateCorrelation)
+ }
+ }
+
+ @Test func chronologicalOverlapIsRejectedEvenWhenScenarioOrderIsCorrect() {
+ var steps = passingSteps()
+ steps[0] = StabilityFinderStepResult(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ correlationID: UUID(),
+ startedAt: timestamp(1),
+ finishedAt: timestamp(3),
+ outcome: .passed,
+ assertions: passingAssertions()
+ )
+ steps[1] = StabilityFinderStepResult(
+ sequenceNumber: 2,
+ scenario: .hydrate,
+ correlationID: UUID(),
+ startedAt: timestamp(2),
+ finishedAt: timestamp(4),
+ outcome: .passed,
+ assertions: passingAssertions()
+ )
+
+ #expect(throws: StabilityFinderRunValidationError.stepTimestampOutOfSequence(
+ previous: .enumerationAndChangeAnchors,
+ current: .hydrate
+ )) {
+ try makeReport(steps: steps)
+ }
+ }
+
+ @Test func everyTerminalResultRequiresBothAssertionClassesExactlyOnce() {
+ let missingServerAssertion = StabilityFinderStepResult(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ correlationID: UUID(),
+ startedAt: timestamp(1),
+ finishedAt: timestamp(2),
+ outcome: .passed,
+ assertions: [
+ StabilityFinderAssertionResult(
+ assertionClass: .finderVisible,
+ outcome: .passed
+ ),
+ ]
+ )
+ var steps = passingSteps()
+ steps[0] = missingServerAssertion
+ #expect(throws: StabilityFinderRunValidationError.missingAssertion(
+ scenario: .enumerationAndChangeAnchors,
+ assertionClass: .serverAuthoritative
+ )) {
+ try makeReport(steps: steps)
+ }
+
+ var duplicateAssertions = passingSteps()
+ duplicateAssertions[0] = StabilityFinderStepResult(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ correlationID: UUID(),
+ startedAt: timestamp(1),
+ finishedAt: timestamp(2),
+ outcome: .passed,
+ assertions: [
+ StabilityFinderAssertionResult(assertionClass: .finderVisible, outcome: .passed),
+ StabilityFinderAssertionResult(assertionClass: .finderVisible, outcome: .passed),
+ StabilityFinderAssertionResult(assertionClass: .serverAuthoritative, outcome: .passed),
+ ]
+ )
+ #expect(throws: StabilityFinderRunValidationError.duplicateAssertion(
+ scenario: .enumerationAndChangeAnchors,
+ assertionClass: .finderVisible
+ )) {
+ try makeReport(steps: duplicateAssertions)
+ }
+ }
+
+ @Test func terminalOutcomeMustAgreeWithAssertionOutcomes() {
+ var malformedPass = passingSteps()
+ malformedPass[0] = step(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ outcome: .passed,
+ assertionOutcome: .failed(.stateMismatch)
+ )
+ #expect(throws: StabilityFinderRunValidationError.assertionOutcomeMismatch(
+ .enumerationAndChangeAnchors
+ )) {
+ try makeReport(steps: malformedPass)
+ }
+
+ var malformedCheckpoint = passingSteps()
+ malformedCheckpoint[15] = step(
+ sequenceNumber: 16,
+ scenario: .supportedContextualActions,
+ outcome: .checkpoint(.variableContextualUI),
+ assertionOutcome: .passed
+ )
+ #expect(throws: StabilityFinderRunValidationError.assertionOutcomeMismatch(
+ .supportedContextualActions
+ )) {
+ try makeReport(steps: malformedCheckpoint)
+ }
+ }
+
+ @Test func invalidIntervalsAndDecodedDurationTamperingAreRejected() throws {
+ var invalidInterval = passingSteps()
+ invalidInterval[0] = StabilityFinderStepResult(
+ sequenceNumber: 1,
+ scenario: .enumerationAndChangeAnchors,
+ correlationID: UUID(),
+ startedAt: timestamp(2),
+ finishedAt: timestamp(1),
+ outcome: .passed,
+ assertions: passingAssertions()
+ )
+ #expect(throws: StabilityFinderRunValidationError.invalidStepInterval(
+ .enumerationAndChangeAnchors
+ )) {
+ try makeReport(steps: invalidInterval)
+ }
+
+ let report = try makeReport()
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .sortedKeys
+ let encoded = try encoder.encode(report)
+ let original = try #require(String(data: encoded, encoding: .utf8))
+ let tampered = original.replacingOccurrences(
+ of: "\"durationMilliseconds\":1000",
+ with: "\"durationMilliseconds\":999999"
+ )
+ #expect(tampered != original)
+ #expect(throws: StabilityFinderRunValidationError.invalidStepDuration(
+ .enumerationAndChangeAnchors
+ )) {
+ try JSONDecoder().decode(
+ StabilityFinderRunReport.self,
+ from: Data(tampered.utf8)
+ )
+ }
+ }
+
+ @Test func reportCodableRoundTripContainsNoPrivateValueFields() throws {
+ let report = try makeReport()
+ let data = try JSONEncoder().encode(report)
+ let encoded = try #require(String(data: data, encoding: .utf8))
+
+ #expect(try JSONDecoder().decode(StabilityFinderRunReport.self, from: data) == report)
+ for prohibitedKey in [
+ "\"name\":", "\"path\":", "\"url\":", "\"accountID\":",
+ "\"remoteID\":", "\"diagnosticText\":", "\"message\":",
+ ] {
+ #expect(encoded.contains(prohibitedKey) == false)
+ }
+ }
+
+ @Test func iso8601EvidenceRoundTripPreservesMillisecondDurations() throws {
+ let runStart = Date(timeIntervalSince1970: 0.125)
+ let runFinish = Date(timeIntervalSince1970: 20.875)
+ let preflight = StabilityFinderPreflightCheck.allCases.map {
+ StabilityFinderPreflightResult(
+ check: $0,
+ outcome: .passed,
+ recordedAt: Date(timeIntervalSince1970: 0.25)
+ )
+ }
+ let steps = StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ StabilityFinderStepResult(
+ sequenceNumber: UInt16(index + 1),
+ scenario: scenario,
+ correlationID: UUID(),
+ startedAt: Date(timeIntervalSince1970: Double(index + 1) + 0.125),
+ finishedAt: Date(timeIntervalSince1970: Double(index + 1) + 0.875),
+ outcome: .passed,
+ assertions: passingAssertions()
+ )
+ }
+ let report = try StabilityFinderRunReport(
+ correlationID: UUID(),
+ startedAt: runStart,
+ finishedAt: runFinish,
+ preflightResults: preflight,
+ stepResults: steps
+ )
+ let encoder = JSONEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+
+ let decoded = try decoder.decode(
+ StabilityFinderRunReport.self,
+ from: encoder.encode(report)
+ )
+
+ #expect(decoded.durationMilliseconds == 20_750)
+ #expect(decoded.stepResults.map(\.durationMilliseconds) == Array(repeating: 750, count: 16))
+ }
+
+ private func makeReport(
+ preflight: [StabilityFinderPreflightResult]? = nil,
+ steps: [StabilityFinderStepResult]? = nil
+ ) throws -> StabilityFinderRunReport {
+ try StabilityFinderRunReport(
+ correlationID: UUID(uuidString: "10000000-0000-0000-0000-000000000001")!,
+ startedAt: timestamp(0),
+ finishedAt: timestamp(20),
+ preflightResults: preflight ?? passingPreflight(),
+ stepResults: steps ?? passingSteps()
+ )
+ }
+
+ private func passingPreflight() -> [StabilityFinderPreflightResult] {
+ StabilityFinderPreflightCheck.allCases.map {
+ StabilityFinderPreflightResult(
+ check: $0,
+ outcome: .passed,
+ recordedAt: timestamp(0)
+ )
+ }
+ }
+
+ private func passingSteps() -> [StabilityFinderStepResult] {
+ StabilityFinderScenario.allCases.enumerated().map { index, scenario in
+ step(sequenceNumber: UInt16(index + 1), scenario: scenario)
+ }
+ }
+
+ private func step(
+ sequenceNumber: UInt16,
+ scenario: StabilityFinderScenario,
+ correlationID: UUID = UUID(),
+ outcome: StabilityFinderStepOutcome = .passed,
+ assertionOutcome: StabilityFinderAssertionOutcome = .passed
+ ) -> StabilityFinderStepResult {
+ StabilityFinderStepResult(
+ sequenceNumber: sequenceNumber,
+ scenario: scenario,
+ correlationID: correlationID,
+ startedAt: timestamp(Int(sequenceNumber)),
+ finishedAt: timestamp(Int(sequenceNumber) + 1),
+ outcome: outcome,
+ assertions: StabilityFinderAssertionClass.allCases.map {
+ StabilityFinderAssertionResult(
+ assertionClass: $0,
+ outcome: assertionOutcome
+ )
+ }
+ )
+ }
+
+ private func passingAssertions() -> [StabilityFinderAssertionResult] {
+ StabilityFinderAssertionClass.allCases.map {
+ StabilityFinderAssertionResult(assertionClass: $0, outcome: .passed)
+ }
+ }
+
+ private func timestamp(_ seconds: Int) -> Date {
+ Date(timeIntervalSince1970: TimeInterval(seconds))
+ }
+}
diff --git a/potassiumProviderTests/StabilityLabRemoteCoordinatorTests.swift b/potassiumProviderTests/StabilityLabRemoteCoordinatorTests.swift
new file mode 100644
index 0000000..95aea7b
--- /dev/null
+++ b/potassiumProviderTests/StabilityLabRemoteCoordinatorTests.swift
@@ -0,0 +1,841 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@Suite("Stability Lab remote lifecycle")
+struct StabilityLabRemoteCoordinatorTests {
+ private let driveID = 7
+ private let driveRootFileID = 1
+ private let rootFileID = 100
+ private let markerFileID = 101
+ private let identifier = UUID(uuidString: "50000000-0000-0000-0000-000000000001")!
+ private let createdAt = Date(timeIntervalSince1970: 1_800_000_000)
+
+ @Test func ordinaryDomainRejectionMakesNoRemoteCall() async {
+ let remote = StabilityLabRemoteFake()
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+ let ordinaryDomain = StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: 90,
+ rootFileID: 91,
+ encryptionMode: .legacyPlaintext
+ )
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.ordinaryDomainRegistered) {
+ _ = try await coordinator.provision(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ registeredDomainsProvider: { [ordinaryDomain] }
+ )
+ }
+
+ #expect(await remote.snapshot().totalCallCount == 0)
+ }
+
+ @Test func provisionCreatesTopLevelRootAndReadableFixedMarker() async throws {
+ let driveRoot = item(
+ id: driveRootFileID,
+ parentID: 0,
+ type: "dir",
+ etag: "drive-root"
+ )
+ let createdRoot = item(
+ id: rootFileID,
+ parentID: driveRootFileID,
+ type: "dir",
+ etag: "root"
+ )
+ let uploadedMarker = item(
+ id: markerFileID,
+ parentID: rootFileID,
+ type: "file",
+ etag: "marker"
+ )
+ let remote = StabilityLabRemoteFake(
+ items: [driveRootFileID: driveRoot],
+ createDirectoryResponse: createdRoot,
+ uploadResponse: uploadedMarker
+ )
+ let coordinator = StabilityLabRemoteCoordinator(
+ remote: remote,
+ makeUUID: { identifier },
+ now: { createdAt }
+ )
+
+ let configuration = try await coordinator.provision(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ registeredDomainsProvider: { [] }
+ )
+ let snapshot = await remote.snapshot()
+ let uploadedMarkerData = try #require(snapshot.uploadedData)
+ let decodedMarker = try JSONDecoder().decode(
+ StabilityLabOwnershipMarker.self,
+ from: uploadedMarkerData
+ )
+
+ #expect(configuration.driveID == driveID)
+ #expect(configuration.driveRootFileID == driveRootFileID)
+ #expect(configuration.rootFileID == rootFileID)
+ #expect(configuration.ownershipMarkerFileID == markerFileID)
+ #expect(configuration.ownershipMarker == decodedMarker)
+ #expect(decodedMarker.identifier == identifier)
+ #expect(decodedMarker.driveID == driveID)
+ #expect(decodedMarker.rootFileID == rootFileID)
+ #expect(snapshot.createdParentFileID == driveRootFileID)
+ #expect(snapshot.uploadedParentFileID == rootFileID)
+ #expect(snapshot.uploadConflictStrategy == .error)
+ #expect(snapshot.replaceCallCount == 0)
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ #if STABILITY
+ @MainActor
+ @Test func appProvisionRechecksStoredDomainsImmediatelyBeforeRegistration() async throws {
+ let directory = FileManager.default.temporaryDirectory
+ .appendingPathComponent(UUID().uuidString, isDirectory: true)
+ defer { try? FileManager.default.removeItem(at: directory) }
+
+ let driveRoot = item(
+ id: driveRootFileID,
+ parentID: 0,
+ type: "dir",
+ etag: "drive-root"
+ )
+ let createdRoot = item(
+ id: rootFileID,
+ parentID: 2,
+ type: "dir",
+ etag: "root"
+ )
+ let uploadedMarker = item(
+ id: markerFileID,
+ parentID: rootFileID,
+ type: "file",
+ etag: "marker"
+ )
+ let drive = KDriveDriveSummary(
+ id: driveID,
+ name: "fixture",
+ accountID: 0,
+ role: "admin",
+ status: "active",
+ isInMaintenance: false
+ )
+ let remote = StabilityLabRemoteFake(
+ items: [driveRootFileID: driveRoot, 2: item(id: 2, parentID: driveRootFileID, type: "dir", name: "Private")],
+ pages: [StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(items: [item(id: 2, parentID: driveRootFileID, type: "dir", name: "Private")], nextCursor: nil, hasMore: false)],
+ createDirectoryResponse: createdRoot,
+ uploadResponse: uploadedMarker
+ )
+ let domainStore = StabilityLabRegistrationRaceStore()
+ let registrar = StabilityLabRegistrationRecorder()
+ let privateCanary = UUID().uuidString
+ let account = ProviderAccount(
+ accountIdentifier: ProviderConstants.legacyAccountIdentifier,
+ displayName: "fixture",
+ authenticationKind: .manualAccessToken
+ )
+ let tokenStore = InMemoryOAuthTokenStore(token: KDriveOAuthToken(
+ accessToken: privateCanary,
+ tokenType: "Synthetic",
+ refreshToken: nil,
+ scope: nil,
+ idToken: nil,
+ expiresAt: nil
+ ))
+ let databaseURL = directory.appendingPathComponent("state.sqlite3")
+ let model = PotassiumProviderAppModel(
+ accountStore: ProviderAccountFileStore(
+ directoryURL: directory.appendingPathComponent("accounts", isDirectory: true)
+ ),
+ domainStore: domainStore,
+ tokenStore: tokenStore,
+ domainRegistrar: registrar,
+ snapshotStore: try KDriveSnapshotSQLiteStore(databaseURL: databaseURL),
+ eventStore: try KDriveProviderEventSQLiteStore(databaseURL: databaseURL),
+ automaticallyReloadStoredState: false,
+ initialAccounts: [account],
+ initialDrivesByAccountIdentifier: [account.accountIdentifier: [drive]],
+ fileProviderFactory: { _ in remote }
+ )
+
+ await model.provisionStabilityLab(
+ accountIdentifier: account.accountIdentifier,
+ drive: drive
+ )
+
+ #expect(registrar.addedConfigurations.isEmpty)
+ #expect(await domainStore.hasInjectedOrdinaryConfiguration())
+ #expect(model.errorMessage != nil)
+ }
+ #endif
+
+ @Test func provisionRejectsExternalDriveBeforeRemoteMutation() async {
+ let remote = StabilityLabRemoteFake(
+ drives: [
+ KDriveDriveSummary(
+ id: driveID,
+ name: "fixture",
+ accountID: 0,
+ role: "external",
+ status: "active",
+ isInMaintenance: false
+ ),
+ ]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.driveAccessNotVerified) {
+ _ = try await coordinator.provision(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ registeredDomainsProvider: { [] }
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.totalCallCount == 1)
+ #expect(snapshot.createdParentFileID == nil)
+ #expect(snapshot.uploadedParentFileID == nil)
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func provisionRechecksDomainIsolationImmediatelyBeforeCreate() async {
+ let ordinary = StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: 0,
+ rootFileID: 0,
+ encryptionMode: .legacyPlaintext
+ )
+ let evidence = StabilityLabDomainEvidenceSequence([[], [ordinary]])
+ let remote = StabilityLabRemoteFake(items: [
+ driveRootFileID: item(
+ id: driveRootFileID,
+ parentID: 0,
+ type: "dir"
+ ),
+ ])
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.ordinaryDomainRegistered) {
+ _ = try await coordinator.provision(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ registeredDomainsProvider: { await evidence.next() }
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.createdParentFileID == nil)
+ #expect(snapshot.uploadedParentFileID == nil)
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func resetConsumesEveryPageAndPreservesRootAndMarker() async throws {
+ let marker = makeMarker()
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let laterChild = item(id: 103, parentID: rootFileID, type: "file")
+ let earlierChild = item(id: 102, parentID: rootFileID, type: "dir")
+ let remote = StabilityLabRemoteFake(
+ items: [
+ rootFileID: root,
+ markerFileID: markerItem,
+ 102: earlierChild,
+ 103: laterChild,
+ ],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(
+ items: [markerItem, laterChild],
+ nextCursor: "second-page",
+ hasMore: true
+ ),
+ "second-page": KDriveItemPage(
+ items: [earlierChild],
+ nextCursor: nil,
+ hasMore: false
+ ),
+ ]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ let result = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { [registeredLab(marker: marker)] },
+ confirmation: try confirmation(marker: marker)
+ )
+ let snapshot = await remote.snapshot()
+
+ #expect(result.trashedImmediateChildCount == 2)
+ #expect(snapshot.listingCursors == [nil, "second-page"])
+ #expect(snapshot.trashedFileIDs == [102, 103])
+ #expect(snapshot.trashedFileIDs.contains(rootFileID) == false)
+ #expect(snapshot.trashedFileIDs.contains(markerFileID) == false)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func paginationCursorLoopFailsBeforeAnyTrashMutation() async throws {
+ let marker = makeMarker()
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let child = item(id: 102, parentID: rootFileID, type: "file")
+ let repeatingPage = KDriveItemPage(
+ items: [markerItem, child],
+ nextCursor: "repeat",
+ hasMore: true
+ )
+ let remote = StabilityLabRemoteFake(
+ items: [rootFileID: root, markerFileID: markerItem, 102: child],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: repeatingPage,
+ "repeat": repeatingPage,
+ ]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.repeatedDirectoryCursor) {
+ _ = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { [registeredLab(marker: marker)] },
+ confirmation: try confirmation(marker: marker)
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func resetRechecksDomainIsolationBeforeEachTrashMutation() async throws {
+ let marker = makeMarker()
+ let registered = registeredLab(marker: marker)
+ let ordinary = StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: 0,
+ rootFileID: 0,
+ encryptionMode: .legacyPlaintext
+ )
+ let evidence = StabilityLabDomainEvidenceSequence([[registered], [ordinary]])
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let child = item(id: 102, parentID: rootFileID, type: "file")
+ let remote = StabilityLabRemoteFake(
+ items: [rootFileID: root, markerFileID: markerItem, 102: child],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(
+ items: [markerItem, child],
+ nextCursor: nil,
+ hasMore: false
+ ),
+ ]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabResetPlanningError.self) {
+ _ = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { await evidence.next() },
+ confirmation: try confirmation(marker: marker)
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func resetRejectsTargetMovedAfterPlanningBeforeTrashCall() async throws {
+ let marker = makeMarker()
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let listedChild = item(id: 102, parentID: rootFileID, type: "file")
+ let movedChild = item(id: 102, parentID: 999, type: "file")
+ let remote = StabilityLabRemoteFake(
+ items: [rootFileID: root, markerFileID: markerItem, 102: listedChild],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(
+ items: [markerItem, listedChild],
+ nextCursor: nil,
+ hasMore: false
+ ),
+ ],
+ itemResponseQueues: [102: [movedChild]]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.resetTargetIdentityChanged) {
+ _ = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { [registeredLab(marker: marker)] },
+ confirmation: try confirmation(marker: marker)
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func resetRejectsFreshRootDriftBeforeTrashCall() async throws {
+ let marker = makeMarker()
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let movedRoot = item(id: rootFileID, parentID: 999, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let child = item(id: 102, parentID: rootFileID, type: "file")
+ let remote = StabilityLabRemoteFake(
+ items: [rootFileID: root, markerFileID: markerItem, 102: child],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(
+ items: [markerItem, child],
+ nextCursor: nil,
+ hasMore: false
+ ),
+ ],
+ itemResponseQueues: [rootFileID: [root, movedRoot]]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.invalidProvisionedRoot) {
+ _ = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { [registeredLab(marker: marker)] },
+ confirmation: try confirmation(marker: marker)
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ @Test func resetRejectsFreshMarkerDriftBeforeTrashCall() async throws {
+ let marker = makeMarker()
+ let changedMarker = StabilityLabOwnershipMarker(
+ identifier: UUID(uuidString: "50000000-0000-0000-0000-000000000099")!,
+ driveID: driveID,
+ rootFileID: rootFileID,
+ createdAt: createdAt
+ )
+ let root = item(id: rootFileID, parentID: driveRootFileID, type: "dir")
+ let markerItem = item(id: markerFileID, parentID: rootFileID, type: "file")
+ let child = item(id: 102, parentID: rootFileID, type: "file")
+ let remote = StabilityLabRemoteFake(
+ items: [rootFileID: root, markerFileID: markerItem, 102: child],
+ markerData: try JSONEncoder().encode(marker),
+ pages: [
+ StabilityLabRemoteFake.initialCursorKey: KDriveItemPage(
+ items: [markerItem, child],
+ nextCursor: nil,
+ hasMore: false
+ ),
+ ],
+ downloadDataQueue: [
+ try JSONEncoder().encode(marker),
+ try JSONEncoder().encode(changedMarker),
+ ]
+ )
+ let coordinator = StabilityLabRemoteCoordinator(remote: remote)
+
+ await #expect(throws: StabilityLabRemoteCoordinatorError.ownershipMarkerMismatch) {
+ _ = try await coordinator.reset(
+ configuration: configuration(marker: marker),
+ configuredEncryptionMode: .legacyPlaintext,
+ registeredDomainsProvider: { [registeredLab(marker: marker)] },
+ confirmation: try confirmation(marker: marker)
+ )
+ }
+
+ let snapshot = await remote.snapshot()
+ #expect(snapshot.trashedFileIDs.isEmpty)
+ #expect(snapshot.permanentlyDeletedFileIDs.isEmpty)
+ }
+
+ private func makeMarker() -> StabilityLabOwnershipMarker {
+ StabilityLabOwnershipMarker(
+ identifier: identifier,
+ driveID: driveID,
+ rootFileID: rootFileID,
+ createdAt: createdAt
+ )
+ }
+
+ private func configuration(
+ marker: StabilityLabOwnershipMarker
+ ) -> StabilityLabRemoteConfiguration {
+ StabilityLabRemoteConfiguration(
+ driveID: driveID,
+ driveRootFileID: driveRootFileID,
+ rootFileID: rootFileID,
+ ownershipMarkerFileID: markerFileID,
+ ownershipMarker: marker
+ )
+ }
+
+ private func registeredLab(
+ marker: StabilityLabOwnershipMarker
+ ) -> StabilityLabRegisteredDomain {
+ StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: driveID,
+ rootFileID: rootFileID,
+ encryptionMode: .legacyPlaintext,
+ ownershipMarkerIdentifier: marker.identifier
+ )
+ }
+
+ private func confirmation(
+ marker: StabilityLabOwnershipMarker
+ ) throws -> StabilityLabResetConfirmation {
+ try StabilityLabResetConfirmation(
+ typedPhrase: StabilityLabResetConfirmation.requiredPhrase,
+ marker: marker
+ )
+ }
+
+ private func item(
+ id: Int,
+ parentID: Int,
+ type: String,
+ etag: String? = "etag",
+ name: String = "fixture"
+ ) -> KDriveRemoteItem {
+ KDriveRemoteItem(
+ id: id,
+ name: name,
+ type: type,
+ status: "active",
+ driveID: driveID,
+ parentID: parentID,
+ path: nil,
+ size: type == "file" ? 10 : nil,
+ mimeType: type == "file" ? "application/json" : nil,
+ createdAt: createdAt,
+ modifiedAt: createdAt,
+ revisedAt: createdAt,
+ updatedAt: createdAt,
+ etag: etag
+ )
+ }
+
+}
+
+private actor StabilityLabDomainEvidenceSequence {
+ private var values: [[StabilityLabRegisteredDomain]]
+
+ init(_ values: [[StabilityLabRegisteredDomain]]) {
+ self.values = values
+ }
+
+ func next() -> [StabilityLabRegisteredDomain] {
+ guard values.count > 1 else { return values.first ?? [] }
+ return values.removeFirst()
+ }
+}
+
+private actor StabilityLabRegistrationRaceStore: DomainConfigurationStoring {
+ private var savedLab: ProviderDomainConfiguration?
+ private var readsAfterSave = 0
+ private var didInjectOrdinaryConfiguration = false
+
+ func allConfigurations() -> [ProviderDomainConfiguration] {
+ guard let savedLab else { return [] }
+ readsAfterSave += 1
+ guard readsAfterSave > 1 else { return [savedLab] }
+ didInjectOrdinaryConfiguration = true
+ return [savedLab, ordinaryConfiguration]
+ }
+
+ func configuration(domainIdentifier: String) -> ProviderDomainConfiguration? {
+ guard let savedLab else { return nil }
+ if savedLab.domainIdentifier == domainIdentifier { return savedLab }
+ return ordinaryConfiguration.domainIdentifier == domainIdentifier
+ ? ordinaryConfiguration
+ : nil
+ }
+
+ func save(_ configuration: ProviderDomainConfiguration) {
+ savedLab = configuration
+ }
+
+ func remove(domainIdentifier: String) {
+ if savedLab?.domainIdentifier == domainIdentifier {
+ savedLab = nil
+ }
+ }
+
+ func hasInjectedOrdinaryConfiguration() -> Bool {
+ didInjectOrdinaryConfiguration
+ }
+
+ private var ordinaryConfiguration: ProviderDomainConfiguration {
+ ProviderDomainConfiguration(
+ domainIdentifier: "ordinary-race-fixture",
+ displayName: "fixture",
+ driveID: 900,
+ driveName: "fixture",
+ rootFileID: 901
+ )
+ }
+}
+
+@MainActor
+private final class StabilityLabRegistrationRecorder: ProviderDomainRegistering {
+ private(set) var addedConfigurations: [ProviderDomainConfiguration] = []
+
+ func addDomain(for configuration: ProviderDomainConfiguration) async throws {
+ addedConfigurations.append(configuration)
+ }
+
+ func removeDomain(for configuration: ProviderDomainConfiguration) async throws {}
+
+ func registeredDomainIdentifiers() async throws -> Set { [] }
+}
+
+private enum StabilityLabRemoteFakeError: Error {
+ case unexpectedCall
+}
+
+private struct StabilityLabRemoteFakeSnapshot: Sendable {
+ let totalCallCount: Int
+ let createdParentFileID: Int?
+ let uploadedParentFileID: Int?
+ let uploadConflictStrategy: KDriveUploadConflictStrategy?
+ let uploadedData: Data?
+ let replaceCallCount: Int
+ let listingCursors: [String?]
+ let trashedFileIDs: [Int]
+ let permanentlyDeletedFileIDs: [Int]
+}
+
+private actor StabilityLabRemoteFake: KDriveFileProviding {
+ static let initialCursorKey = ""
+
+ private var items: [Int: KDriveRemoteItem]
+ private let drives: [KDriveDriveSummary]
+ private var markerData: Data?
+ private var downloadDataQueue: [Data]
+ private let pages: [String: KDriveItemPage]
+ private var itemResponseQueues: [Int: [KDriveRemoteItem]]
+ private let createDirectoryResponse: KDriveRemoteItem?
+ private let uploadResponse: KDriveRemoteItem?
+
+ private var totalCallCount = 0
+ private var createdParentFileID: Int?
+ private var uploadedParentFileID: Int?
+ private var uploadConflictStrategy: KDriveUploadConflictStrategy?
+ private var uploadedData: Data?
+ private var replaceCallCount = 0
+ private var listingCursors: [String?] = []
+ private var trashedFileIDs: [Int] = []
+ private var permanentlyDeletedFileIDs: [Int] = []
+
+ init(
+ items: [Int: KDriveRemoteItem] = [:],
+ drives: [KDriveDriveSummary] = [
+ KDriveDriveSummary(
+ id: 7,
+ name: "fixture",
+ accountID: 0,
+ role: "admin",
+ status: "active",
+ isInMaintenance: false
+ ),
+ ],
+ markerData: Data? = nil,
+ pages: [String: KDriveItemPage] = [:],
+ itemResponseQueues: [Int: [KDriveRemoteItem]] = [:],
+ downloadDataQueue: [Data] = [],
+ createDirectoryResponse: KDriveRemoteItem? = nil,
+ uploadResponse: KDriveRemoteItem? = nil
+ ) {
+ self.items = items
+ self.drives = drives
+ self.markerData = markerData
+ self.pages = pages
+ self.itemResponseQueues = itemResponseQueues
+ self.downloadDataQueue = downloadDataQueue
+ self.createDirectoryResponse = createDirectoryResponse
+ self.uploadResponse = uploadResponse
+ }
+
+ func snapshot() -> StabilityLabRemoteFakeSnapshot {
+ StabilityLabRemoteFakeSnapshot(
+ totalCallCount: totalCallCount,
+ createdParentFileID: createdParentFileID,
+ uploadedParentFileID: uploadedParentFileID,
+ uploadConflictStrategy: uploadConflictStrategy,
+ uploadedData: uploadedData,
+ replaceCallCount: replaceCallCount,
+ listingCursors: listingCursors,
+ trashedFileIDs: trashedFileIDs,
+ permanentlyDeletedFileIDs: permanentlyDeletedFileIDs
+ )
+ }
+
+ func listDrives() async throws -> [KDriveDriveSummary] {
+ totalCallCount += 1
+ return drives
+ }
+
+ func item(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
+ totalCallCount += 1
+ if var queued = itemResponseQueues[fileID], queued.isEmpty == false {
+ let result = queued.removeFirst()
+ itemResponseQueues[fileID] = queued
+ return result
+ }
+ guard let item = items[fileID] else {
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+ return item
+ }
+
+ func listDirectory(
+ driveID: Int,
+ folderID: Int,
+ cursor: String?,
+ limit: Int
+ ) async throws -> KDriveItemPage {
+ totalCallCount += 1
+ listingCursors.append(cursor)
+ guard let page = pages[cursor ?? Self.initialCursorKey] else {
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+ return page
+ }
+
+ func listAdvancedDirectory(
+ driveID: Int,
+ folderID: Int,
+ cursor: String?,
+ limit: Int
+ ) async throws -> KDriveAdvancedItemPage {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func listTrash(
+ driveID: Int,
+ cursor: String?,
+ limit: Int
+ ) async throws -> KDriveItemPage {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func downloadFile(driveID: Int, fileID: Int) async throws -> Data {
+ totalCallCount += 1
+ if downloadDataQueue.isEmpty == false {
+ return downloadDataQueue.removeFirst()
+ }
+ guard let markerData else {
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+ return markerData
+ }
+
+ func thumbnail(
+ driveID: Int,
+ fileID: Int,
+ width: Int?,
+ height: Int?
+ ) async throws -> Data {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func uploadFile(
+ driveID: Int,
+ parentID: Int,
+ fileName: String,
+ contents: Data,
+ lastModifiedAt: Date?,
+ conflictStrategy: KDriveUploadConflictStrategy,
+ clientToken: String?,
+ contentHash: String?
+ ) async throws -> KDriveRemoteItem {
+ totalCallCount += 1
+ guard let uploadResponse else {
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+ uploadedParentFileID = parentID
+ uploadConflictStrategy = conflictStrategy
+ uploadedData = contents
+ markerData = contents
+ items[uploadResponse.id] = uploadResponse
+ return uploadResponse
+ }
+
+ func replaceFile(
+ driveID: Int,
+ fileID: Int,
+ expectedETag: String,
+ clientToken: String,
+ contentHash: String,
+ contents: Data,
+ lastModifiedAt: Date?
+ ) async throws -> KDriveRemoteItem {
+ totalCallCount += 1
+ replaceCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func createDirectory(
+ driveID: Int,
+ parentID: Int,
+ name: String
+ ) async throws -> KDriveRemoteItem {
+ totalCallCount += 1
+ guard let createDirectoryResponse else {
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+ createdParentFileID = parentID
+ items[createDirectoryResponse.id] = createDirectoryResponse
+ return createDirectoryResponse
+ }
+
+ func renameItem(driveID: Int, fileID: Int, name: String) async throws {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func moveItem(
+ driveID: Int,
+ fileID: Int,
+ destinationParentID: Int,
+ name: String?
+ ) async throws {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func updateModificationDate(driveID: Int, fileID: Int, date: Date) async throws {
+ totalCallCount += 1
+ throw StabilityLabRemoteFakeError.unexpectedCall
+ }
+
+ func trashItem(driveID: Int, fileID: Int) async throws {
+ totalCallCount += 1
+ trashedFileIDs.append(fileID)
+ }
+
+ func deleteTrashedItem(driveID: Int, fileID: Int) async throws {
+ totalCallCount += 1
+ permanentlyDeletedFileIDs.append(fileID)
+ }
+}
diff --git a/potassiumProviderTests/StabilityLabSafetyTests.swift b/potassiumProviderTests/StabilityLabSafetyTests.swift
new file mode 100644
index 0000000..f65482d
--- /dev/null
+++ b/potassiumProviderTests/StabilityLabSafetyTests.swift
@@ -0,0 +1,547 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+@Suite("Stability Lab safety")
+struct StabilityLabSafetyTests {
+ @Test func markerSurvivesRemoteAndDomainDateEncodings() throws {
+ var marker = makeMarker()
+ // Simulate a pre-fix remote marker containing subsecond precision.
+ marker.createdAt = Date(timeIntervalSince1970: 1_800_000_000.875)
+ let remote = try JSONDecoder().decode(StabilityLabOwnershipMarker.self, from: JSONEncoder().encode(marker))
+ let domainEncoder = JSONEncoder(); domainEncoder.dateEncodingStrategy = .iso8601
+ let domainDecoder = JSONDecoder(); domainDecoder.dateDecodingStrategy = .iso8601
+ let local = try domainDecoder.decode(StabilityLabOwnershipMarker.self, from: domainEncoder.encode(marker))
+ #expect(remote == local)
+ #expect(remote.identifier == marker.identifier)
+ #expect(remote.rootFileID == marker.rootFileID)
+ }
+
+ @Test func privateParentMustBeVerifiedAndCannotBeTheLabRoot() {
+ var input = makeInput(marker: makeMarker())
+ input.expectedMarker.parentFileID = 10
+ input.root.parentFileID = 10
+ input.root.ownershipMarker = input.expectedMarker
+ #expect(!StabilityLabSafety.preflight(input).isAllowed)
+ input.root.verifiedPrivateParentFileID = 10
+ #expect(StabilityLabSafety.preflight(input).isAllowed)
+ input.expectedMarker.parentFileID = input.expectedMarker.rootFileID
+ input.root.parentFileID = input.expectedMarker.rootFileID
+ input.root.verifiedPrivateParentFileID = input.expectedMarker.rootFileID
+ input.root.ownershipMarker = input.expectedMarker
+ #expect(!StabilityLabSafety.preflight(input).isAllowed)
+ }
+
+ @Test func ownershipMarkerRoundTripsWithoutAccountOrPathFields() throws {
+ let marker = makeMarker()
+ let data = try JSONEncoder().encode(marker)
+ let encoded = try #require(String(data: data, encoding: .utf8))
+
+ #expect(try JSONDecoder().decode(StabilityLabOwnershipMarker.self, from: data) == marker)
+ #expect(encoded.contains("account") == false)
+ #expect(encoded.contains("path") == false)
+ #expect(encoded.contains("name") == false)
+ #expect(encoded.contains("markerFileID") == false)
+ }
+
+ @Test func safeDedicatedPlaintextRootPassesPreflight() {
+ let marker = makeMarker()
+ let result = StabilityLabSafety.preflight(makeInput(marker: marker))
+
+ #expect(result.isAllowed)
+ #expect(result.issues.isEmpty)
+ }
+
+ @Test func preflightRejectsTheDriveRootEvenWhenItHasTheExpectedMarker() {
+ let marker = makeMarker(rootFileID: 1)
+ let input = makeInput(
+ marker: marker,
+ root: StabilityLabRootObservation(
+ driveID: marker.driveID,
+ fileID: marker.rootFileID,
+ parentFileID: marker.rootFileID,
+ driveRootFileID: marker.rootFileID,
+ hasVerifiedLabOwnership: true,
+ ownershipMarker: marker
+ )
+ )
+
+ let result = StabilityLabSafety.preflight(input)
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.driveRootSelected))
+ }
+
+ @Test func preflightRejectsAnyOrdinaryRegisteredDomain() {
+ let marker = makeMarker()
+ let ordinary = StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: 400,
+ rootFileID: 500,
+ encryptionMode: .legacyPlaintext
+ )
+ let input = makeInput(marker: marker, registeredDomains: [ordinary])
+
+ let result = StabilityLabSafety.preflight(input)
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.ordinaryDomainRegistered))
+ }
+
+ @Test func preflightRejectsBothOpaqueVaultFormats() {
+ let marker = makeMarker()
+
+ for mode in [ProviderEncryptionMode.opaqueVaultV1, .opaqueVaultV2] {
+ let result = StabilityLabSafety.preflight(makeInput(
+ marker: marker,
+ configuredEncryptionMode: mode
+ ))
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.unsupportedEncryptedDomain))
+ }
+ }
+
+ @Test func preflightRejectsMissingAndMismatchedOwnershipMarkers() {
+ let expected = makeMarker()
+ let missing = StabilityLabSafety.preflight(makeInput(
+ marker: expected,
+ root: makeRoot(marker: expected, ownershipMarker: nil)
+ ))
+ let mismatchedMarker = StabilityLabOwnershipMarker(
+ identifier: UUID(),
+ driveID: expected.driveID,
+ rootFileID: expected.rootFileID,
+ createdAt: expected.createdAt
+ )
+ let mismatched = StabilityLabSafety.preflight(makeInput(
+ marker: expected,
+ root: makeRoot(marker: expected, ownershipMarker: mismatchedMarker)
+ ))
+
+ #expect(missing.issues.contains(.ownershipMarkerMissing))
+ #expect(mismatched.issues.contains(.ownershipMarkerMismatch))
+ }
+
+ @Test func preflightRejectsMarkerAndObservedRootIdentityDrift() {
+ let marker = makeMarker()
+ let wrongRoot = StabilityLabRootObservation(
+ driveID: marker.driveID,
+ fileID: marker.rootFileID + 1,
+ parentFileID: 1,
+ driveRootFileID: 1,
+ hasVerifiedLabOwnership: true,
+ ownershipMarker: marker
+ )
+ let result = StabilityLabSafety.preflight(makeInput(marker: marker, root: wrongRoot))
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.rootIdentityMismatch))
+ }
+
+ @Test func preflightRejectsAnUnboundOwnershipMarkerFileIdentity() {
+ let marker = StabilityLabOwnershipMarker(
+ identifier: UUID(),
+ driveID: 10,
+ rootFileID: 20
+ )
+
+ let result = StabilityLabSafety.preflight(makeInput(
+ marker: marker,
+ expectedOwnershipMarkerFileID: 0
+ ))
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.invalidOwnershipMarkerFileIdentity))
+ }
+
+ @Test func preflightRejectsRootsThatAreNotTopLevelOrLackVerifiedLabOwnership() {
+ let marker = makeMarker()
+ let root = StabilityLabRootObservation(
+ driveID: marker.driveID,
+ fileID: marker.rootFileID,
+ parentFileID: 99,
+ driveRootFileID: 1,
+ hasVerifiedLabOwnership: false,
+ ownershipMarker: marker
+ )
+ let result = StabilityLabSafety.preflight(makeInput(marker: marker, root: root))
+
+ #expect(result.issues.contains(.rootIsNotTopLevel))
+ #expect(result.issues.contains(.rootOwnershipNotVerified))
+ }
+
+ @Test func preflightRejectsStaleOrDuplicateLabRegistrations() {
+ let marker = makeMarker()
+ let stale = StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: marker.driveID,
+ rootFileID: marker.rootFileID + 1,
+ encryptionMode: .legacyPlaintext,
+ ownershipMarkerIdentifier: marker.identifier
+ )
+ let matching = StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: marker.driveID,
+ rootFileID: marker.rootFileID,
+ encryptionMode: .legacyPlaintext,
+ ownershipMarkerIdentifier: marker.identifier
+ )
+ let result = StabilityLabSafety.preflight(makeInput(
+ marker: marker,
+ registeredDomains: [stale, matching]
+ ))
+
+ #expect(result.issues.contains(.registeredLabDomainMismatch))
+ #expect(result.issues.contains(.multipleStabilityLabDomains))
+ }
+
+ @Test func preflightRejectsAMissingRegisteredLabDomain() {
+ let marker = makeMarker()
+ let result = StabilityLabSafety.preflight(makeInput(
+ marker: marker,
+ registeredDomains: []
+ ))
+
+ #expect(result.isAllowed == false)
+ #expect(result.issues.contains(.registeredLabDomainMissing))
+ }
+
+ @Test func destructiveConfirmationRequiresTheExactTypedPhrase() throws {
+ let marker = makeMarker()
+
+ for invalidPhrase in [
+ "delete stability lab contents",
+ "DELETE STABILITY LAB CONTENTS ",
+ "DELETE STABILITY LAB",
+ "",
+ ] {
+ #expect(throws: StabilityLabResetConfirmationError.typedPhraseMismatch) {
+ _ = try StabilityLabResetConfirmation(typedPhrase: invalidPhrase, marker: marker)
+ }
+ }
+
+ _ = try StabilityLabResetConfirmation(
+ typedPhrase: StabilityLabResetConfirmation.requiredPhrase,
+ marker: marker
+ )
+ }
+
+ @Test func resetPlanDeletesOnlySortedImmediateRootContents() throws {
+ let marker = makeMarker()
+ let input = makeInput(marker: marker)
+ let confirmation = try makeConfirmation(marker: marker)
+ let inventory = makeInventory(
+ marker: marker,
+ children: [
+ StabilityLabRootChild(fileID: 90, parentFileID: marker.rootFileID),
+ StabilityLabRootChild(fileID: 40, parentFileID: marker.rootFileID),
+ ]
+ )
+
+ let plan = try StabilityLabSafety.planReset(
+ input: input,
+ inventory: inventory,
+ confirmation: confirmation
+ )
+
+ #expect(plan.actions == [
+ .trashImmediateChild(fileID: 40),
+ .trashImmediateChild(fileID: 90),
+ ])
+ #expect(plan.preservedRootFileID == marker.rootFileID)
+ #expect(plan.preservedOwnershipMarkerFileID == ownershipMarkerFileID)
+ #expect(plan.deletesRoot == false)
+ }
+
+ @Test func emptyCompleteInventoryProducesAValidNoOpPlan() throws {
+ let marker = makeMarker()
+ let plan = try StabilityLabSafety.planReset(
+ input: makeInput(marker: marker),
+ inventory: makeInventory(marker: marker),
+ confirmation: makeConfirmation(marker: marker)
+ )
+
+ #expect(plan.actions.isEmpty)
+ #expect(plan.deletesRoot == false)
+ }
+
+ @Test func resetPlanningRecomputesAndEnforcesPreflight() throws {
+ let marker = makeMarker()
+ let unsafeInput = makeInput(
+ marker: marker,
+ registeredDomains: [StabilityLabRegisteredDomain(
+ purpose: .ordinary,
+ driveID: marker.driveID,
+ rootFileID: 700,
+ encryptionMode: .legacyPlaintext
+ )]
+ )
+
+ do {
+ _ = try StabilityLabSafety.planReset(
+ input: unsafeInput,
+ inventory: makeInventory(marker: marker),
+ confirmation: makeConfirmation(marker: marker)
+ )
+ Issue.record("Unsafe preflight unexpectedly produced a reset plan")
+ } catch let error as StabilityLabResetPlanningError {
+ guard case .preflightRejected(let issues) = error else {
+ Issue.record("Unexpected reset planning error: \(error)")
+ return
+ }
+ #expect(issues.contains(.ordinaryDomainRegistered))
+ }
+ }
+
+ @Test func resetPlanningRejectsAConfirmationForAStaleRoot() throws {
+ let current = makeMarker()
+ let stale = makeMarker(rootFileID: current.rootFileID + 1)
+
+ #expect(throws: StabilityLabResetPlanningError.confirmationDoesNotMatchRoot) {
+ _ = try StabilityLabSafety.planReset(
+ input: makeInput(marker: current),
+ inventory: makeInventory(marker: current),
+ confirmation: makeConfirmation(marker: stale)
+ )
+ }
+ }
+
+ @Test func resetPlanningRejectsIncompleteOrOversizedInventories() throws {
+ let marker = makeMarker()
+ let input = makeInput(marker: marker)
+ let confirmation = try makeConfirmation(marker: marker)
+
+ #expect(throws: StabilityLabResetPlanningError.incompleteInventory) {
+ _ = try StabilityLabSafety.planReset(
+ input: input,
+ inventory: makeInventory(marker: marker, isComplete: false),
+ confirmation: confirmation
+ )
+ }
+ #expect(throws: StabilityLabResetPlanningError.maximumRootChildrenExceeded(limit: 1)) {
+ _ = try StabilityLabSafety.planReset(
+ input: input,
+ inventory: makeInventory(
+ marker: marker,
+ children: [
+ StabilityLabRootChild(fileID: 40, parentFileID: marker.rootFileID),
+ StabilityLabRootChild(fileID: 41, parentFileID: marker.rootFileID),
+ ]
+ ),
+ confirmation: confirmation,
+ policy: StabilityLabResetPolicy(maximumRootChildren: 1)
+ )
+ }
+ }
+
+ @Test func resetPlanningRejectsRootDriveRootWrongParentAndDuplicateTargets() throws {
+ let marker = makeMarker()
+ let input = makeInput(marker: marker)
+ let confirmation = try makeConfirmation(marker: marker)
+
+ let cases: [(StabilityLabRootInventory, StabilityLabResetPlanningError)] = [
+ (
+ makeInventory(
+ marker: marker,
+ children: [StabilityLabRootChild(fileID: marker.rootFileID, parentFileID: marker.rootFileID)]
+ ),
+ .rootIncludedInContents
+ ),
+ (
+ makeInventory(
+ marker: marker,
+ children: [StabilityLabRootChild(fileID: 1, parentFileID: marker.rootFileID)]
+ ),
+ .driveRootIncludedInContents
+ ),
+ (
+ makeInventory(
+ marker: marker,
+ children: [StabilityLabRootChild(fileID: 40, parentFileID: 999)]
+ ),
+ .contentIsNotImmediateChild
+ ),
+ (
+ makeInventory(
+ marker: marker,
+ children: [
+ StabilityLabRootChild(fileID: 40, parentFileID: marker.rootFileID),
+ StabilityLabRootChild(fileID: 40, parentFileID: marker.rootFileID),
+ ]
+ ),
+ .duplicateContentIdentifier
+ ),
+ ]
+
+ for (inventory, expectedError) in cases {
+ #expect(throws: expectedError) {
+ _ = try StabilityLabSafety.planReset(
+ input: input,
+ inventory: inventory,
+ confirmation: confirmation
+ )
+ }
+ }
+ }
+
+ @Test func resetPlanningRejectsMissingOwnershipMarkerFileEvidence() throws {
+ let marker = makeMarker()
+ let input = makeInput(marker: marker)
+ let confirmation = try makeConfirmation(marker: marker)
+
+ let inventories = [
+ StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: nil,
+ children: [
+ StabilityLabRootChild(fileID: ownershipMarkerFileID, parentFileID: marker.rootFileID),
+ ]
+ ),
+ StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: ownershipMarkerFileID,
+ children: []
+ ),
+ ]
+
+ for inventory in inventories {
+ #expect(throws: StabilityLabResetPlanningError.ownershipMarkerFileEvidenceMissing) {
+ _ = try StabilityLabSafety.planReset(
+ input: input,
+ inventory: inventory,
+ confirmation: confirmation
+ )
+ }
+ }
+ }
+
+ @Test func resetPlanningRejectsDuplicateOwnershipMarkerFileEvidence() throws {
+ let marker = makeMarker()
+ let inventory = StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: ownershipMarkerFileID,
+ children: [
+ StabilityLabRootChild(fileID: ownershipMarkerFileID, parentFileID: marker.rootFileID),
+ StabilityLabRootChild(fileID: ownershipMarkerFileID, parentFileID: marker.rootFileID),
+ ]
+ )
+
+ #expect(throws: StabilityLabResetPlanningError.ownershipMarkerFileEvidenceDuplicate) {
+ _ = try StabilityLabSafety.planReset(
+ input: makeInput(marker: marker),
+ inventory: inventory,
+ confirmation: makeConfirmation(marker: marker)
+ )
+ }
+ }
+
+ @Test func resetPlanningRejectsMismatchedOrMisparentedOwnershipMarkerFileEvidence() throws {
+ let marker = makeMarker()
+ let confirmation = try makeConfirmation(marker: marker)
+ let mismatched = StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: ownershipMarkerFileID + 1,
+ children: [
+ StabilityLabRootChild(fileID: ownershipMarkerFileID + 1, parentFileID: marker.rootFileID),
+ ]
+ )
+ let misparented = StabilityLabRootInventory(
+ isComplete: true,
+ ownershipMarkerFileID: ownershipMarkerFileID,
+ children: [
+ StabilityLabRootChild(fileID: ownershipMarkerFileID, parentFileID: marker.rootFileID + 1),
+ ]
+ )
+
+ #expect(throws: StabilityLabResetPlanningError.ownershipMarkerFileEvidenceMismatch) {
+ _ = try StabilityLabSafety.planReset(
+ input: makeInput(marker: marker),
+ inventory: mismatched,
+ confirmation: confirmation
+ )
+ }
+ #expect(throws: StabilityLabResetPlanningError.ownershipMarkerFileIsNotImmediateChild) {
+ _ = try StabilityLabSafety.planReset(
+ input: makeInput(marker: marker),
+ inventory: misparented,
+ confirmation: confirmation
+ )
+ }
+ }
+
+ private func makeMarker(rootFileID: Int = 20) -> StabilityLabOwnershipMarker {
+ StabilityLabOwnershipMarker(
+ identifier: UUID(uuidString: "20EAF760-A682-4E38-BF1B-4EE4D33979F0")!,
+ driveID: 10,
+ rootFileID: rootFileID,
+ createdAt: Date(timeIntervalSince1970: 1_787_000_000)
+ )
+ }
+
+ private func makeRoot(
+ marker: StabilityLabOwnershipMarker,
+ ownershipMarker: StabilityLabOwnershipMarker?
+ ) -> StabilityLabRootObservation {
+ StabilityLabRootObservation(
+ driveID: marker.driveID,
+ fileID: marker.rootFileID,
+ parentFileID: 1,
+ driveRootFileID: 1,
+ hasVerifiedLabOwnership: true,
+ ownershipMarker: ownershipMarker
+ )
+ }
+
+ private func makeInput(
+ marker: StabilityLabOwnershipMarker,
+ expectedOwnershipMarkerFileID: Int = 30,
+ configuredEncryptionMode: ProviderEncryptionMode = .legacyPlaintext,
+ root: StabilityLabRootObservation? = nil,
+ registeredDomains: [StabilityLabRegisteredDomain]? = nil
+ ) -> StabilityLabPreflightInput {
+ StabilityLabPreflightInput(
+ expectedMarker: marker,
+ expectedOwnershipMarkerFileID: expectedOwnershipMarkerFileID,
+ configuredEncryptionMode: configuredEncryptionMode,
+ root: root ?? makeRoot(marker: marker, ownershipMarker: marker),
+ registeredDomains: registeredDomains ?? [
+ StabilityLabRegisteredDomain(
+ purpose: .stabilityLab,
+ driveID: marker.driveID,
+ rootFileID: marker.rootFileID,
+ encryptionMode: .legacyPlaintext,
+ ownershipMarkerIdentifier: marker.identifier
+ ),
+ ]
+ )
+ }
+
+ private func makeConfirmation(
+ marker: StabilityLabOwnershipMarker
+ ) throws -> StabilityLabResetConfirmation {
+ try StabilityLabResetConfirmation(
+ typedPhrase: StabilityLabResetConfirmation.requiredPhrase,
+ marker: marker
+ )
+ }
+
+ private func makeInventory(
+ marker: StabilityLabOwnershipMarker,
+ isComplete: Bool = true,
+ children: [StabilityLabRootChild] = []
+ ) -> StabilityLabRootInventory {
+ StabilityLabRootInventory(
+ isComplete: isComplete,
+ ownershipMarkerFileID: ownershipMarkerFileID,
+ children: [
+ StabilityLabRootChild(
+ fileID: ownershipMarkerFileID,
+ parentFileID: marker.rootFileID
+ ),
+ ] + children
+ )
+ }
+
+ private var ownershipMarkerFileID: Int { 30 }
+}
diff --git a/potassiumProviderTests/StabilityLaunchPreparationTests.swift b/potassiumProviderTests/StabilityLaunchPreparationTests.swift
new file mode 100644
index 0000000..f1486a6
--- /dev/null
+++ b/potassiumProviderTests/StabilityLaunchPreparationTests.swift
@@ -0,0 +1,49 @@
+#if os(macOS) && STABILITY
+import Foundation
+import PotassiumProviderCore
+import Testing
+@testable import potassiumProvider
+
+@MainActor
+struct StabilityLaunchPreparationTests {
+ @Test func cachedPreflightActivelyRequestsAnObservationBetweenProcessChecks() async throws {
+ var calls: [String] = []
+ try await StabilityWarmLaunchObservation.request(
+ verifyProcess: { calls.append("verify") },
+ signalWorkingSet: { calls.append("signal") })
+ #expect(calls == ["verify", "signal", "verify"])
+ }
+
+ @Test func changedProcessPreventsSignalOrRejectsItsAcknowledgement() async throws {
+ for changeBeforeSignal in [true, false] {
+ var changed = changeBeforeSignal, signalCount = 0
+ await #expect(throws: StabilityLaunchPreparationError.initialProcessChanged) {
+ try await StabilityWarmLaunchObservation.request(
+ verifyProcess: { if changed { throw StabilityLaunchPreparationError.initialProcessChanged } },
+ signalWorkingSet: { signalCount += 1; changed = true })
+ }
+ #expect(signalCount == (changeBeforeSignal ? 0 : 1))
+ }
+ }
+
+ @Test func requestFailureCannotAdvancePreparation() async throws {
+ var verified = 0
+ await #expect(throws: URLError.self) {
+ try await StabilityWarmLaunchObservation.request(
+ verifyProcess: { verified += 1 },
+ signalWorkingSet: { throw URLError(.cannotConnectToHost) })
+ }
+ #expect(verified == 1)
+ }
+
+ @Test func failureEvidenceContainsNoErrorPayloadAndCannotCertifyAcceptance() throws {
+ let canary = "private-" + UUID().uuidString
+ let error = NSError(domain: canary, code: 17, userInfo: [NSLocalizedDescriptionKey: canary])
+ let failure = StabilityLaunchPreparationFailure(stage: .launchPreparation, error: error)
+ let data = try JSONEncoder().encode(failure)
+ #expect(!String(decoding: data, as: UTF8.self).contains(canary))
+ #expect(failure.schemaVersion == 1 && !failure.eligibleForAcceptance)
+ #expect(failure.stage == .launchPreparation && failure.reason == "unclassified" && failure.errorCode == 17)
+ }
+}
+#endif
diff --git a/potassiumProviderTests/StabilityLiveEvidenceTests.swift b/potassiumProviderTests/StabilityLiveEvidenceTests.swift
new file mode 100644
index 0000000..d68033b
--- /dev/null
+++ b/potassiumProviderTests/StabilityLiveEvidenceTests.swift
@@ -0,0 +1,214 @@
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@Suite("Live Finder evidence")
+struct StabilityLiveEvidenceTests {
+ private let time = Date(timeIntervalSince1970: 1_800_000_000)
+ private let subject = UUID()
+ private let correlation = UUID()
+ private let process = UUID()
+ private let hash = String(repeating: "a", count: 40)
+
+ private func step(_ scenario: StabilityFinderScenario, conflict: Bool = false, cancellation: Bool = false,
+ workingSet: Bool = false, actions: Int = 1, actionsHash: String? = nil, metadata: UUID? = nil) -> StabilityFinderStepResult {
+ StabilityFinderStepResult(sequenceNumber: 1, scenario: scenario, correlationID: correlation,
+ startedAt: time, finishedAt: time.addingTimeInterval(10), outcome: .passed, assertions: [],
+ liveEvidence: StabilityLiveStepEvidence(subjects: [subject], observedUIActions: actions,
+ expectedExtensionCodeHash: hash, conflictBarrierReached: conflict,
+ cancellationObserved: cancellation, workingSetMemberObserved: workingSet, expectedActionsCodeHash: actionsHash,
+ expectedWorkingSetMetadataAlias: metadata))
+ }
+
+ private func span(_ operation: ProviderDiagnosticOperation, terminal: ProviderDiagnosticPhase = .completed,
+ subject override: UUID? = nil, offset: TimeInterval = 0, parent: UUID? = nil, progress: Bool = false,
+ source: ProviderDiagnosticSource = .fileProviderExtension, fields: [ProviderDiagnosticField] = [],
+ metadata: UUID? = nil, terminalOffset: TimeInterval? = nil,
+ failureClass: ProviderDiagnosticErrorClass? = nil, failureCode: Int? = nil) -> [ProviderDiagnosticEvent] {
+ let id = UUID()
+ let phases: [ProviderDiagnosticPhase] = progress ? [.started, .progress, terminal] : [.started, terminal]
+ return phases.enumerated().map { index, phase in
+ ProviderDiagnosticEvent(occurredAt: time.addingTimeInterval(offset + (phase == terminal ? terminalOffset ?? Double(index) : Double(index))), spanID: id,
+ parentSpanID: parent, subjectAlias: override ?? subject, itemMetadataAlias: phase == .completed ? metadata : nil,
+ processInstanceID: process, processCodeHash: hash,
+ errorCode: phase == .failed ? failureCode : nil,
+ correlationID: correlation, source: source, operation: operation, phase: phase, fieldShape: fields,
+ errorClass: phase == .cancelled ? .cancellation : phase == .failed ? failureClass : nil,
+ progressPercentBucket: phase == .progress ? 10 : nil)
+ }
+ }
+
+ @Test(arguments: [StabilityFinderScenario.hydrate, .download])
+ func cachedContentsAndUnrelatedCallbacksCannotProveFetch(_ scenario: StabilityFinderScenario) {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(scenario), diagnostics: span(.itemLookup))
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(scenario), diagnostics: span(.fetchContents, subject: UUID()))
+ }
+ }
+
+ @Test func missingTelemetryAndUIAreRejected() {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: [])
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingUIEvidence) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate, actions: 0), diagnostics: span(.fetchContents))
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingSpanStart) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: Array(span(.fetchContents).dropFirst()))
+ }
+ }
+
+ @Test func settlingRetainsCompleteSpansWithoutBorrowingOtherCallbacks() throws {
+ let events = span(.fetchContents, offset: 9, terminalOffset: 4)
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: events)
+ #expect(throws: StabilityLiveEvidenceError.pendingOperations) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: [events[0]])
+ }
+ #expect(throws: StabilityLiveEvidenceError.pendingOperations) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate),
+ diagnostics: [events[0]] + span(.fetchContents, subject: UUID(), offset: 9, terminalOffset: 4))
+ }
+ #expect(throws: StabilityLiveEvidenceError.contradictoryTerminal) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: events + [events[1]])
+ }
+ #expect(throws: StabilityLiveEvidenceError.unexpectedFailure) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate),
+ diagnostics: span(.fetchContents, terminal: .failed, offset: 9, terminalOffset: 4))
+ }
+ // A cached earlier fetch or a later unrelated fetch cannot satisfy this step.
+ for unrelated in [span(.fetchContents, offset: -2, terminalOffset: 4),
+ span(.fetchContents, offset: 12)] {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.hydrate), diagnostics: unrelated)
+ }
+ }
+ }
+
+ @Test func requiredMappingsDistinguishTrashFromDeletion() throws {
+ try StabilityLiveEvidenceValidator.validate(step: step(.trash), diagnostics: span(.modifyItem, fields: [.parent, .trash]))
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.trash), diagnostics: span(.modifyItem, fields: [.contents]))
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.trash), diagnostics: span(.deleteItem))
+ }
+ try StabilityLiveEvidenceValidator.validate(step: step(.permanentDeletion), diagnostics: span(.deleteItem))
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: span(.itemLookup))
+ }
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: span(.restoreTrashedItem))
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: span(.modifyItem, fields: [.parent]))
+ for fields: [ProviderDiagnosticField] in [[.lastUsedDate], [.parent, .trash]] {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: span(.modifyItem, fields: fields))
+ }
+ }
+ }
+
+ @Test func untriggeredConflictCannotPass() {
+ #expect(throws: StabilityLiveEvidenceError.missingConflict) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.concurrentRemotePreserveBoth), diagnostics: span(.modifyItem) + span(.uploadFile))
+ }
+ }
+
+ @Test func handledActive404RequiresMatchingSuccessfulTrashAndParentSpans() throws {
+ let parent = span(.itemLookup, terminalOffset: 4)
+ let parentID = parent.first!.spanID!
+ let failed = span(.itemLookup, terminal: .failed, parent: parentID, failureClass: .notFound, failureCode: 404)
+ let trash = span(.trashedItem, offset: 2, parent: parentID)
+ let restore = span(.restoreTrashedItem, offset: 5)
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: parent + failed + trash + restore)
+ // Recovery metadata never substitutes for the actual Restore action.
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: parent + failed + trash)
+ }
+ for incomplete in [
+ parent + failed,
+ failed + trash,
+ parent + failed + span(.trashedItem, offset: 2, parent: UUID()),
+ parent + failed + span(.trashedItem, subject: UUID(), offset: 2, parent: parentID),
+ parent + failed + span(.trashedItem, parent: parentID, terminalOffset: 0),
+ parent + failed + span(.trashedItem, terminal: .failed, offset: 2, parent: parentID),
+ parent + trash + span(.itemLookup, terminal: .failed, parent: parentID, failureClass: .notFound, failureCode: 403),
+ trash + span(.itemLookup, terminal: .failed, failureClass: .notFound, failureCode: 404)
+ ] {
+ #expect(throws: StabilityLiveEvidenceError.unexpectedFailure) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.restore), diagnostics: incomplete + restore)
+ }
+ }
+ }
+
+ @Test(arguments: [StabilityFinderScenario.editAndUpload, .rename, .move])
+ func incidentalMetadataCallbacksCannotProveTheIntendedMutation(_ scenario: StabilityFinderScenario) throws {
+ let field: ProviderDiagnosticField = scenario == .editAndUpload ? .contents : scenario == .rename ? .filename : .parent
+ try StabilityLiveEvidenceValidator.validate(step: step(scenario), diagnostics: span(.modifyItem, fields: [field]))
+ for fields: [ProviderDiagnosticField] in [[.lastUsedDate], [.contentModificationDate], [.parent, .trash]] {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(scenario), diagnostics: span(.modifyItem, fields: fields))
+ }
+ }
+ }
+
+ @Test func cancellationNeedsProgressAndSubsequentSuccessWithDifferentOperation() throws {
+ let cancelled = span(.fetchContents, terminal: .cancelled, progress: true)
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.cancellationAndProgress, cancellation: true), diagnostics: cancelled)
+ }
+ let events = cancelled + span(.fetchContents, offset: 3)
+ try StabilityLiveEvidenceValidator.validate(step: step(.cancellationAndProgress, cancellation: true), diagnostics: events)
+ #expect(throws: StabilityLiveEvidenceError.contradictoryTerminal) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.cancellationAndProgress, cancellation: true), diagnostics: events + [cancelled.last!])
+ }
+ #expect(throws: StabilityLiveEvidenceError.missingCancellation) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.cancellationAndProgress), diagnostics: span(.fetchContents))
+ }
+ }
+
+ @Test func rootEnumerationCannotProveWorkingSetMembership() {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.workingSetRefresh, workingSet: true), diagnostics: span(.enumerateItems))
+ }
+ }
+
+ @Test func cachedWorkingSetMetadataOrUnparentedMembershipCannotPass() throws {
+ let expected = UUID(), enumeration = span(.enumerateChanges)
+ let proof = step(.workingSetRefresh, workingSet: true, metadata: expected)
+ let parent = enumeration.first!.spanID!
+ try StabilityLiveEvidenceValidator.validate(step: proof,
+ diagnostics: enumeration + span(.workingSetRefresh, parent: parent, metadata: expected))
+ for events in [span(.workingSetRefresh, parent: parent, metadata: UUID()),
+ span(.workingSetRefresh, parent: parent), span(.workingSetRefresh, metadata: expected)] {
+ #expect(throws: StabilityLiveEvidenceError.missingWorkingSet) {
+ try StabilityLiveEvidenceValidator.validate(step: proof, diagnostics: enumeration + events)
+ }
+ }
+ }
+
+ @Test func diagnosticAliasesAreStableWithinRunAndChangeBetweenRuns() {
+ let run = UUID()
+ #expect(StabilityDiagnosticIdentity.alias(for: "synthetic", runID: run) == StabilityDiagnosticIdentity.alias(for: "synthetic", runID: run))
+ #expect(StabilityDiagnosticIdentity.alias(for: "synthetic", runID: run) != StabilityDiagnosticIdentity.alias(for: "synthetic", runID: UUID()))
+ #expect(StabilityDiagnosticIdentity.alias(for: "synthetic", runID: run) != StabilityDiagnosticIdentity.alias(for: "other", runID: run))
+ }
+
+ @Test func contextualPanelsRequireTheAttestedActionsBuildAndEveryMutation() throws {
+ let callbacks = span(.favoriteItem) + span(.favoriteItem) + span(.duplicateItem)
+ let panelOperations: [ProviderDiagnosticOperation] = [.createShareLink, .updateShareLink, .deleteShareLink, .restoreFileVersion]
+ let panels = panelOperations.flatMap { span($0, source: .actionExtension) }
+ let proof = step(.supportedContextualActions, actionsHash: hash)
+ try StabilityLiveEvidenceValidator.validate(step: proof, diagnostics: callbacks + panels)
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.supportedContextualActions), diagnostics: callbacks + panels)
+ }
+ #expect(throws: StabilityLiveEvidenceError.wrongExtensionBuild) {
+ try StabilityLiveEvidenceValidator.validate(step: step(.supportedContextualActions, actionsHash: String(repeating: "b", count: 40)), diagnostics: callbacks + panels)
+ }
+ for operation in panelOperations {
+ #expect(throws: StabilityLiveEvidenceError.missingCallback) {
+ try StabilityLiveEvidenceValidator.validate(step: proof, diagnostics: callbacks + panels.filter { $0.operation != operation })
+ }
+ }
+ }
+}
diff --git a/potassiumProviderTests/StabilityRunConfinementTests.swift b/potassiumProviderTests/StabilityRunConfinementTests.swift
new file mode 100644
index 0000000..2cc2722
--- /dev/null
+++ b/potassiumProviderTests/StabilityRunConfinementTests.swift
@@ -0,0 +1,59 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+@Suite("Live run target confinement")
+struct StabilityRunConfinementTests {
+ @Test func acceptsFreshOwnedAncestry() async throws {
+ let items = [10: item(10, parent: 2), 11: item(11, parent: 10), 12: item(12, parent: 11)]
+ try await verify(12, items: items)
+ }
+
+ @Test func rejectsLabRootAndUnownedSelection() async {
+ for target in [2, 99] {
+ await #expect(throws: StabilityRunConfinementError.unownedTarget) {
+ try await verify(target, items: [10: item(10, parent: 2)])
+ }
+ }
+ }
+
+ @Test func rejectsAnOwnedItemMovedOutsideTheRun() async {
+ let items = [10: item(10, parent: 2), 12: item(12, parent: 99)]
+ await #expect(throws: StabilityRunConfinementError.invalidAncestry) {
+ try await verify(12, items: items)
+ }
+ }
+
+ @Test func rejectsChangedIdentityOrDrive() async {
+ for replacement in [item(13, parent: 10), item(12, parent: 10, drive: 8)] {
+ await #expect(throws: StabilityRunConfinementError.identityDrift) {
+ try await verify(12, items: [10: item(10, parent: 2), 12: replacement])
+ }
+ }
+ }
+
+ @Test func rejectsCyclesAndDisplacedRunRoot() async {
+ for items in [
+ [10: item(10, parent: 2), 11: item(11, parent: 12), 12: item(12, parent: 11)],
+ [10: item(10, parent: 99), 12: item(12, parent: 10)],
+ [10: item(10, parent: 2, directory: false), 12: item(12, parent: 10)]
+ ] {
+ await #expect(throws: StabilityRunConfinementError.invalidAncestry) {
+ try await verify(12, items: items)
+ }
+ }
+ }
+
+ private func verify(_ target: Int, items: [Int: KDriveRemoteItem]) async throws {
+ try await StabilityRunConfinement.verify(targetID: target, runRootID: 10, labRootID: 2,
+ driveID: 7, ownedIDs: [10, 11, 12]) { identifier in
+ try #require(items[identifier])
+ }
+ }
+
+ private func item(_ id: Int, parent: Int, drive: Int = 7, directory: Bool = true) -> KDriveRemoteItem {
+ KDriveRemoteItem(id: id, name: "synthetic", type: directory ? "dir" : "file", status: "active",
+ driveID: drive, parentID: parent, path: nil, size: nil, mimeType: nil,
+ createdAt: nil, modifiedAt: .distantPast, updatedAt: .distantPast)
+ }
+}
diff --git a/potassiumProviderTests/VaultConflictReplayTests.swift b/potassiumProviderTests/VaultConflictReplayTests.swift
new file mode 100644
index 0000000..d412a4d
--- /dev/null
+++ b/potassiumProviderTests/VaultConflictReplayTests.swift
@@ -0,0 +1,182 @@
+import Foundation
+@testable import PotassiumProviderCore
+import Testing
+
+struct VaultConflictReplayTests {
+ @Test(arguments: Array(UInt64(1)...32))
+ func seededContentRacesPreserveEveryVersionAfterPersistentReplay(seed: UInt64) async throws {
+ var random = ConflictSeedGenerator(seed: seed)
+ let id = VaultItemIdentifier(rawValue: random.uuid())
+ let base = item(id: id, bytes: Data("base".utf8))
+ let create = VaultTransaction(id: random.uuid(), parents: VaultFrontier(), deviceID: random.uuid(),
+ createdAt: Date(timeIntervalSince1970: 1), operation: .upsert(base))
+ var transactions = [create]
+ var contents: [VaultRevision: Data] = [:]
+ for index in 0..<6 {
+ let bytes = Data("seed-\(seed)-writer-\(index)".utf8)
+ var changed = item(id: id, bytes: bytes)
+ changed.filename = index.isMultiple(of: 2) ? "First.txt" : "Second.txt"
+ changed.metadataRevision = try VaultRevisionDigests.metadata(for: changed)
+ contents[changed.contentRevision] = bytes
+ transactions.append(VaultTransaction(id: random.uuid(), parents: VaultFrontier(transactionIDs: [create.id]),
+ deviceID: random.uuid(), createdAt: Date(timeIntervalSince1970: 2), baseItem: base, operation: .upsert(changed)))
+ }
+ let expected = try VaultJournalReducer.reduce(transactions)
+ var permutations: [[VaultTransaction]] = [transactions, Array(transactions.reversed())]
+ for _ in 0..<12 { permutations.append(transactions.shuffled(using: &random)) }
+ for order in permutations {
+ let observed = try VaultJournalReducer.reduce(order)
+ if observed != expected || preservationViolation(observed, transactions: order) {
+ let minimal = try minimize(order) { input in
+ let result = try VaultJournalReducer.reduce(input)
+ return try result != VaultJournalReducer.reduce(input.sorted { $0.id.uuidString < $1.id.uuidString }) ||
+ preservationViolation(result, transactions: input)
+ }
+ let file = try persistFailure(seed: seed, transactions: minimal)
+ Issue.record("Vault replay or preservation failed; seed \(seed); synthetic reproducer: \(file.path)")
+ }
+ #expect(observed == expected)
+ #expect(!preservationViolation(observed, transactions: order))
+ #expect(Set(observed.items.values.map(\.contentRevision)) == Set(contents.keys))
+ #expect(observed.items.count == 6)
+ #expect(observed.conflicts.filter { $0.kind == .content }.count == 5)
+ for value in observed.items.values {
+ let bytes = try #require(contents[value.contentRevision])
+ #expect(VaultRevision(hashing: bytes) == value.contentRevision)
+ #expect(value.parentID == nil && !value.isTrashed)
+ }
+ }
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let key = VaultKeyMaterial(data: Data(repeating: 7, count: 32))!, vaultID = VaultIdentifier(rawValue: random.uuid())
+ for name in ["first", "second"] {
+ let database = directory.appendingPathComponent(name + ".sqlite3")
+ let store = try VaultSQLiteStore(databaseURL: database, domainIdentifier: name, vaultID: vaultID, rootKey: key)
+ try await store.replace(with: expected)
+ let restarted = try VaultSQLiteStore(databaseURL: database, domainIdentifier: name, vaultID: vaultID, rootKey: key)
+ #expect(try await restarted.state().items == expected.items)
+ // Reapplying the same complete journal does not create more copies.
+ try await restarted.replace(with: VaultJournalReducer.reduce(transactions))
+ #expect(try await restarted.state().items == expected.items)
+ }
+ }
+
+ @Test func competingMetadataUsesCanonicalWinnerInEveryPermutation() throws {
+ let base = item(id: VaultItemIdentifier(), bytes: Data("same bytes".utf8))
+ let create = VaultTransaction(id: orderedID(1), parents: VaultFrontier(), deviceID: orderedID(10), operation: .upsert(base))
+ var first = base, second = base
+ first.filename = "First.txt"; first.metadataRevision = try VaultRevisionDigests.metadata(for: first)
+ second.filename = "Second.txt"; second.metadataRevision = try VaultRevisionDigests.metadata(for: second)
+ let a = VaultTransaction(id: orderedID(2), parents: VaultFrontier(transactionIDs: [create.id]), deviceID: orderedID(10), baseItem: base, operation: .upsert(first))
+ let b = VaultTransaction(id: orderedID(3), parents: VaultFrontier(transactionIDs: [create.id]), deviceID: orderedID(11), baseItem: base, operation: .upsert(second))
+ let expected = try VaultJournalReducer.reduce([create, a, b])
+ for order in [[create, b, a], [a, create, b], [a, b, create], [b, create, a], [b, a, create]] {
+ #expect(try VaultJournalReducer.reduce(order) == expected)
+ }
+ #expect(expected.items.count == 1)
+ #expect(expected.items[base.id]?.filename == "Second.txt")
+ #expect(expected.items[base.id]?.parentID == nil)
+ #expect(expected.items[base.id]?.metadataRevision == second.metadataRevision)
+ #expect(expected.items[base.id]?.contentRevision == base.contentRevision)
+ #expect(expected.conflicts.contains { $0.kind == .metadata })
+ }
+
+ @Test func stalePurgePreservesEditedIdentity() throws {
+ let base = item(id: VaultItemIdentifier(), bytes: Data("base".utf8))
+ let create = VaultTransaction(id: orderedID(1), parents: VaultFrontier(), deviceID: orderedID(10), operation: .upsert(base))
+ let changed = item(id: base.id, bytes: Data("new".utf8))
+ let edit = VaultTransaction(id: orderedID(2), parents: VaultFrontier(transactionIDs: [create.id]), deviceID: orderedID(10), baseItem: base, operation: .upsert(changed))
+ let purge = VaultTransaction(id: orderedID(3), parents: VaultFrontier(transactionIDs: [create.id]), deviceID: orderedID(11), baseItem: base,
+ operation: .purge(itemID: base.id, baseContentRevision: base.contentRevision, baseMetadataRevision: base.metadataRevision))
+ for order in [[create, edit, purge], [purge, edit, create], [edit, create, purge]] {
+ let result = try VaultJournalReducer.reduce(order)
+ #expect(result.items[base.id]?.contentRevision == changed.contentRevision)
+ #expect(result.conflicts.contains { $0.kind == .deletionRejected })
+ }
+ }
+
+ @Test func duplicateJournalDeliveryFailsClosedWithoutChangingPersistentState() async throws {
+ let base = item(id: VaultItemIdentifier(), bytes: Data("base".utf8))
+ let transaction = VaultTransaction(parents: VaultFrontier(), deviceID: UUID(), operation: .upsert(base))
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let key = VaultKeyMaterial(data: Data(repeating: 8, count: 32))!
+ let store = try VaultSQLiteStore(databaseURL: directory.appendingPathComponent("journal.sqlite3"),
+ domainIdentifier: "synthetic", vaultID: VaultIdentifier(), rootKey: key)
+ let before = try VaultJournalReducer.reduce([transaction])
+ try await store.replace(with: before)
+ await #expect(throws: VaultJournalError.duplicateTransaction(transaction.id)) {
+ try await store.replace(with: VaultJournalReducer.reduce([transaction, transaction]))
+ }
+ #expect(try await store.state().items == before.items)
+ }
+
+ @Test func reproducerReductionRetainsRequiredCausalAncestors() throws {
+ let base = item(id: VaultItemIdentifier(), bytes: Data("base".utf8))
+ let create = VaultTransaction(id: orderedID(1), parents: VaultFrontier(), deviceID: orderedID(10), operation: .upsert(base))
+ let leaf = VaultTransaction(id: orderedID(2), parents: VaultFrontier(transactionIDs: [create.id]), deviceID: orderedID(10), baseItem: base, operation: .upsert(base))
+ let unrelated = VaultTransaction(id: orderedID(3), parents: VaultFrontier(), deviceID: orderedID(11), operation: .upsert(item(id: VaultItemIdentifier(), bytes: Data())))
+ let reduced = try minimize([unrelated, leaf, create]) { input in
+ _ = try VaultJournalReducer.reduce(input)
+ return input.contains { $0.id == leaf.id }
+ }
+ #expect(Set(reduced.map(\.id)) == [create.id, leaf.id])
+ }
+
+ /// Independent oracle for this family of concurrent writes. Subsequence
+ /// reduction recomputes expectations from remaining writers, so minimizing
+ /// cannot mistake an intentionally removed version for data loss.
+ private func preservationViolation(_ result: VaultReducedState, transactions: [VaultTransaction]) -> Bool {
+ let writes = transactions.filter { $0.baseItem != nil }.sorted { $0.id.uuidString < $1.id.uuidString }
+ let desired = writes.compactMap { transaction -> VaultItem? in
+ if case .upsert(let item) = transaction.operation { return item }
+ return nil
+ }
+ guard let winner = desired.first else { return false }
+ return Set(result.items.values.map(\.contentRevision)) != Set(desired.map(\.contentRevision)) ||
+ result.items.count != desired.count || result.conflicts.filter { $0.kind == .content }.count != desired.count - 1 ||
+ result.items[winner.id]?.contentRevision != winner.contentRevision ||
+ result.items.values.contains { $0.parentID != nil || $0.isTrashed }
+ }
+
+ private func item(id: VaultItemIdentifier, bytes: Data) -> VaultItem {
+ VaultItem(id: id, parentID: nil, filename: "Synthetic.txt", isDirectory: false,
+ createdAt: Date(timeIntervalSince1970: 1), modifiedAt: Date(timeIntervalSince1970: 1),
+ plaintextSize: Int64(bytes.count), contentRevision: VaultRevision(hashing: bytes), metadataRevision: VaultRevision(hashing: Data("metadata".utf8)))
+ }
+ private func orderedID(_ value: Int) -> UUID { UUID(uuidString: String(format: "00000000-0000-0000-0000-%012d", value))! }
+ private func minimize(_ input: [VaultTransaction], fails: ([VaultTransaction]) throws -> Bool) throws -> [VaultTransaction] {
+ var current = input, changed = true
+ while changed {
+ changed = false
+ for transaction in current {
+ guard !current.contains(where: { $0.parents.transactionIDs.contains(transaction.id) }) else { continue }
+ let candidate = current.filter { $0.id != transaction.id }
+ if try fails(candidate) { current = candidate; changed = true; break }
+ }
+ }
+ return current
+ }
+ private func persistFailure(seed: UInt64, transactions: [VaultTransaction]) throws -> URL {
+ struct Reproducer: Encodable { let schemaVersion = 1; let seed: UInt64; let transactions: [VaultTransaction] }
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent("potassium-conflict-failures")
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
+ let file = directory.appendingPathComponent("vault-\(seed)-\(UUID().uuidString).json")
+ try JSONEncoder().encode(Reproducer(seed: seed, transactions: transactions)).write(to: file, options: .withoutOverwriting)
+ return file
+ }
+}
+
+private struct ConflictSeedGenerator: RandomNumberGenerator {
+ var state: UInt64
+ init(seed: UInt64) { state = seed }
+ mutating func next() -> UInt64 {
+ state = state &* 6364136223846793005 &+ 1442695040888963407
+ return state
+ }
+ mutating func uuid() -> UUID {
+ let first = next(), second = next()
+ return UUID(uuidString: String(format: "%08X-%04X-%04X-%04X-%012llX", UInt32(truncatingIfNeeded: first >> 32),
+ UInt16(truncatingIfNeeded: first >> 16), UInt16(truncatingIfNeeded: first), UInt16(truncatingIfNeeded: second >> 48), second & 0xFFFFFFFFFFFF))!
+ }
+}
diff --git a/potassiumProviderTests/WorkingSetMutationDeliveryTests.swift b/potassiumProviderTests/WorkingSetMutationDeliveryTests.swift
new file mode 100644
index 0000000..6ca8264
--- /dev/null
+++ b/potassiumProviderTests/WorkingSetMutationDeliveryTests.swift
@@ -0,0 +1,78 @@
+import Foundation
+import PotassiumProviderCore
+import Testing
+
+struct WorkingSetMutationDeliveryTests {
+ @Test func confirmedMutationIsDurableAndDeliveredWithoutAnotherRemoteCrawl() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let url = directory.appendingPathComponent("Snapshots.sqlite3")
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: url)
+ let base = ConflictTestRemote.item(3, name: "Original.txt", parent: 1)
+ let sibling = ConflictTestRemote.item(4, name: "Sibling.txt", parent: 1)
+ let moved = ConflictTestRemote.item(3, name: "Original.txt", parent: 2, revision: 2, size: 15)
+ let time = Date(timeIntervalSince1970: 100)
+ let initial = try await store.commitWorkingSetPoll(domainIdentifier: "synthetic", containerSnapshotUpdates: [],
+ items: [base, sibling], changes: KDriveSnapshotChangeSet(updatedItems: [base, sibling], deletedItemIDs: []), completedAt: time)
+ #expect(try await store.publishKnownWorkingSetItem(moved, replacing: base, domainIdentifier: "synthetic", recordedAt: time.addingTimeInterval(1)))
+ let reopened = try KDriveSnapshotSQLiteStore(databaseURL: url)
+ let result = try await KDriveWorkingSetChangeDelivery.changes(domainIdentifier: "synthetic", from: initial.anchor, store: reopened) {
+ Issue.record("Committed changes must not wait behind a remote crawl")
+ throw URLError(.timedOut)
+ }
+ #expect(result?.changes.updatedItems == [moved])
+ #expect(result?.changes.deletedItemIDs == [])
+ #expect(try await reopened.workingSetSnapshot(domainIdentifier: "synthetic")?.items == [moved, sibling])
+ #expect(try await reopened.lastSuccessfulWorkingSetPoll(domainIdentifier: "synthetic") == time)
+ #expect(try await reopened.publishKnownWorkingSetItem(moved, replacing: base, domainIdentifier: "synthetic", recordedAt: time.addingTimeInterval(2)))
+ #expect(try await reopened.workingSetSnapshot(domainIdentifier: "synthetic")?.anchor == result?.anchor)
+ #expect(try await reopened.publishKnownWorkingSetItem(base, replacing: base, domainIdentifier: "synthetic", recordedAt: time.addingTimeInterval(3)) == false)
+ #expect(try await reopened.workingSetSnapshot(domainIdentifier: "synthetic")?.items.first == moved)
+ }
+
+ @Test func pollPreparedBeforeMutationCannotOverwriteItOrAdvanceCursors() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ let base = ConflictTestRemote.item(3, name: "Original.txt", parent: 1)
+ let moved = ConflictTestRemote.item(3, name: "Original.txt", parent: 2)
+ let time = Date(timeIntervalSince1970: 100)
+ let initial = try await store.commitWorkingSetPoll(domainIdentifier: "synthetic", containerSnapshotUpdates: [], items: [base],
+ changes: KDriveSnapshotChangeSet(updatedItems: [base], deletedItemIDs: []), completedAt: time)
+ #expect(try await store.publishKnownWorkingSetItem(moved, replacing: base, domainIdentifier: "synthetic", recordedAt: time))
+ let container = KDriveSnapshot(anchor: "stale", serverCursor: "stale-cursor", isFullyEnumerated: true, usesAdvancedListing: true, items: [base])
+ await #expect(throws: KDriveSnapshotStoreError.staleSnapshot(domainIdentifier: "synthetic", containerIdentifier: "working-set")) {
+ try await store.commitWorkingSetPoll(domainIdentifier: "synthetic",
+ containerSnapshotUpdates: [.init(containerIdentifier: "1", snapshot: container, condition: .missing)],
+ items: [base], changes: KDriveSnapshotChangeSet(updatedItems: [base], deletedItemIDs: []),
+ completedAt: time.addingTimeInterval(10), condition: .matchingAnchor(initial.anchor))
+ }
+ #expect(try await store.snapshot(domainIdentifier: "synthetic", containerIdentifier: "1") == nil)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "synthetic") == time)
+ #expect(try await store.workingSetChanges(domainIdentifier: "synthetic", from: initial.anchor)?.changes.updatedItems == [moved])
+ }
+
+ @Test func emptyJournalStillRefreshesAndExpiredAnchorDoesNotPass() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ _ = try await store.claimWorkingSetPoll(domainIdentifier: "synthetic", now: Date(), minimumInterval: 0)
+ let initial = try #require(await store.workingSetSnapshot(domainIdentifier: "synthetic"))
+ let item = ConflictTestRemote.item(3, name: "New.txt", parent: 1)
+ let calls = WorkingSetRefreshTestGate()
+ let result = try await KDriveWorkingSetChangeDelivery.changes(domainIdentifier: "synthetic", from: initial.anchor, store: store) {
+ await calls.recordCall()
+ _ = try await store.publishKnownWorkingSetItem(item, replacing: nil, domainIdentifier: "synthetic", recordedAt: Date())
+ }
+ #expect(await calls.entered && result?.changes.updatedItems == [item])
+ let expired = try await KDriveWorkingSetChangeDelivery.changes(domainIdentifier: "synthetic", from: "expired", store: store) {}
+ #expect(expired == nil)
+ }
+
+
+}
+
+private actor WorkingSetRefreshTestGate {
+ private(set) var entered = false
+ func recordCall() { entered = true }
+}
diff --git a/potassiumProviderTests/WorkingSetPollSchedulingTests.swift b/potassiumProviderTests/WorkingSetPollSchedulingTests.swift
new file mode 100644
index 0000000..3a12015
--- /dev/null
+++ b/potassiumProviderTests/WorkingSetPollSchedulingTests.swift
@@ -0,0 +1,97 @@
+import Foundation
+import Testing
+@testable import PotassiumProviderCore
+
+@Suite("Working-set poll scheduling")
+struct WorkingSetPollSchedulingTests {
+ @Test(.timeLimit(.minutes(1)), arguments: [false, true])
+ func queuedMaterializationsShareOnlyASubsequentSuccessfulPoll(firstFails: Bool) async throws {
+ let scheduler = WorkingSetPollScheduling(), work = ControlledPolls(failFirst: firstFails)
+ let first = Task { try await scheduler.withPermit(domainIdentifier: "fixture", coalescePending: true) { try await work.poll() } }
+ defer { first.cancel() }
+ try await work.waitForStarted(1)
+ let queued = (0..<3).map { _ in Task {
+ try await scheduler.withPermit(domainIdentifier: "fixture", coalescePending: true) { try await work.poll() }
+ } }
+ defer { queued.forEach { $0.cancel() } }
+ try await wait { await scheduler.pendingRequestCount(domainIdentifier: "fixture") == 4 }
+ try await work.allow(1)
+ try await work.waitForStarted(2)
+ try await work.allow(2)
+ if firstFails { await #expect(throws: PollFailure.self) { try await first.value } }
+ else { #expect(try await first.value.didPoll) }
+ var performed = 0
+ for task in queued { if try await task.value.didPoll { performed += 1 } }
+ #expect(performed == 1)
+ #expect(await work.started == 2)
+ #expect(await scheduler.pendingRequestCount(domainIdentifier: "fixture") == 0)
+ }
+
+ @Test(.timeLimit(.minutes(1))) func materializationArrivingDuringIORequiresAnotherPollAndCancellationDoesNotPoll() async throws {
+ let scheduler = WorkingSetPollScheduling(), work = ControlledPolls()
+ let first = Task { try await scheduler.withPermit(domainIdentifier: "fixture", coalescePending: true) { try await work.poll() } }
+ defer { first.cancel() }
+ try await work.waitForStarted(1)
+ let cancelled = Task { try await scheduler.withPermit(domainIdentifier: "fixture", coalescePending: true) { try await work.poll() } }
+ defer { cancelled.cancel() }
+ try await wait { await scheduler.pendingRequestCount(domainIdentifier: "fixture") == 2 }
+ cancelled.cancel()
+ await #expect(throws: CancellationError.self) { try await cancelled.value }
+ let later = Task { try await scheduler.withPermit(domainIdentifier: "fixture", coalescePending: true) { try await work.poll() } }
+ defer { later.cancel() }
+ try await wait { await scheduler.pendingRequestCount(domainIdentifier: "fixture") == 2 }
+ try await work.allow(1)
+ #expect(try await first.value.didPoll)
+ try await work.waitForStarted(2)
+ try await work.allow(2)
+ #expect(try await later.value.didPoll)
+ #expect(await work.started == 2)
+ #expect(await scheduler.pendingRequestCount(domainIdentifier: "fixture") == 0)
+ }
+
+ private func wait(_ condition: () async -> Bool) async throws {
+ // The enclosing test deadline bounds this observation. A loaded simulator
+ // must not turn a scheduling-order assertion into a three-second race.
+ while !(await condition()) {
+ try await Task.sleep(for: .milliseconds(20))
+ }
+ }
+}
+
+private enum PollFailure: Error { case injected }
+private actor ControlledPolls {
+ let failFirst: Bool
+ private(set) var started = 0
+ private let starts = AsyncStream.makeStream()
+ private var releases: [Int: AsyncStream.Continuation] = [:]
+ init(failFirst: Bool = false) { self.failFirst = failFirst }
+
+ func waitForStarted(_ count: Int) async throws {
+ if started >= count { return }
+ for await number in starts.stream {
+ if number >= count { return }
+ }
+ throw CancellationError()
+ }
+
+ func allow(_ number: Int) throws {
+ let release = try #require(releases[number])
+ release.yield(())
+ release.finish()
+ }
+
+ func poll() async throws -> KDriveWorkingSetPollOutcome {
+ started += 1
+ let number = started
+ let release = AsyncStream.makeStream()
+ releases[number] = release.continuation
+ defer { releases[number] = nil }
+ starts.continuation.yield(number)
+ var iterator = release.stream.makeAsyncIterator()
+ _ = await iterator.next()
+ try Task.checkCancellation()
+ if failFirst && number == 1 { throw PollFailure.injected }
+ return KDriveWorkingSetPollOutcome(didPoll: true,
+ changes: KDriveSnapshotChangeSet(updatedItems: [], deletedItemIDs: []), snapshot: nil)
+ }
+}
diff --git a/potassiumProviderTests/WorkingSetSyncTests.swift b/potassiumProviderTests/WorkingSetSyncTests.swift
index 187fa30..8386560 100644
--- a/potassiumProviderTests/WorkingSetSyncTests.swift
+++ b/potassiumProviderTests/WorkingSetSyncTests.swift
@@ -1,9 +1,181 @@
import Foundation
+import PotassiumChannelCore
import Testing
import PotassiumProviderCore
@Suite(.serialized)
struct WorkingSetSyncTests {
+ @Test(arguments: [false, true])
+ func newerMutationStopsObsoletePollBeforeMoreRequestsOrCursorWrites(duringFolder: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ let base = makeWorkingSetItem(id: 30, name: "Base.txt", updatedAt: 1_000)
+ let edited = makeWorkingSetItem(id: 30, name: "Edited.txt", updatedAt: 1_001)
+ let previousTime = Date(timeIntervalSince1970: 1_000)
+ let initial = try await store.commitWorkingSetPoll(domainIdentifier: "domain-1", containerSnapshotUpdates: [],
+ items: [base], changes: KDriveSnapshotChangeSet(updatedItems: [base], deletedItemIDs: []), completedAt: previousTime)
+ try await store.replaceMaterializedItems((10..<18).map { .init(fileID: $0, isContainer: true) }, domainIdentifier: "domain-1")
+ let publish: @Sendable () async throws -> Void = {
+ let published = try await store.publishKnownWorkingSetItem(edited, replacing: base,
+ domainIdentifier: "domain-1", recordedAt: previousTime.addingTimeInterval(1))
+ #expect(published)
+ }
+ let remote = WorkingSetRemoteMock(relevantItems: [], advancedResponses: [
+ "": KDriveAdvancedItemPage(items: [base], actions: [], actionItems: [], nextCursor: "obsolete", hasMore: false)
+ ], itemByID: [:], partialResults: [], beforeRelevantReturn: duringFolder ? nil : publish,
+ beforeAdvancedReturn: duringFolder ? publish : nil)
+ let coordinator = makeCoordinator(remote: remote, store: store)
+ let delivery = try await KDriveWorkingSetChangeDelivery.changes(domainIdentifier: "domain-1", from: initial.anchor, store: store) {
+ let outcome = try await coordinator.poll(now: previousTime.addingTimeInterval(100))
+ #expect(!outcome.didPoll && outcome.snapshot?.items == [edited])
+ }
+ #expect(delivery?.changes.updatedItems == [edited])
+ #expect(await remote.advancedRequestCount() <= (duringFolder ? 4 : 0))
+ if duringFolder { #expect(await remote.advancedRequestCount() > 0) }
+ #expect(await remote.requestedPartialFileIDs().isEmpty)
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: "10") == nil)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == previousTime)
+ }
+ @Test(.timeLimit(.minutes(1)))
+ func independentFoldersOverlapWithinBoundAndCommitCompleteOrderedState() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ try await store.replaceMaterializedItems((10..<18).map { .init(fileID: $0, isContainer: true) }, domainIdentifier: "domain-1")
+ let gate = WorkingSetContainerGate()
+ let remote = WorkingSetRemoteMock(relevantItems: [], advancedResponses: [:], itemByID: [:], partialResults: [],
+ advancedResponse: { folderID in try await gate.page(folderID: folderID) })
+ let coordinator = makeCoordinator(remote: remote, store: store)
+ let time = Date(timeIntervalSince1970: 2_000)
+ let polling = Task { try await coordinator.poll(now: time) }
+ defer { polling.cancel() }
+ try await gate.waitForStarted(4)
+ #expect(await gate.peakActive == 4)
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: "10") == nil)
+ await gate.release([13, 12, 11, 10])
+ try await gate.waitForStarted(8)
+ // Completed first-batch cursors remain uncommitted while the rest waits.
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: "10") == nil)
+ await gate.release([17, 16, 15, 14])
+ let result = try await polling.value
+ let expected = (10..<18).map { makeWorkingSetItem(id: $0 * 10, name: "Fixture.txt", parentID: $0, updatedAt: 1_000) }
+ #expect(result.didPoll && result.snapshot?.items == expected)
+ #expect(result.changes.updatedItems == expected)
+ #expect(await gate.peakActive == 4)
+ #expect(await gate.finished == 8)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == time)
+ for folderID in 10..<18 {
+ let snapshot = try #require(await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: String(folderID)))
+ #expect(snapshot.serverCursor == "cursor-\(folderID)")
+ #expect(snapshot.items == expected.filter { $0.parentID == folderID })
+ }
+ }
+
+ @Test(.timeLimit(.minutes(1)), arguments: [false, true])
+ func cancelledOrThrottledFolderBatchCannotAdvanceAnyCursor(cancel: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ try await store.replaceMaterializedItems((10..<18).map { .init(fileID: $0, isContainer: true) }, domainIdentifier: "domain-1")
+ let gate = WorkingSetContainerGate(throttle: !cancel)
+ let remote = WorkingSetRemoteMock(relevantItems: [], advancedResponses: [:], itemByID: [:], partialResults: [],
+ advancedResponse: { folderID in try await gate.page(folderID: folderID) })
+ let coordinator = makeCoordinator(remote: remote, store: store)
+ let polling = Task { try await coordinator.poll(now: Date(timeIntervalSince1970: 2_000)) }
+ defer { polling.cancel() }
+ try await gate.waitForStarted(4)
+ if cancel {
+ polling.cancel()
+ await #expect(throws: CancellationError.self) { try await polling.value }
+ } else {
+ await gate.release([10])
+ // Retry-After survives; the poll neither retries nor commits a prefix.
+ await #expect(throws: APIClientError.unacceptableStatusCode(429, body: "synthetic", metadata: .init(retryAfter: "60"))) {
+ try await polling.value
+ }
+ }
+ #expect(await gate.startedCount == 4)
+ #expect(await gate.finished == 4)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == nil)
+ #expect(await remote.requestedPartialFileIDs().isEmpty)
+ for folderID in 10..<18 {
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: String(folderID)) == nil)
+ }
+ }
+
+ @Test func immediateMaterializationPollsCannotOverlapEachOther() async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ let remote = WorkingSetRemoteMock(relevantItems: [], advancedResponses: [:], itemByID: [:], partialResults: [],
+ relevantDelay: .milliseconds(40))
+ let coordinator = makeCoordinator(remote: remote, store: store)
+ try await withThrowingTaskGroup(of: Void.self) { group in
+ for _ in 0..<4 {
+ group.addTask { _ = try await coordinator.poll(now: Date(timeIntervalSince1970: 1_000), minimumInterval: 0) }
+ }
+ try await group.waitForAll()
+ }
+ #expect(await remote.relevantRequestCount() == 4)
+ #expect(await remote.peakConcurrentRelevantRequests() == 1)
+ }
+ @Test(arguments: [false, true])
+ func equivalentServerResultPreservesNewerGenerationButDifferentContentsStillReject(differentContents: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ let item = makeWorkingSetItem(id: 30, name: "Server.txt", updatedAt: 1_000)
+ let current = KDriveSnapshot(anchor: "newer-local-anchor", serverCursor: "same-server-cursor", isFullyEnumerated: true,
+ usesAdvancedListing: true, items: [item])
+ try await store.save(current, domainIdentifier: "domain-1", containerIdentifier: "10")
+ let prepared = KDriveSnapshot(anchor: "prepared-local-anchor", serverCursor: "same-server-cursor", isFullyEnumerated: true,
+ usesAdvancedListing: true, items: differentContents ? [] : [item])
+ let updates = [KDriveWorkingSetContainerSnapshotUpdate(containerIdentifier: "10", snapshot: prepared,
+ condition: .matching(anchor: "old-local-anchor", serverCursor: "old-server-cursor"))]
+ let now = Date(timeIntervalSince1970: 2_000)
+ if differentContents {
+ await #expect(throws: KDriveSnapshotStoreError.staleSnapshot(domainIdentifier: "domain-1", containerIdentifier: "10")) {
+ try await store.commitWorkingSetPoll(domainIdentifier: "domain-1", containerSnapshotUpdates: updates,
+ items: [], changes: KDriveSnapshotChangeSet(updatedItems: [], deletedItemIDs: [item.id]), completedAt: now)
+ }
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == nil)
+ } else {
+ _ = try await store.commitWorkingSetPoll(domainIdentifier: "domain-1", containerSnapshotUpdates: updates,
+ items: [item], changes: KDriveSnapshotChangeSet(updatedItems: [item], deletedItemIDs: []), completedAt: now)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == now)
+ }
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: "10") == current)
+ }
+ @Test(arguments: [false, true])
+ func concurrentEnumerationIsRetriedWithoutAdvancingARejectedWatermark(persistentRace: Bool) async throws {
+ let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let store = try KDriveSnapshotSQLiteStore(databaseURL: directory.appendingPathComponent("Snapshots.sqlite3"))
+ try await store.replaceMaterializedItems([KDriveMaterializedItem(fileID: 10, isContainer: true)], domainIdentifier: "domain-1")
+ let old = makeWorkingSetItem(id: 30, name: "Old.txt", updatedAt: 1_000)
+ let fresh = makeWorkingSetItem(id: 31, name: "Fresh.txt", updatedAt: 1_001)
+ let race = WorkingSetSnapshotRace(store: store, item: fresh, persistent: persistentRace)
+ let remote = WorkingSetRemoteMock(relevantItems: [], advancedResponses: [
+ "": KDriveAdvancedItemPage(items: [old], actions: [], actionItems: [], nextCursor: "initial", hasMore: false),
+ "concurrent": KDriveAdvancedItemPage(items: [], actions: [], actionItems: [], nextCursor: "final", hasMore: false),
+ ], itemByID: [:], partialResults: [], beforePartial: { try await race.inject() })
+ let coordinator = makeCoordinator(remote: remote, store: store)
+ let now = Date(timeIntervalSince1970: 2_000)
+ if persistentRace {
+ await #expect(throws: KDriveSnapshotStoreError.staleSnapshot(domainIdentifier: "domain-1", containerIdentifier: "10")) {
+ try await coordinator.poll(now: now)
+ }
+ #expect(await remote.relevantRequestCount() == 3)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == nil)
+ #expect(try await store.snapshot(domainIdentifier: "domain-1", containerIdentifier: "10")?.items == [fresh])
+ } else {
+ let result = try await coordinator.poll(now: now)
+ #expect(result.snapshot?.items == [fresh])
+ #expect(await remote.relevantRequestCount() == 2)
+ #expect(try await store.lastSuccessfulWorkingSetPoll(domainIdentifier: "domain-1") == now)
+ }
+ }
@Test func sqliteStorePersistsMaterializationThrottleAndChainedChanges() async throws {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("working-set-store-tests-\(UUID().uuidString)", isDirectory: true)
@@ -355,12 +527,20 @@ struct WorkingSetSyncTests {
}
private actor WorkingSetRemoteMock: KDriveFileProviding, KDriveWorkingSetRemoteProviding {
+ private let beforePartial: (@Sendable () async throws -> Void)?
+ private let beforeRelevantReturn: (@Sendable () async throws -> Void)?
+ private let beforeAdvancedReturn: (@Sendable () async throws -> Void)?
+ private let advancedResponse: (@Sendable (Int) async throws -> KDriveAdvancedItemPage)?
+ private var advancedCalls = 0
private let relevantItems: [KDriveRemoteItem]
private let advancedResponses: [String: KDriveAdvancedItemPage]
private let itemByID: [Int: KDriveRemoteItem]
private let partialResults: [KDrivePartialActivityResult]
private let failsPartialListing: Bool
private var relevantCalls = 0
+ private let relevantDelay: Duration
+ private var activeRelevantCalls = 0
+ private var peakRelevantCalls = 0
private var partialFileIDs: [Int] = []
private var partialSince: Date?
@@ -369,21 +549,37 @@ private actor WorkingSetRemoteMock: KDriveFileProviding, KDriveWorkingSetRemoteP
advancedResponses: [String: KDriveAdvancedItemPage],
itemByID: [Int: KDriveRemoteItem],
partialResults: [KDrivePartialActivityResult],
- failsPartialListing: Bool = false
+ failsPartialListing: Bool = false,
+ beforePartial: (@Sendable () async throws -> Void)? = nil,
+ relevantDelay: Duration = .zero,
+ beforeRelevantReturn: (@Sendable () async throws -> Void)? = nil,
+ beforeAdvancedReturn: (@Sendable () async throws -> Void)? = nil,
+ advancedResponse: (@Sendable (Int) async throws -> KDriveAdvancedItemPage)? = nil
) {
self.relevantItems = relevantItems
self.advancedResponses = advancedResponses
self.itemByID = itemByID
self.partialResults = partialResults
self.failsPartialListing = failsPartialListing
+ self.beforePartial = beforePartial
+ self.relevantDelay = relevantDelay
+ self.beforeRelevantReturn = beforeRelevantReturn
+ self.beforeAdvancedReturn = beforeAdvancedReturn
+ self.advancedResponse = advancedResponse
}
func listWorkingSetRelevantItems(driveID: Int, latestLimit: Int) async throws -> [KDriveRemoteItem] {
relevantCalls += 1
+ activeRelevantCalls += 1
+ peakRelevantCalls = max(peakRelevantCalls, activeRelevantCalls)
+ defer { activeRelevantCalls -= 1 }
+ if relevantDelay > .zero { try await Task.sleep(for: relevantDelay) }
+ try await beforeRelevantReturn?()
return relevantItems
}
func listPartialActivities(driveID: Int, fileIDs: [Int], since: Date) async throws -> [KDrivePartialActivityResult] {
+ try await beforePartial?()
partialFileIDs.append(contentsOf: fileIDs)
partialSince = since
if failsPartialListing { throw WorkingSetRemoteMockError.unimplemented }
@@ -393,6 +589,8 @@ private actor WorkingSetRemoteMock: KDriveFileProviding, KDriveWorkingSetRemoteP
func requestedPartialFileIDs() -> [Int] { partialFileIDs.sorted() }
func requestedPartialSince() -> Date? { partialSince }
func relevantRequestCount() -> Int { relevantCalls }
+ func peakConcurrentRelevantRequests() -> Int { peakRelevantCalls }
+ func advancedRequestCount() -> Int { advancedCalls }
func item(driveID: Int, fileID: Int) async throws -> KDriveRemoteItem {
guard let item = itemByID[fileID] else { throw WorkingSetRemoteMockError.unimplemented }
@@ -400,6 +598,9 @@ private actor WorkingSetRemoteMock: KDriveFileProviding, KDriveWorkingSetRemoteP
}
func listAdvancedDirectory(driveID: Int, folderID: Int, cursor: String?, limit: Int) async throws -> KDriveAdvancedItemPage {
+ advancedCalls += 1
+ try await beforeAdvancedReturn?()
+ if let advancedResponse { return try await advancedResponse(folderID) }
guard let page = advancedResponses[cursor ?? ""] else {
throw WorkingSetRemoteMockError.unimplemented
}
@@ -421,6 +622,23 @@ private actor WorkingSetRemoteMock: KDriveFileProviding, KDriveWorkingSetRemoteP
func deleteTrashedItem(driveID: Int, fileID: Int) async throws { throw WorkingSetRemoteMockError.unimplemented }
}
+private actor WorkingSetSnapshotRace {
+ let store: KDriveSnapshotSQLiteStore
+ let item: KDriveRemoteItem
+ let persistent: Bool
+ var injected = false
+ init(store: KDriveSnapshotSQLiteStore, item: KDriveRemoteItem, persistent: Bool) {
+ self.store = store; self.item = item; self.persistent = persistent
+ }
+ func inject() async throws {
+ guard persistent || !injected else { return }
+ injected = true
+ try await store.save(KDriveSnapshot(anchor: UUID().uuidString, serverCursor: "concurrent",
+ isFullyEnumerated: true, usesAdvancedListing: true, items: [item]),
+ domainIdentifier: "domain-1", containerIdentifier: "10")
+ }
+}
+
private enum WorkingSetRemoteMockError: Error {
case unimplemented
}
@@ -446,3 +664,38 @@ private func makeWorkingSetItem(
updatedAt: Date(timeIntervalSince1970: updatedAt)
)
}
+
+/// Suspension is controlled by requests, not subsecond sleeps or CPU scheduling.
+private actor WorkingSetContainerGate {
+ let throttle: Bool
+ private var waiters: [Int: AsyncStream.Continuation] = [:]
+ private let starts = AsyncStream.makeStream()
+ private(set) var startedCount = 0
+ private(set) var finished = 0
+ private(set) var peakActive = 0
+ init(throttle: Bool = false) { self.throttle = throttle }
+ func page(folderID: Int) async throws -> KDriveAdvancedItemPage {
+ let release = AsyncStream.makeStream()
+ waiters[folderID] = release.continuation
+ startedCount += 1
+ peakActive = max(peakActive, waiters.count)
+ starts.continuation.yield(startedCount)
+ defer { waiters[folderID] = nil; finished += 1 }
+ var iterator = release.stream.makeAsyncIterator()
+ _ = await iterator.next()
+ try Task.checkCancellation()
+ if throttle { throw APIClientError.unacceptableStatusCode(429, body: "synthetic", metadata: .init(retryAfter: "60")) }
+ return KDriveAdvancedItemPage(items: [makeWorkingSetItem(id: folderID * 10, name: "Fixture.txt", parentID: folderID, updatedAt: 1_000)],
+ actions: [], actionItems: [], nextCursor: "cursor-\(folderID)", hasMore: false)
+ }
+ func waitForStarted(_ count: Int) async throws {
+ if startedCount >= count { return }
+ for await number in starts.stream {
+ if number >= count { return }
+ }
+ throw CancellationError()
+ }
+ func release(_ folders: [Int]) {
+ for folder in folders { waiters[folder]?.yield(()); waiters[folder]?.finish() }
+ }
+}
diff --git a/potassiumProviderTests/potassiumProviderTests.swift b/potassiumProviderTests/potassiumProviderTests.swift
index d673dab..e69d52c 100644
--- a/potassiumProviderTests/potassiumProviderTests.swift
+++ b/potassiumProviderTests/potassiumProviderTests.swift
@@ -78,6 +78,41 @@ struct PotassiumProviderCoreTests {
#expect(configuration.accountIdentifier == ProviderConstants.legacyAccountIdentifier)
#expect(configuration.driveID == 42)
#expect(configuration.knownFolderLayout == .legacyPrivate)
+ #expect(configuration.purpose == .ordinary)
+ #expect(configuration.stabilityLab == nil)
+ }
+
+ @Test func stabilityLabDomainConfigurationRoundTripsOwnershipEvidence() throws {
+ let marker = StabilityLabOwnershipMarker(
+ identifier: UUID(uuidString: "4AAB04E6-E73B-4D4D-B99C-9A3CC0949797")!,
+ driveID: 42,
+ rootFileID: 84,
+ createdAt: Date(timeIntervalSince1970: 1_000)
+ )
+ let configuration = ProviderDomainConfiguration(
+ domainIdentifier: "stability-domain",
+ accountIdentifier: ProviderConstants.legacyAccountIdentifier,
+ displayName: "Stability Lab",
+ driveID: marker.driveID,
+ driveName: "Development",
+ rootFileID: marker.rootFileID,
+ purpose: .stabilityLab,
+ stabilityLab: ProviderStabilityLabConfiguration(
+ driveRootFileID: 1,
+ markerFileID: 85,
+ ownershipMarker: marker
+ ),
+ createdAt: marker.createdAt,
+ updatedAt: marker.createdAt
+ )
+
+ let encoded = try JSONEncoder().encode(configuration)
+ let decoded = try JSONDecoder().decode(ProviderDomainConfiguration.self, from: encoded)
+
+ #expect(decoded == configuration)
+ #expect(decoded.hasConsistentPurposeConfiguration)
+ #expect(decoded.isCompatible(with: .stability))
+ #expect(decoded.isCompatible(with: .standard) == false)
}
@Test func newDomainConfigurationUsesMachineNamespaceKnownFolderLayout() {
@@ -88,6 +123,8 @@ struct PotassiumProviderCoreTests {
)
#expect(configuration.knownFolderLayout == .machineNamespace)
+ #expect(configuration.isCompatible(with: .standard))
+ #expect(configuration.isCompatible(with: .stability) == false)
}
@Test func inMemoryTokenStoreScopesTokensAndMigratesLegacyToken() async throws {
@@ -1431,6 +1468,7 @@ struct PotassiumProviderCoreTests {
@Test func kdriveServiceLoadsDriveRolesFromDriveInitOnly() async throws {
await KDriveDiscoveryURLProtocol.reset()
+ let diagnostics = KDriveDiagnosticCapture()
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [KDriveDiscoveryURLProtocol.self]
let session = URLSession(configuration: configuration)
@@ -1440,7 +1478,9 @@ struct PotassiumProviderCoreTests {
bearerToken: "redacted-token",
apiBaseURL: URL(string: "https://api.example.test")!,
driveBaseURL: URL(string: "https://drive.example.test")!,
- session: session
+ session: session,
+ diagnosticRecorder: diagnostics,
+ diagnosticSource: .app
)
let drives = try await service.listDrives()
@@ -1458,6 +1498,12 @@ struct PotassiumProviderCoreTests {
#expect(driveComponents.path == "/2/drive/init")
#expect(driveComponents.queryItems?.contains(URLQueryItem(name: "with", value: "drives")) == true)
#expect(driveRequest.value(forHTTPHeaderField: "Authorization") == "Bearer redacted-token")
+
+ let diagnosticEvents = await diagnostics.events()
+ #expect(diagnosticEvents.map(\.phase) == [.started, .completed])
+ #expect(diagnosticEvents.map(\.operation) == [.listDrives, .listDrives])
+ #expect(diagnosticEvents.map(\.routeTemplate) == [.driveDiscovery, .driveDiscovery])
+ #expect(Set(diagnosticEvents.map(\.correlationID)).count == 1)
}
@Test func kdriveServiceConditionallyReplacesFileByID() async throws {
@@ -1500,7 +1546,7 @@ struct PotassiumProviderCoreTests {
#expect(request.value(forHTTPHeaderField: "Content-Type") == "application/octet-stream")
#expect(request.value(forHTTPHeaderField: "If-Match") == "etag-before")
#expect(query["total_size"] == "4")
- #expect(query["with"] == "etag")
+ #expect(query["with"] == "etag,is_favorite")
#expect(query["client_token"] == "0123456789abcdef0123456789abcdef")
#expect(query["total_chunk_hash"] == "sha256:abcd")
#expect(query["last_modified_at"] == "1700000001")
@@ -1559,6 +1605,7 @@ struct PotassiumProviderCoreTests {
@Test func kdriveServiceExposesLazyObservableDownloadOperation() async throws {
await KDriveDataRequestCapturingURLProtocol.reset()
+ let diagnostics = KDriveDiagnosticCapture()
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [KDriveDataRequestCapturingURLProtocol.self]
let session = URLSession(configuration: configuration)
@@ -1567,18 +1614,82 @@ struct PotassiumProviderCoreTests {
let service = PotassiumKDriveService(
bearerToken: "redacted-token",
apiBaseURL: URL(string: "https://api.example.test")!,
- session: session
+ session: session,
+ diagnosticRecorder: diagnostics
)
let operation = try service.downloadFileOperation(driveID: 100, fileID: 42)
#expect(operation.progress.totalUnitCount >= -1)
#expect(await KDriveDataRequestCapturingURLProtocol.peekLastRequest() == nil)
+ try await Task.sleep(for: .milliseconds(25))
+ #expect(await diagnostics.events().isEmpty)
let data = try await operation.value
let request = try #require(await KDriveDataRequestCapturingURLProtocol.lastRequest())
#expect(data == KDriveDataRequestCapturingURLProtocol.responseData)
#expect(request.url?.path == "/2/drive/100/files/42/download")
+ let diagnosticEvents = await diagnostics.events()
+ // Sampler cadence may produce several progress buckets, especially
+ // with weighted child progress. Assert lifecycle, not scheduler timing.
+ #expect(diagnosticEvents.filter { $0.phase != .progress }.map(\.phase) == [.started, .completed])
+ #expect(diagnosticEvents.first?.phase == .started)
+ #expect(diagnosticEvents.last?.phase == .completed)
+ let buckets = diagnosticEvents.filter { $0.phase == .progress }.compactMap(\.progressPercentBucket)
+ #expect(buckets.count == diagnosticEvents.filter { $0.phase == .progress }.count)
+ #expect(buckets == buckets.sorted())
+ #expect(Set(diagnosticEvents.compactMap(\.spanID)).count == 1)
+ }
+
+ @Test func missingShareLinkIsRecordedAsSuccessfulOptionalResult() async throws {
+ let diagnostics = KDriveDiagnosticCapture()
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.protocolClasses = [KDriveNotFoundURLProtocol.self]
+ let session = URLSession(configuration: configuration)
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(
+ bearerToken: "",
+ apiBaseURL: URL(string: "https://api.example.test")!,
+ session: session,
+ diagnosticRecorder: diagnostics
+ )
+
+ let result = try await service.shareLink(driveID: 100, fileID: 42)
+
+ #expect(result == nil)
+ let events = await diagnostics.events()
+ #expect(events.map(\.phase) == [.started, .completed])
+ #expect(events.last?.statusClass == .success)
+ #expect(events.last?.errorClass == nil)
+ }
+
+ @Test func concurrentLazyTransferStartAndCancelShareOneDiagnosticSpan() async throws {
+ await KDriveDataRequestCapturingURLProtocol.reset()
+ let diagnostics = SuspendingKDriveDiagnosticCapture()
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.protocolClasses = [KDriveDataRequestCapturingURLProtocol.self]
+ let session = URLSession(configuration: configuration)
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(
+ bearerToken: "",
+ apiBaseURL: URL(string: "https://api.example.test")!,
+ session: session,
+ diagnosticRecorder: diagnostics
+ )
+ let operation = try service.downloadFileOperation(driveID: 100, fileID: 42)
+ let valueTask = Task { try? await operation.value }
+
+ await diagnostics.waitForFirstStart()
+ operation.cancel()
+ for _ in 0..<200 { await Task.yield() }
+ #expect(await diagnostics.startedCount() == 1)
+
+ await diagnostics.release()
+ _ = await valueTask.value
+ let events = await diagnostics.waitForTerminal()
+ #expect(events.filter { $0.phase == .started }.count == 1)
+ #expect(events.filter { [.completed, .failed, .cancelled].contains($0.phase) }.count == 1)
+ #expect(Set(events.compactMap(\.spanID)).count == 1)
}
@Test func kdriveServiceFetchesThumbnailThroughPotassiumRoute() async throws {
@@ -1611,6 +1722,39 @@ struct PotassiumProviderCoreTests {
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer redacted-token")
}
+ @Test(arguments: ["metadata", "directory", "trash", "working-set"], [false, true, nil] as [Bool?])
+ func favoriteStateIsRequestedAndUnknownRemainsUnknown(route: String, favorite: Bool?) async throws {
+ var response = try #require(JSONSerialization.jsonObject(with: Self.fileUploadResponseData) as? [String: Any])
+ var file = try #require(response["data"] as? [String: Any])
+ file["is_favorite"] = favorite
+ response["data"] = route == "metadata" ? file : [file] as Any
+ response["cursor"] = NSNull()
+ response["has_more"] = false
+ await KDriveJSONRequestCapturingURLProtocol.reset(responseData: try JSONSerialization.data(withJSONObject: response))
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.protocolClasses = [KDriveJSONRequestCapturingURLProtocol.self]
+ let session = URLSession(configuration: configuration)
+ defer { session.invalidateAndCancel() }
+ let service = PotassiumKDriveService(bearerToken: "redacted-token",
+ apiBaseURL: URL(string: "https://api.example.test")!, session: session)
+ let item: KDriveRemoteItem
+ switch route {
+ case "metadata": item = try await service.item(driveID: 100, fileID: 42)
+ case "directory": item = try #require(await service.listDirectory(driveID: 100, folderID: 7, cursor: nil, limit: 50).items.first)
+ case "trash": item = try #require(await service.listTrash(driveID: 100, cursor: nil, limit: 50).items.first)
+ default: item = try #require(await service.listWorkingSetRelevantItems(driveID: 100, latestLimit: 50).first)
+ }
+ #expect(item.id == 42)
+ #expect(item.parentID == 7)
+ #expect(item.name == "Edited.jpg")
+ #expect(item.size == 4)
+ #expect(item.isFavorite == favorite)
+ let request = try #require(await KDriveJSONRequestCapturingURLProtocol.lastRequest())
+ let requestURL = try #require(request.url)
+ let query = try #require(URLComponents(url: requestURL, resolvingAgainstBaseURL: false)?.queryItems)
+ #expect(query.contains(URLQueryItem(name: "with", value: "etag,is_favorite")))
+ }
+
@Test func kdriveServiceFetchesInitialAdvancedListingThroughPotassiumRoute() async throws {
await KDriveJSONRequestCapturingURLProtocol.reset(responseData: Self.advancedListingResponseData)
let configuration = URLSessionConfiguration.ephemeral
@@ -1632,7 +1776,7 @@ struct PotassiumProviderCoreTests {
#expect(request.httpMethod == "GET")
#expect(components.path == "/3/drive/100/files/42/listing")
- #expect(queryItems.contains(URLQueryItem(name: "with", value: "files.capabilities")))
+ #expect(queryItems.contains(URLQueryItem(name: "with", value: "files.capabilities,files.is_favorite")))
#expect(queryItems.contains { $0.name == "with" && $0.value?.contains("etag") == true } == false)
#expect(queryItems.contains(URLQueryItem(name: "limit", value: "50")))
#expect(queryItems.contains(URLQueryItem(name: "order_by", value: "type")))
@@ -1640,6 +1784,8 @@ struct PotassiumProviderCoreTests {
#expect(queryItems.contains(URLQueryItem(name: "order_for[name]", value: "asc")))
#expect(queryItems.contains(URLQueryItem(name: "order_for[type]", value: "asc")))
#expect(page.items.first?.id == 43)
+ #expect(page.items.first?.isFavorite == false)
+ #expect(page.actionItems.first?.isFavorite == true)
#expect(page.actions.first?.action == "file_update")
#expect(page.actions.last?.action == "file_rename")
#expect(page.actionItems.first?.id == 44)
@@ -1668,7 +1814,7 @@ struct PotassiumProviderCoreTests {
#expect(request.httpMethod == "GET")
#expect(components.path == "/3/drive/100/files/42/listing/continue")
- #expect(queryItems.contains(URLQueryItem(name: "with", value: "files.capabilities")))
+ #expect(queryItems.contains(URLQueryItem(name: "with", value: "files.capabilities,files.is_favorite")))
#expect(queryItems.contains { $0.name == "with" && $0.value?.contains("etag") == true } == false)
#expect(queryItems.contains(URLQueryItem(name: "cursor", value: "old-cursor")))
}
@@ -1707,7 +1853,7 @@ struct PotassiumProviderCoreTests {
let advancedURL = try #require(advancedRequest.url)
let advancedComponents = try #require(URLComponents(url: advancedURL, resolvingAgainstBaseURL: false))
#expect(advancedComponents.path == "/3/drive/100/files/42/listing")
- #expect(advancedComponents.queryItems?.contains(URLQueryItem(name: "with", value: "files.capabilities")) == true)
+ #expect(advancedComponents.queryItems?.contains(URLQueryItem(name: "with", value: "files.capabilities,files.is_favorite")) == true)
}
@Test func kdriveServicePropagatesContinuedAdvancedListing422WithoutChangingListingModes() async throws {
@@ -1744,7 +1890,7 @@ struct PotassiumProviderCoreTests {
let advancedComponents = try #require(URLComponents(url: advancedURL, resolvingAgainstBaseURL: false))
#expect(advancedComponents.path == "/3/drive/100/files/42/listing/continue")
#expect(advancedComponents.queryItems?.contains(URLQueryItem(name: "cursor", value: "advanced-cursor")) == true)
- #expect(advancedComponents.queryItems?.contains(URLQueryItem(name: "with", value: "files.capabilities")) == true)
+ #expect(advancedComponents.queryItems?.contains(URLQueryItem(name: "with", value: "files.capabilities,files.is_favorite")) == true)
}
@Test func kdriveServiceFallsBackToDirectoryListingWithoutETagAfter422() async throws {
@@ -1766,11 +1912,11 @@ struct PotassiumProviderCoreTests {
let firstURL = try #require(requests.first?.url)
let firstComponents = try #require(URLComponents(url: firstURL, resolvingAgainstBaseURL: false))
let firstQuery = firstComponents.queryItems ?? []
- #expect(firstQuery.contains(URLQueryItem(name: "with", value: "etag")))
+ #expect(firstQuery.contains(URLQueryItem(name: "with", value: "etag,is_favorite")))
let secondURL = try #require(requests.last?.url)
let secondComponents = try #require(URLComponents(url: secondURL, resolvingAgainstBaseURL: false))
let secondQuery = secondComponents.queryItems ?? []
- #expect(secondQuery.contains { $0.name == "with" } == false)
+ #expect(secondQuery.contains(URLQueryItem(name: "with", value: "is_favorite")))
#expect(page.items.map(\.id) == [43])
#expect(page.hasMore == false)
}
@@ -2015,6 +2161,7 @@ struct PotassiumProviderCoreTests {
#expect(model.drives(for: account.accountIdentifier) == [drive])
}
+ #if !STABILITY
@MainActor
@Test func appModelKeepsMultipleAccountsAndLogsOutIndependently() async throws {
let directory = temporaryDirectory()
@@ -2279,6 +2426,7 @@ struct PotassiumProviderCoreTests {
#expect(failure.recoverySuggestion?.contains("My Mac") == true)
#expect(failure.diagnosticSummary?.contains("usable File Provider extension") == true)
}
+ #endif
@MainActor
@Test func appModelRevealsAndSignalsOneConfiguredDrive() async throws {
@@ -2802,6 +2950,28 @@ private final class KDriveDataRequestCapturingURLProtocol: URLProtocol {
override func stopLoading() {}
}
+private final class KDriveNotFoundURLProtocol: URLProtocol {
+ override class func canInit(with request: URLRequest) -> Bool { true }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ let response = HTTPURLResponse(
+ url: request.url!,
+ statusCode: 404,
+ httpVersion: "HTTP/1.1",
+ headerFields: ["Content-Type": "application/json"]
+ )!
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: Data("{}".utf8))
+ client?.urlProtocolDidFinishLoading(self)
+ }
+
+ override func stopLoading() {}
+}
+
private final class KDriveJSONRequestCapturingURLProtocol: URLProtocol {
private static let capture = CapturedURLRequestStore()
private static let responseStore = CapturedResponseStore()
@@ -2943,6 +3113,55 @@ private final class KDriveListing422URLProtocol: URLProtocol {
"""#.utf8)
}
+private actor KDriveDiagnosticCapture: ProviderDiagnosticRecording {
+ private var recordedEvents: [ProviderDiagnosticEvent] = []
+
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) {
+ recordedEvents.append(event)
+ }
+
+ func events() -> [ProviderDiagnosticEvent] {
+ recordedEvents
+ }
+}
+
+private actor SuspendingKDriveDiagnosticCapture: ProviderDiagnosticRecording {
+ private var isBlocked = true
+ private var recordedEvents: [ProviderDiagnosticEvent] = []
+
+ func recordDiagnostic(_ event: ProviderDiagnosticEvent) async {
+ recordedEvents.append(event)
+ if event.phase == .started {
+ while isBlocked {
+ await Task.yield()
+ }
+ }
+ }
+
+ func waitForFirstStart() async {
+ while recordedEvents.contains(where: { $0.phase == .started }) == false {
+ await Task.yield()
+ }
+ }
+
+ func startedCount() -> Int {
+ recordedEvents.filter { $0.phase == .started }.count
+ }
+
+ func release() {
+ isBlocked = false
+ }
+
+ func waitForTerminal() async -> [ProviderDiagnosticEvent] {
+ while recordedEvents.contains(where: {
+ [.completed, .failed, .cancelled].contains($0.phase)
+ }) == false {
+ await Task.yield()
+ }
+ return recordedEvents
+ }
+}
+
private final class KDriveDiscoveryURLProtocol: URLProtocol {
private static let capture = KDriveDiscoveryCapture()
diff --git a/potassiumProviderUITests/UITestApplication.swift b/potassiumProviderUITests/UITestApplication.swift
new file mode 100644
index 0000000..13f9477
--- /dev/null
+++ b/potassiumProviderUITests/UITestApplication.swift
@@ -0,0 +1,28 @@
+import XCTest
+
+/// Select the product beside this test runner, even if another checkout has
+/// registered an application with the same bundle identifier on this Mac.
+@MainActor
+enum UITestApplication {
+ static func make(for testCase: XCTestCase) -> XCUIApplication {
+ #if os(macOS)
+ let productDirectory = Bundle(for: potassiumProviderUITests.self).bundleURL
+ .deletingLastPathComponent() // PlugIns
+ .deletingLastPathComponent() // Contents
+ .deletingLastPathComponent() // test runner application
+ .deletingLastPathComponent() // build products
+ let appURL = productDirectory.appendingPathComponent("potassiumProvider.app")
+ XCTAssertEqual(Bundle(url: appURL)?.bundleIdentifier, "net.weavee.potassiumProvider",
+ "The UI runner must launch its sibling build product.")
+ let app = XCUIApplication(url: appURL)
+ #else
+ let app = XCUIApplication()
+ #endif
+ // Launch/performance screenshots must also contain synthetic state.
+ app.launchEnvironment["POTASSIUM_UI_TEST_FIXTURE"] = "setup-navigation"
+ testCase.addTeardownBlock { @MainActor in
+ if app.state != .notRunning { app.terminate() }
+ }
+ return app
+ }
+}
diff --git a/potassiumProviderUITests/potassiumProviderUITests.swift b/potassiumProviderUITests/potassiumProviderUITests.swift
index 4d324a5..64172b9 100644
--- a/potassiumProviderUITests/potassiumProviderUITests.swift
+++ b/potassiumProviderUITests/potassiumProviderUITests.swift
@@ -25,7 +25,7 @@ final class potassiumProviderUITests: XCTestCase {
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
- let app = XCUIApplication()
+ let app = UITestApplication.make(for: self)
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
@@ -57,7 +57,7 @@ final class potassiumProviderUITests: XCTestCase {
availableDrive.tap()
#endif
- XCTAssertTrue(app.buttons["drive.addToFiles"].waitForExistence(timeout: 5))
+ XCTAssertTrue(app.buttons["drive.addToFiles"].waitForExistence(timeout: 5), app.windows["net.weavee.potassiumProvider.main-window"].debugDescription)
XCTAssertTrue(app.buttons["drive.createEncryptedVault"].exists)
XCTAssertTrue(app.staticTexts["This drive is currently in maintenance."].exists)
}
@@ -123,21 +123,25 @@ final class potassiumProviderUITests: XCTestCase {
let addAccount = app.buttons["setup.addAccount"]
XCTAssertTrue(addAccount.waitForExistence(timeout: 5))
+ #if os(macOS)
+ XCTAssertLessThanOrEqual(addAccount.frame.width, 700)
+ #endif
addAccount.tap()
XCTAssertTrue(app.buttons["addAccount.oauth"].waitForExistence(timeout: 5))
#if os(macOS)
XCTAssertFalse(app.secureTextFields["addAccount.manualToken"].exists)
- let advanced = app.descendants(matching: .any)["addAccount.advanced"]
+ let advanced = app.disclosureTriangles["Advanced"]
XCTAssertTrue(advanced.waitForExistence(timeout: 5))
- advanced.tap()
+ advanced.click()
#endif
- XCTAssertTrue(app.secureTextFields["addAccount.manualToken"].waitForExistence(timeout: 5))
- XCTAssertTrue(app.staticTexts["Advanced"].exists)
-
+ XCTAssertTrue(app.secureTextFields["addAccount.manualToken"].waitForExistence(timeout: 5), app.windows["net.weavee.potassiumProvider.main-window"].debugDescription)
#if os(macOS)
- XCTAssertLessThanOrEqual(addAccount.frame.width, 700)
+ XCTAssertTrue(app.disclosureTriangles["Advanced"].exists)
+ #else
+ XCTAssertTrue(app.staticTexts["Advanced"].exists)
#endif
+
}
#if os(macOS)
@@ -150,9 +154,9 @@ final class potassiumProviderUITests: XCTestCase {
XCTAssertTrue(addAccount.waitForExistence(timeout: 5))
addAccount.click()
- XCTAssertTrue(app.staticTexts["Sign in to Infomaniak"].waitForExistence(timeout: 5))
+ XCTAssertTrue(text(containing: "Sign in to Infomaniak", in: app).waitForExistence(timeout: 5))
XCTAssertTrue(app.buttons["addAccount.oauth"].exists)
- XCTAssertTrue(app.staticTexts["Advanced"].exists)
+ XCTAssertTrue(app.disclosureTriangles["Advanced"].exists)
XCTAssertFalse(app.secureTextFields["addAccount.manualToken"].exists)
}
@@ -202,8 +206,8 @@ final class potassiumProviderUITests: XCTestCase {
XCTAssertTrue(
app.staticTexts["No Owned kDrives Available"].waitForExistence(timeout: 5)
)
- XCTAssertFalse(app.buttons["Load Drives"].exists)
- XCTAssertFalse(app.buttons["Refresh Drives"].exists)
+ XCTAssertFalse(app.scrollViews.buttons["Load Drives"].exists)
+ XCTAssertFalse(app.scrollViews.buttons["Refresh Drives"].exists)
XCTAssertTrue(app.buttons["account.refreshDrives"].waitForExistence(timeout: 5))
}
#endif
@@ -394,16 +398,29 @@ final class potassiumProviderUITests: XCTestCase {
#endif
@MainActor
- func testLaunchPerformance() throws {
- // This measures how long it takes to launch your application.
- measure(metrics: [XCTApplicationLaunchMetric()]) {
- XCUIApplication().launch()
+ func testLaunchToSetupReadinessPerformance() throws {
+ let app = UITestApplication.make(for: self)
+ let options = XCTMeasureOptions()
+ options.invocationOptions = [.manuallyStart]
+ // Measure the complete launch-to-interaction interval. XCTest's system
+ // launch signposts intermittently omit samples on the hosted Mac even
+ // when every app launch and window assertion succeeds. This benchmark
+ // includes automation overhead and is not a first-frame measurement.
+ measure(metrics: [XCTClockMetric()], options: options) {
+ app.terminate()
+ startMeasuring()
+ app.launch()
+ app.activate()
+ openSetup(in: app)
+ let addAccount = app.buttons["setup.addAccount"]
+ XCTAssertTrue(addAccount.waitForExistence(timeout: 5))
+ XCTAssertTrue(addAccount.isEnabled)
}
}
@MainActor
private func launchSetupFixture(named fixtureName: String = "setup-navigation") -> XCUIApplication {
- let app = XCUIApplication()
+ let app = UITestApplication.make(for: self)
app.launchEnvironment["POTASSIUM_UI_TEST_FIXTURE"] = fixtureName
app.launch()
return app
diff --git a/potassiumProviderUITests/potassiumProviderUITestsLaunchTests.swift b/potassiumProviderUITests/potassiumProviderUITestsLaunchTests.swift
index 1cb7fd6..56ee541 100644
--- a/potassiumProviderUITests/potassiumProviderUITestsLaunchTests.swift
+++ b/potassiumProviderUITests/potassiumProviderUITestsLaunchTests.swift
@@ -19,7 +19,7 @@ final class potassiumProviderUITestsLaunchTests: XCTestCase {
@MainActor
func testLaunch() throws {
- let app = XCUIApplication()
+ let app = UITestApplication.make(for: self)
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
diff --git a/scripts/run-finder-stability.sh b/scripts/run-finder-stability.sh
new file mode 100755
index 0000000..43f7bcf
--- /dev/null
+++ b/scripts/run-finder-stability.sh
@@ -0,0 +1,276 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+DERIVED_DATA_PATH="${POTASSIUM_STABILITY_DERIVED_DATA:-${TMPDIR:-/private/tmp}/potassiumProviderFinderStabilityDerivedData}"
+APP_PATH=""
+BUILD_APP=0
+INSTALLED_APP_PATH="$HOME/Applications/Potassium Stability.app"
+MODE="preflight"
+REQUEST_PERMISSIONS=0
+CONFIRMED_LIVE=0
+CONFIRMED_RECOVERY=0
+CONFLICT_CASE=""
+EXTENSION_STATE=""
+INCLUDE_PERMANENT_DELETION=0
+
+usage() {
+ cat <<'USAGE'
+Usage:
+ scripts/run-finder-stability.sh [--app PATH | --build] [--preflight] [--provision --yes-live] [--run --yes-live] [--conflicts [--case CASE] --yes-live] [--watch] [--recover-stale-run --yes-recover] [--request-permissions]
+
+Options:
+ --app PATH Use a specific existing macOS Stability app.
+ --build Build and install at ~/Applications/Potassium Stability.app.
+ --preflight Verify lab, domain, and consent state without Finder or remote mutation (default).
+ --provision Create or resume the isolated lab inside Private using the saved Keychain account.
+ --watch Show sanitized active-run diagnostics without mutation.
+ --run Execute the verified disposable-root scenario sequence.
+ --include-permanent-deletion Include scenario 12 and its exact-item confirmation; requires --run. Default: defer deletion and continue 13–16.
+ --conflicts Run independent conflict cases, each with fresh fixtures and evidence.
+ --case CASE Select one conflict case; requires --conflicts.
+ --extension-state MODE Require fresh or running extension evidence for --run or --conflicts.
+ --recover-stale-run Preserve and abandon a local run whose owner process has exited.
+ --yes-live Required with --run, --provision, or --conflicts; confirms the saved development account and lab may be mutated.
+ --yes-recover Required with --recover-stale-run; confirms local evidence recovery.
+ --request-permissions Ask macOS to present Accessibility/Finder Automation consent prompts.
+ --help Show this help.
+
+Credentials stay in the app's existing OAuth or manual-token Keychain flow.
+The installed Stability app is reused unless --build is supplied or it is absent.
+Exit 4 means all 15 selected scenarios passed with deletion deferred, not full acceptance.
+USAGE
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --build)
+ BUILD_APP=1
+ shift
+ ;;
+ --app)
+ if [[ $# -lt 2 || -z "$2" ]]; then
+ echo "error: --app requires a path" >&2
+ exit 2
+ fi
+ APP_PATH="$2"
+ shift 2
+ ;;
+ --preflight)
+ MODE="preflight"
+ shift
+ ;;
+ --provision)
+ MODE="provision"
+ shift
+ ;;
+ --watch)
+ MODE="watch"
+ shift
+ ;;
+ --run)
+ MODE="run"
+ shift
+ ;;
+ --conflicts)
+ MODE="conflicts"
+ shift
+ ;;
+ --include-permanent-deletion)
+ if [[ "$INCLUDE_PERMANENT_DELETION" -eq 1 ]]; then exit 2; fi
+ INCLUDE_PERMANENT_DELETION=1
+ shift
+ ;;
+ --extension-state)
+ if [[ $# -lt 2 || -z "$2" || -n "$EXTENSION_STATE" ]]; then exit 2; fi
+ EXTENSION_STATE="$2"
+ shift 2
+ ;;
+ --case)
+ if [[ $# -lt 2 || -z "$2" ]]; then exit 2; fi
+ CONFLICT_CASE="$2"
+ shift 2
+ ;;
+ --recover-stale-run)
+ MODE="recover"
+ shift
+ ;;
+ --yes-live)
+ CONFIRMED_LIVE=1
+ shift
+ ;;
+ --yes-recover)
+ CONFIRMED_RECOVERY=1
+ shift
+ ;;
+ --request-permissions)
+ REQUEST_PERMISSIONS=1
+ shift
+ ;;
+ --help|-h)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "error: unknown option" >&2
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+
+if [[ "$INCLUDE_PERMANENT_DELETION" -eq 1 && "$MODE" != "run" ]]; then
+ echo "error: --include-permanent-deletion requires --run" >&2
+ exit 2
+fi
+if [[ -n "$EXTENSION_STATE" && ( ( "$MODE" != "conflicts" && "$MODE" != "run" ) || ( "$EXTENSION_STATE" != "fresh" && "$EXTENSION_STATE" != "running" ) ) ]]; then
+ echo "error: --extension-state requires --run or --conflicts and fresh or running" >&2
+ exit 2
+fi
+if [[ ( "$MODE" == "run" || "$MODE" == "provision" || "$MODE" == "conflicts" ) && "$CONFIRMED_LIVE" -ne 1 ]]; then
+ echo "error: --run, --provision, and --conflicts require --yes-live" >&2
+ exit 2
+fi
+if [[ "$MODE" != "run" && "$MODE" != "provision" && "$MODE" != "conflicts" && "$CONFIRMED_LIVE" -eq 1 ]]; then
+ echo "error: --yes-live is accepted only with --run, --provision, or --conflicts" >&2
+ exit 2
+fi
+if [[ "$MODE" == "recover" && "$CONFIRMED_RECOVERY" -ne 1 ]]; then
+ echo "error: --recover-stale-run requires --yes-recover" >&2
+ exit 2
+fi
+if [[ "$MODE" != "recover" && "$CONFIRMED_RECOVERY" -eq 1 ]]; then
+ echo "error: --yes-recover is accepted only with --recover-stale-run" >&2
+ exit 2
+fi
+if [[ "$MODE" == "recover" && "$CONFIRMED_LIVE" -eq 1 ]]; then
+ echo "error: --yes-live is not accepted with --recover-stale-run" >&2
+ exit 2
+fi
+if [[ "$MODE" == "recover" && "$REQUEST_PERMISSIONS" -eq 1 ]]; then
+ echo "error: --request-permissions is not accepted with --recover-stale-run" >&2
+ exit 2
+fi
+
+if [[ -n "$CONFLICT_CASE" && "$MODE" != "conflicts" ]]; then
+ echo "error: --case requires --conflicts" >&2
+ exit 2
+fi
+if [[ -n "$CONFLICT_CASE" ]]; then
+ case "$CONFLICT_CASE" in
+ content-before-preflight|content-after-preflight|rename-rename|move-move|edit-rename|edit-move) ;;
+ *) echo "error: unknown conflict case" >&2; exit 2 ;;
+ esac
+fi
+
+if [[ -n "$APP_PATH" && "$BUILD_APP" -eq 1 ]]; then
+ echo "error: --app and --build are mutually exclusive" >&2
+ exit 2
+fi
+if [[ -z "$APP_PATH" ]]; then
+ APP_PATH="$INSTALLED_APP_PATH"
+ if [[ ! -d "$APP_PATH" ]]; then BUILD_APP=1; fi
+fi
+if [[ "$BUILD_APP" -eq 1 ]]; then
+ # Replacing a running bundle can mix code versions and invalidate evidence.
+ if ps -axo comm= | /usr/bin/grep -Fq "$APP_PATH/Contents/MacOS/potassiumProvider"; then
+ echo "error: stop the installed Stability app before rebuilding" >&2
+ exit 2
+ fi
+ if [[ -e "$HOME/Library/Group Containers/group.net.weavee.potassiumProvider/StabilityRuns/current-run.json" ]]; then
+ echo "error: finish or explicitly recover the active Stability run before rebuilding" >&2
+ exit 2
+ fi
+ echo "Building the macOS Stability app..."
+ env -u INFOMANIAK_TOKEN xcodebuild build \
+ -project "$PROJECT_ROOT/potassiumProvider.xcodeproj" \
+ -scheme potassiumProvider-Stability \
+ -configuration Stability \
+ -destination 'platform=macOS' \
+ -allowProvisioningUpdates \
+ -derivedDataPath "$DERIVED_DATA_PATH"
+ BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/Stability/potassiumProvider.app"
+ /usr/bin/codesign --verify --deep --strict "$BUILT_APP_PATH"
+ mkdir -p "$(dirname "$APP_PATH")"
+ INSTALL_STAGING="$(mktemp -d "$(dirname "$APP_PATH")/.potassium-stability-install.XXXXXX")"
+ /usr/bin/ditto "$BUILT_APP_PATH" "$INSTALL_STAGING/candidate.bundle"
+ /usr/bin/codesign --verify --deep --strict "$INSTALL_STAGING/candidate.bundle"
+ trap 'if [[ ! -d "$APP_PATH" && -n "${BACKUP_PATH:-}" && -d "$BACKUP_PATH" ]]; then mv "$BACKUP_PATH" "$APP_PATH"; fi' EXIT
+ # Only the selected app's embedded processes are restarted, and only after
+ # excluding an active run. Never terminate fileproviderd or other providers.
+ while read -r PROVIDER_PID PROVIDER_COMMAND; do
+ case "$PROVIDER_COMMAND" in
+ "$APP_PATH/Contents/PlugIns/potassiumProviderFileProvider.appex/Contents/MacOS/potassiumProviderFileProvider"|"$APP_PATH/Contents/PlugIns/potassiumProviderActions.appex/Contents/MacOS/potassiumProviderActions")
+ kill -TERM "$PROVIDER_PID" 2>/dev/null || true
+ ;;
+ esac
+ done < <(ps -axo pid=,comm=)
+ if [[ -d "$APP_PATH" ]]; then
+ # Keep one recoverable backup outside LaunchServices' app directories.
+ BACKUP_ROOT="$HOME/Library/Application Support/potassiumProvider/StabilityBuildBackup"
+ mkdir -p "$BACKUP_ROOT"
+ BACKUP_PATH="$(mktemp -d "$BACKUP_ROOT/build.XXXXXX")/previous.bundle"
+ mv "$APP_PATH" "$BACKUP_PATH"
+ fi
+ mv "$INSTALL_STAGING/candidate.bundle" "$APP_PATH"
+ rmdir "$INSTALL_STAGING"
+ /usr/bin/codesign --verify --deep --strict "$APP_PATH"
+ for EXTENSION in potassiumProviderFileProvider potassiumProviderActions; do
+ if [[ -n "${BACKUP_PATH:-}" ]]; then
+ /usr/bin/pluginkit -r "$BACKUP_PATH/Contents/PlugIns/$EXTENSION.appex" 2>/dev/null || true
+ fi
+ /usr/bin/pluginkit -r "$BUILT_APP_PATH/Contents/PlugIns/$EXTENSION.appex" 2>/dev/null || true
+ /usr/bin/pluginkit -a "$APP_PATH/Contents/PlugIns/$EXTENSION.appex"
+ done
+ trap - EXIT
+fi
+
+EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/potassiumProvider"
+if [[ ! -x "$EXECUTABLE_PATH" ]]; then
+ echo "error: Stability app executable not found" >&2
+ exit 2
+fi
+
+COMMAND_ARGS=(--finder-stability "$MODE")
+if [[ "$INCLUDE_PERMANENT_DELETION" -eq 1 ]]; then COMMAND_ARGS+=(--include-permanent-deletion); fi
+if [[ -n "$EXTENSION_STATE" ]]; then COMMAND_ARGS+=(--extension-state "$EXTENSION_STATE"); fi
+if [[ -n "$CONFLICT_CASE" ]]; then COMMAND_ARGS+=(--case "$CONFLICT_CASE"); fi
+if [[ "$MODE" == "run" || "$MODE" == "provision" || "$MODE" == "conflicts" ]]; then
+ COMMAND_ARGS+=(--yes-live)
+fi
+if [[ "$MODE" == "recover" ]]; then
+ COMMAND_ARGS+=(--yes-recover)
+fi
+if [[ "$REQUEST_PERMISSIONS" -eq 1 ]]; then
+ COMMAND_ARGS+=(--request-permissions)
+fi
+
+# LaunchServices gives the signed app its own macOS privacy identity. Keeping
+# stdout in a local file also lets the app survive the invoking terminal closing.
+if [[ "$MODE" == "watch" || "$MODE" == "recover" ]]; then
+ exec env -u INFOMANIAK_TOKEN "$EXECUTABLE_PATH" "${COMMAND_ARGS[@]}"
+fi
+RUN_LOG="$(mktemp -t potassium-finder-stability)"
+RUN_ERROR_LOG="${RUN_LOG}.stderr"
+: > "$RUN_ERROR_LOG"
+chmod 600 "$RUN_LOG" "$RUN_ERROR_LOG"
+echo "Local runner log: $RUN_LOG"
+TAIL_PID=""
+trap 'if [[ -n "$TAIL_PID" ]]; then kill "$TAIL_PID" 2>/dev/null || true; fi' EXIT
+/usr/bin/tail -n +1 -f "$RUN_LOG" &
+TAIL_PID=$!
+set +e
+env -u INFOMANIAK_TOKEN /usr/bin/open -n -W --stdout "$RUN_LOG" --stderr "$RUN_ERROR_LOG" \
+ -a "$APP_PATH" --args "${COMMAND_ARGS[@]}"
+LAUNCH_STATUS=$?
+set -e
+if [[ "$LAUNCH_STATUS" -ne 0 ]]; then exit "$LAUNCH_STATUS"; fi
+# open returns launch status, not the app's exit code. Interpret only the exact
+# closed terminal messages emitted by FinderStabilityCommandResult.
+if /usr/bin/grep -q '^finder stability run: evidence bundle sealed$' "$RUN_LOG"; then exit 0; fi
+if /usr/bin/grep -q '^finder stability run: 15 scenarios passed; permanent deletion deferred; evidence bundle sealed; full acceptance incomplete$' "$RUN_LOG"; then exit 4; fi
+if /usr/bin/grep -q '^finder stability preflight: ready$' "$RUN_LOG"; then exit 0; fi
+if /usr/bin/grep -q '^finder stability checkpoint:' "$RUN_LOG"; then exit 3; fi
+if /usr/bin/grep -q '^finder stability rejected:' "$RUN_LOG"; then exit 2; fi
+exit 1