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."])