ManagerTests.swift

27.2 KB · raw

1import AppKit2import Foundation3import Testing45@testable import Haven67private final class ManagerURLProtocol: URLProtocol, @unchecked Sendable {8  override class func canInit(with request: URLRequest) -> Bool { true }9  override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }10  override func startLoading() {11    let path = request.url!.path12    let statusCode = path.contains("fail-http") ? 500 : 20013    let data: Data14    if path.contains("image.png") {15      data = onePixelPNG16    } else if path.contains("invalid.png") {17      data = Data("not an image".utf8)18    } else {19      data = Data("wallpaper".utf8)20    }21    let response = HTTPURLResponse(22      url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!23    client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)24    client?.urlProtocol(self, didLoad: data)25    client?.urlProtocolDidFinishLoading(self)26  }27  override func stopLoading() {}28}2930private func managerSession() -> URLSession {31  let configuration = URLSessionConfiguration.ephemeral32  configuration.protocolClasses = [ManagerURLProtocol.self]33  return URLSession(configuration: configuration)34}3536private let onePixelPNG = Data(37  base64Encoded:38    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="39)!4041@MainActor42private final class DesktopImageStub: DesktopImageSetting {43  var receivedURLs: [URL] = []4445  func setDesktopImageURL(46    _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any]47  ) throws {48    receivedURLs.append(url)49  }50}5152@Suite("Favorites persistence", .serialized)53struct FavoritesManagerTests {54  private let defaultsKey = "favoriteWallpapers"5556  @Test("Toggling adds and removes a favorite")57  func toggleFavorite() {58    UserDefaults.standard.removeObject(forKey: defaultsKey)59    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }60    let manager = FavoritesManager()61    let wallpaper = makeWallpaper()6263    manager.toggleFavorite(wallpaper)64    #expect(manager.favorites == [wallpaper])65    #expect(manager.isFavorite(wallpaper))6667    manager.toggleFavorite(wallpaper)68    #expect(manager.favorites.isEmpty)69    #expect(!manager.isFavorite(wallpaper))70  }7172  @Test("Favorites survive manager recreation")73  func favoritesPersist() {74    UserDefaults.standard.removeObject(forKey: defaultsKey)75    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }76    let wallpaper = makeWallpaper()77    FavoritesManager().toggleFavorite(wallpaper)7879    let reloaded = FavoritesManager()8081    #expect(reloaded.favorites == [wallpaper])82  }8384  /// Encoder that always fails. Subclassing JSONEncoder lets us keep the85  /// real `UserDefaults` and validation paths and only flip the encode side.86  final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {87    let testError = NSError(88      domain: "test", code: 7,89      userInfo: [NSLocalizedDescriptionKey: "encode boom"])90    override func encode<T>(_ value: T) throws -> Data where T: Encodable {91      throw testError92    }93  }9495  @Test("Encode failure surfaces through persistenceError without losing in-memory state")96  func encodeFailureIsSurfaced() {97    UserDefaults.standard.removeObject(forKey: defaultsKey)98    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }99    let manager = FavoritesManager(encoder: ThrowingEncoder())100101    manager.toggleFavorite(makeWallpaper())102103    #expect(manager.favorites.count == 1)104    #expect(manager.persistenceError?.contains("encode boom") == true)105  }106107  @Test("Corrupt stored data surfaces through persistenceError on load")108  func corruptDataIsSurfaced() {109    UserDefaults.standard.set(Data("not wallpaper json".utf8), forKey: defaultsKey)110    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }111112    let manager = FavoritesManager()113114    #expect(manager.favorites.isEmpty)115    #expect(manager.persistenceError?.contains("Failed to load favorites") == true)116  }117118}119120@Suite("History persistence", .serialized)121struct HistoryManagerTests {122  private let defaultsKey = "wallpaperHistory"123124  @Test("History keeps newest items first and promotes duplicates")125  func orderingAndPromotion() {126    UserDefaults.standard.removeObject(forKey: defaultsKey)127    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }128    let manager = HistoryManager()129    let first = makeWallpaper(id: "first")130    let second = makeWallpaper(id: "second")131132    manager.addToHistory(first)133    manager.addToHistory(second)134    manager.addToHistory(first)135136    #expect(manager.history.map(\.id) == ["first", "second"])137  }138139  @Test("History is capped at fifty entries")140  func historyLimit() {141    UserDefaults.standard.removeObject(forKey: defaultsKey)142    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }143    let manager = HistoryManager()144145    for index in 0..<55 {146      manager.addToHistory(makeWallpaper(id: "item-\(index)"))147    }148149    #expect(manager.history.count == 50)150    #expect(manager.history.first?.id == "item-54")151    #expect(manager.history.last?.id == "item-5")152  }153154  @Test("History survives recreation and clear removes persisted data")155  func persistenceAndClear() {156    UserDefaults.standard.removeObject(forKey: defaultsKey)157    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }158    HistoryManager().addToHistory(makeWallpaper(id: "persisted"))159160    let reloaded = HistoryManager()161    #expect(reloaded.history.map(\.id) == ["persisted"])162163    reloaded.clearHistory()164    #expect(HistoryManager().history.isEmpty)165  }166167  final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {168    let testError = NSError(169      domain: "test", code: 9,170      userInfo: [NSLocalizedDescriptionKey: "history encode boom"])171    override func encode<T>(_ value: T) throws -> Data where T: Encodable {172      throw testError173    }174  }175176  @Test("Encode failure surfaces through persistenceError without losing in-memory state")177  func encodeFailureIsSurfaced() {178    UserDefaults.standard.removeObject(forKey: defaultsKey)179    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }180    let manager = HistoryManager(encoder: ThrowingEncoder())181182    manager.addToHistory(makeWallpaper(id: "one"))183    manager.addToHistory(makeWallpaper(id: "two"))184185    #expect(manager.history.count == 2)186    #expect(manager.persistenceError?.contains("history encode boom") == true)187  }188189  @Test("Corrupt stored data surfaces through persistenceError on load")190  func corruptDataIsSurfaced() {191    UserDefaults.standard.set(Data("not history json".utf8), forKey: defaultsKey)192    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }193194    let manager = HistoryManager()195196    #expect(manager.history.isEmpty)197    #expect(manager.persistenceError?.contains("Failed to load history") == true)198  }199200  @Test("clearHistory surfaces persistenceError when the encoder fails")201  func clearHistoryEncoderFailureIsSurfaced() {202    UserDefaults.standard.removeObject(forKey: defaultsKey)203    defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }204    // Seed history first via the regular encoder so we know there's an205    // entry to clear.206    HistoryManager().addToHistory(makeWallpaper(id: "seeded"))207208    let manager = HistoryManager(encoder: ThrowingEncoder())209    manager.clearHistory()210211    #expect(manager.history.isEmpty)212    #expect(manager.persistenceError?.contains("history encode boom") == true)213  }214}215216@Suite("Image cache")217struct ImageCacheTests {218  @Test("Inserted images are returned for the same URL")219  func insertAndRetrieve() {220    let url = URL(string: "https://example.com/\(UUID().uuidString).png")!221    let image = NSImage(size: NSSize(width: 10, height: 20))222223    ImageCache.shared.insertImage(image, for: url)224225    #expect(ImageCache.shared.image(for: url) === image)226  }227228  @Test("Different URLs do not collide")229  func keysDoNotCollide() {230    let base = UUID().uuidString231    let firstURL = URL(string: "https://example.com/\(base)-first.png")!232    let secondURL = URL(string: "https://example.com/\(base)-second.png")!233234    ImageCache.shared.insertImage(NSImage(size: NSSize(width: 1, height: 1)), for: firstURL)235236    #expect(ImageCache.shared.image(for: secondURL) == nil)237  }238239  @Test("Fetch decodes and caches successful image responses")240  func fetchAndCache() async throws {241    let cache = ImageCache(session: managerSession())242    let url = URL(string: "https://example.com/image.png")!243244    let first = try await cache.fetchImage(url: url)245    let second = try await cache.fetchImage(url: url)246247    #expect(first === second)248  }249250  @Test("Fetch rejects HTTP errors and invalid image bytes")251  func fetchFailures() async {252    let cache = ImageCache(session: managerSession())253    let httpError = URL(string: "https://example.com/fail-http.png")!254    let invalid = URL(string: "https://example.com/invalid.png")!255    await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: httpError) }256    await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: invalid) }257  }258}259260@Suite("Wallpaper file management", .serialized)261@MainActor262struct WallpaperManagerTests {263  @Test("Download validates HTTP status and writes response data")264  func download() async throws {265    let manager = WallpaperManager(session: managerSession())266    let url = URL(string: "https://example.com/\(UUID().uuidString).jpg")!267    let downloaded = try await manager.download(from: url)268    defer { try? FileManager.default.removeItem(at: downloaded) }269270    #expect(try Data(contentsOf: downloaded) == Data("wallpaper".utf8))271272    let errorURL = URL(string: "https://example.com/fail-http.jpg")!273    await #expect(throws: WallhavenError.self) { try await manager.download(from: errorURL) }274  }275276  @Test("Desktop setter receives explicit and default screens")277  @MainActor278  func desktopSetter() throws {279    guard let screen = NSScreen.screens.first else { return }280    var received: [NSScreen] = []281    let manager = WallpaperManager(desktopSetter: { _, target in received.append(target) })282    let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")283284    try manager.setDesktopImage(from: url, screen: screen)285    try manager.setDesktopImage(from: url)286287    #expect(received.count == 2)288    #expect(received[0] === screen)289  }290291  @Test("Default desktop adapter forwards the image to its workspace")292  func defaultDesktopAdapter() throws {293    guard let screen = NSScreen.screens.first else { return }294    let workspace = DesktopImageStub()295    let manager = WallpaperManager(workspace: workspace)296    let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")297298    try manager.setDesktopImage(from: url, screen: screen)299300    #expect(workspace.receivedURLs == [url])301  }302303  @Test("Set wallpaper composes download and desktop update")304  @MainActor305  func setWallpaper() async throws {306    guard let screen = NSScreen.screens.first else { return }307    var receivedURL: URL?308    let manager = WallpaperManager(309      session: managerSession(), desktopSetter: { url, _ in receivedURL = url })310    let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!311312    try await manager.setWallpaper(from: remote, screen: screen)313    defer { if let receivedURL { try? FileManager.default.removeItem(at: receivedURL) } }314315    // Temp file uses a UUID stem (to avoid collisions) but preserves the316    // original extension so downstream consumers can identify the format.317    #expect(receivedURL?.pathExtension == remote.pathExtension)318    #expect(receivedURL?.lastPathComponent.hasPrefix("haven-") == true)319    #expect(receivedURL?.lastPathComponent != remote.lastPathComponent)320    #expect(try Data(contentsOf: receivedURL!) == Data("wallpaper".utf8))321  }322323  @Test("Saving creates the destination folder and copies the file")324  func saveCreatesFolderAndCopies() throws {325    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)326    defer { try? FileManager.default.removeItem(at: root) }327    let source = root.appendingPathComponent("source/wallpaper.jpg")328    let destination = root.appendingPathComponent("nested/destination")329    try FileManager.default.createDirectory(330      at: source.deletingLastPathComponent(), withIntermediateDirectories: true)331    try Data("first".utf8).write(to: source)332333    try WallpaperManager.shared.saveToFolder(from: source, to: destination)334335    let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))336    #expect(copied == Data("first".utf8))337  }338339  @Test("Saving replaces an existing file with fresh data")340  func saveReplacesExistingFile() throws {341    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)342    defer { try? FileManager.default.removeItem(at: root) }343    let source = root.appendingPathComponent("source/wallpaper.jpg")344    let destination = root.appendingPathComponent("destination")345    try FileManager.default.createDirectory(346      at: source.deletingLastPathComponent(), withIntermediateDirectories: true)347    try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)348    try Data("new".utf8).write(to: source)349    try Data("old".utf8).write(to: destination.appendingPathComponent("wallpaper.jpg"))350351    try WallpaperManager.shared.saveToFolder(from: source, to: destination)352353    let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))354    #expect(copied == Data("new".utf8))355  }356357  @Test("Saving the same logical wallpaper leaves the existing file untouched")358  func saveSameContentIsIdempotent() throws {359    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)360    defer { try? FileManager.default.removeItem(at: root) }361    let source = root.appendingPathComponent("source/wallpaper.jpg")362    let destination = root.appendingPathComponent("destination")363    try FileManager.default.createDirectory(364      at: source.deletingLastPathComponent(), withIntermediateDirectories: true)365    try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)366    try Data("identical".utf8).write(to: source)367    let existingURL = destination.appendingPathComponent("wallpaper.jpg")368    try Data("identical".utf8).write(to: existingURL)369    let originalAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)370371    try WallpaperManager.shared.saveToFolder(from: source, to: destination)372373    let updatedAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)374    #expect(try Data(contentsOf: existingURL) == Data("identical".utf8))375    #expect(376      (updatedAttributes[.modificationDate] as? Date)377        == (originalAttributes[.modificationDate] as? Date)378    )379  }380381  @Test("Saving into the same path as the source does not destroy data")382  func saveSamePathAsSource() throws {383    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)384    defer { try? FileManager.default.removeItem(at: root) }385    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)386    let source = root.appendingPathComponent("wallpaper.jpg")387    try Data("payload".utf8).write(to: source)388389    try WallpaperManager.shared.saveToFolder(from: source, to: root)390391    #expect(try Data(contentsOf: source) == Data("payload".utf8))392  }393394  @Test("Distinct URLs sharing a lastPathComponent produce distinct temp files")395  func distinctDownloadsDoNotCollide() async throws {396    let manager = WallpaperManager(session: managerSession())397    let first = URL(string: "https://example.com/aaa/wallpaper.jpg")!398    let second = URL(string: "https://example.com/bbb/wallpaper.jpg")!399400    let firstFile = try await manager.download(from: first)401    let secondFile = try await manager.download(from: second)402    defer {403      try? FileManager.default.removeItem(at: firstFile)404      try? FileManager.default.removeItem(at: secondFile)405    }406407    #expect(firstFile.path != secondFile.path)408    #expect(firstFile.lastPathComponent != secondFile.lastPathComponent)409    #expect(try Data(contentsOf: firstFile) == Data("wallpaper".utf8))410    #expect(try Data(contentsOf: secondFile) == Data("wallpaper".utf8))411  }412413  @Test("Repeated downloads of the same URL produce independent temp files")414  func repeatedDownloadsAreIndependent() async throws {415    let manager = WallpaperManager(session: managerSession())416    let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!417418    let first = try await manager.download(from: remote)419    let second = try await manager.download(from: remote)420    defer {421      try? FileManager.default.removeItem(at: first)422      try? FileManager.default.removeItem(at: second)423    }424425    #expect(first.path != second.path)426    #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))427    #expect(try Data(contentsOf: second) == Data("wallpaper".utf8))428  }429430  @Test("makeTempURL preserves the source extension when present")431  func makeTempURLPreservesExtension() {432    let withExtension = URL(string: "https://example.com/foo/wallpaper.png")!433    let withoutExtension = URL(string: "https://example.com/foo/wallpaper")!434435    #expect(WallpaperManager.makeTempURL(for: withExtension).pathExtension == "png")436    #expect(WallpaperManager.makeTempURL(for: withoutExtension).pathExtension == "img")437    #expect(438      WallpaperManager.makeTempURL(for: withExtension).path439        != WallpaperManager.makeTempURL(for: withExtension).path)440  }441}442443@Suite("Autostart persistence", .serialized)444@MainActor445struct AutostartManagerTests {446  @Test("Enable writes a launch agent and disable removes it")447  func enableAndDisable() throws {448    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)449    defer { try? FileManager.default.removeItem(at: root) }450    let manager = AutostartManager(451      libraryDirectory: root, executablePath: { "/Applications/Haven.app/Contents/MacOS/Haven" })452453    #expect(!manager.isEnabled)454    manager.isEnabled = true455    #expect(manager.isEnabled)456    manager.isEnabled = true457458    let plist = root.appendingPathComponent("LaunchAgents/com.june.Haven.plist")459    let contents =460      try PropertyListSerialization.propertyList(461        from: Data(contentsOf: plist), format: nil) as? [String: Any]462    #expect(contents?["Label"] as? String == "com.june.Haven")463    #expect(464      contents?["ProgramArguments"] as? [String]465        == ["/Applications/Haven.app/Contents/MacOS/Haven"])466    #expect(contents?["RunAtLoad"] as? Bool == true)467468    manager.isEnabled = false469    #expect(!manager.isEnabled)470    manager.isEnabled = false471  }472473  @Test("Missing executable leaves autostart disabled")474  func missingExecutable() {475    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)476    defer { try? FileManager.default.removeItem(at: root) }477    let manager = AutostartManager(libraryDirectory: root, executablePath: { nil })478479    manager.isEnabled = true480481    #expect(!manager.isEnabled)482  }483484  @Test("Default executable provider writes only inside the injected library")485  func defaultExecutableProvider() {486    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)487    defer { try? FileManager.default.removeItem(at: root) }488    let manager = AutostartManager(libraryDirectory: root)489    manager.isEnabled = true490    #expect(manager.isEnabled)491    manager.isEnabled = false492  }493494  @Test("Unwritable launch-agent roots do not enable autostart")495  func writeFailure() throws {496    let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)497    defer { try? FileManager.default.removeItem(at: file) }498    try Data("not a directory".utf8).write(to: file)499    let manager = AutostartManager(libraryDirectory: file, executablePath: { "/tmp/Haven" })500    manager.isEnabled = true501    #expect(!manager.isEnabled)502  }503504  @Test("Removal failures leave the launch agent enabled")505  func removalFailure() {506    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)507    defer { try? FileManager.default.removeItem(at: root) }508    let manager = AutostartManager(509      libraryDirectory: root, executablePath: { "/tmp/Haven" },510      removePlist: { _ in throw CocoaError(.fileWriteNoPermission) })511    manager.isEnabled = true512    manager.isEnabled = false513    #expect(manager.isEnabled)514  }515}516517@Suite("Local wallpaper library", .serialized)518@MainActor519struct LocalWallpapersManagerTests {520  private func waitForScan(_ manager: LocalWallpapersManager) async {521    for _ in 0..<100 where manager.isLoading {522      try? await Task.sleep(nanoseconds: 10_000_000)523    }524  }525526  @Test("Scan filters files, reads metadata, and deletes wallpapers")527  func scanAndDelete() async throws {528    let defaults = UserDefaults.standard529    let oldPath = defaults.string(forKey: "saveFolderPath")530    defaults.removeObject(forKey: "saveFolderPath")531    defer {532      if let oldPath {533        defaults.set(oldPath, forKey: "saveFolderPath")534      } else {535        defaults.removeObject(forKey: "saveFolderPath")536      }537    }538    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)539    defer { try? FileManager.default.removeItem(at: root) }540    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)541    try onePixelPNG.write(to: root.appendingPathComponent("wallpaper.png"))542    try Data("broken".utf8).write(to: root.appendingPathComponent("broken.png"))543    try Data("ignored".utf8).write(to: root.appendingPathComponent("notes.txt"))544    let manager = LocalWallpapersManager()545546    manager.scanFolder(path: root.path)547    manager.scanFolder(path: root.path)548    await waitForScan(manager)549550    let wallpaper = try #require(manager.wallpapers.first)551    #expect(manager.wallpapers.count == 1)552    #expect(wallpaper.thumbnailURL == wallpaper.url)553    #expect(wallpaper.filename == "wallpaper.png")554    #expect(wallpaper.resolution == "1x1")555    #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("broken.png")) == nil)556    #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("missing.png")) == nil)557558    manager.deleteWallpaper(wallpaper)559    #expect(manager.wallpapers.isEmpty)560    manager.deleteWallpaper(wallpaper)561    #expect(manager.errorMessage?.hasPrefix("Failed to delete") == true)562  }563564  @Test("Invalid scan roots surface an error")565  func scanError() async throws {566    let defaults = UserDefaults.standard567    defaults.removeObject(forKey: "saveFolderPath")568    let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)569    defer { try? FileManager.default.removeItem(at: file) }570    try Data("file".utf8).write(to: file)571    let manager = LocalWallpapersManager()572573    manager.scanFolder(path: file.path)574    await waitForScan(manager)575576    #expect(manager.errorMessage?.hasPrefix("Failed to scan folder") == true)577    #expect(!manager.isLoading)578  }579580  @Test("Concurrent scan requests are dispatched, not silently dropped")581  func concurrentScansAreDispatched() async throws {582    let defaults = UserDefaults.standard583    let oldPath = defaults.string(forKey: "saveFolderPath")584    defaults.removeObject(forKey: "saveFolderPath")585    defer {586      if let oldPath {587        defaults.set(oldPath, forKey: "saveFolderPath")588      } else {589        defaults.removeObject(forKey: "saveFolderPath")590      }591    }592    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)593    defer { try? FileManager.default.removeItem(at: root) }594    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)595596    let manager = LocalWallpapersManager()597    // First explicit scan baseline on an empty folder.598    manager.scanFolder(path: root.path)599    await waitForScan(manager)600    #expect(manager.wallpapers.isEmpty)601602    // Drop two files in succession. Each scan after a write must observe the603    // new file rather than be silently dropped because the previous one is604    // still in flight.605    try onePixelPNG.write(to: root.appendingPathComponent("first.png"))606    try onePixelPNG.write(to: root.appendingPathComponent("second.png"))607    manager.scanFolder(path: root.path)608    manager.scanFolder(path: root.path)609    await waitForScan(manager)610    #expect(manager.wallpapers.contains(where: { $0.filename == "first.png" }))611    #expect(manager.wallpapers.contains(where: { $0.filename == "second.png" }))612    #expect(!manager.isLoading)613    #expect(manager.errorMessage == nil)614  }615616  @Test("Reissuing a scan during an in-flight scan observes later state via generation")617  func scanDuringFlightWinsForLatestGeneration() async throws {618    let defaults = UserDefaults.standard619    let oldPath = defaults.string(forKey: "saveFolderPath")620    defaults.removeObject(forKey: "saveFolderPath")621    defer {622      if let oldPath {623        defaults.set(oldPath, forKey: "saveFolderPath")624      } else {625        defaults.removeObject(forKey: "saveFolderPath")626      }627    }628    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)629    defer { try? FileManager.default.removeItem(at: root) }630    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)631    let manager = LocalWallpapersManager()632633    // Issue three scans in rapid succession against an empty folder. Under634    // the old guard !isLoading logic, two of these would be silently635    // dropped; under the generation strategy all three are dispatched and636    // only the latest commits. The manager must still end up stable.637    manager.scanFolder(path: root.path)638    manager.scanFolder(path: root.path)639    manager.scanFolder(path: root.path)640    await waitForScan(manager)641    #expect(!manager.isLoading)642    #expect(manager.wallpapers.isEmpty)643    #expect(manager.errorMessage == nil)644645    // Now write a file and trigger a new scan; the new scan must observe it.646    try onePixelPNG.write(to: root.appendingPathComponent("added.png"))647    manager.scanFolder(path: root.path)648    await waitForScan(manager)649    #expect(manager.wallpapers.contains(where: { $0.filename == "added.png" }))650  }651652  @Test("Monitoring configured folders observes filesystem changes and stops")653  func monitoring() async throws {654    let defaults = UserDefaults.standard655    let oldPath = defaults.string(forKey: "saveFolderPath")656    let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)657    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)658    defaults.set(root.path, forKey: "saveFolderPath")659    defer {660      try? FileManager.default.removeItem(at: root)661      if let oldPath {662        defaults.set(oldPath, forKey: "saveFolderPath")663      } else {664        defaults.removeObject(forKey: "saveFolderPath")665      }666    }667    let manager = LocalWallpapersManager()668    await waitForScan(manager)669670    try onePixelPNG.write(to: root.appendingPathComponent("observed.png"))671    try? await Task.sleep(nanoseconds: 100_000_000)672    await waitForScan(manager)673    #expect(manager.wallpapers.contains(where: { $0.filename == "observed.png" }))674675    manager.stopMonitoring()676    try? await Task.sleep(nanoseconds: 500_000_000)677678    let invalidMonitor = FolderMonitor(folderPath: root.appendingPathComponent("missing").path) {}679    invalidMonitor.start()680    invalidMonitor.closeFileDescriptor()681682    let descriptor = open("/dev/null", O_RDONLY)683    let descriptorMonitor = FolderMonitor(684      folderPath: "/dev/null", fileDescriptor: descriptor, callback: {})685    descriptorMonitor.closeFileDescriptor()686    #expect(fcntl(descriptor, F_GETFD) == -1)687  }688}