ralph-loop[epic-a-to-d-mainline]: iteration 2

This commit is contained in:
zhukang
2026-03-23 17:35:05 +08:00
parent 0550568a2b
commit 78ecca3a15
15 changed files with 731 additions and 40 deletions

View File

@@ -391,29 +391,7 @@ final class AtlasAppModel: ObservableObject {
}
func refreshApps() async {
guard !isAppActionRunning else {
return
}
selection = .apps
isAppActionRunning = true
activePreviewAppID = nil
activeUninstallAppID = nil
currentAppPreview = nil
currentPreviewedAppID = nil
latestAppsSummary = AtlasL10n.string("model.apps.refreshing")
do {
let output = try await workspaceController.listApps()
withAnimation(.snappy(duration: 0.24)) {
snapshot = output.snapshot
latestAppsSummary = output.summary
}
} catch {
latestAppsSummary = error.localizedDescription
}
isAppActionRunning = false
await reloadAppsInventory(navigateToApps: true, resetPreview: true)
}
func previewAppUninstall(appID: UUID) async {
@@ -473,6 +451,8 @@ final class AtlasAppModel: ObservableObject {
return
}
let restoredItem = snapshot.recoveryItems.first(where: { $0.id == itemID })
let shouldRefreshAppsAfterRestore = restoredItem?.isAppPayload == true
restoringRecoveryItemID = itemID
do {
@@ -481,8 +461,21 @@ final class AtlasAppModel: ObservableObject {
snapshot = output.snapshot
latestScanSummary = output.summary
smartCleanExecutionIssue = nil
if shouldRefreshAppsAfterRestore {
currentAppPreview = nil
currentPreviewedAppID = nil
latestAppsSummary = output.summary
}
}
if shouldRefreshAppsAfterRestore {
await reloadAppsInventory(
navigateToApps: false,
resetPreview: true,
loadingSummary: output.summary
)
} else {
await refreshPlanPreview()
}
await refreshPlanPreview()
} catch {
let persistedState = repository.loadState()
withAnimation(.snappy(duration: 0.24)) {
@@ -605,6 +598,40 @@ final class AtlasAppModel: ObservableObject {
}
}
private func reloadAppsInventory(
navigateToApps: Bool,
resetPreview: Bool,
loadingSummary: String? = nil
) async {
guard !isAppActionRunning else {
return
}
if navigateToApps {
selection = .apps
}
isAppActionRunning = true
activePreviewAppID = nil
activeUninstallAppID = nil
if resetPreview {
currentAppPreview = nil
currentPreviewedAppID = nil
}
latestAppsSummary = loadingSummary ?? AtlasL10n.string("model.apps.refreshing")
do {
let output = try await workspaceController.listApps()
withAnimation(.snappy(duration: 0.24)) {
snapshot = output.snapshot
latestAppsSummary = output.summary
}
} catch {
latestAppsSummary = error.localizedDescription
}
isAppActionRunning = false
}
private func filter<Element>(
_ elements: [Element],
route: AtlasRoute,
@@ -627,6 +654,15 @@ final class AtlasAppModel: ObservableObject {
}
}
private extension RecoveryItem {
var isAppPayload: Bool {
if case .app = payload {
return true
}
return false
}
}
private extension AtlasAppModel {
func resolvedTargetPaths(for item: ActionItem) -> [String] {
if let targetPaths = item.targetPaths, !targetPaths.isEmpty {

View File

@@ -284,6 +284,72 @@ final class AtlasAppModelTests: XCTestCase {
XCTAssertNil(model.smartCleanExecutionIssue)
}
func testRestoreAppRecoveryItemClearsPreviewAndRefreshesInventoryWithoutLeavingHistory() async throws {
let repository = makeRepository()
let app = AppFootprint(
id: UUID(),
name: "Recovered App",
bundleIdentifier: "com.example.recovered",
bundlePath: "/Applications/Recovered App.app",
bytes: 2_048,
leftoverItems: 9
)
let recoveryItem = RecoveryItem(
id: UUID(),
title: app.name,
detail: "Restorable app payload",
originalPath: app.bundlePath,
bytes: app.bytes,
deletedAt: Date(),
expiresAt: Date().addingTimeInterval(3600),
payload: .app(
AtlasAppRecoveryPayload(
app: app,
uninstallEvidence: AtlasAppUninstallEvidence(
bundlePath: app.bundlePath,
bundleBytes: app.bytes,
reviewOnlyGroups: []
)
)
),
restoreMappings: nil
)
let state = AtlasWorkspaceState(
snapshot: AtlasWorkspaceSnapshot(
reclaimableSpaceBytes: 0,
findings: [],
apps: [app],
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,
appsInventoryProvider: RestoredInventoryProvider()
)
let model = AtlasAppModel(repository: repository, workerService: worker)
await model.previewAppUninstall(appID: app.id)
XCTAssertNotNil(model.currentAppPreview)
XCTAssertEqual(model.currentPreviewedAppID, app.id)
model.navigate(to: .history)
await model.restoreRecoveryItem(recoveryItem.id)
XCTAssertEqual(model.selection, .history)
XCTAssertNil(model.currentAppPreview)
XCTAssertNil(model.currentPreviewedAppID)
XCTAssertEqual(model.snapshot.apps.first?.leftoverItems, 1)
XCTAssertEqual(model.latestAppsSummary, AtlasL10n.string("application.apps.loaded.one"))
XCTAssertFalse(model.snapshot.recoveryItems.contains(where: { $0.id == recoveryItem.id }))
}
func testRestoreExpiredRecoveryItemReloadsPersistedState() async throws {
let baseDate = Date(timeIntervalSince1970: 1_710_000_000)
let clock = TestClock(now: baseDate)
@@ -496,6 +562,20 @@ private struct FakeInventoryProvider: AtlasAppInventoryProviding {
}
}
private struct RestoredInventoryProvider: AtlasAppInventoryProviding {
func collectInstalledApps() async throws -> [AppFootprint] {
[
AppFootprint(
name: "Recovered App",
bundleIdentifier: "com.example.recovered",
bundlePath: "/Applications/Recovered App.app",
bytes: 2_048,
leftoverItems: 1
)
]
}
}
private struct FailingSmartCleanProvider: AtlasSmartCleanScanProviding {
func collectSmartCleanScan() async throws -> AtlasSmartCleanScanResult {
throw NSError(domain: "AtlasAppModelTests", code: 1, userInfo: [NSLocalizedDescriptionKey: "Fixture scan failed."])

View File

@@ -42,6 +42,50 @@ public struct AtlasWorkspaceState: Codable, Hashable, Sendable {
}
}
public enum AtlasWorkspaceStateSchemaVersion {
public static let current = 1
}
public struct AtlasPersistedWorkspaceState: Codable, Hashable, Sendable {
public var schemaVersion: Int
public var savedAt: Date
public var snapshot: AtlasWorkspaceSnapshot
public var currentPlan: ActionPlan
public var settings: AtlasSettings
public init(
schemaVersion: Int = AtlasWorkspaceStateSchemaVersion.current,
savedAt: Date = Date(),
snapshot: AtlasWorkspaceSnapshot,
currentPlan: ActionPlan,
settings: AtlasSettings
) {
self.schemaVersion = schemaVersion
self.savedAt = savedAt
self.snapshot = snapshot
self.currentPlan = currentPlan
self.settings = settings
}
public init(
schemaVersion: Int = AtlasWorkspaceStateSchemaVersion.current,
savedAt: Date = Date(),
state: AtlasWorkspaceState
) {
self.init(
schemaVersion: schemaVersion,
savedAt: savedAt,
snapshot: state.snapshot,
currentPlan: state.currentPlan,
settings: state.settings
)
}
public var workspaceState: AtlasWorkspaceState {
AtlasWorkspaceState(snapshot: snapshot, currentPlan: currentPlan, settings: settings)
}
}
public enum AtlasScaffoldWorkspace {
public static func state(language: AtlasLanguage = AtlasL10n.currentLanguage) -> AtlasWorkspaceState {
let snapshot = AtlasWorkspaceSnapshot(

View File

@@ -355,14 +355,45 @@ public struct AtlasAppUninstallEvidence: Codable, Hashable, Sendable {
}
}
public enum AtlasRecoveryPayloadSchemaVersion {
public static let current = 1
}
public struct AtlasAppRecoveryPayload: Codable, Hashable, Sendable {
public var schemaVersion: Int
public var app: AppFootprint
public var uninstallEvidence: AtlasAppUninstallEvidence
public init(app: AppFootprint, uninstallEvidence: AtlasAppUninstallEvidence) {
public init(
schemaVersion: Int = AtlasRecoveryPayloadSchemaVersion.current,
app: AppFootprint,
uninstallEvidence: AtlasAppUninstallEvidence
) {
self.schemaVersion = schemaVersion
self.app = app
self.uninstallEvidence = uninstallEvidence
}
private enum CodingKeys: String, CodingKey {
case schemaVersion
case app
case uninstallEvidence
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.schemaVersion = try container.decodeIfPresent(Int.self, forKey: .schemaVersion)
?? AtlasRecoveryPayloadSchemaVersion.current
self.app = try container.decode(AppFootprint.self, forKey: .app)
self.uninstallEvidence = try container.decode(AtlasAppUninstallEvidence.self, forKey: .uninstallEvidence)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(schemaVersion, forKey: .schemaVersion)
try container.encode(app, forKey: .app)
try container.encode(uninstallEvidence, forKey: .uninstallEvidence)
}
}
public enum RecoveryPayload: Codable, Hashable, Sendable {

View File

@@ -494,6 +494,16 @@
"history.detail.recovery.deleted" = "Deleted";
"history.detail.recovery.window" = "Retention window";
"history.detail.recovery.window.open" = "Still recoverable";
"history.detail.recovery.evidence.title" = "Recorded Recovery Evidence";
"history.detail.recovery.evidence.subtitle" = "Atlas keeps the app payload, review-only groups, and restore path records together for this recovery item.";
"history.detail.recovery.evidence.payload" = "Recorded payload";
"history.detail.recovery.evidence.schema" = "Payload schema";
"history.detail.recovery.evidence.reviewGroups" = "Review-only groups";
"history.detail.recovery.evidence.reviewGroups.detail.one" = "%@ · 1 item";
"history.detail.recovery.evidence.reviewGroups.detail.other" = "%1$@ · %2$d items";
"history.detail.recovery.evidence.restorePaths" = "Restore path records";
"history.detail.recovery.evidence.restorePaths.detail.one" = "1 original-path to Trash-path record was saved for this item.";
"history.detail.recovery.evidence.restorePaths.detail.other" = "%d original-path to Trash-path records were saved for this item.";
"history.detail.recovery.reviewOnly.title" = "Review-Only Leftover Evidence";
"history.detail.recovery.reviewOnly.subtitle.one" = "1 related leftover item was recorded during uninstall.";
"history.detail.recovery.reviewOnly.subtitle.other" = "%d related leftover items were recorded during uninstall.";

View File

@@ -494,6 +494,16 @@
"history.detail.recovery.deleted" = "删除时间";
"history.detail.recovery.window" = "保留窗口";
"history.detail.recovery.window.open" = "仍可恢复";
"history.detail.recovery.evidence.title" = "恢复证据记录";
"history.detail.recovery.evidence.subtitle" = "Atlas 会把这条恢复项的应用载荷、仅供复核分组和恢复路径记录一起保留下来。";
"history.detail.recovery.evidence.payload" = "已记录载荷";
"history.detail.recovery.evidence.schema" = "载荷版本";
"history.detail.recovery.evidence.reviewGroups" = "仅供复核分组";
"history.detail.recovery.evidence.reviewGroups.detail.one" = "%@ · 1 项";
"history.detail.recovery.evidence.reviewGroups.detail.other" = "%1$@ · %2$d 项";
"history.detail.recovery.evidence.restorePaths" = "恢复路径记录";
"history.detail.recovery.evidence.restorePaths.detail.one" = "这条恢复项记录了 1 组原路径与废纸篓路径映射。";
"history.detail.recovery.evidence.restorePaths.detail.other" = "这条恢复项记录了 %d 组原路径与废纸篓路径映射。";
"history.detail.recovery.reviewOnly.title" = "仅供复核的残留证据";
"history.detail.recovery.reviewOnly.subtitle.one" = "这次卸载还记录了 1 个相关残留项目。";
"history.detail.recovery.reviewOnly.subtitle.other" = "这次卸载还记录了 %d 个相关残留项目。";

View File

@@ -65,6 +65,7 @@ final class AtlasDomainTests: XCTestCase {
return XCTFail("Expected app payload")
}
XCTAssertEqual(appPayload.schemaVersion, AtlasRecoveryPayloadSchemaVersion.current)
XCTAssertEqual(appPayload.app.name, "Legacy App")
XCTAssertEqual(appPayload.app.leftoverItems, 2)
XCTAssertEqual(appPayload.uninstallEvidence.reviewOnlyGroupCount, 0)

View File

@@ -981,6 +981,54 @@ private struct HistoryRecoveryDetailView: View {
.strokeBorder(AtlasColor.border, lineWidth: 1)
)
if appRecoveryPayload != nil || item.hasPhysicalRestorePath {
AtlasInfoCard(
title: AtlasL10n.string("history.detail.recovery.evidence.title"),
subtitle: AtlasL10n.string("history.detail.recovery.evidence.subtitle"),
tone: item.hasPhysicalRestorePath ? .success : .neutral
) {
VStack(alignment: .leading, spacing: AtlasSpacing.md) {
if let payload = appRecoveryPayload {
AtlasKeyValueRow(
title: AtlasL10n.string("history.detail.recovery.evidence.payload"),
value: payload.app.name,
detail: payload.app.bundleIdentifier
)
AtlasKeyValueRow(
title: AtlasL10n.string("history.detail.recovery.evidence.schema"),
value: "\(payload.schemaVersion)",
detail: item.hasPhysicalRestorePath
? AtlasL10n.string("history.detail.recovery.callout.available.fileBacked.title")
: AtlasL10n.string("history.detail.recovery.callout.available.stateOnly.title")
)
if payload.uninstallEvidence.reviewOnlyGroupCount > 0 {
AtlasKeyValueRow(
title: AtlasL10n.string("history.detail.recovery.evidence.reviewGroups"),
value: "\(payload.uninstallEvidence.reviewOnlyGroupCount)",
detail: appReviewOnlyGroupSummary(for: payload)
)
}
}
if let restoreMappings = item.restoreMappings, !restoreMappings.isEmpty {
AtlasMachineTextBlock(
title: AtlasL10n.string("history.detail.recovery.evidence.restorePaths"),
value: restoreMappings
.map { "\($0.originalPath)\n-> \($0.trashedPath)" }
.joined(separator: "\n\n"),
detail: AtlasL10n.string(
restoreMappings.count == 1
? "history.detail.recovery.evidence.restorePaths.detail.one"
: "history.detail.recovery.evidence.restorePaths.detail.other",
restoreMappings.count
)
)
}
}
}
}
if let payload = appRecoveryPayload, payload.uninstallEvidence.reviewOnlyItemCount > 0 {
AtlasInfoCard(
title: AtlasL10n.string("history.detail.recovery.reviewOnly.title"),
@@ -1128,6 +1176,32 @@ private struct HistoryRecoveryDetailView: View {
? AtlasL10n.string("history.restore.hint.fileBacked")
: AtlasL10n.string("history.restore.hint.stateOnly")
}
private func appReviewOnlyGroupSummary(for payload: AtlasAppRecoveryPayload) -> String {
payload.uninstallEvidence.reviewOnlyGroups.map { group in
let categoryLabel: String
switch group.category {
case .supportFiles:
categoryLabel = AtlasL10n.string("infrastructure.plan.uninstall.review.supportFiles")
case .caches:
categoryLabel = AtlasL10n.string("infrastructure.plan.uninstall.review.caches")
case .preferences:
categoryLabel = AtlasL10n.string("infrastructure.plan.uninstall.review.preferences")
case .logs:
categoryLabel = AtlasL10n.string("infrastructure.plan.uninstall.review.logs")
case .launchItems:
categoryLabel = AtlasL10n.string("infrastructure.plan.uninstall.review.launchItems")
}
return AtlasL10n.string(
group.items.count == 1
? "history.detail.recovery.evidence.reviewGroups.detail.one"
: "history.detail.recovery.evidence.reviewGroups.detail.other",
categoryLabel,
group.items.count
)
}
.joined(separator: "")
}
}
private extension TaskRun {

View File

@@ -189,10 +189,14 @@ public enum AtlasSmartCleanExecutionSupport {
home + "/Library/Logs",
home + "/Library/Suggestions",
home + "/Library/Messages/Caches",
home + "/Library/Developer/CoreSimulator/Caches",
home + "/Library/Developer/CoreSimulator/tmp",
home + "/Library/Developer/Xcode/DerivedData",
home + "/Library/pnpm/store",
home + "/.npm",
home + "/.npm_cache",
home + "/.gradle/caches",
home + "/.ivy2/cache",
home + "/.oh-my-zsh/cache",
home + "/.cache",
home + "/.pytest_cache",
@@ -220,6 +224,7 @@ public enum AtlasSmartCleanExecutionSupport {
home + "/.android/build-cache",
home + "/.android/cache",
home + "/.cache/swift-package-manager",
home + "/.swiftpm/cache",
home + "/.expo/expo-go",
home + "/.expo/android-apk-cache",
home + "/.expo/ios-simulator-app-cache",
@@ -319,9 +324,10 @@ public struct AtlasWorkspaceRepository: Sendable {
if FileManager.default.fileExists(atPath: stateFileURL.path) {
do {
let data = try Data(contentsOf: stateFileURL)
let decoded = try decoder.decode(AtlasWorkspaceState.self, from: data)
let decodedResult = try decodePersistedState(from: data, using: decoder)
let decoded = decodedResult.state
let normalized = normalizedState(decoded)
if normalized != decoded {
if decodedResult.usedLegacyShape || normalized != decoded {
_ = try? saveState(normalized)
}
return normalized
@@ -361,7 +367,12 @@ public struct AtlasWorkspaceRepository: Sendable {
let data: Data
do {
data = try encoder.encode(normalizedState)
data = try encoder.encode(
AtlasPersistedWorkspaceState(
savedAt: nowProvider(),
state: normalizedState
)
)
} catch {
throw AtlasWorkspaceRepositoryError.encodeFailed(error.localizedDescription)
}
@@ -403,6 +414,14 @@ public struct AtlasWorkspaceRepository: Sendable {
return normalized
}
private func decodePersistedState(from data: Data, using decoder: JSONDecoder) throws -> (state: AtlasWorkspaceState, usedLegacyShape: Bool) {
if let persisted = try? decoder.decode(AtlasPersistedWorkspaceState.self, from: data) {
return (persisted.workspaceState, false)
}
return (try decoder.decode(AtlasWorkspaceState.self, from: data), true)
}
private static var defaultStateFileURL: URL {
if let explicit = ProcessInfo.processInfo.environment["ATLAS_STATE_FILE"], !explicit.isEmpty {
return URL(fileURLWithPath: explicit)

View File

@@ -18,6 +18,35 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(loaded.snapshot.apps.count, state.snapshot.apps.count)
}
func testRepositoryPersistsVersionedWorkspaceEnvelope() throws {
let fileURL = temporaryStateFileURL()
let repository = AtlasWorkspaceRepository(stateFileURL: fileURL)
XCTAssertNoThrow(try repository.saveState(AtlasScaffoldWorkspace.state()))
let data = try Data(contentsOf: fileURL)
let persisted = try JSONDecoder().decode(AtlasPersistedWorkspaceState.self, from: data)
XCTAssertEqual(persisted.schemaVersion, AtlasWorkspaceStateSchemaVersion.current)
XCTAssertFalse(persisted.snapshot.apps.isEmpty)
}
func testRepositoryLoadsLegacyWorkspaceStateAndRewritesEnvelope() throws {
let fileURL = temporaryStateFileURL()
let legacyState = AtlasScaffoldWorkspace.state()
try FileManager.default.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try JSONEncoder().encode(legacyState).write(to: fileURL)
let repository = AtlasWorkspaceRepository(stateFileURL: fileURL)
let loaded = repository.loadState()
XCTAssertEqual(loaded.snapshot.apps.count, legacyState.snapshot.apps.count)
let migratedData = try Data(contentsOf: fileURL)
let persisted = try JSONDecoder().decode(AtlasPersistedWorkspaceState.self, from: migratedData)
XCTAssertEqual(persisted.schemaVersion, AtlasWorkspaceStateSchemaVersion.current)
XCTAssertEqual(persisted.snapshot.apps.count, legacyState.snapshot.apps.count)
}
func testRepositorySaveStateThrowsForInvalidParentURL() {
let repository = AtlasWorkspaceRepository(
@@ -439,6 +468,40 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isFindingExecutionSupported(finding))
}
func testGradleCacheTargetIsSupportedExecutionTarget() {
let targetURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gradle/caches/modules-2/files-2.1/atlas-fixture.bin")
let finding = Finding(
id: UUID(),
title: "Gradle cache",
detail: targetURL.path,
bytes: 1,
risk: .safe,
category: "Developer tools",
targetPaths: [targetURL.path]
)
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isSupportedExecutionTarget(targetURL))
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isFindingExecutionSupported(finding))
}
func testCoreSimulatorCacheTargetIsSupportedExecutionTarget() {
let targetURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Developer/CoreSimulator/Caches/atlas-fixture/device-cache.db")
let finding = Finding(
id: UUID(),
title: "CoreSimulator cache",
detail: targetURL.path,
bytes: 1,
risk: .safe,
category: "Developer tools",
targetPaths: [targetURL.path]
)
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isSupportedExecutionTarget(targetURL))
XCTAssertTrue(AtlasSmartCleanExecutionSupport.isFindingExecutionSupported(finding))
}
func testContainerCacheTargetIsSupportedExecutionTarget() {
let targetURL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Containers/com.example.preview/Data/Library/Caches/cache.db")
@@ -661,6 +724,39 @@ final class AtlasInfrastructureTests: XCTestCase {
XCTAssertEqual(secondScan.snapshot.reclaimableSpaceBytes, 0)
}
func testScanExecuteRescanRemovesExecutedGradleCacheTargetFromRealResults() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser
let targetDirectory = home.appendingPathComponent(".gradle/caches/AtlasExecutionTests/" + UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: targetDirectory, withIntermediateDirectories: true)
let targetFile = targetDirectory.appendingPathComponent("modules.bin")
try Data("gradle-cache".utf8).write(to: targetFile)
let provider = FileBackedSmartCleanProvider(targetFileURL: targetFile, title: "Gradle cache")
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 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))
let secondScan = try await worker.submit(AtlasRequestEnvelope(command: .startScan(taskID: UUID())))
XCTAssertEqual(secondScan.snapshot.findings.count, 0)
XCTAssertEqual(secondScan.snapshot.reclaimableSpaceBytes, 0)
}
func testScanExecuteRescanRemovesExecutedContainerCacheTargetFromRealResults() async throws {
let repository = AtlasWorkspaceRepository(stateFileURL: temporaryStateFileURL())
let home = FileManager.default.homeDirectoryForCurrentUser

View File

@@ -3,8 +3,59 @@ import AtlasDomain
import AtlasProtocol
import Foundation
public struct AtlasFixtureAppDescriptor: Hashable, Sendable {
public let scenario: String
public let appName: String
public let bundleIdentifier: String
public let hasLaunchAgent: Bool
public let expectedReviewOnlyCategories: [String]
public init(
scenario: String,
appName: String,
bundleIdentifier: String,
hasLaunchAgent: Bool,
expectedReviewOnlyCategories: [String]
) {
self.scenario = scenario
self.appName = appName
self.bundleIdentifier = bundleIdentifier
self.hasLaunchAgent = hasLaunchAgent
self.expectedReviewOnlyCategories = expectedReviewOnlyCategories
}
}
public enum AtlasTestingFixtures {
public static let workspace = AtlasScaffoldWorkspace.snapshot()
public static let request = AtlasRequestEnvelope(command: .inspectPermissions)
public static let firstFinding = AtlasScaffoldFixtures.findings.first
public static let appEvidenceFixtures: [AtlasFixtureAppDescriptor] = [
AtlasFixtureAppDescriptor(
scenario: "mainstream-gui",
appName: "Atlas Fixture Browser",
bundleIdentifier: "com.example.atlas.fixture.browser",
hasLaunchAgent: false,
expectedReviewOnlyCategories: ["support files", "caches", "preferences"]
),
AtlasFixtureAppDescriptor(
scenario: "developer-heavy",
appName: "Atlas Fixture Dev",
bundleIdentifier: "com.example.atlas.fixture.dev",
hasLaunchAgent: true,
expectedReviewOnlyCategories: ["support files", "caches", "logs", "launch items"]
),
AtlasFixtureAppDescriptor(
scenario: "sparse-leftovers",
appName: "Atlas Fixture Sparse",
bundleIdentifier: "com.example.atlas.fixture.sparse",
hasLaunchAgent: false,
expectedReviewOnlyCategories: ["saved state"]
),
]
public static let smartCleanSafeRoots: [String] = [
"~/Library/Developer/CoreSimulator/Caches",
"~/.gradle/caches",
"~/.ivy2/cache",
]
}

View File

@@ -0,0 +1,50 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURE_SCRIPT="$SCRIPT_DIR/apps-manual-fixtures.sh"
print_guide() {
cat <<'EOF'
Apps Evidence Acceptance Guide
1. Run Atlas and open the Apps screen.
2. Verify these fixture apps appear:
- Atlas Fixture Browser
- Atlas Fixture Dev
- Atlas Fixture Sparse
3. For each fixture app, build the uninstall plan and confirm:
- preview categories match the expected review-only evidence
- recoverable bundle removal is separated from review-only evidence
- observed paths are listed for review-only groups
4. Execute uninstall for Atlas Fixture Dev and confirm:
- completion summary mentions real removal and review-only categories
- History shows the uninstall with review-only evidence still informational
5. Restore the Atlas Fixture Dev recovery item and confirm:
- the app reappears in Apps after the restore-driven inventory refresh
- stale uninstall preview is cleared
- History shows restore-path evidence when supported
6. Re-run Apps refresh and verify leftover counts remain consistent with current disk state.
7. Clean up fixtures when done.
EOF
}
case "${1:-guide}" in
setup)
"$FIXTURE_SCRIPT" create
print_guide
;;
status)
"$FIXTURE_SCRIPT" status
;;
cleanup)
"$FIXTURE_SCRIPT" cleanup
;;
guide)
print_guide
;;
*)
echo "Usage: $0 [setup|status|cleanup|guide]" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,178 @@
#!/bin/bash
set -euo pipefail
APPS_ROOT="$HOME/Applications"
SUPPORT_ROOT="$HOME/Library/Application Support"
CACHE_ROOT="$HOME/Library/Caches"
PREFERENCES_ROOT="$HOME/Library/Preferences"
LOG_ROOT="$HOME/Library/Logs"
STATE_ROOT="$HOME/Library/Saved Application State"
LAUNCH_AGENTS_ROOT="$HOME/Library/LaunchAgents"
FIXTURES=(
"Atlas Fixture Browser|com.example.atlas.fixture.browser|support,caches,preferences"
"Atlas Fixture Dev|com.example.atlas.fixture.dev|support,caches,logs,launch"
"Atlas Fixture Sparse|com.example.atlas.fixture.sparse|saved-state"
)
create_blob() {
local path="$1"
local size_kb="$2"
mkdir -p "$(dirname "$path")"
if command -v mkfile > /dev/null 2>&1; then
mkfile "${size_kb}k" "$path"
else
dd if=/dev/zero of="$path" bs=1024 count="$size_kb" status=none
fi
}
write_info_plist() {
local plist_path="$1"
local bundle_id="$2"
local app_name="$3"
cat > "$plist_path" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>${bundle_id}</string>
<key>CFBundleName</key>
<string>${app_name}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>fixture</string>
</dict>
</plist>
EOF
}
create_app_bundle() {
local app_name="$1"
local bundle_id="$2"
local bundle_path="$APPS_ROOT/${app_name}.app"
local contents_path="$bundle_path/Contents"
local executable_path="$contents_path/MacOS/fixture"
local plist_path="$contents_path/Info.plist"
mkdir -p "$(dirname "$executable_path")"
printf '#!/bin/sh\nexit 0\n' > "$executable_path"
chmod +x "$executable_path"
write_info_plist "$plist_path" "$bundle_id" "$app_name"
create_blob "$bundle_path/Contents/Resources/fixture.dat" 128
}
create_leftovers() {
local app_name="$1"
local bundle_id="$2"
local categories="$3"
IFS=',' read -r -a parts <<< "$categories"
for category in "${parts[@]}"; do
case "$category" in
support)
create_blob "$SUPPORT_ROOT/$bundle_id/settings.json" 32
;;
caches)
create_blob "$CACHE_ROOT/$bundle_id/cache.bin" 48
;;
preferences)
create_blob "$PREFERENCES_ROOT/$bundle_id.plist" 4
;;
logs)
create_blob "$LOG_ROOT/$bundle_id/runtime.log" 24
;;
launch)
mkdir -p "$LAUNCH_AGENTS_ROOT"
cat > "$LAUNCH_AGENTS_ROOT/$bundle_id.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${bundle_id}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/true</string>
</array>
</dict>
</plist>
EOF
;;
saved-state)
create_blob "$STATE_ROOT/$bundle_id.savedState/data.data" 8
;;
esac
done
}
cleanup_fixture() {
local app_name="$1"
local bundle_id="$2"
rm -rf \
"$APPS_ROOT/${app_name}.app" \
"$SUPPORT_ROOT/$bundle_id" \
"$CACHE_ROOT/$bundle_id" \
"$PREFERENCES_ROOT/$bundle_id.plist" \
"$LOG_ROOT/$bundle_id" \
"$STATE_ROOT/$bundle_id.savedState" \
"$LAUNCH_AGENTS_ROOT/$bundle_id.plist"
}
print_status() {
local found=false
for fixture in "${FIXTURES[@]}"; do
IFS='|' read -r app_name bundle_id categories <<< "$fixture"
local bundle_path="$APPS_ROOT/${app_name}.app"
if [[ -d "$bundle_path" ]]; then
found=true
echo "Fixture: $app_name ($bundle_id)"
du -sh "$bundle_path" "$SUPPORT_ROOT/$bundle_id" "$CACHE_ROOT/$bundle_id" \
"$PREFERENCES_ROOT/$bundle_id.plist" "$LOG_ROOT/$bundle_id" \
"$STATE_ROOT/$bundle_id.savedState" "$LAUNCH_AGENTS_ROOT/$bundle_id.plist" 2> /dev/null || true
echo "Expected review-only categories: $categories"
echo ""
fi
done
if [[ "$found" == false ]]; then
echo "No Apps manual fixtures found."
fi
}
create_fixtures() {
for fixture in "${FIXTURES[@]}"; do
IFS='|' read -r app_name bundle_id categories <<< "$fixture"
cleanup_fixture "$app_name" "$bundle_id"
create_app_bundle "$app_name" "$bundle_id"
create_leftovers "$app_name" "$bundle_id" "$categories"
done
echo "Created Apps manual fixtures:"
print_status
}
cleanup_fixtures() {
for fixture in "${FIXTURES[@]}"; do
IFS='|' read -r app_name bundle_id _ <<< "$fixture"
cleanup_fixture "$app_name" "$bundle_id"
done
echo "Removed Apps manual fixtures."
}
case "${1:-create}" in
create)
create_fixtures
;;
status)
print_status
;;
cleanup)
cleanup_fixtures
;;
*)
echo "Usage: $0 [create|status|cleanup]" >&2
exit 1
;;
esac

View File

@@ -34,35 +34,40 @@ run_ui_acceptance() {
return 1
}
echo "[1/10] Shared package tests"
echo "[1/11] Shared package tests"
swift test --package-path Packages
echo "[2/10] App package tests"
echo "[2/11] App package tests"
swift test --package-path Apps
echo "[3/10] Worker and helper builds"
echo "[3/11] Worker and helper builds"
swift build --package-path XPC
swift test --package-path Helpers
swift build --package-path Testing
echo "[4/10] Native packaging"
echo "[4/11] Fixture automation scripts"
bash -n ./scripts/atlas/smart-clean-manual-fixtures.sh
bash -n ./scripts/atlas/apps-manual-fixtures.sh
bash -n ./scripts/atlas/apps-evidence-acceptance.sh
echo "[5/11] Native packaging"
./scripts/atlas/package-native.sh
echo "[5/10] Bundle structure verification"
echo "[6/11] Bundle structure verification"
./scripts/atlas/verify-bundle-contents.sh
echo "[6/10] DMG install verification"
echo "[7/11] DMG install verification"
KEEP_INSTALLED_APP=1 ./scripts/atlas/verify-dmg-install.sh
echo "[7/10] Installed app launch smoke"
echo "[8/11] Installed app launch smoke"
./scripts/atlas/verify-app-launch.sh
echo "[8/10] Native UI automation"
echo "[9/11] Native UI automation"
run_ui_acceptance
echo "[9/10] Signing preflight"
echo "[10/11] Signing preflight"
./scripts/atlas/signing-preflight.sh || true
echo "[10/10] Acceptance summary"
echo "[11/11] Acceptance summary"
echo "Artifacts available in dist/native"
ls -lah dist/native

View File

@@ -4,8 +4,11 @@ set -euo pipefail
CACHE_ROOT="$HOME/Library/Caches/AtlasExecutionFixturesCache"
LOG_ROOT="$HOME/Library/Logs/AtlasExecutionFixturesLogs"
DERIVED_ROOT="$HOME/Library/Developer/Xcode/DerivedData/AtlasExecutionFixturesDerivedData"
CORESIM_ROOT="$HOME/Library/Developer/CoreSimulator/Caches/AtlasExecutionFixturesCoreSimulator"
PYCACHE_ROOT="$HOME/Library/Caches/AtlasExecutionFixturesPycache"
PNPM_ROOT="$HOME/Library/pnpm/store/v3/files/AtlasExecutionFixturesPnpm"
GRADLE_ROOT="$HOME/.gradle/caches/AtlasExecutionFixturesGradle"
IVY_ROOT="$HOME/.ivy2/cache/AtlasExecutionFixturesIvy"
create_blob() {
local path="$1"
@@ -20,7 +23,7 @@ create_blob() {
print_status() {
local existing=false
for path in "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT"; do
for path in "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$CORESIM_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT" "$GRADLE_ROOT" "$IVY_ROOT"; do
if [[ -e "$path" ]]; then
existing=true
du -sh "$path"
@@ -39,9 +42,12 @@ create_fixtures() {
create_blob "$CACHE_ROOT/cache-b.bin" 12
create_blob "$LOG_ROOT/app.log" 8
create_blob "$DERIVED_ROOT/Build/Logs/build-products.bin" 16
create_blob "$CORESIM_ROOT/device-cache.db" 6
mkdir -p "$PYCACHE_ROOT/project/__pycache__"
create_blob "$PYCACHE_ROOT/project/__pycache__/sample.cpython-312.pyc" 4
create_blob "$PNPM_ROOT/package.tgz" 10
create_blob "$GRADLE_ROOT/modules.bin" 10
create_blob "$IVY_ROOT/artifact.bin" 6
echo "Created Smart Clean manual fixtures:"
print_status
@@ -50,7 +56,7 @@ create_fixtures() {
}
cleanup_fixtures() {
rm -rf "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT"
rm -rf "$CACHE_ROOT" "$LOG_ROOT" "$DERIVED_ROOT" "$CORESIM_ROOT" "$PYCACHE_ROOT" "$PNPM_ROOT" "$GRADLE_ROOT" "$IVY_ROOT"
echo "Removed Smart Clean manual fixtures."
}