test/docs: freeze recovery credibility contract
This commit is contained in:
135
Docs/Execution/Recovery-Contract-2026-03-13.md
Normal file
135
Docs/Execution/Recovery-Contract-2026-03-13.md
Normal 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.
|
||||||
@@ -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.
|
||||||
@@ -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/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/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/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-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-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
|
- `Execution/Smart-Clean-Manual-Verification-2026-03-09.md` — local-machine fixture workflow for validating real Smart Clean execution and restore
|
||||||
|
|||||||
172
Docs/plans/2026-03-13-recovery-credibility.md
Normal file
172
Docs/plans/2026-03-13-recovery-credibility.md
Normal 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"
|
||||||
|
```
|
||||||
@@ -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 {
|
func testExecuteAppUninstallRemovesAppAndCreatesRecoveryEntry() async throws {
|
||||||
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
|
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
|
||||||
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: true)
|
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: true)
|
||||||
@@ -724,6 +819,84 @@ final class AtlasInfrastructureTests: XCTestCase {
|
|||||||
XCTAssertEqual(result.snapshot.taskRuns.first?.kind, .uninstallApp)
|
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 {
|
private func temporaryStateFileURL() -> URL {
|
||||||
FileManager.default.temporaryDirectory
|
FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||||
@@ -761,3 +934,35 @@ private struct FileBackedSmartCleanProvider: AtlasSmartCleanScanProviding {
|
|||||||
return AtlasSmartCleanScanResult(findings: [finding], summary: "Found 1 reclaimable item.")
|
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)"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user