ralph-loop[epic-a-to-d-mainline]: iteration 2
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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."])
|
||||
|
||||
Reference in New Issue
Block a user