1 Commits

Author SHA1 Message Date
zhukang
a5dd042a85 test/docs: freeze recovery credibility contract 2026-03-13 01:06:50 +08:00
5 changed files with 607 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
# Recovery Contract — 2026-03-13
## Goal
Freeze Atlas recovery semantics against the behavior that is actually shipped today.
## Scope
- `ATL-221` physical restore for file-backed recoverable actions where safe
- `ATL-222` restore validation on real file-backed test cases
- `ATL-223` README, in-app, and release-facing recovery claim audit
- `ATL-224` recovery contract and acceptance evidence freeze
## Canonical Contract
### 1. What a recovery item means
- Every recoverable destructive flow must produce a structured `RecoveryItem`.
- A `RecoveryItem` may carry `restoreMappings` that pair the original path with the actual path returned from Trash.
- `restoreMappings` are the only shipped proof that Atlas can claim an on-disk return path for that item.
### 2. When Atlas can claim physical restore
Atlas can claim physical on-disk restore only when all of the following are true:
- the recovery item still exists in Atlas history
- its retention window is still open
- the recovery item contains at least one `restoreMappings` entry
- the trashed source still exists on disk
- the original destination path does not already exist
- the required execution capability is available:
- direct move for supported user-trashable targets such as `~/Library/Caches/*` and `~/Library/pnpm/store/*`
- helper-backed restore for protected targets such as app bundles under `/Applications` or `~/Applications`
### 3. When Atlas restores state only
If a recovery item has no `restoreMappings`, Atlas may still restore Atlas workspace state by rehydrating the saved `Finding` or `AppFootprint` payload.
State-only restore means:
- the item reappears in Atlas UI state
- the action remains auditable in History
- Atlas does not claim the underlying file or bundle returned on disk
### 4. Failure behavior
Restore remains fail-closed. Atlas rejects the restore request instead of claiming success when:
- the trash source no longer exists
- the original destination already exists
- the target falls outside the supported direct/helper allowlist
- a required helper capability is unavailable
### 5. History and completion wording
- Disk-backed restores must use the disk-specific completion summary.
- State-only restores must use the Atlas-only completion summary.
- Mixed restore batches must report both clauses instead of collapsing everything into a physical-restore claim.
## Accepted Physical Restore Surface
### Direct restore paths
The currently proven direct restore surface is the same safe structured subset used by Smart Clean execution:
- `~/Library/Caches/*`
- `~/Library/pnpm/store/*`
- other targets explicitly allowed by `AtlasSmartCleanExecutionSupport.isDirectlyTrashable`
### Helper-backed restore paths
The currently proven helper-backed restore surface includes app bundles that require the privileged helper path:
- `/Applications/*.app`
- `~/Applications/*.app`
## Claim Audit
The following surfaces now match the frozen contract and must stay aligned:
- `README.md`
- `README.zh-CN.md`
- `Docs/Protocol.md`
- `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings`
No additional README or in-app narrowing is required in this slice because the shipped wording already distinguishes:
- supported on-disk restore
- Atlas-only state restoration
- fail-closed behavior for unsupported or unprovable actions
## Release-Note-Safe Wording
Future release notes must stay within these statements unless the restore surface expands and new evidence is added:
- `Recoverable items can be restored when a supported recovery path is available.`
- `Some recoverable items restore on disk, while older or unstructured records restore Atlas state only.`
- `Atlas only claims physical return when it recorded a supported restore path for that item.`
- `Unsupported or unavailable restore paths fail closed instead of being reported as restored.`
Avoid saying:
- `All recoverable items restore physically.`
- `History always returns deleted files to disk.`
- `Restore succeeded` when Atlas only rehydrated workspace state.
## Acceptance Evidence
### Automated proof points
- `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- `testRestoreRecoveryItemPhysicallyRestoresRealTargets`
- `testExecuteAppUninstallRestorePhysicallyRestoresAppBundle`
- `testRestoreItemsStateOnlySummaryDoesNotClaimOnDiskRestore`
- `testRestoreItemsMixedSummaryIncludesDiskAndStateOnlyClauses`
- `testScanExecuteRescanRemovesExecutedTargetFromRealResults`
- `testScanExecuteRescanRemovesExecutedPnpmStoreTargetFromRealResults`
- `Docs/Execution/Smart-Clean-Manual-Verification-2026-03-09.md`
- `Docs/Execution/Smart-Clean-QA-Checklist-2026-03-09.md`
### What the evidence proves
- direct-trash file-backed recovery physically returns a real file to its original path
- helper-backed app uninstall recovery physically returns a real app bundle to its original path
- Atlas-only recovery records do not overclaim on-disk restore
- mixed restore batches preserve truthful summaries when disk-backed and Atlas-only items are restored together
- supported file-backed targets still satisfy `scan -> execute -> rescan` credibility checks
## Remaining Limits
- Physical restore is intentionally partial, not universal.
- Older or unstructured recovery entries remain Atlas-state-only unless they carry `restoreMappings`.
- Broader restore scope must not ship without new allowlist review, automated coverage, and matching copy updates.

View File

@@ -0,0 +1,93 @@
# Recovery Credibility Gate Review
## Gate
- `Recovery Credibility`
## Review Date
- `2026-03-13`
## Scope Reviewed
- `ATL-221` implement physical restore for file-backed recoverable actions where safe
- `ATL-222` validate shipped restore behavior on real file-backed test cases
- `ATL-223` narrow README, in-app, and release-note recovery claims if needed
- `ATL-224` freeze recovery contract and acceptance evidence
- `ATL-225` recovery credibility gate review
## Readiness Checklist
- [x] Required P0 tasks complete
- [x] Docs updated
- [x] Risks reviewed
- [x] Open questions below threshold
- [x] Next-stage inputs available
## Evidence Reviewed
- `Docs/Protocol.md`
- `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md`
- `Docs/Execution/Smart-Clean-QA-Checklist-2026-03-09.md`
- `Docs/Execution/Smart-Clean-Manual-Verification-2026-03-09.md`
- `Docs/Execution/Recovery-Contract-2026-03-13.md`
- `README.md`
- `README.zh-CN.md`
- `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings`
## Automated Validation Summary
- `swift test --package-path Packages --filter AtlasInfrastructureTests` — pass
- `swift test --package-path Packages` — pass
## Gate Assessment
### ATL-221 Physical Restore Surface
- File-backed recovery items now restore physically when Atlas recorded `restoreMappings` from a real Trash move.
- Supported direct-trash targets restore back to their original on-disk path.
- Protected app-bundle targets restore through the helper-backed path instead of claiming an unproven direct move.
- Restore remains fail-closed when the source, destination, or capability contract is not satisfied.
### ATL-222 Shipped Restore Evidence
- Automated tests now cover both proven physical restore classes:
- direct-trash file-backed Smart Clean targets
- helper-backed app uninstall targets
- State-only recovery remains explicitly covered so Atlas does not regress into overclaiming physical restore.
- Mixed restore summaries are covered so a batch containing both kinds of items stays truthful.
### ATL-223 Claim Audit
- README and localized in-app strings already reflect the narrowed recovery promise.
- No new copy narrowing was required in this slice.
- This gate freezes a release-note-safe wording set in `Docs/Execution/Recovery-Contract-2026-03-13.md` so future release notes cannot overstate restore behavior.
### ATL-224 Contract Freeze
- The recovery contract is now explicit, evidence-backed, and tied to shipped protocol fields and worker behavior.
- The contract distinguishes physical restore from Atlas-only state rehydration and documents the exact failure conditions.
## Remaining Limits
- Physical restore is still partial and depends on supported `restoreMappings`.
- Older or unstructured recovery items still restore Atlas state only.
- Broader restore coverage, including additional protected or system-managed targets, must not be described as shipped until new allowlist and QA evidence exist.
## Decision
- `Pass with Conditions`
## Conditions
- Release-facing copy must continue to use the frozen wording in `Docs/Execution/Recovery-Contract-2026-03-13.md`.
- Any future restore-surface expansion must add automated proof for the new target class before copy is widened.
- Candidate-build QA should still rerun the manual restore checklist on packaged artifacts before external distribution.
## Follow-up Actions
- Reuse the frozen recovery contract in future release notes and internal beta notices.
- Add new restore targets only after allowlist review, helper-path review, and contract tests land together.
- Re-run packaged-app manual restore verification when signed distribution work resumes.

View File

@@ -30,6 +30,8 @@ This directory contains the working product, design, engineering, and compliance
- `Execution/Execution-Chain-Audit-2026-03-09.md` — end-to-end review of real vs scaffold execution paths and release-facing trust gaps
- `Execution/Implementation-Plan-ATL-201-202-205-2026-03-12.md` — implementation plan for internal-beta hardening tasks ATL-201, ATL-202, and ATL-205
- `Execution/Execution-Credibility-Gate-Review-2026-03-12.md` — gate review for ATL-211, ATL-212, and ATL-215 Smart Clean execution credibility work
- `Execution/Recovery-Contract-2026-03-13.md` — frozen recovery semantics, claim boundaries, and acceptance evidence for ATL-221 through ATL-224
- `Execution/Recovery-Credibility-Gate-Review-2026-03-13.md` — gate review for ATL-221 through ATL-225 recovery credibility work
- `Execution/Smart-Clean-Execution-Coverage-2026-03-09.md` — user-facing summary of what Smart Clean can execute for real today
- `Execution/Smart-Clean-QA-Checklist-2026-03-09.md` — QA checklist for scan, execute, rescan, and physical restore validation
- `Execution/Smart-Clean-Manual-Verification-2026-03-09.md` — local-machine fixture workflow for validating real Smart Clean execution and restore

View File

@@ -0,0 +1,172 @@
# Recovery Credibility Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Freeze Atlas recovery semantics against shipped behavior by adding missing restore coverage and publishing explicit acceptance evidence and a gate review for ATL-221 through ATL-225.
**Architecture:** The worker already supports restore mappings for file-backed recovery items and Atlas-only rehydration for older/state-only records. This slice should avoid widening restore scope; instead it should prove the current contract with focused automated tests, then freeze that contract in execution docs and a recovery gate review.
**Tech Stack:** Swift Package Manager, XCTest, Markdown docs
---
### Task 1: Add helper-backed app restore coverage
**Files:**
- Modify: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- Check: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift`
**Step 1: Write the failing test**
Add a test that:
- creates a fake installed app under `~/Applications/AtlasExecutionTests/...`
- injects a stub `AtlasPrivilegedActionExecuting`
- executes app uninstall
- restores the resulting recovery item
- asserts the app bundle returns to its original path and the restore summary uses the disk-backed wording
**Step 2: Run test to verify it fails**
Run: `swift test --package-path Packages --filter AtlasInfrastructureTests/testExecuteAppUninstallRestorePhysicallyRestoresAppBundle`
Expected: FAIL until the stub/helper-backed path is wired correctly in the test.
**Step 3: Write minimal implementation**
Implement only the test support needed:
- a stub helper executor that handles `.trashItems` and `.restoreItem`
- deterministic assertions for returned `restoreMappings`
**Step 4: Run test to verify it passes**
Run: `swift test --package-path Packages --filter AtlasInfrastructureTests/testExecuteAppUninstallRestorePhysicallyRestoresAppBundle`
Expected: PASS
**Step 5: Commit**
```bash
git add Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift
git commit -m "test: cover helper-backed app restore"
```
### Task 2: Add mixed recovery summary coverage
**Files:**
- Modify: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- Check: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift:1086`
**Step 1: Write the failing test**
Add a test that restores:
- one recovery item with `restoreMappings`
- one recovery item without `restoreMappings`
Assert the task summary contains both:
- disk restore wording
- Atlas-only restore wording
**Step 2: Run test to verify it fails**
Run: `swift test --package-path Packages --filter AtlasInfrastructureTests/testRestoreItemsMixedSummaryIncludesDiskAndStateOnlyClauses`
Expected: FAIL if the combined contract is not proven yet.
**Step 3: Write minimal implementation**
If needed, adjust only test fixtures or summary generation so mixed restores preserve both clauses without overstating physical restore.
**Step 4: Run test to verify it passes**
Run: `swift test --package-path Packages --filter AtlasInfrastructureTests/testRestoreItemsMixedSummaryIncludesDiskAndStateOnlyClauses`
Expected: PASS
**Step 5: Commit**
```bash
git add Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift
git commit -m "test: cover mixed recovery summaries"
```
### Task 3: Freeze recovery contract and evidence
**Files:**
- Create: `Docs/Execution/Recovery-Contract-2026-03-13.md`
- Create: `Docs/Execution/Recovery-Credibility-Gate-Review-2026-03-13.md`
- Modify: `Docs/README.md`
- Check: `Docs/Protocol.md`
- Check: `README.md`
- Check: `README.zh-CN.md`
- Check: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings`
- Check: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings`
**Step 1: Write the contract doc**
Document exactly what Atlas promises today:
- file-backed recovery physically restores only when `restoreMappings` exist
- Atlas-only recovery rehydrates workspace state without claiming on-disk return
- helper-backed restore is required for protected paths like app bundles
- restore fails closed when the trash source is gone, the destination already exists, or helper capability is unavailable
**Step 2: Write the evidence section**
Reference automated proof points:
- direct-trash cache restore test
- helper-backed app uninstall restore test
- mixed summary/state-only tests
- existing `scan -> execute -> rescan` coverage for supported targets
**Step 3: Write the gate review**
Mirror the existing execution gate format and record:
- scope reviewed (`ATL-221` to `ATL-225`)
- evidence reviewed
- automated validation summary
- remaining limits
- decision and follow-up conditions
**Step 4: Update docs index**
Add the new recovery contract and gate review docs to `Docs/README.md`.
**Step 5: Commit**
```bash
git add Docs/Execution/Recovery-Contract-2026-03-13.md Docs/Execution/Recovery-Credibility-Gate-Review-2026-03-13.md Docs/README.md
git commit -m "docs: freeze recovery contract and gate evidence"
```
### Task 4: Run focused validation
**Files:**
- Check: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- Check: `Docs/Execution/Recovery-Contract-2026-03-13.md`
- Check: `Docs/Execution/Recovery-Credibility-Gate-Review-2026-03-13.md`
**Step 1: Run targeted infrastructure tests**
Run: `swift test --package-path Packages --filter AtlasInfrastructureTests`
Expected: PASS
**Step 2: Run broader package tests**
Run: `swift test --package-path Packages`
Expected: PASS
**Step 3: Sanity-check docs claims**
Verify every new doc line matches one of:
- protocol contract
- localized UI copy
- automated test evidence
**Step 4: Summarize remaining limits**
Call out that:
- physical restore is still partial by design
- unsupported or older recovery items remain Atlas-state-only
- broader restore scope should not expand without new allowlist and QA evidence
**Step 5: Commit**
```bash
git add Docs/README.md Docs/Execution/Recovery-Contract-2026-03-13.md Docs/Execution/Recovery-Credibility-Gate-Review-2026-03-13.md Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift
git commit -m "chore: validate recovery credibility slice"
```

View File

@@ -709,6 +709,101 @@ final class AtlasInfrastructureTests: XCTestCase {
)
}
func testRestoreItemsMixedSummaryIncludesDiskAndStateOnlyClauses() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser
let targetDirectory = home.appendingPathComponent("Library/Caches/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: targetDirectory, withIntermediateDirectories: true)
let targetFile = targetDirectory.appendingPathComponent("sample.cache")
try Data("cache".utf8).write(to: targetFile)
var trashedURL: NSURL?
try FileManager.default.trashItem(at: targetFile, resultingItemURL: &trashedURL)
let trashedPath = try XCTUnwrap((trashedURL as URL?)?.path)
addTeardownBlock {
try? FileManager.default.removeItem(at: targetDirectory)
if let trashedURL {
try? FileManager.default.removeItem(at: trashedURL as URL)
}
}
let fileBackedFinding = Finding(
id: UUID(),
title: "Disk-backed fixture",
detail: targetFile.path,
bytes: 5,
risk: .safe,
category: "Developer tools",
targetPaths: [targetFile.path]
)
let stateOnlyFinding = Finding(
id: UUID(),
title: "Atlas-only fixture",
detail: "State-only recovery item",
bytes: 7,
risk: .safe,
category: "Developer tools"
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 0,
findings: [],
apps: [],
taskRuns: [],
recoveryItems: [
RecoveryItem(
id: UUID(),
title: fileBackedFinding.title,
detail: fileBackedFinding.detail,
originalPath: targetFile.path,
bytes: fileBackedFinding.bytes,
deletedAt: Date(),
expiresAt: Date().addingTimeInterval(3600),
payload: .finding(fileBackedFinding),
restoreMappings: [RecoveryPathMapping(originalPath: targetFile.path, trashedPath: trashedPath)]
),
RecoveryItem(
id: UUID(),
title: stateOnlyFinding.title,
detail: stateOnlyFinding.detail,
originalPath: "~/Library/Caches/AtlasOnly",
bytes: stateOnlyFinding.bytes,
deletedAt: Date(),
expiresAt: Date().addingTimeInterval(3600),
payload: .finding(stateOnlyFinding),
restoreMappings: nil
),
],
permissions: [],
healthSnapshot: nil
),
currentPlan: ActionPlan(title: "Review 0 selected findings", items: [], estimatedBytes: 0),
settings: AtlasScaffoldWorkspace.state().settings
)
_ = try repository.saveState(state)
let restoreItemIDs = state.snapshot.recoveryItems.map(\.id)
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: false)
let restore = try await worker.submit(AtlasRequestEnvelope(command: .restoreItems(taskID: UUID(), itemIDs: restoreItemIDs)))
if case let .accepted(task) = restore.response.response {
XCTAssertEqual(task.kind, .restore)
} else {
XCTFail("Expected accepted restore response")
}
XCTAssertTrue(FileManager.default.fileExists(atPath: targetFile.path))
XCTAssertTrue(restore.snapshot.findings.contains(where: { $0.id == fileBackedFinding.id }))
XCTAssertTrue(restore.snapshot.findings.contains(where: { $0.id == stateOnlyFinding.id }))
XCTAssertEqual(
restore.snapshot.taskRuns.first?.summary,
[
AtlasL10n.string("infrastructure.restore.summary.disk.one", language: state.settings.language),
AtlasL10n.string("infrastructure.restore.summary.state.one", language: state.settings.language),
].joined(separator: " ")
)
}
func testExecuteAppUninstallRemovesAppAndCreatesRecoveryEntry() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: true)
@@ -724,6 +819,84 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(result.snapshot.taskRuns.first?.kind, .uninstallApp)
}
func testExecuteAppUninstallRestorePhysicallyRestoresAppBundle() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let fileManager = FileManager.default
let appRoot = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Applications/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
let appBundleURL = appRoot.appendingPathComponent("Atlas Restore Test.app", isDirectory: true)
try fileManager.createDirectory(at: appBundleURL, withIntermediateDirectories: true)
let executableURL = appBundleURL.appendingPathComponent("Contents/MacOS/AtlasRestoreTest")
try fileManager.createDirectory(at: executableURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try Data("#!/bin/sh\nexit 0\n".utf8).write(to: executableURL)
addTeardownBlock {
try? FileManager.default.removeItem(at: appRoot)
}
let app = AppFootprint(
id: UUID(),
name: "Atlas Restore Test",
bundleIdentifier: "com.atlas.restore-test",
bundlePath: appBundleURL.path,
bytes: 17,
leftoverItems: 1
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: app.bytes,
findings: [],
apps: [app],
taskRuns: [],
recoveryItems: [],
permissions: [],
healthSnapshot: nil
),
currentPlan: ActionPlan(title: "Review 0 selected findings", items: [], estimatedBytes: 0),
settings: AtlasScaffoldWorkspace.state().settings
)
_ = try repository.saveState(state)
let worker = AtlasScaffoldWorkerService(
repository: repository,
helperExecutor: StubPrivilegedHelperExecutor(),
allowStateOnlyCleanExecution: false
)
let execute = try await worker.submit(
AtlasRequestEnvelope(command: .executeAppUninstall(appID: app.id))
)
if case let .accepted(task) = execute.response.response {
XCTAssertEqual(task.kind, .uninstallApp)
} else {
XCTFail("Expected accepted uninstall response")
}
XCTAssertFalse(fileManager.fileExists(atPath: appBundleURL.path))
XCTAssertFalse(execute.snapshot.apps.contains(where: { $0.id == app.id }))
let recoveryItem = try XCTUnwrap(execute.snapshot.recoveryItems.first)
XCTAssertEqual(recoveryItem.restoreMappings?.first?.originalPath, appBundleURL.path)
XCTAssertNotNil(recoveryItem.restoreMappings?.first?.trashedPath)
let restore = try await worker.submit(
AtlasRequestEnvelope(command: .restoreItems(taskID: UUID(), itemIDs: [recoveryItem.id]))
)
if case let .accepted(task) = restore.response.response {
XCTAssertEqual(task.kind, .restore)
} else {
XCTFail("Expected accepted restore response")
}
XCTAssertTrue(fileManager.fileExists(atPath: appBundleURL.path))
XCTAssertTrue(restore.snapshot.apps.contains(where: { $0.id == app.id }))
XCTAssertEqual(
restore.snapshot.taskRuns.first?.summary,
AtlasL10n.string("infrastructure.restore.summary.disk.one", language: state.settings.language)
)
}
private func temporaryStateFileURL() -> URL {
FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
@@ -761,3 +934,35 @@ private struct FileBackedSmartCleanProvider: AtlasSmartCleanScanProviding {
return AtlasSmartCleanScanResult(findings: [finding], summary: "Found 1 reclaimable item.")
}
}
private actor StubPrivilegedHelperExecutor: AtlasPrivilegedActionExecuting {
func perform(_ action: AtlasHelperAction) async throws -> AtlasHelperActionResult {
let fileManager = FileManager.default
let targetURL = URL(fileURLWithPath: action.targetPath)
switch action.kind {
case .trashItems:
var trashedURL: NSURL?
try fileManager.trashItem(at: targetURL, resultingItemURL: &trashedURL)
return AtlasHelperActionResult(
action: action,
success: true,
message: "Moved item to Trash.",
resolvedPath: (trashedURL as URL?)?.path
)
case .restoreItem:
let destinationPath = try XCTUnwrap(action.destinationPath)
let destinationURL = URL(fileURLWithPath: destinationPath)
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try fileManager.moveItem(at: targetURL, to: destinationURL)
return AtlasHelperActionResult(
action: action,
success: true,
message: "Restored item from Trash.",
resolvedPath: destinationURL.path
)
case .removeLaunchService, .repairOwnership:
throw NSError(domain: "StubPrivilegedHelperExecutor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unsupported test helper action: \(action.kind)"])
}
}
}