4887ee5521b8

fix stale pagination flag and Swift 6 scan capture warning (v0.6.0)

time
2026-07-16 20:58 UTC · 2 months ago
machine
koh2
agent
minimax-m3
session
resolve audited findings
parent
8d1af4a87126
5 files Sources/Haven/Managers/LocalWallpapersManager.swift+14 −13Sources/Haven/UI/AppState.swift+1 −0Sources/Haven/UI/Views/LocalView.swift+2 −1Tests/HavenTests/AppStateTests.swift+14 −8Tests/HavenTests/ViewCoverageTests.swift+28 −28
Sources/Haven/Managers/LocalWallpapersManager.swift+14 −13
@@ -77,20 +77,21 @@7777       generation: requestGeneration,7878       imageExtensions: imageExtensions)7979 80-    Task.detached(priority: .userInitiated) { [weak self] in81-      let result = LocalWallpapersManager.performScan(request: snapshot)82-      await MainActor.run {83-        guard let self = self else { return }84-        guard self.scanGeneration == snapshot.generation else { return }85-        switch result {86-        case .success(let wallpapers):87-          self.wallpapers = wallpapers88-          self.errorMessage = nil89-        case .failure(let message):90-          self.errorMessage = message91-        }92-        self.isLoading = false80+    let scanTask = Task.detached(priority: .userInitiated) {81+      LocalWallpapersManager.performScan(request: snapshot)82+    }83+    Task { @MainActor [weak self] in84+      guard let self = self else { return }85+      let result = await scanTask.value86+      guard self.scanGeneration == snapshot.generation else { return }87+      switch result {88+      case .success(let wallpapers):89+        self.wallpapers = wallpapers90+        self.errorMessage = nil91+      case .failure(let message):92+        self.errorMessage = message9393       }94+      self.isLoading = false9495     }9596   }9697 

view file

Sources/Haven/UI/AppState.swift+1 −0
@@ -180,6 +180,7 @@180180 181181   func fetchWallpapers(reset: Bool = true, refreshing: Bool = false) async {182182     if reset {183+      isFetchingMore = false183184       searchGeneration &+= 1184185     }185186     let requestGeneration = searchGeneration

view file

Sources/Haven/UI/Views/LocalView.swift+2 −1
@@ -60,7 +60,8 @@6060     }6161     .alert(6262       "Delete Wallpaper?", isPresented: $showingDeleteConfirmation, presenting: wallpaperToDelete,63-      actions: deleteActions, message: deleteMessage)63+      actions: deleteActions, message: deleteMessage64+    )6465     .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)6566     .onAppear {6667       if appState.saveToFolderEnabled {

view file

Tests/HavenTests/AppStateTests.swift+14 −8
@@ -369,10 +369,12 @@369369     // the first response lands AFTER the second has already committed. The370370     // generation guard should drop the stale first response.371371     search.resume(372-      .success(([makeWallpaper(id: "newer")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),372+      .success(373+        ([makeWallpaper(id: "newer")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),373374       for: 2)374375     search.resume(375-      .success(([makeWallpaper(id: "stale")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),376+      .success(377+        ([makeWallpaper(id: "stale")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),376378       for: 1)377379 378380     _ = await (firstTask.value, secondTask.value)@@ -391,9 +393,11 @@391393     let firstReset = Task { await state.fetchWallpapers() }392394     await search.waitForPending(1)393395     search.resume(394-      .success((395-        [makeWallpaper(id: "first-reset")],396-        Meta(current_page: 1, last_page: 2, per_page: 24, total: 2))),396+      .success(397+        (398+          [makeWallpaper(id: "first-reset")],399+          Meta(current_page: 1, last_page: 2, per_page: 24, total: 2)400+        )),397401       for: 1)398402 399403     // Drain the first reset so isLoading is false, then trigger a pagination.@@ -409,9 +413,11 @@409413 410414     // Resolve new reset first → wallpapers replaced with [newer-reset].411415     search.resume(412-      .success((413-        [makeWallpaper(id: "newer-reset")],414-        Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),416+      .success(417+        (418+          [makeWallpaper(id: "newer-reset")],419+          Meta(current_page: 1, last_page: 1, per_page: 24, total: 1)420+        )),415421       for: 3)416422 417423     // Then resolve the (now stale) pagination response. Its return value

view file

Tests/HavenTests/ViewCoverageTests.swift+28 −28
@@ -236,37 +236,37 @@236236       ))237237   }238238 239-@Suite("Local wallpaper thumbnail downsampling", .serialized)240-@MainActor241-struct LocalWallpaperItemThumbnailTests {242-  private static let onePixelPNG = Data(243-    base64Encoded:244-      "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="245-  )!239+  @Suite("Local wallpaper thumbnail downsampling", .serialized)240+  @MainActor241+  struct LocalWallpaperItemThumbnailTests {242+    private static let onePixelPNG = Data(243+      base64Encoded:244+        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="245+    )!246246 247-  @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")248-  func thumbnailDecodeAndDownsample() throws {249-    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)250-    defer { try? FileManager.default.removeItem(at: root) }251-    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)252-    let png = root.appendingPathComponent("image.png")253-    try Self.onePixelPNG.write(to: png)247+    @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")248+    func thumbnailDecodeAndDownsample() throws {249+      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)250+      defer { try? FileManager.default.removeItem(at: root) }251+      try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)252+      let png = root.appendingPathComponent("image.png")253+      try Self.onePixelPNG.write(to: png)254254 255-    let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)256-    #expect(image != nil)257-    // ImageIO preserves the original pixel dimensions for tiny sources, so258-    // we only assert that decoding returned something usable.259-    #expect(image?.size.width ?? 0 >= 1)260-  }255+      let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)256+      #expect(image != nil)257+      // ImageIO preserves the original pixel dimensions for tiny sources, so258+      // we only assert that decoding returned something usable.259+      #expect(image?.size.width ?? 0 >= 1)260+    }261261 262-  @Test("makeThumbnail returns nil for a missing file rather than trapping")263-  func missingFileReturnsNil() {264-    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)265-    defer { try? FileManager.default.removeItem(at: root) }266-    try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)267-    let missing = root.appendingPathComponent("missing.png")262+    @Test("makeThumbnail returns nil for a missing file rather than trapping")263+    func missingFileReturnsNil() {264+      let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)265+      defer { try? FileManager.default.removeItem(at: root) }266+      try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)267+      let missing = root.appendingPathComponent("missing.png")268268 269-    #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)269+      #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)270+    }270271   }271-}272272 }

view file