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 85 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 205 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 }258259 @Test("Default cache limits are finite and configurable")260 func defaultAndInjectedLimits() {261 262 #expect(ImageCache.defaultCountLimit > 0)263 #expect(ImageCache.defaultTotalCostLimit > 0)264265 let tight = ImageCache(countLimit: 2, totalCostLimit: 1024)266 let first = URL(string: "https://example.com/\(UUID().uuidString).png")!267 let second = URL(string: "https://example.com/\(UUID().uuidString).png")!268 let third = URL(string: "https://example.com/\(UUID().uuidString).png")!269 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: first)270 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: second)271 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: third)272273 274 275 let survivors = [first, second, third].filter { tight.image(for: $0) != nil }276 #expect(survivors.count <= 2)277 }278}279280@Suite("Wallpaper file management", .serialized)281@MainActor282struct WallpaperManagerTests {283 @Test("Download validates HTTP status and writes response data")284 func download() async throws {285 let manager = WallpaperManager(session: managerSession())286 let url = URL(string: "https://example.com/\(UUID().uuidString).jpg")!287 let downloaded = try await manager.download(from: url)288 defer { try? FileManager.default.removeItem(at: downloaded) }289290 #expect(try Data(contentsOf: downloaded) == Data("wallpaper".utf8))291292 let errorURL = URL(string: "https://example.com/fail-http.jpg")!293 await #expect(throws: WallhavenError.self) { try await manager.download(from: errorURL) }294 }295296 @Test("Desktop setter receives explicit and default screens")297 @MainActor298 func desktopSetter() throws {299 guard let screen = NSScreen.screens.first else { return }300 var received: [NSScreen] = []301 let manager = WallpaperManager(desktopSetter: { _, target in received.append(target) })302 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")303304 try manager.setDesktopImage(from: url, screen: screen)305 try manager.setDesktopImage(from: url)306307 #expect(received.count == 2)308 #expect(received[0] === screen)309 }310311 @Test("Default desktop adapter forwards the image to its workspace")312 func defaultDesktopAdapter() throws {313 guard let screen = NSScreen.screens.first else { return }314 let workspace = DesktopImageStub()315 let manager = WallpaperManager(workspace: workspace)316 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")317318 try manager.setDesktopImage(from: url, screen: screen)319320 #expect(workspace.receivedURLs == [url])321 }322323 @Test("Set wallpaper composes download and desktop update")324 @MainActor325 func setWallpaper() async throws {326 guard let screen = NSScreen.screens.first else { return }327 var receivedURL: URL?328 let manager = WallpaperManager(329 session: managerSession(), desktopSetter: { url, _ in receivedURL = url })330 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!331332 try await manager.setWallpaper(from: remote, screen: screen)333 defer { if let receivedURL { try? FileManager.default.removeItem(at: receivedURL) } }334335 336 337 #expect(receivedURL?.pathExtension == remote.pathExtension)338 #expect(receivedURL?.lastPathComponent.hasPrefix("haven-") == true)339 #expect(receivedURL?.lastPathComponent != remote.lastPathComponent)340 #expect(try Data(contentsOf: receivedURL!) == Data("wallpaper".utf8))341 }342343 @Test("Saving creates the destination folder and copies the file")344 func saveCreatesFolderAndCopies() throws {345 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)346 defer { try? FileManager.default.removeItem(at: root) }347 let source = root.appendingPathComponent("source/wallpaper.jpg")348 let destination = root.appendingPathComponent("nested/destination")349 try FileManager.default.createDirectory(350 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)351 try Data("first".utf8).write(to: source)352353 try WallpaperManager.shared.saveToFolder(from: source, to: destination)354355 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))356 #expect(copied == Data("first".utf8))357 }358359 @Test("Saving replaces an existing file with fresh data")360 func saveReplacesExistingFile() throws {361 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)362 defer { try? FileManager.default.removeItem(at: root) }363 let source = root.appendingPathComponent("source/wallpaper.jpg")364 let destination = root.appendingPathComponent("destination")365 try FileManager.default.createDirectory(366 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)367 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)368 try Data("new".utf8).write(to: source)369 try Data("old".utf8).write(to: destination.appendingPathComponent("wallpaper.jpg"))370371 try WallpaperManager.shared.saveToFolder(from: source, to: destination)372373 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))374 #expect(copied == Data("new".utf8))375 }376377 @Test("Saving the same logical wallpaper leaves the existing file untouched")378 func saveSameContentIsIdempotent() throws {379 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)380 defer { try? FileManager.default.removeItem(at: root) }381 let source = root.appendingPathComponent("source/wallpaper.jpg")382 let destination = root.appendingPathComponent("destination")383 try FileManager.default.createDirectory(384 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)385 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)386 try Data("identical".utf8).write(to: source)387 let existingURL = destination.appendingPathComponent("wallpaper.jpg")388 try Data("identical".utf8).write(to: existingURL)389 let originalAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)390391 try WallpaperManager.shared.saveToFolder(from: source, to: destination)392393 let updatedAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)394 #expect(try Data(contentsOf: existingURL) == Data("identical".utf8))395 #expect(396 (updatedAttributes[.modificationDate] as? Date)397 == (originalAttributes[.modificationDate] as? Date)398 )399 }400401 @Test("Saving into the same path as the source does not destroy data")402 func saveSamePathAsSource() throws {403 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)404 defer { try? FileManager.default.removeItem(at: root) }405 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)406 let source = root.appendingPathComponent("wallpaper.jpg")407 try Data("payload".utf8).write(to: source)408409 try WallpaperManager.shared.saveToFolder(from: source, to: root)410411 #expect(try Data(contentsOf: source) == Data("payload".utf8))412 }413414 @Test("Distinct URLs sharing a lastPathComponent produce distinct temp files")415 func distinctDownloadsDoNotCollide() async throws {416 let manager = WallpaperManager(session: managerSession())417 let first = URL(string: "https://example.com/aaa/wallpaper.jpg")!418 let second = URL(string: "https://example.com/bbb/wallpaper.jpg")!419420 let firstFile = try await manager.download(from: first)421 let secondFile = try await manager.download(from: second)422 defer {423 try? FileManager.default.removeItem(at: firstFile)424 try? FileManager.default.removeItem(at: secondFile)425 }426427 #expect(firstFile.path != secondFile.path)428 #expect(firstFile.lastPathComponent != secondFile.lastPathComponent)429 #expect(try Data(contentsOf: firstFile) == Data("wallpaper".utf8))430 #expect(try Data(contentsOf: secondFile) == Data("wallpaper".utf8))431 }432433 @Test("Repeated downloads of the same URL produce independent temp files")434 func repeatedDownloadsAreIndependent() async throws {435 let manager = WallpaperManager(session: managerSession())436 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!437438 let first = try await manager.download(from: remote)439 let second = try await manager.download(from: remote)440 defer {441 try? FileManager.default.removeItem(at: first)442 try? FileManager.default.removeItem(at: second)443 }444445 #expect(first.path != second.path)446 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))447 #expect(try Data(contentsOf: second) == Data("wallpaper".utf8))448 }449450 @Test("makeTempURL preserves the source extension when present")451 func makeTempURLPreservesExtension() {452 let withExtension = URL(string: "https://example.com/foo/wallpaper.png")!453 let withoutExtension = URL(string: "https://example.com/foo/wallpaper")!454455 #expect(WallpaperManager.makeTempURL(for: withExtension).pathExtension == "png")456 #expect(WallpaperManager.makeTempURL(for: withoutExtension).pathExtension == "img")457 #expect(458 WallpaperManager.makeTempURL(for: withExtension).path459 != WallpaperManager.makeTempURL(for: withExtension).path)460 }461462 @Test("setDesktopImage without screens throws instead of force-unwrapping")463 func setDesktopImageWithoutScreensThrows() {464 let manager = WallpaperManager(465 desktopSetter: { _, _ in },466 mainScreen: { nil }, firstScreen: { nil })467 let url = URL(fileURLWithPath: "/tmp/no-screens.jpg")468469 #expect(throws: WallhavenError.self) {470 try manager.setDesktopImage(from: url)471 }472 }473474 @Test("setDesktopImage prefers main over first screen when both are available")475 func setDesktopImagePrefersMain() throws {476 let mainScreen = NSScreen.main ?? NSScreen.screens.first477 guard let mainScreen else { return }478 let otherScreen = mainScreen479 var captured: [NSScreen] = []480 let manager = WallpaperManager(481 desktopSetter: { _, target in captured.append(target) },482 mainScreen: { mainScreen }, firstScreen: { otherScreen })483 let url = URL(fileURLWithPath: "/tmp/main-preferred.jpg")484485 try manager.setDesktopImage(from: url)486487 #expect(captured == [mainScreen])488 }489490 @Test("setDesktopImage falls back to first screen when main is missing")491 func setDesktopImageFallsBackToFirst() throws {492 guard let first = NSScreen.screens.first else { return }493 var captured: [NSScreen] = []494 let manager = WallpaperManager(495 desktopSetter: { _, target in captured.append(target) },496 mainScreen: { nil }, firstScreen: { first })497 let url = URL(fileURLWithPath: "/tmp/fallback.jpg")498499 try manager.setDesktopImage(from: url)500501 #expect(captured == [first])502 }503504 @Test("setDesktopImage default firstScreen closure calls NSScreen.screens.first")505 func setDesktopImageDefaultFirstScreenClosure() throws {506 guard let first = NSScreen.screens.first else { return }507 var captured: [NSScreen] = []508 509 510 let manager = WallpaperManager(511 desktopSetter: { _, target in captured.append(target) },512 mainScreen: { nil })513 let url = URL(fileURLWithPath: "/tmp/default-first.jpg")514515 try manager.setDesktopImage(from: url)516517 #expect(captured == [first])518 }519}520521@Suite("Autostart persistence", .serialized)522@MainActor523struct AutostartManagerTests {524 @Test("Enable writes a launch agent and disable removes it")525 func enableAndDisable() throws {526 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)527 defer { try? FileManager.default.removeItem(at: root) }528 let manager = AutostartManager(529 libraryDirectory: root, executablePath: { "/Applications/Haven.app/Contents/MacOS/Haven" })530531 #expect(!manager.isEnabled)532 manager.isEnabled = true533 #expect(manager.isEnabled)534 manager.isEnabled = true535536 let plist = root.appendingPathComponent("LaunchAgents/com.june.Haven.plist")537 let contents =538 try PropertyListSerialization.propertyList(539 from: Data(contentsOf: plist), format: nil) as? [String: Any]540 #expect(contents?["Label"] as? String == "com.june.Haven")541 #expect(542 contents?["ProgramArguments"] as? [String]543 == ["/Applications/Haven.app/Contents/MacOS/Haven"])544 #expect(contents?["RunAtLoad"] as? Bool == true)545546 manager.isEnabled = false547 #expect(!manager.isEnabled)548 manager.isEnabled = false549 }550551 @Test("Missing executable leaves autostart disabled")552 func missingExecutable() {553 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)554 defer { try? FileManager.default.removeItem(at: root) }555 let manager = AutostartManager(libraryDirectory: root, executablePath: { nil })556557 manager.isEnabled = true558559 #expect(!manager.isEnabled)560 }561562 @Test("Default executable provider writes only inside the injected library")563 func defaultExecutableProvider() {564 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)565 defer { try? FileManager.default.removeItem(at: root) }566 let manager = AutostartManager(libraryDirectory: root)567 manager.isEnabled = true568 #expect(manager.isEnabled)569 manager.isEnabled = false570 }571572 @Test("Unwritable launch-agent roots do not enable autostart")573 func writeFailure() throws {574 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)575 defer { try? FileManager.default.removeItem(at: file) }576 try Data("not a directory".utf8).write(to: file)577 let manager = AutostartManager(libraryDirectory: file, executablePath: { "/tmp/Haven" })578 manager.isEnabled = true579 #expect(!manager.isEnabled)580 }581582 @Test("Removal failures leave the launch agent enabled")583 func removalFailure() {584 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)585 defer { try? FileManager.default.removeItem(at: root) }586 let manager = AutostartManager(587 libraryDirectory: root, executablePath: { "/tmp/Haven" },588 removePlist: { _ in throw CocoaError(.fileWriteNoPermission) })589 manager.isEnabled = true590 manager.isEnabled = false591 #expect(manager.isEnabled)592 }593594 @Test("LaunchAgents directory-creation failures surface through lastError")595 func launchAgentsDirectoryFailureIsSurfaced() {596 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)597 defer { try? FileManager.default.removeItem(at: file) }598 try? Data("not a directory".utf8).write(to: file)599 let manager = AutostartManager(600 libraryDirectory: file, executablePath: { "/tmp/Haven" })601602 603 #expect(manager.lastError?.contains("LaunchAgents") == true)604 }605606 @Test("lastError is cleared by a successful enable/disable cycle")607 func lastErrorClearsOnSuccess() throws {608 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)609 defer { try? FileManager.default.removeItem(at: root) }610 let manager = AutostartManager(611 libraryDirectory: root, executablePath: { "/tmp/Haven" },612 writePlist: { _, _ in throw CocoaError(.fileWriteNoPermission) })613614 manager.isEnabled = true615 #expect(manager.lastError?.contains("Failed to enable autostart") == true)616617 618 let successManager = AutostartManager(619 libraryDirectory: root, executablePath: { "/tmp/Haven" })620 successManager.isEnabled = true621 successManager.isEnabled = false622 #expect(successManager.lastError == nil)623 }624}625626@Suite("Local wallpaper library", .serialized)627@MainActor628struct LocalWallpapersManagerTests {629 private func waitForScan(_ manager: LocalWallpapersManager) async {630 for _ in 0..<100 where manager.isLoading {631 try? await Task.sleep(nanoseconds: 10_000_000)632 }633 }634635 @Test("Scan filters files, reads metadata, and deletes wallpapers")636 func scanAndDelete() async throws {637 let defaults = UserDefaults.standard638 let oldPath = defaults.string(forKey: "saveFolderPath")639 defaults.removeObject(forKey: "saveFolderPath")640 defer {641 if let oldPath {642 defaults.set(oldPath, forKey: "saveFolderPath")643 } else {644 defaults.removeObject(forKey: "saveFolderPath")645 }646 }647 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)648 defer { try? FileManager.default.removeItem(at: root) }649 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)650 try onePixelPNG.write(to: root.appendingPathComponent("wallpaper.png"))651 try Data("broken".utf8).write(to: root.appendingPathComponent("broken.png"))652 try Data("ignored".utf8).write(to: root.appendingPathComponent("notes.txt"))653 let manager = LocalWallpapersManager()654655 manager.scanFolder(path: root.path)656 manager.scanFolder(path: root.path)657 await waitForScan(manager)658659 let wallpaper = try #require(manager.wallpapers.first)660 #expect(manager.wallpapers.count == 1)661 #expect(wallpaper.thumbnailURL == wallpaper.url)662 #expect(wallpaper.filename == "wallpaper.png")663 #expect(wallpaper.resolution == "1x1")664 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("broken.png")) == nil)665 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("missing.png")) == nil)666667 manager.deleteWallpaper(wallpaper)668 #expect(manager.wallpapers.isEmpty)669 manager.deleteWallpaper(wallpaper)670 #expect(manager.errorMessage?.hasPrefix("Failed to delete") == true)671 }672673 @Test("Invalid scan roots surface an error")674 func scanError() async throws {675 let defaults = UserDefaults.standard676 defaults.removeObject(forKey: "saveFolderPath")677 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)678 defer { try? FileManager.default.removeItem(at: file) }679 try Data("file".utf8).write(to: file)680 let manager = LocalWallpapersManager()681682 manager.scanFolder(path: file.path)683 await waitForScan(manager)684685 #expect(manager.errorMessage?.hasPrefix("Failed to scan folder") == true)686 #expect(!manager.isLoading)687 }688689 @Test("Concurrent scan requests are dispatched, not silently dropped")690 func concurrentScansAreDispatched() async throws {691 let defaults = UserDefaults.standard692 let oldPath = defaults.string(forKey: "saveFolderPath")693 defaults.removeObject(forKey: "saveFolderPath")694 defer {695 if let oldPath {696 defaults.set(oldPath, forKey: "saveFolderPath")697 } else {698 defaults.removeObject(forKey: "saveFolderPath")699 }700 }701 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)702 defer { try? FileManager.default.removeItem(at: root) }703 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)704705 let manager = LocalWallpapersManager()706 707 manager.scanFolder(path: root.path)708 await waitForScan(manager)709 #expect(manager.wallpapers.isEmpty)710711 712 713 714 try onePixelPNG.write(to: root.appendingPathComponent("first.png"))715 try onePixelPNG.write(to: root.appendingPathComponent("second.png"))716 manager.scanFolder(path: root.path)717 manager.scanFolder(path: root.path)718 await waitForScan(manager)719 #expect(manager.wallpapers.contains(where: { $0.filename == "first.png" }))720 #expect(manager.wallpapers.contains(where: { $0.filename == "second.png" }))721 #expect(!manager.isLoading)722 #expect(manager.errorMessage == nil)723 }724725 @Test("Reissuing a scan during an in-flight scan observes later state via generation")726 func scanDuringFlightWinsForLatestGeneration() async throws {727 let defaults = UserDefaults.standard728 let oldPath = defaults.string(forKey: "saveFolderPath")729 defaults.removeObject(forKey: "saveFolderPath")730 defer {731 if let oldPath {732 defaults.set(oldPath, forKey: "saveFolderPath")733 } else {734 defaults.removeObject(forKey: "saveFolderPath")735 }736 }737 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)738 defer { try? FileManager.default.removeItem(at: root) }739 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)740 let manager = LocalWallpapersManager()741742 743 744 745 746 manager.scanFolder(path: root.path)747 manager.scanFolder(path: root.path)748 manager.scanFolder(path: root.path)749 await waitForScan(manager)750 #expect(!manager.isLoading)751 #expect(manager.wallpapers.isEmpty)752 #expect(manager.errorMessage == nil)753754 755 try onePixelPNG.write(to: root.appendingPathComponent("added.png"))756 manager.scanFolder(path: root.path)757 await waitForScan(manager)758 #expect(manager.wallpapers.contains(where: { $0.filename == "added.png" }))759 }760761 @Test("Monitoring configured folders observes filesystem changes and stops")762 func monitoring() async throws {763 let defaults = UserDefaults.standard764 let oldPath = defaults.string(forKey: "saveFolderPath")765 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)766 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)767 defaults.set(root.path, forKey: "saveFolderPath")768 defer {769 try? FileManager.default.removeItem(at: root)770 if let oldPath {771 defaults.set(oldPath, forKey: "saveFolderPath")772 } else {773 defaults.removeObject(forKey: "saveFolderPath")774 }775 }776 let manager = LocalWallpapersManager()777 await waitForScan(manager)778779 try onePixelPNG.write(to: root.appendingPathComponent("observed.png"))780 try? await Task.sleep(nanoseconds: 100_000_000)781 await waitForScan(manager)782 #expect(manager.wallpapers.contains(where: { $0.filename == "observed.png" }))783784 manager.stopMonitoring()785 try? await Task.sleep(nanoseconds: 500_000_000)786787 let invalidMonitor = FolderMonitor(folderPath: root.appendingPathComponent("missing").path) {}788 #expect(invalidMonitor.start() == false)789 #expect(invalidMonitor.openFailureMessage?.isEmpty == false)790 invalidMonitor.closeFileDescriptor()791792 793 794 795 let broken = FolderMonitor(796 folderPath: root.path, callback: {},797 openPath: { _, _ in -1 })798 #expect(broken.start() == false)799 #expect(broken.openFailureMessage?.localizedCaseInsensitiveContains("could not watch") == true)800801 let descriptor = open("/dev/null", O_RDONLY)802 let descriptorMonitor = FolderMonitor(803 folderPath: "/dev/null", fileDescriptor: descriptor, callback: {})804 descriptorMonitor.closeFileDescriptor()805 #expect(fcntl(descriptor, F_GETFD) == -1)806 }807808 @Test("startMonitoring surfaces folder-monitor open failures through errorMessage")809 func startMonitoringSurfacesMonitorFailures() async {810 811 812 813 814 let defaults = UserDefaults.standard815 let oldPath = defaults.string(forKey: "saveFolderPath")816 defaults.removeObject(forKey: "saveFolderPath")817 defer {818 if let oldPath {819 defaults.set(oldPath, forKey: "saveFolderPath")820 } else {821 defaults.removeObject(forKey: "saveFolderPath")822 }823 }824 825 826 let missing = FileManager.default.temporaryDirectory827 .appendingPathComponent("haven-missing-\(UUID().uuidString)").path828 let manager = LocalWallpapersManager()829 manager.startMonitoring(folderPath: missing)830 #expect(manager.errorMessage?.isEmpty == false)831 manager.stopMonitoring()832 }833}