Internal beta hardening: pnpm cleanup and recovery semantics

- update internal beta hardening docs and Smart Clean execution coverage for the 2026-03-16 hardening slice

- add pnpm store detection, execution fixtures, and regression coverage across adapters, infrastructure, and app model tests

- distinguish file-backed vs Atlas-only recovery messaging in History and restore summaries

- preserve completed Smart Clean recovery entries when later targets fail, and report failed findings explicitly instead of losing side effects
This commit is contained in:
zhukang
2026-03-12 23:19:18 +08:00
parent 534329b72f
commit 11405a4b55
13 changed files with 963 additions and 45 deletions

View File

@@ -75,7 +75,7 @@ final class AtlasAppModelTests: XCTestCase {
XCTAssertTrue(model.latestScanSummary.contains("2 reclaimable item"))
}
func testExecuteCurrentPlanMovesFindingsIntoRecovery() async throws {
func testExecuteCurrentPlanOnlyRecordsRecoveryForRealSideEffects() async throws {
let repository = makeRepository()
let worker = AtlasScaffoldWorkerService(
repository: repository,
@@ -88,7 +88,7 @@ final class AtlasAppModelTests: XCTestCase {
await model.runSmartCleanScan()
await model.executeCurrentPlan()
XCTAssertGreaterThan(model.snapshot.recoveryItems.count, initialRecoveryCount)
XCTAssertEqual(model.snapshot.recoveryItems.count, initialRecoveryCount)
XCTAssertEqual(model.snapshot.taskRuns.first?.kind, .executePlan)
XCTAssertGreaterThan(model.latestScanProgress, 0)
}

View File

@@ -1,6 +1,15 @@
# Internal Beta Hardening Week Plan
# Internal Beta Hardening Week — 2026-03-16
## Window
## Context
This document now serves two purposes:
- the **week plan** for the internal-beta hardening window
- the **execution record** for the hardening work completed on **2026-03-12** before the week formally opens
The path `Docs/Execution/Internal-Beta-Hardening-Week-2026-03-16.md` did not exist at the start of the hardening pass. One side of the rebase introduced the week plan, while the other introduced the initial execution record, so this merged version keeps both.
## Planned Window
- `2026-03-16` to `2026-03-20`
@@ -103,3 +112,183 @@ Do not expand into:
- Public release work remains blocked by missing Apple signing and notarization credentials
- Recovery still requires explicit wording discipline wherever physical restore is not yet guaranteed
## Outcome Snapshot
### Landed in this pass
- Added one new real Smart Clean execute target class for `~/Library/pnpm/store/*`.
- Added stronger worker-side truthfulness so Atlas only records recovery/history side effects when a real file move happened.
- Split History recovery messaging between:
- file-backed restore entries with `restoreMappings`
- Atlas-only recovery entries with no supported on-disk restore path
- Rebuilt the latest native artifacts and verified packaged install plus fresh-state launch.
### Current blocker
- Interactive bilingual UI automation on this machine is **blocked** by macOS Accessibility trust for the current terminal process.
- `./scripts/atlas/ui-automation-preflight.sh` reported `Accessibility trusted for current process: false` on **2026-03-12**.
This means the packaged-build install and fresh-state launch checks below are complete, but a full click-through clean-machine bilingual UI walkthrough still requires either:
- Accessibility trust to be granted on this Mac, or
- a separate clean machine for the final interactive pass.
## Packaged-Build Evidence
### Latest artifacts built on 2026-03-12
- App: `dist/native/Atlas for Mac.app`
- DMG: `dist/native/Atlas-for-Mac.dmg`
- PKG: `dist/native/Atlas-for-Mac.pkg`
- ZIP: `dist/native/Atlas-for-Mac.zip`
- Checksums: `dist/native/Atlas-for-Mac.sha256`
### Checksum record
```text
b85425649c5d781f234cdf1690ce01f330e3216d963cbf7d8f720a2e66611ffa Atlas-for-Mac.zip
2d5f480110d13f83c38e2296fafaa72617fc122d694d78c2c32c3a260f0ae110 Atlas-for-Mac.dmg
d71c45b0312ceeb045e390d851e246fe7f59e90961f2a482cfb21ee4f65d56ec Atlas-for-Mac.pkg
```
### Verified commands
- `./scripts/atlas/package-native.sh`**pass** on 2026-03-12
- `KEEP_INSTALLED_APP=1 ./scripts/atlas/verify-dmg-install.sh`**pass** on 2026-03-12
- `STATE_DIR="$PWD/.build/atlas-hardening-fresh-state-2026-03-12" ./scripts/atlas/verify-app-launch.sh`**pass** on 2026-03-12
### Fresh-state file evidence
- State directory: `.build/atlas-hardening-fresh-state-2026-03-12`
- New state file: `.build/atlas-hardening-fresh-state-2026-03-12/workspace-state.json`
- First-launch persisted language in that brand-new state file: `zh-Hans`
This is a machine-local fresh-state packaged-build verification, not a claim of having used a second physical clean Mac.
## Must-Deliver Status
### 1. Clean-machine bilingual QA
**Status:** `Partially complete / locally blocked`
Completed evidence:
- Packaged install path verified to `~/Applications/Atlas for Mac.app`
- Fresh-state packaged launch verified with a brand-new workspace-state directory
- Default first-launch language persisted as `zh-Hans`
- Language-switch persistence covered by app-model test evidence
- Smart Clean, Apps, and Recovery trust paths covered by package and app tests listed below
Remaining blocker:
- Interactive packaged-app UI walkthrough for first launch + bilingual control verification is blocked on local Accessibility trust
### 2. Fresh-state verification with latest packaged build
**Status:** `Complete`
- Latest packaged build created in `dist/native`
- DMG install verification passed
- Fresh-state launch verification passed against `.build/atlas-hardening-fresh-state-2026-03-12`
### 3. One concrete increment in real Smart Clean execute coverage
**Status:** `Complete`
New safe direct-trash target class added:
- `~/Library/pnpm/store/*`
Code path:
- Allowlist: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift`
- Parser/title recognition: `Packages/AtlasCoreAdapters/Sources/AtlasCoreAdapters/MoleSmartCleanAdapter.swift`
### 4. Stronger `scan -> execute -> rescan` contract evidence
**Status:** `Complete`
New and existing contract evidence now covers:
- existing cache-backed real path
- new pnpm-store real path
- stale-target handling where Atlas must not claim a physical move
### 5. History/completion surfaces only claim real side effects
**Status:** `Complete`
Behavior tightened so that:
- no recovery entry is created when the selected Smart Clean target is already absent on disk
- restore summaries distinguish file-backed restore from Atlas-only state restoration
- History callouts and restore button hints distinguish on-disk restore from Atlas-only restore
## Test Evidence
### Adapter + infrastructure
- `swift test --package-path Packages --filter MoleSmartCleanAdapterTests`**pass** on 2026-03-12
- `swift test --package-path Packages --filter AtlasInfrastructureTests`**pass** on 2026-03-12
Key tests:
- `testParseDetailedFindingsBuildsExecutableTargets`
- `testPnpmStoreTargetIsSupportedExecutionTarget`
- `testScanExecuteRescanRemovesExecutedPnpmStoreTargetFromRealResults`
- `testExecutePlanDoesNotCreateRecoveryEntryWhenTargetIsAlreadyGone`
- `testRestoreItemsStateOnlySummaryDoesNotClaimOnDiskRestore`
- `testRestoreRecoveryItemPhysicallyRestoresRealTargets`
### App-model coverage
- `swift test --package-path Apps --filter AtlasAppModelTests`**pass** on 2026-03-12
Key tests:
- `testSetLanguagePersistsThroughWorkerAndUpdatesLocalization`
- `testPreferredXPCWorkerPathFailsClosedWhenScanIsRejected`
- `testExecuteCurrentPlanExposesExplicitExecutionIssueWhenWorkerRejectsExecution`
- `testExecuteCurrentPlanOnlyRecordsRecoveryForRealSideEffects`
- `testRestoreRecoveryItemReturnsFindingToWorkspace`
## QA Matrix
| Area | Evidence | Status |
| --- | --- | --- |
| First launch | packaged app launch smoke with new state dir | Pass |
| Install path | DMG install validation to `~/Applications` | Pass |
| Default language | fresh packaged state file persisted `zh-Hans` | Pass |
| Language switching | app-model persistence test; UI click-through still blocked locally | Partial |
| Smart Clean execute | package tests + real file-backed contract tests | Pass |
| Apps | app-model and infrastructure uninstall/recovery tests | Pass |
| History / Recovery | file-backed vs Atlas-only summary/copy split + restore tests | Pass |
## Copy Guardrails After This Pass
Do keep saying:
- `Smart Clean only claims real cleanup for supported targets it actually moved.`
- `History distinguishes on-disk restore from Atlas-only restoration.`
- `Recovery can only claim physical return when a supported restore mapping exists.`
Do not say:
- `Every recoverable item returns to disk.`
- `Smart Clean moved an item` when the file was already absent.
- `Restore succeeded on disk` for Atlas-only recovery records.
## Files Changed for Hardening
- `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift`
- `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- `Packages/AtlasCoreAdapters/Sources/AtlasCoreAdapters/MoleSmartCleanAdapter.swift`
- `Packages/AtlasCoreAdapters/Tests/AtlasCoreAdaptersTests/MoleSmartCleanAdapterTests.swift`
- `Packages/AtlasFeaturesHistory/Sources/AtlasFeaturesHistory/HistoryFeatureView.swift`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings`
- `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings`
- `Apps/AtlasApp/Tests/AtlasAppTests/AtlasAppModelTests.swift`
- `scripts/atlas/smart-clean-manual-fixtures.sh`
- `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md`
- `Docs/Execution/Smart-Clean-Manual-Verification-2026-03-09.md`

View File

@@ -31,6 +31,7 @@ These user-owned targets can be moved to Trash directly by the worker when they
- `~/Library/Suggestions/*`
- `~/Library/Messages/Caches/*`
- `~/Library/Developer/Xcode/DerivedData/*`
- `~/Library/pnpm/store/*`
- `~/.npm/*`
- `~/.npm_cache/*`
- `~/.oh-my-zsh/cache/*`
@@ -117,6 +118,11 @@ That means:
- the item can reappear in Atlas UI state
- the underlying file may not be physically restored on disk
The History surface now needs to reflect this split explicitly:
- file-backed recovery entries can claim on-disk return only when `restoreMappings` exist
- Atlas-only recovery entries must describe themselves as workspace-state restoration, not physical file restoration
## Product Messaging Guidance
Use these statements consistently in user-facing communication:

View File

@@ -28,6 +28,7 @@ The helper creates disposable fixtures under these locations:
- `~/Library/Logs/AtlasExecutionFixturesLogs`
- `~/Library/Developer/Xcode/DerivedData/AtlasExecutionFixturesDerivedData`
- `~/Library/Caches/AtlasExecutionFixturesPycache`
- `~/Library/pnpm/store/v3/files/AtlasExecutionFixturesPnpm`
These locations are chosen because the current Smart Clean implementation can execute and restore them for real.
@@ -41,7 +42,7 @@ These locations are chosen because the current Smart Clean implementation can ex
Expected:
- The script prints the created roots and files.
- `status` shows non-zero size under all four fixture roots.
- `status` shows non-zero size under all five fixture roots.
### 2. Confirm upstream dry-run sees the fixtures
@@ -54,6 +55,7 @@ Expected:
- `~/Library/Caches`
- `~/Library/Logs`
- `~/Library/Developer/Xcode/DerivedData`
- `~/Library/pnpm/store`
- The fixture helper `status` output gives you the exact on-disk paths to compare before and after execution.
### 3. Run Smart Clean scan in the app

View File

@@ -0,0 +1,166 @@
# Internal Beta Hardening Week Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make the internal-beta candidate truthful for the 2026-03-16 hardening week by adding one new real Smart Clean execution path, tightening history/recovery claims, and capturing packaged-build QA evidence.
**Architecture:** Keep the existing real scan → worker execute → recovery-mapping model, but extend the Smart Clean execution allowlist for one high-value safe target class that the upstream Mole runtime already exports. Tighten worker summaries and History UI copy so Atlas only claims physical side effects when a real file move or restore mapping exists, then document packaged-build fresh-state verification in a dedicated execution note.
**Tech Stack:** Swift Package Manager, SwiftUI, XCTest, shell packaging scripts, Xcode build/package flow.
---
### Task 1: Add One New Safe Smart Clean Target Class
**Files:**
- Modify: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift`
- Modify: `Packages/AtlasCoreAdapters/Sources/AtlasCoreAdapters/MoleSmartCleanAdapter.swift`
- Test: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
- Test: `Packages/AtlasCoreAdapters/Tests/AtlasCoreAdaptersTests/MoleSmartCleanAdapterTests.swift`
**Step 1: Write the failing adapter and support tests**
Add tests for a `~/Library/pnpm/store/...` target so the repo proves:
- the detailed scan parser exposes a recognizable pnpm finding with structured `targetPaths`
- the worker allowlist treats `~/Library/pnpm/store/*` as a directly trashable safe target
- a file-backed `scan -> execute -> rescan` flow works for that target class
**Step 2: Run the focused tests to verify they fail**
Run:
```bash
swift test --package-path Packages --filter MoleSmartCleanAdapterTests
swift test --package-path Packages --filter AtlasInfrastructureTests
```
Expected:
- the pnpm target is not yet recognized or supported
**Step 3: Add the minimal implementation**
Implement:
- `~/Library/pnpm/store/*` in `AtlasSmartCleanExecutionSupport.isDirectlyTrashable`
- a readable title mapping such as `pnpm store` in `MoleSmartCleanAdapter.makeDetailedTitle`
**Step 4: Re-run the focused tests**
Run the same package tests and confirm the pnpm-target coverage passes.
**Step 5: Commit**
```bash
git add Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift Packages/AtlasCoreAdapters/Sources/AtlasCoreAdapters/MoleSmartCleanAdapter.swift Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift Packages/AtlasCoreAdapters/Tests/AtlasCoreAdaptersTests/MoleSmartCleanAdapterTests.swift
git commit -m "feat: support pnpm store smart clean execution"
```
### Task 2: Make History and Completion Claims Match Real Side Effects
**Files:**
- Modify: `Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift`
- Modify: `Packages/AtlasFeaturesHistory/Sources/AtlasFeaturesHistory/HistoryFeatureView.swift`
- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings`
- Modify: `Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings`
- Test: `Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift`
**Step 1: Write the failing truthfulness tests**
Add tests that prove:
- executing a supported plan whose file is already gone does not create a recovery item that implies Atlas moved it
- restore summaries distinguish physical restore from Atlas-state-only restore records
**Step 2: Run the focused infrastructure tests to verify failure**
Run:
```bash
swift test --package-path Packages --filter AtlasInfrastructureTests
```
Expected:
- current summaries and recovery bookkeeping still overclaim physical side effects
**Step 3: Implement the worker-side fix**
Update the scaffold worker so that:
- only findings with real `RecoveryPathMapping` entries create recovery items and count as “moved”
- missing-on-disk findings are cleared without being counted as physical execution
- restore summaries explicitly distinguish on-disk restore from Atlas-only state restoration
**Step 4: Implement the History UI copy split**
Update `HistoryFeatureView` and localization keys so recovery callouts and button hints reflect:
- supported file-backed restore paths
- Atlas-only restore records with no physical restore path
**Step 5: Re-run the focused tests**
Run the package tests again and confirm the truthfulness assertions pass.
**Step 6: Commit**
```bash
git add Packages/AtlasInfrastructure/Sources/AtlasInfrastructure/AtlasInfrastructure.swift Packages/AtlasFeaturesHistory/Sources/AtlasFeaturesHistory/HistoryFeatureView.swift Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings Packages/AtlasInfrastructure/Tests/AtlasInfrastructureTests/AtlasInfrastructureTests.swift
git commit -m "fix: keep history claims aligned with real side effects"
```
### Task 3: Capture Hardening-Week QA and Packaged-Build Evidence
**Files:**
- Create: `Docs/Execution/Internal-Beta-Hardening-Week-2026-03-16.md`
- Modify: `Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md`
- Modify: `Docs/Execution/Smart-Clean-Manual-Verification-2026-03-09.md`
- Modify: `scripts/atlas/smart-clean-manual-fixtures.sh`
**Step 1: Update the manual fixture coverage**
Add the new safe target class to the fixture script and manual verification doc so QA can create disposable evidence for the new real execution path.
**Step 2: Build the latest packaged app**
Run:
```bash
./scripts/atlas/package-native.sh
```
Expected:
- latest `.app`, `.dmg`, and `.pkg` artifacts land in `dist/native`
**Step 3: Verify install and fresh-state launch**
Run:
```bash
KEEP_INSTALLED_APP=1 ./scripts/atlas/verify-dmg-install.sh
STATE_DIR="$PWD/.build/atlas-hardening-fresh-state" ./scripts/atlas/verify-app-launch.sh
```
Expected:
- packaged install succeeds
- packaged app launches against a brand-new workspace-state directory
**Step 4: Document bilingual QA and contract evidence**
Create `Docs/Execution/Internal-Beta-Hardening-Week-2026-03-16.md` with:
- clean-machine bilingual QA checklist/results for first launch, language switching, install path, Smart Clean, Apps, and History/Recovery
- fresh-state packaged-build verification notes and exact artifact paths
- test evidence for the new safe target class and `scan -> execute -> rescan`
- explicit copy constraints that prevent restore/execution overclaiming
**Step 5: Commit**
```bash
git add Docs/Execution/Internal-Beta-Hardening-Week-2026-03-16.md Docs/Execution/Smart-Clean-Execution-Coverage-2026-03-09.md Docs/Execution/Smart-Clean-Manual-Verification-2026-03-09.md scripts/atlas/smart-clean-manual-fixtures.sh dist/native
git commit -m "docs: capture internal beta hardening evidence"
```

View File

@@ -151,6 +151,7 @@ public struct MoleSmartCleanAdapter: AtlasSmartCleanScanProviding {
if path.contains("component_crx_cache") { return "Chrome component cache" }
if path.contains("googleupdater") { return "Google Updater cache" }
if path.contains("deriveddata") { return "Xcode DerivedData" }
if path.contains("/library/pnpm/store") { return "pnpm store" }
if path.contains("/__pycache__") || last == "__pycache__" { return "Python bytecode cache" }
if path.contains("/.next/cache") { return "Next.js build cache" }
if path.contains("/.npm/") || path.hasSuffix("/.npm") || path.contains("_cacache") { return "npm cache" }

View File

@@ -33,11 +33,13 @@ final class MoleSmartCleanAdapterTests: XCTestCase {
Developer tools /Users/test/Library/Developer/Xcode/DerivedData/ProjectA 1024
Developer tools /Users/test/Library/Developer/Xcode/DerivedData/ProjectB 2048
Browsers /Users/test/Library/Caches/Google/Chrome/Default/Cache_Data 512
Developer tools /Users/test/Library/pnpm/store/v3/files/atlas-fixture/package.tgz 256
""".write(to: fileURL, atomically: true, encoding: .utf8)
let findings = MoleSmartCleanAdapter.parseDetailedFindings(from: fileURL)
XCTAssertTrue(findings.contains(where: { $0.title == "Xcode DerivedData" && ($0.targetPaths?.count ?? 0) == 2 }))
XCTAssertTrue(findings.contains(where: { $0.title == "Chrome cache" && ($0.targetPaths?.first?.contains("Chrome/Default") ?? false) }))
XCTAssertTrue(findings.contains(where: { $0.title == "pnpm store" && ($0.targetPaths?.first?.contains("/Library/pnpm/store") ?? false) }))
}
}

View File

@@ -122,8 +122,20 @@
"infrastructure.execute.summary.clean.one" = "Moved 1 Smart Clean item into recovery.";
"infrastructure.execute.summary.clean.other" = "Moved %d Smart Clean items into recovery.";
"infrastructure.execute.summary.clean.mixed" = "Moved %d Smart Clean items into recovery; %d advanced items still need review.";
"infrastructure.execute.summary.clean.none" = "No selected Smart Clean items were moved on disk.";
"infrastructure.execute.summary.clean.stale.one" = "1 selected Smart Clean item was already gone on disk and was cleared from the current scan.";
"infrastructure.execute.summary.clean.stale.other" = "%d selected Smart Clean items were already gone on disk and were cleared from the current scan.";
"infrastructure.execute.summary.clean.review.one" = "1 selected item remains review-only in this build.";
"infrastructure.execute.summary.clean.review.other" = "%d selected items remain review-only in this build.";
"infrastructure.execute.summary.clean.failed.one" = "1 selected Smart Clean item could not be moved and remains in the current scan.";
"infrastructure.execute.summary.clean.failed.other" = "%d selected Smart Clean items could not be moved and remain in the current scan.";
"infrastructure.restore.summary.one" = "Restored 1 item back into the workspace.";
"infrastructure.restore.summary.other" = "Restored %d items back into the workspace.";
"infrastructure.restore.summary.none" = "No recovery items were restored.";
"infrastructure.restore.summary.disk.one" = "Restored 1 recovery item to its original on-disk path.";
"infrastructure.restore.summary.disk.other" = "Restored %d recovery items to their original on-disk paths.";
"infrastructure.restore.summary.state.one" = "Re-added 1 recovery item to Atlas only because no on-disk restore path was available.";
"infrastructure.restore.summary.state.other" = "Re-added %d recovery items to Atlas only because no on-disk restore paths were available.";
"infrastructure.apps.loaded.one" = "Loaded 1 app footprint.";
"infrastructure.apps.loaded.other" = "Loaded %d app footprints.";
"infrastructure.apps.preview.summary" = "Generated an uninstall plan for %@.";
@@ -465,9 +477,21 @@
"history.detail.recovery.callout.available.detail" = "Restore it while the retention window remains open. On-disk return is available only when Atlas has a supported restore path for this item.";
"history.detail.recovery.callout.expiring.title" = "Restore soon if you still need this";
"history.detail.recovery.callout.expiring.detail" = "This recovery item is close to the end of its retention window.";
"history.detail.recovery.callout.available.fileBacked.title" = "This item can return to its original path";
"history.detail.recovery.callout.available.fileBacked.detail" = "Atlas recorded a supported restore path for this item, so restoring it should move it back on disk while the retention window remains open.";
"history.detail.recovery.callout.expiring.fileBacked.title" = "Restore soon if you still need this on disk";
"history.detail.recovery.callout.expiring.fileBacked.detail" = "This recovery item still has an on-disk restore path, but its retention window is close to expiring.";
"history.detail.recovery.callout.available.stateOnly.title" = "This item only restores inside Atlas";
"history.detail.recovery.callout.available.stateOnly.detail" = "Atlas can re-add this record to its workspace state, but this item does not currently have a supported on-disk restore path.";
"history.detail.recovery.callout.expiring.stateOnly.title" = "Restore soon if you still need this Atlas record";
"history.detail.recovery.callout.expiring.stateOnly.detail" = "This Atlas-only recovery record is close to the end of its retention window and will not move a file back on disk.";
"history.restore.action" = "Restore";
"history.restore.action.stateOnly" = "Restore in Atlas";
"history.restore.running" = "Restoring…";
"history.restore.running.stateOnly" = "Restoring in Atlas…";
"history.restore.hint" = "Restores this item while its recovery window is still open. On-disk return is available only for supported file-backed items.";
"history.restore.hint.fileBacked" = "Restores this item while its recovery window is still open and moves it back to its original on-disk path when the supported restore mapping is still available.";
"history.restore.hint.stateOnly" = "Re-adds this item to Atlas while its recovery window is still open. This action does not claim an on-disk restore path.";
"history.run.footnote.finished" = "Started %@ • Finished %@";
"history.run.footnote.running" = "Started %@ • Still in progress";
"history.recovery.footnote.deleted" = "Deleted %@";

View File

@@ -122,8 +122,20 @@
"infrastructure.execute.summary.clean.one" = "已将 1 个智能清理项目移入恢复区。";
"infrastructure.execute.summary.clean.other" = "已将 %d 个智能清理项目移入恢复区。";
"infrastructure.execute.summary.clean.mixed" = "已将 %d 个智能清理项目移入恢复区;仍有 %d 个高级项目需复核。";
"infrastructure.execute.summary.clean.none" = "没有已选智能清理项目在磁盘上被移动。";
"infrastructure.execute.summary.clean.stale.one" = "有 1 个已选智能清理项目在磁盘上已经不存在Atlas 已将它从当前扫描结果中移除。";
"infrastructure.execute.summary.clean.stale.other" = "有 %d 个已选智能清理项目在磁盘上已经不存在Atlas 已将它们从当前扫描结果中移除。";
"infrastructure.execute.summary.clean.review.one" = "仍有 1 个已选项目在这个构建中只能复核,不能直接执行。";
"infrastructure.execute.summary.clean.review.other" = "仍有 %d 个已选项目在这个构建中只能复核,不能直接执行。";
"infrastructure.execute.summary.clean.failed.one" = "有 1 个已选智能清理项目未能移动,仍保留在当前扫描结果中。";
"infrastructure.execute.summary.clean.failed.other" = "有 %d 个已选智能清理项目未能移动,仍保留在当前扫描结果中。";
"infrastructure.restore.summary.one" = "已将 1 个项目恢复回工作区。";
"infrastructure.restore.summary.other" = "已将 %d 个项目恢复回工作区。";
"infrastructure.restore.summary.none" = "没有恢复任何项目。";
"infrastructure.restore.summary.disk.one" = "已将 1 个恢复项还原回原始磁盘路径。";
"infrastructure.restore.summary.disk.other" = "已将 %d 个恢复项还原回各自的原始磁盘路径。";
"infrastructure.restore.summary.state.one" = "由于没有可用的磁盘恢复路径Atlas 只将 1 个恢复项重新加入了应用内状态。";
"infrastructure.restore.summary.state.other" = "由于没有可用的磁盘恢复路径Atlas 只将 %d 个恢复项重新加入了应用内状态。";
"infrastructure.apps.loaded.one" = "已载入 1 个应用占用项。";
"infrastructure.apps.loaded.other" = "已载入 %d 个应用占用项。";
"infrastructure.apps.preview.summary" = "已为 %@ 生成卸载计划。";
@@ -465,9 +477,21 @@
"history.detail.recovery.callout.available.detail" = "只要保留窗口还在,你就可以恢复。只有当 Atlas 为该项目记录了受支持的恢复路径时,它才会真正回到磁盘。";
"history.detail.recovery.callout.expiring.title" = "如果还需要它,请尽快恢复";
"history.detail.recovery.callout.expiring.detail" = "这个恢复项已经接近保留窗口的结束时间。";
"history.detail.recovery.callout.available.fileBacked.title" = "这个项目可以回到原始路径";
"history.detail.recovery.callout.available.fileBacked.detail" = "Atlas 为这个项目记录了受支持的恢复路径,因此只要保留窗口仍然开放,恢复操作就应当把它移回磁盘上的原始位置。";
"history.detail.recovery.callout.expiring.fileBacked.title" = "如果你还需要它回到磁盘,请尽快恢复";
"history.detail.recovery.callout.expiring.fileBacked.detail" = "这个恢复项仍带有磁盘恢复路径,但它的保留窗口已经接近到期。";
"history.detail.recovery.callout.available.stateOnly.title" = "这个项目只会在 Atlas 内恢复";
"history.detail.recovery.callout.available.stateOnly.detail" = "Atlas 可以把这条记录重新加入工作区状态,但当前没有受支持的磁盘恢复路径。";
"history.detail.recovery.callout.expiring.stateOnly.title" = "如果你还需要这条 Atlas 记录,请尽快恢复";
"history.detail.recovery.callout.expiring.stateOnly.detail" = "这条仅限 Atlas 的恢复记录已经接近保留窗口结束时间,恢复操作不会把文件移回磁盘。";
"history.restore.action" = "恢复";
"history.restore.action.stateOnly" = "在 Atlas 中恢复";
"history.restore.running" = "正在恢复…";
"history.restore.running.stateOnly" = "正在 Atlas 中恢复…";
"history.restore.hint" = "只要恢复窗口仍然开放,就可以恢复这个项目。只有受支持的文件型项目才会真正回到磁盘。";
"history.restore.hint.fileBacked" = "只要恢复窗口仍然开放,并且受支持的恢复映射仍然可用,恢复操作就会把这个项目移回原始磁盘路径。";
"history.restore.hint.stateOnly" = "只要恢复窗口仍然开放,此操作会把这个项目重新加入 Atlas。它不声明存在磁盘恢复路径。";
"history.run.footnote.finished" = "开始于 %@ • 结束于 %@";
"history.run.footnote.running" = "开始于 %@ • 仍在进行中";
"history.recovery.footnote.deleted" = "删除于 %@";

View File

@@ -935,14 +935,10 @@ private struct HistoryRecoveryDetailView: View {
}
AtlasCallout(
title: item.isExpiringSoon
? AtlasL10n.string("history.detail.recovery.callout.expiring.title")
: AtlasL10n.string("history.detail.recovery.callout.available.title"),
detail: item.isExpiringSoon
? AtlasL10n.string("history.detail.recovery.callout.expiring.detail")
: AtlasL10n.string("history.detail.recovery.callout.available.detail"),
tone: item.isExpiringSoon ? .warning : .success,
systemImage: item.isExpiringSoon ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
title: recoveryCalloutTitle,
detail: recoveryCalloutDetail,
tone: recoveryCalloutTone,
systemImage: recoveryCalloutSystemImage
)
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
@@ -1028,13 +1024,71 @@ private struct HistoryRecoveryDetailView: View {
}
private var restoreButton: some View {
Button(isRestoring ? AtlasL10n.string("history.restore.running") : AtlasL10n.string("history.restore.action")) {
Button(isRestoring ? restoreRunningTitle : restoreActionTitle) {
onRestore()
}
.buttonStyle(.atlasPrimary)
.disabled(!canRestore)
.accessibilityIdentifier("history.restore.\(item.id.uuidString)")
.accessibilityHint(AtlasL10n.string("history.restore.hint"))
.accessibilityHint(restoreHint)
}
private var recoveryCalloutTitle: String {
switch (item.hasPhysicalRestorePath, item.isExpiringSoon) {
case (true, true):
return AtlasL10n.string("history.detail.recovery.callout.expiring.fileBacked.title")
case (true, false):
return AtlasL10n.string("history.detail.recovery.callout.available.fileBacked.title")
case (false, true):
return AtlasL10n.string("history.detail.recovery.callout.expiring.stateOnly.title")
case (false, false):
return AtlasL10n.string("history.detail.recovery.callout.available.stateOnly.title")
}
}
private var recoveryCalloutDetail: String {
switch (item.hasPhysicalRestorePath, item.isExpiringSoon) {
case (true, true):
return AtlasL10n.string("history.detail.recovery.callout.expiring.fileBacked.detail")
case (true, false):
return AtlasL10n.string("history.detail.recovery.callout.available.fileBacked.detail")
case (false, true):
return AtlasL10n.string("history.detail.recovery.callout.expiring.stateOnly.detail")
case (false, false):
return AtlasL10n.string("history.detail.recovery.callout.available.stateOnly.detail")
}
}
private var recoveryCalloutTone: AtlasTone {
if item.isExpiringSoon {
return .warning
}
return item.hasPhysicalRestorePath ? .success : .neutral
}
private var recoveryCalloutSystemImage: String {
if item.isExpiringSoon {
return "exclamationmark.triangle.fill"
}
return item.hasPhysicalRestorePath ? "checkmark.circle.fill" : "rectangle.stack.badge.person.crop"
}
private var restoreActionTitle: String {
item.hasPhysicalRestorePath
? AtlasL10n.string("history.restore.action")
: AtlasL10n.string("history.restore.action.stateOnly")
}
private var restoreRunningTitle: String {
item.hasPhysicalRestorePath
? AtlasL10n.string("history.restore.running")
: AtlasL10n.string("history.restore.running.stateOnly")
}
private var restoreHint: String {
item.hasPhysicalRestorePath
? AtlasL10n.string("history.restore.hint.fileBacked")
: AtlasL10n.string("history.restore.hint.stateOnly")
}
}
@@ -1057,6 +1111,10 @@ private extension TaskRun {
}
private extension RecoveryItem {
var hasPhysicalRestorePath: Bool {
!(restoreMappings ?? []).isEmpty
}
var isExpiringSoon: Bool {
guard let expiresAt else {
return false

View File

@@ -206,6 +206,7 @@ public enum AtlasSmartCleanExecutionSupport {
home + "/Library/Suggestions",
home + "/Library/Messages/Caches",
home + "/Library/Developer/Xcode/DerivedData",
home + "/Library/pnpm/store",
home + "/.npm",
home + "/.npm_cache",
home + "/.oh-my-zsh/cache",
@@ -598,10 +599,19 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
response: .accepted(task: AtlasTaskDescriptor(taskID: taskID, kind: .executePlan))
)
var restoreMappingsByFindingID: [UUID: [RecoveryPathMapping]] = [:]
var executionResult = SmartCleanExecutionResult()
if !executableFindings.isEmpty {
do {
restoreMappingsByFindingID = try await executeSmartCleanFindings(executableFindings)
executionResult = try await executeSmartCleanFindings(executableFindings)
} catch let failure as SmartCleanExecutionFailure {
executionResult = failure.result
if !allowStateOnlyCleanExecution && !executionResult.hasRecordedOutcome {
return rejectedResult(
for: request,
code: .executionUnavailable,
reason: failure.localizedDescription
)
}
} catch {
if !allowStateOnlyCleanExecution {
return rejectedResult(
@@ -613,27 +623,31 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
}
}
let recoveryItems = executableFindings.map { makeRecoveryItem(for: $0, deletedAt: Date(), restoreMappings: restoreMappingsByFindingID[$0.id]) }
let executedIDs = Set(executableFindings.map(\.id))
state.snapshot.findings.removeAll { executedIDs.contains($0.id) }
let physicallyExecutedFindings = executableFindings.filter {
!(executionResult.restoreMappingsByFindingID[$0.id] ?? []).isEmpty
}
let staleFindings = executableFindings.filter { executionResult.staleFindingIDs.contains($0.id) }
let failedFindings = executableFindings.filter { executionResult.failedFindingIDs.contains($0.id) }
let recoveryItems = physicallyExecutedFindings.map {
makeRecoveryItem(for: $0, deletedAt: Date(), restoreMappings: executionResult.restoreMappingsByFindingID[$0.id])
}
let removedFindingIDs = Set(physicallyExecutedFindings.map(\.id)).union(staleFindings.map(\.id))
state.snapshot.findings.removeAll { removedFindingIDs.contains($0.id) }
state.snapshot.recoveryItems.insert(contentsOf: recoveryItems, at: 0)
recalculateReclaimableSpace()
state.currentPlan = makePreviewPlan(findingIDs: state.snapshot.findings.map(\.id))
let executedCount = executableFindings.count
let summary = skippedCount == 0
? AtlasL10n.string(executedCount == 1 ? "infrastructure.execute.summary.clean.one" : "infrastructure.execute.summary.clean.other", language: state.settings.language, executedCount)
: AtlasL10n.string(
"infrastructure.execute.summary.clean.mixed",
language: state.settings.language,
executedCount,
skippedCount
)
let summary = smartCleanExecutionSummary(
executedCount: physicallyExecutedFindings.count,
staleCount: staleFindings.count,
reviewOnlyCount: skippedCount,
failedCount: failedFindings.count
)
let completedRun = TaskRun(
id: taskID,
kind: .executePlan,
status: .completed,
status: failedFindings.isEmpty ? .completed : .failed,
summary: summary,
startedAt: request.issuedAt,
finishedAt: Date()
@@ -642,6 +656,9 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
state.snapshot.taskRuns.insert(completedRun, at: 0)
await persistState(context: "execute Smart Clean plan")
let events = progressEvents(taskID: taskID, total: 3) + [AtlasEventEnvelope(event: .taskFinished(completedRun))]
if let failureReason = executionResult.failureReason {
await auditStore.append("Smart Clean execution recorded partial failure: \(failureReason)")
}
await auditStore.append("Executed Smart Clean plan \(planID.uuidString)")
return AtlasWorkerCommandResult(request: request, response: response, events: events, snapshot: state.snapshot)
}
@@ -657,10 +674,14 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
)
}
var physicalRestoreCount = 0
var atlasOnlyRestoreCount = 0
for item in itemsToRestore {
if let restoreMappings = item.restoreMappings, !restoreMappings.isEmpty {
do {
try await restoreRecoveryMappings(restoreMappings)
physicalRestoreCount += 1
} catch {
return rejectedResult(
for: request,
@@ -668,6 +689,8 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
reason: error.localizedDescription
)
}
} else {
atlasOnlyRestoreCount += 1
}
switch item.payload {
@@ -692,7 +715,7 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
id: taskID,
kind: .restore,
status: .completed,
summary: "Restored \(itemsToRestore.count) recovery item\(itemsToRestore.count == 1 ? "" : "s").",
summary: restoreSummary(physicalRestoreCount: physicalRestoreCount, atlasOnlyRestoreCount: atlasOnlyRestoreCount),
startedAt: request.issuedAt,
finishedAt: Date()
)
@@ -853,22 +876,54 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
}
}
private func executeSmartCleanFindings(_ findings: [Finding]) async throws -> [UUID: [RecoveryPathMapping]] {
var mappingsByFindingID: [UUID: [RecoveryPathMapping]] = [:]
private func executeSmartCleanFindings(_ findings: [Finding]) async throws -> SmartCleanExecutionResult {
var result = SmartCleanExecutionResult()
for finding in findings {
let targetPaths = Array(Set(finding.targetPaths ?? [])).sorted()
guard !targetPaths.isEmpty else {
throw AtlasWorkspaceRepositoryError.writeFailed("Smart Clean finding is missing executable targets.")
result.failedFindingIDs.insert(finding.id)
result.failureReason = result.failureReason ?? "Smart Clean finding is missing executable targets."
throw SmartCleanExecutionFailure(result: result)
}
var mappings: [RecoveryPathMapping] = []
for targetPath in targetPaths {
if let mapping = try await trashSmartCleanTarget(at: targetPath) {
mappings.append(mapping)
do {
let preparedTargetPaths = try prepareSmartCleanTargetPaths(targetPaths)
var mappings: [RecoveryPathMapping] = []
for targetPath in preparedTargetPaths {
if let mapping = try await trashSmartCleanTarget(at: targetPath) {
mappings.append(mapping)
}
}
if mappings.isEmpty {
result.staleFindingIDs.insert(finding.id)
} else {
result.restoreMappingsByFindingID[finding.id] = mappings
}
} catch {
result.failedFindingIDs.insert(finding.id)
result.failureReason = result.failureReason ?? error.localizedDescription
throw SmartCleanExecutionFailure(result: result)
}
mappingsByFindingID[finding.id] = mappings
}
return mappingsByFindingID
return result
}
private func prepareSmartCleanTargetPaths(_ targetPaths: [String]) throws -> [String] {
var preparedTargetPaths: [String] = []
for targetPath in targetPaths {
let targetURL = URL(fileURLWithPath: targetPath).resolvingSymlinksInPath()
guard FileManager.default.fileExists(atPath: targetURL.path) else {
continue
}
if shouldUseHelperForSmartCleanTarget(targetURL) {
guard helperExecutor != nil else {
throw AtlasWorkspaceRepositoryError.writeFailed("Bundled helper unavailable for Smart Clean target: \(targetURL.path)")
}
} else if !isDirectlyTrashableSmartCleanTarget(targetURL) {
throw AtlasWorkspaceRepositoryError.writeFailed("Smart Clean target is outside the supported execution allowlist: \(targetURL.path)")
}
preparedTargetPaths.append(targetURL.path)
}
return preparedTargetPaths
}
private func trashSmartCleanTarget(at targetPath: String) async throws -> RecoveryPathMapping? {
@@ -943,6 +998,86 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
AtlasSmartCleanExecutionSupport.isDirectlyTrashable(targetURL)
}
private func smartCleanExecutionSummary(executedCount: Int, staleCount: Int, reviewOnlyCount: Int, failedCount: Int) -> String {
var clauses: [String] = []
if executedCount > 0 {
clauses.append(
AtlasL10n.string(
executedCount == 1 ? "infrastructure.execute.summary.clean.one" : "infrastructure.execute.summary.clean.other",
language: state.settings.language,
executedCount
)
)
}
if staleCount > 0 {
clauses.append(
AtlasL10n.string(
staleCount == 1 ? "infrastructure.execute.summary.clean.stale.one" : "infrastructure.execute.summary.clean.stale.other",
language: state.settings.language,
staleCount
)
)
}
if reviewOnlyCount > 0 {
clauses.append(
AtlasL10n.string(
reviewOnlyCount == 1 ? "infrastructure.execute.summary.clean.review.one" : "infrastructure.execute.summary.clean.review.other",
language: state.settings.language,
reviewOnlyCount
)
)
}
if failedCount > 0 {
clauses.append(
AtlasL10n.string(
failedCount == 1 ? "infrastructure.execute.summary.clean.failed.one" : "infrastructure.execute.summary.clean.failed.other",
language: state.settings.language,
failedCount
)
)
}
guard !clauses.isEmpty else {
return AtlasL10n.string("infrastructure.execute.summary.clean.none", language: state.settings.language)
}
return clauses.joined(separator: " ")
}
private func restoreSummary(physicalRestoreCount: Int, atlasOnlyRestoreCount: Int) -> String {
var clauses: [String] = []
if physicalRestoreCount > 0 {
clauses.append(
AtlasL10n.string(
physicalRestoreCount == 1 ? "infrastructure.restore.summary.disk.one" : "infrastructure.restore.summary.disk.other",
language: state.settings.language,
physicalRestoreCount
)
)
}
if atlasOnlyRestoreCount > 0 {
clauses.append(
AtlasL10n.string(
atlasOnlyRestoreCount == 1 ? "infrastructure.restore.summary.state.one" : "infrastructure.restore.summary.state.other",
language: state.settings.language,
atlasOnlyRestoreCount
)
)
}
guard !clauses.isEmpty else {
return AtlasL10n.string("infrastructure.restore.summary.none", language: state.settings.language)
}
return clauses.joined(separator: " ")
}
private func recoveryExpiryDate(from deletedAt: Date) -> Date {
deletedAt.addingTimeInterval(TimeInterval(state.settings.recoveryRetentionDays * 86_400))
@@ -1088,6 +1223,25 @@ public actor AtlasScaffoldWorkerService: AtlasWorkerServing {
}
}
private struct SmartCleanExecutionResult {
var restoreMappingsByFindingID: [UUID: [RecoveryPathMapping]] = [:]
var staleFindingIDs: Set<UUID> = []
var failedFindingIDs: Set<UUID> = []
var failureReason: String?
var hasRecordedOutcome: Bool {
!restoreMappingsByFindingID.isEmpty || !staleFindingIDs.isEmpty
}
}
private struct SmartCleanExecutionFailure: LocalizedError {
let result: SmartCleanExecutionResult
var errorDescription: String? {
result.failureReason
}
}
import AtlasProtocol
import Foundation
@@ -1224,4 +1378,3 @@ public actor AtlasPrivilegedHelperClient: AtlasPrivilegedActionExecuting {
return [appHelper, xpcHelper]
}
}

View File

@@ -102,6 +102,88 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(restoreResult.snapshot.recoveryItems.count, 0)
}
func testExecutePlanPreservesCompletedRecoveryEntriesWhenLaterFindingFailsInStateOnlyMode() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let fileManager = FileManager.default
let home = fileManager.homeDirectoryForCurrentUser
let cacheDirectory = home.appendingPathComponent("Library/Caches/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
let cacheFile = cacheDirectory.appendingPathComponent("sample.cache")
try Data("cache".utf8).write(to: cacheFile)
let helperDirectory = home.appendingPathComponent("Applications/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: helperDirectory, withIntermediateDirectories: true)
let helperFile = helperDirectory.appendingPathComponent("HelperRequired.app")
try Data("helper".utf8).write(to: helperFile)
addTeardownBlock {
try? FileManager.default.removeItem(at: helperDirectory)
}
let supportedFinding = Finding(
id: UUID(),
title: "Sample cache",
detail: cacheFile.path,
bytes: 5,
risk: .safe,
category: "Developer tools",
targetPaths: [cacheFile.path]
)
let helperRequiredFinding = Finding(
id: UUID(),
title: "Helper required cleanup",
detail: helperFile.path,
bytes: 7,
risk: .safe,
category: "Developer tools",
targetPaths: [helperFile.path]
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 12,
findings: [supportedFinding, helperRequiredFinding],
apps: [],
taskRuns: [],
recoveryItems: [],
permissions: [],
healthSnapshot: nil
),
currentPlan: ActionPlan(
title: "Review 2 selected findings",
items: [
ActionItem(id: supportedFinding.id, title: "Move Sample cache to Trash", detail: supportedFinding.detail, kind: .removeCache, recoverable: true),
ActionItem(id: helperRequiredFinding.id, title: "Move Helper required cleanup to Trash", detail: helperRequiredFinding.detail, kind: .removeCache, recoverable: true),
],
estimatedBytes: 12
),
settings: AtlasScaffoldWorkspace.state().settings
)
_ = try repository.saveState(state)
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: true)
let result = try await worker.submit(AtlasRequestEnvelope(command: .executePlan(planID: state.currentPlan.id)))
if case let .accepted(task) = result.response.response {
XCTAssertEqual(task.kind, .executePlan)
} else {
XCTFail("Expected accepted execute-plan response")
}
XCTAssertFalse(fileManager.fileExists(atPath: cacheFile.path))
XCTAssertTrue(fileManager.fileExists(atPath: helperFile.path))
XCTAssertEqual(result.snapshot.recoveryItems.count, 1)
XCTAssertEqual(result.snapshot.recoveryItems.first?.title, supportedFinding.title)
XCTAssertEqual(result.snapshot.findings.map(\.id), [helperRequiredFinding.id])
XCTAssertEqual(result.snapshot.taskRuns.first?.status, .failed)
XCTAssertEqual(
result.snapshot.taskRuns.first?.summary,
[
AtlasL10n.string("infrastructure.execute.summary.clean.one", language: state.settings.language, 1),
AtlasL10n.string("infrastructure.execute.summary.clean.failed.one", language: state.settings.language, 1),
].joined(separator: " ")
)
}
func testStartScanRejectsWhenProviderFailsWithoutFallback() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let worker = AtlasScaffoldWorkerService(
@@ -222,6 +304,59 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(result.snapshot.recoveryItems.count, initialState.snapshot.recoveryItems.count)
}
func testExecutePlanRejectsWhenFailClosedExecutionFailsBeforeAnySideEffect() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let fileManager = FileManager.default
let helperDirectory = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Applications/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try fileManager.createDirectory(at: helperDirectory, withIntermediateDirectories: true)
let helperFile = helperDirectory.appendingPathComponent("HelperRequired.app")
try Data("helper".utf8).write(to: helperFile)
addTeardownBlock {
try? FileManager.default.removeItem(at: helperDirectory)
}
let finding = Finding(
id: UUID(),
title: "Helper required cleanup",
detail: helperFile.path,
bytes: 7,
risk: .safe,
category: "Developer tools",
targetPaths: [helperFile.path]
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 7,
findings: [finding],
apps: [],
taskRuns: [],
recoveryItems: [],
permissions: [],
healthSnapshot: nil
),
currentPlan: ActionPlan(
title: "Review 1 selected finding",
items: [ActionItem(id: finding.id, title: "Move Helper required cleanup to Trash", detail: finding.detail, kind: .removeCache, recoverable: true)],
estimatedBytes: 7
),
settings: AtlasScaffoldWorkspace.state().settings
)
_ = try repository.saveState(state)
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: false)
let result = try await worker.submit(AtlasRequestEnvelope(command: .executePlan(planID: state.currentPlan.id)))
guard case let .rejected(code, reason) = result.response.response else {
return XCTFail("Expected rejected execute-plan response")
}
XCTAssertEqual(code, .executionUnavailable)
XCTAssertTrue(reason.contains("Bundled helper unavailable"))
XCTAssertTrue(fileManager.fileExists(atPath: helperFile.path))
XCTAssertEqual(result.snapshot.findings.map(\.id), [finding.id])
XCTAssertEqual(result.snapshot.recoveryItems.count, 0)
}
func testZcompdumpTargetIsSupportedExecutionTarget() {
let targetURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".zcompdump")
let finding = Finding(
@@ -238,6 +373,23 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isFindingExecutionSupported(finding))
}
func testPnpmStoreTargetIsSupportedExecutionTarget() {
let targetURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/pnpm/store/v3/files/atlas-fixture/package.tgz")
let finding = Finding(
id: UUID(),
title: "pnpm store",
detail: targetURL.path,
bytes: 1,
risk: .safe,
category: "Developer tools",
targetPaths: [targetURL.path]
)
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isSupportedExecutionTarget(targetURL))
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isFindingExecutionSupported(finding))
}
func testExecutePlanTrashesRealTargetsWhenAvailable() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser
@@ -315,6 +467,89 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(secondScan.snapshot.reclaimableSpaceBytes, 0)
}
func testScanExecuteRescanRemovesExecutedPnpmStoreTargetFromRealResults() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser
let targetDirectory = home.appendingPathComponent("Library/pnpm/store/v3/files/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: targetDirectory, withIntermediateDirectories: true)
let targetFile = targetDirectory.appendingPathComponent("package.tgz")
try Data("pnpm-cache".utf8).write(to: targetFile)
let provider = FileBackedSmartCleanProvider(targetFileURL: targetFile, title: "pnpm store")
let worker = AtlasScaffoldWorkerService(
repository: repository,
smartCleanScanProvider: provider,
allowProviderFailureFallback: false,
allowStateOnlyCleanExecution: false
)
let firstScan = try await worker.submit(AtlasRequestEnvelope(command: .startScan(taskID: UUID())))
XCTAssertEqual(firstScan.snapshot.findings.count, 1)
let planID = try XCTUnwrap(firstScan.previewPlan?.id)
let initialRecoveryCount = firstScan.snapshot.recoveryItems.count
let execute = try await worker.submit(AtlasRequestEnvelope(command: .executePlan(planID: planID)))
if case let .accepted(task) = execute.response.response {
XCTAssertEqual(task.kind, .executePlan)
} else {
XCTFail("Expected accepted execute-plan response")
}
XCTAssertFalse(FileManager.default.fileExists(atPath: targetFile.path))
XCTAssertEqual(execute.snapshot.recoveryItems.count, initialRecoveryCount + 1)
XCTAssertTrue(execute.snapshot.recoveryItems.contains(where: { $0.title == "pnpm store" }))
let secondScan = try await worker.submit(AtlasRequestEnvelope(command: .startScan(taskID: UUID())))
XCTAssertEqual(secondScan.snapshot.findings.count, 0)
XCTAssertEqual(secondScan.snapshot.reclaimableSpaceBytes, 0)
}
func testExecutePlanDoesNotCreateRecoveryEntryWhenTargetIsAlreadyGone() 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")
let finding = Finding(
id: UUID(),
title: "Stale cache",
detail: targetFile.path,
bytes: 5,
risk: .safe,
category: "Developer tools",
targetPaths: [targetFile.path]
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 5,
findings: [finding],
apps: [],
taskRuns: [],
recoveryItems: [],
permissions: [],
healthSnapshot: nil
),
currentPlan: ActionPlan(title: "Review 1 selected finding", items: [ActionItem(id: finding.id, title: "Move Stale cache to Trash", detail: finding.detail, kind: .removeCache, recoverable: true)], estimatedBytes: 5),
settings: AtlasScaffoldWorkspace.state().settings
)
_ = try repository.saveState(state)
let worker = AtlasScaffoldWorkerService(repository: repository, allowStateOnlyCleanExecution: false)
let result = try await worker.submit(AtlasRequestEnvelope(command: .executePlan(planID: state.currentPlan.id)))
if case let .accepted(task) = result.response.response {
XCTAssertEqual(task.kind, .executePlan)
} else {
XCTFail("Expected accepted execute-plan response")
}
XCTAssertEqual(result.snapshot.recoveryItems.count, 0)
XCTAssertEqual(result.snapshot.findings.count, 0)
XCTAssertEqual(
result.snapshot.taskRuns.first?.summary,
AtlasL10n.string("infrastructure.execute.summary.clean.stale.one", language: state.settings.language)
)
}
func testRestoreRecoveryItemPhysicallyRestoresRealTargets() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser
@@ -360,6 +595,60 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTFail("Expected accepted restore response")
}
XCTAssertTrue(FileManager.default.fileExists(atPath: targetFile.path))
XCTAssertEqual(
restore.snapshot.taskRuns.first?.summary,
AtlasL10n.string("infrastructure.restore.summary.disk.one", language: state.settings.language)
)
}
func testRestoreItemsStateOnlySummaryDoesNotClaimOnDiskRestore() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let finding = Finding(
id: UUID(),
title: "Atlas-only fixture",
detail: "State-only recovery item",
bytes: 5,
risk: .safe,
category: "Developer tools"
)
let recoveryItem = RecoveryItem(
id: UUID(),
title: finding.title,
detail: finding.detail,
originalPath: "~/Library/Caches/AtlasOnly",
bytes: 5,
deletedAt: Date(),
expiresAt: Date().addingTimeInterval(3600),
payload: .finding(finding),
restoreMappings: nil
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 0,
findings: [],
apps: [],
taskRuns: [],
recoveryItems: [recoveryItem],
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, allowStateOnlyCleanExecution: false)
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")
}
XCTAssertEqual(
restore.snapshot.taskRuns.first?.summary,
AtlasL10n.string("infrastructure.restore.summary.state.one", language: state.settings.language)
)
}
func testExecuteAppUninstallRemovesAppAndCreatesRecoveryEntry() async throws {
@@ -394,6 +683,8 @@ private struct FailingSmartCleanProvider: AtlasSmartCleanScanProviding {
private struct FileBackedSmartCleanProvider: AtlasSmartCleanScanProviding {
let targetFileURL: URL
var title: String = "Sample cache"
var category: String = "Developer tools"
func collectSmartCleanScan() async throws -> AtlasSmartCleanScanResult {
guard FileManager.default.fileExists(atPath: targetFileURL.path) else {
@@ -402,11 +693,11 @@ private struct FileBackedSmartCleanProvider: AtlasSmartCleanScanProviding {
let size = Int64((try? FileManager.default.attributesOfItem(atPath: targetFileURL.path)[.size] as? NSNumber)?.int64Value ?? 0)
let finding = Finding(
id: UUID(uuidString: "30000000-0000-0000-0000-000000000001") ?? UUID(),
title: "Sample cache",
title: title,
detail: targetFileURL.path,
bytes: size,
risk: .safe,
category: "Developer tools",
category: category,
targetPaths: [targetFileURL.path]
)
return AtlasSmartCleanScanResult(findings: [finding], summary: "Found 1 reclaimable item.")

View File

@@ -5,6 +5,7 @@ CACHE_ROOT="$HOME/Library/Caches/AtlasExecutionFixturesCache"
LOG_ROOT="$HOME/Library/Logs/AtlasExecutionFixturesLogs"
DERIVED_ROOT="$HOME/Library/Developer/Xcode/DerivedData/AtlasExecutionFixturesDerivedData"
PYCACHE_ROOT="$HOME/Library/Caches/AtlasExecutionFixturesPycache"
PNPM_ROOT="$HOME/Library/pnpm/store/v3/files/AtlasExecutionFixturesPnpm"
create_blob() {
local path="$1"
@@ -19,7 +20,7 @@ create_blob() {
print_status() {
local existing=false
for path in "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT"; do
for path in "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT"; do
if [[ -e "$path" ]]; then
existing=true
du -sh "$path"
@@ -40,6 +41,7 @@ create_fixtures() {
create_blob "$DERIVED_ROOT/Build/Logs/build-products.bin" 16
mkdir -p "$PYCACHE_ROOT/project/__pycache__"
create_blob "$PYCACHE_ROOT/project/__pycache__/sample.cpython-312.pyc" 4
create_blob "$PNPM_ROOT/package.tgz" 10
echo "Created Smart Clean manual fixtures:"
print_status
@@ -48,7 +50,7 @@ create_fixtures() {
}
cleanup_fixtures() {
rm -rf "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT"
rm -rf "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT"
echo "Removed Smart Clean manual fixtures."
}