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}35363738private final class CountingURLProtocol: URLProtocol, @unchecked Sendable {39 nonisolated(unsafe) static var onLoad: (() -> Void)?40 override class func canInit(with request: URLRequest) -> Bool { true }41 override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }42 override func startLoading() {43 Self.onLoad?()44 let response = HTTPURLResponse(45 url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!46 client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)47 client?.urlProtocol(self, didLoad: Data("wallpaper".utf8))48 client?.urlProtocolDidFinishLoading(self)49 }50 override func stopLoading() {}51}5253private func countingSession(onLoad: @escaping () -> Void) -> URLSession {54 CountingURLProtocol.onLoad = onLoad55 let configuration = URLSessionConfiguration.ephemeral56 configuration.protocolClasses = [CountingURLProtocol.self]57 return URLSession(configuration: configuration)58}5960private let onePixelPNG = Data(61 base64Encoded:62 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="63)!6465@MainActor66private final class DesktopImageStub: DesktopImageSetting {67 var receivedURLs: [URL] = []6869 func setDesktopImageURL(70 _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any]71 ) throws {72 receivedURLs.append(url)73 }74}7576@Suite("Favorites persistence", .serialized)77struct FavoritesManagerTests {78 private let defaultsKey = "favoriteWallpapers"7980 @Test("Toggling adds and removes a favorite")81 func toggleFavorite() {82 UserDefaults.standard.removeObject(forKey: defaultsKey)83 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }84 let manager = FavoritesManager()85 let wallpaper = makeWallpaper()8687 manager.toggleFavorite(wallpaper)88 #expect(manager.favorites == [wallpaper])89 #expect(manager.isFavorite(wallpaper))9091 manager.toggleFavorite(wallpaper)92 #expect(manager.favorites.isEmpty)93 #expect(!manager.isFavorite(wallpaper))94 }9596 @Test("Favorites survive manager recreation")97 func favoritesPersist() {98 UserDefaults.standard.removeObject(forKey: defaultsKey)99 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }100 let wallpaper = makeWallpaper()101 FavoritesManager().toggleFavorite(wallpaper)102103 let reloaded = FavoritesManager()104105 #expect(reloaded.favorites == [wallpaper])106 }107108 109 110 final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {111 let testError = NSError(112 domain: "test", code: 7,113 userInfo: [NSLocalizedDescriptionKey: "encode boom"])114 override func encode<T>(_ value: T) throws -> Data where T: Encodable {115 throw testError116 }117 }118119 @Test("Encode failure surfaces through persistenceError without losing in-memory state")120 func encodeFailureIsSurfaced() {121 UserDefaults.standard.removeObject(forKey: defaultsKey)122 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }123 let manager = FavoritesManager(encoder: ThrowingEncoder())124125 manager.toggleFavorite(makeWallpaper())126127 #expect(manager.favorites.count == 1)128 #expect(manager.persistenceError?.contains("encode boom") == true)129 }130131 @Test("Corrupt stored data surfaces through persistenceError on load")132 func corruptDataIsSurfaced() {133 UserDefaults.standard.set(Data("not wallpaper json".utf8), forKey: defaultsKey)134 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }135136 let manager = FavoritesManager()137138 #expect(manager.favorites.isEmpty)139 #expect(manager.persistenceError?.contains("Failed to load favorites") == true)140 }141142}143144@Suite("History persistence", .serialized)145struct HistoryManagerTests {146 private let defaultsKey = "wallpaperHistory"147148 @Test("History keeps newest items first and promotes duplicates")149 func orderingAndPromotion() {150 UserDefaults.standard.removeObject(forKey: defaultsKey)151 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }152 let manager = HistoryManager()153 let first = makeWallpaper(id: "first")154 let second = makeWallpaper(id: "second")155156 manager.addToHistory(first)157 manager.addToHistory(second)158 manager.addToHistory(first)159160 #expect(manager.history.map(\.id) == ["first", "second"])161 }162163 @Test("History is capped at fifty entries")164 func historyLimit() {165 UserDefaults.standard.removeObject(forKey: defaultsKey)166 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }167 let manager = HistoryManager()168169 for index in 0..<55 {170 manager.addToHistory(makeWallpaper(id: "item-\(index)"))171 }172173 #expect(manager.history.count == 50)174 #expect(manager.history.first?.id == "item-54")175 #expect(manager.history.last?.id == "item-5")176 }177178 @Test("History survives recreation and clear removes persisted data")179 func persistenceAndClear() {180 UserDefaults.standard.removeObject(forKey: defaultsKey)181 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }182 HistoryManager().addToHistory(makeWallpaper(id: "persisted"))183184 let reloaded = HistoryManager()185 #expect(reloaded.history.map(\.id) == ["persisted"])186187 reloaded.clearHistory()188 #expect(HistoryManager().history.isEmpty)189 }190191 final class ThrowingEncoder: JSONEncoder, @unchecked Sendable {192 let testError = NSError(193 domain: "test", code: 9,194 userInfo: [NSLocalizedDescriptionKey: "history encode boom"])195 override func encode<T>(_ value: T) throws -> Data where T: Encodable {196 throw testError197 }198 }199200 @Test("Encode failure surfaces through persistenceError without losing in-memory state")201 func encodeFailureIsSurfaced() {202 UserDefaults.standard.removeObject(forKey: defaultsKey)203 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }204 let manager = HistoryManager(encoder: ThrowingEncoder())205206 manager.addToHistory(makeWallpaper(id: "one"))207 manager.addToHistory(makeWallpaper(id: "two"))208209 #expect(manager.history.count == 2)210 #expect(manager.persistenceError?.contains("history encode boom") == true)211 }212213 @Test("Corrupt stored data surfaces through persistenceError on load")214 func corruptDataIsSurfaced() {215 UserDefaults.standard.set(Data("not history json".utf8), forKey: defaultsKey)216 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }217218 let manager = HistoryManager()219220 #expect(manager.history.isEmpty)221 #expect(manager.persistenceError?.contains("Failed to load history") == true)222 }223224 @Test("clearHistory surfaces persistenceError when the encoder fails")225 func clearHistoryEncoderFailureIsSurfaced() {226 UserDefaults.standard.removeObject(forKey: defaultsKey)227 defer { UserDefaults.standard.removeObject(forKey: defaultsKey) }228 229 230 HistoryManager().addToHistory(makeWallpaper(id: "seeded"))231232 let manager = HistoryManager(encoder: ThrowingEncoder())233 manager.clearHistory()234235 #expect(manager.history.isEmpty)236 #expect(manager.persistenceError?.contains("history encode boom") == true)237 }238}239240@Suite("Image cache")241struct ImageCacheTests {242 @Test("Inserted images are returned for the same URL")243 func insertAndRetrieve() {244 let url = URL(string: "https://example.com/\(UUID().uuidString).png")!245 let image = NSImage(size: NSSize(width: 10, height: 20))246247 ImageCache.shared.insertImage(image, for: url)248249 #expect(ImageCache.shared.image(for: url) === image)250 }251252 @Test("Different URLs do not collide")253 func keysDoNotCollide() {254 let base = UUID().uuidString255 let firstURL = URL(string: "https://example.com/\(base)-first.png")!256 let secondURL = URL(string: "https://example.com/\(base)-second.png")!257258 ImageCache.shared.insertImage(NSImage(size: NSSize(width: 1, height: 1)), for: firstURL)259260 #expect(ImageCache.shared.image(for: secondURL) == nil)261 }262263 @Test("Fetch decodes and caches successful image responses")264 func fetchAndCache() async throws {265 let cache = ImageCache(session: managerSession())266 let url = URL(string: "https://example.com/image.png")!267268 let first = try await cache.fetchImage(url: url)269 let second = try await cache.fetchImage(url: url)270271 #expect(first === second)272 }273274 @Test("Fetch rejects HTTP errors and invalid image bytes")275 func fetchFailures() async {276 let cache = ImageCache(session: managerSession())277 let httpError = URL(string: "https://example.com/fail-http.png")!278 let invalid = URL(string: "https://example.com/invalid.png")!279 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: httpError) }280 await #expect(throws: WallhavenError.self) { try await cache.fetchImage(url: invalid) }281 }282283 @Test("Default cache limits are finite and configurable")284 func defaultAndInjectedLimits() {285 286 #expect(ImageCache.defaultCountLimit > 0)287 #expect(ImageCache.defaultTotalCostLimit > 0)288289 let tight = ImageCache(countLimit: 2, totalCostLimit: 1024)290 let first = URL(string: "https://example.com/\(UUID().uuidString).png")!291 let second = URL(string: "https://example.com/\(UUID().uuidString).png")!292 let third = URL(string: "https://example.com/\(UUID().uuidString).png")!293 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: first)294 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: second)295 tight.insertImage(NSImage(size: NSSize(width: 4, height: 4)), for: third)296297 298 299 let survivors = [first, second, third].filter { tight.image(for: $0) != nil }300 #expect(survivors.count <= 2)301 }302}303304@Suite("Wallpaper file management", .serialized)305@MainActor306struct WallpaperManagerTests {307 @Test("Download validates HTTP status and writes response data")308 func download() async throws {309 let manager = WallpaperManager(session: managerSession())310 let url = URL(string: "https://example.com/\(UUID().uuidString).jpg")!311 let downloaded = try await manager.download(from: url)312 defer { try? FileManager.default.removeItem(at: downloaded) }313314 #expect(try Data(contentsOf: downloaded) == Data("wallpaper".utf8))315316 let errorURL = URL(string: "https://example.com/fail-http.jpg")!317 await #expect(throws: WallhavenError.self) { try await manager.download(from: errorURL) }318 }319320 @Test("Cached local file persists on disk and is reused without re-downloading")321 func cachedLocalFile() async throws {322 var downloadCount = 0323 let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent(324 UUID().uuidString)325 defer { try? FileManager.default.removeItem(at: cacheDir) }326 let manager = WallpaperManager(327 session: countingSession { downloadCount += 1 },328 cacheDirectoryProvider: { cacheDir })329 let url = URL(string: "https://example.com/wallhaven-abc123.jpg")!330331 let first = try await manager.cachedLocalFile(for: url)332 #expect(downloadCount == 1)333 #expect(first.deletingLastPathComponent().path == cacheDir.path)334 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))335336 337 338 339 let second = try await manager.cachedLocalFile(for: url)340 #expect(downloadCount == 1)341 #expect(second == first)342 #expect(FileManager.default.fileExists(atPath: first.path))343 }344345 @Test("Default cache directory lives under Library/Caches and is reused across instances")346 func defaultCacheDirectoryIsUnderLibraryCaches() async throws {347 var downloadCount = 0348 let url = URL(string: "https://example.com/wallhaven-\(UUID().uuidString).jpg")!349 let manager = WallpaperManager(session: countingSession { downloadCount += 1 })350 defer {351 if let cachesRoot = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)352 .first353 {354 let bundleID = Bundle.main.bundleIdentifier ?? "Haven"355 try? FileManager.default.removeItem(356 at: cachesRoot.appendingPathComponent(bundleID).appendingPathComponent(357 "WallpaperCache"358 ).appendingPathComponent(url.lastPathComponent))359 }360 }361362 let first = try await manager.cachedLocalFile(for: url)363 #expect(downloadCount == 1)364 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))365366 367 368 let anotherManager = WallpaperManager(session: countingSession { downloadCount += 1 })369 let second = try await anotherManager.cachedLocalFile(for: url)370 #expect(downloadCount == 1)371 #expect(second == first)372 }373374 @Test("A caching directory that is unavailable falls back to a plain temp download")375 func cacheDirectoryUnavailableFallsBackToPlainDownload() async throws {376 var downloadCount = 0377 let manager = WallpaperManager(378 session: countingSession { downloadCount += 1 }, cacheDirectoryProvider: { nil })379 let url = URL(string: "https://example.com/wallhaven-abc999.jpg")!380381 let first = try await manager.cachedLocalFile(for: url)382 defer { try? FileManager.default.removeItem(at: first) }383 #expect(downloadCount == 1)384385 386 let second = try await manager.cachedLocalFile(for: url)387 defer { try? FileManager.default.removeItem(at: second) }388 #expect(downloadCount == 2)389 }390391 @Test("Desktop setter receives explicit and default screens")392 @MainActor393 func desktopSetter() throws {394 guard let screen = NSScreen.screens.first else { return }395 var received: [NSScreen] = []396 let manager = WallpaperManager(desktopSetter: { _, target in received.append(target) })397 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")398399 try manager.setDesktopImage(from: url, screen: screen)400 try manager.setDesktopImage(from: url)401402 #expect(received.count == 2)403 #expect(received[0] === screen)404 }405406 @Test("Default desktop adapter forwards the image to its workspace")407 func defaultDesktopAdapter() throws {408 guard let screen = NSScreen.screens.first else { return }409 let workspace = DesktopImageStub()410 let manager = WallpaperManager(workspace: workspace)411 let url = URL(fileURLWithPath: "/tmp/test-wallpaper.jpg")412413 try manager.setDesktopImage(from: url, screen: screen)414415 #expect(workspace.receivedURLs == [url])416 }417418 @Test("Set wallpaper composes download and desktop update")419 @MainActor420 func setWallpaper() async throws {421 guard let screen = NSScreen.screens.first else { return }422 var receivedURL: URL?423 let manager = WallpaperManager(424 session: managerSession(), desktopSetter: { url, _ in receivedURL = url })425 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!426427 try await manager.setWallpaper(from: remote, screen: screen)428 defer { if let receivedURL { try? FileManager.default.removeItem(at: receivedURL) } }429430 431 432 #expect(receivedURL?.pathExtension == remote.pathExtension)433 #expect(receivedURL?.lastPathComponent.hasPrefix("haven-") == true)434 #expect(receivedURL?.lastPathComponent != remote.lastPathComponent)435 #expect(try Data(contentsOf: receivedURL!) == Data("wallpaper".utf8))436 }437438 @Test("Saving creates the destination folder and copies the file")439 func saveCreatesFolderAndCopies() throws {440 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)441 defer { try? FileManager.default.removeItem(at: root) }442 let source = root.appendingPathComponent("source/wallpaper.jpg")443 let destination = root.appendingPathComponent("nested/destination")444 try FileManager.default.createDirectory(445 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)446 try Data("first".utf8).write(to: source)447448 try WallpaperManager.shared.saveToFolder(from: source, to: destination)449450 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))451 #expect(copied == Data("first".utf8))452 }453454 @Test("Saving replaces an existing file with fresh data")455 func saveReplacesExistingFile() throws {456 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)457 defer { try? FileManager.default.removeItem(at: root) }458 let source = root.appendingPathComponent("source/wallpaper.jpg")459 let destination = root.appendingPathComponent("destination")460 try FileManager.default.createDirectory(461 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)462 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)463 try Data("new".utf8).write(to: source)464 try Data("old".utf8).write(to: destination.appendingPathComponent("wallpaper.jpg"))465466 try WallpaperManager.shared.saveToFolder(from: source, to: destination)467468 let copied = try Data(contentsOf: destination.appendingPathComponent("wallpaper.jpg"))469 #expect(copied == Data("new".utf8))470 }471472 @Test("Saving the same logical wallpaper leaves the existing file untouched")473 func saveSameContentIsIdempotent() throws {474 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)475 defer { try? FileManager.default.removeItem(at: root) }476 let source = root.appendingPathComponent("source/wallpaper.jpg")477 let destination = root.appendingPathComponent("destination")478 try FileManager.default.createDirectory(479 at: source.deletingLastPathComponent(), withIntermediateDirectories: true)480 try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)481 try Data("identical".utf8).write(to: source)482 let existingURL = destination.appendingPathComponent("wallpaper.jpg")483 try Data("identical".utf8).write(to: existingURL)484 let originalAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)485486 try WallpaperManager.shared.saveToFolder(from: source, to: destination)487488 let updatedAttributes = try FileManager.default.attributesOfItem(atPath: existingURL.path)489 #expect(try Data(contentsOf: existingURL) == Data("identical".utf8))490 #expect(491 (updatedAttributes[.modificationDate] as? Date)492 == (originalAttributes[.modificationDate] as? Date)493 )494 }495496 @Test("Saving into the same path as the source does not destroy data")497 func saveSamePathAsSource() throws {498 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)499 defer { try? FileManager.default.removeItem(at: root) }500 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)501 let source = root.appendingPathComponent("wallpaper.jpg")502 try Data("payload".utf8).write(to: source)503504 try WallpaperManager.shared.saveToFolder(from: source, to: root)505506 #expect(try Data(contentsOf: source) == Data("payload".utf8))507 }508509 @Test("Distinct URLs sharing a lastPathComponent produce distinct temp files")510 func distinctDownloadsDoNotCollide() async throws {511 let manager = WallpaperManager(session: managerSession())512 let first = URL(string: "https://example.com/aaa/wallpaper.jpg")!513 let second = URL(string: "https://example.com/bbb/wallpaper.jpg")!514515 let firstFile = try await manager.download(from: first)516 let secondFile = try await manager.download(from: second)517 defer {518 try? FileManager.default.removeItem(at: firstFile)519 try? FileManager.default.removeItem(at: secondFile)520 }521522 #expect(firstFile.path != secondFile.path)523 #expect(firstFile.lastPathComponent != secondFile.lastPathComponent)524 #expect(try Data(contentsOf: firstFile) == Data("wallpaper".utf8))525 #expect(try Data(contentsOf: secondFile) == Data("wallpaper".utf8))526 }527528 @Test("Repeated downloads of the same URL produce independent temp files")529 func repeatedDownloadsAreIndependent() async throws {530 let manager = WallpaperManager(session: managerSession())531 let remote = URL(string: "https://example.com/\(UUID().uuidString).jpg")!532533 let first = try await manager.download(from: remote)534 let second = try await manager.download(from: remote)535 defer {536 try? FileManager.default.removeItem(at: first)537 try? FileManager.default.removeItem(at: second)538 }539540 #expect(first.path != second.path)541 #expect(try Data(contentsOf: first) == Data("wallpaper".utf8))542 #expect(try Data(contentsOf: second) == Data("wallpaper".utf8))543 }544545 @Test("makeTempURL preserves the source extension when present")546 func makeTempURLPreservesExtension() {547 let withExtension = URL(string: "https://example.com/foo/wallpaper.png")!548 let withoutExtension = URL(string: "https://example.com/foo/wallpaper")!549550 #expect(WallpaperManager.makeTempURL(for: withExtension).pathExtension == "png")551 #expect(WallpaperManager.makeTempURL(for: withoutExtension).pathExtension == "img")552 #expect(553 WallpaperManager.makeTempURL(for: withExtension).path554 != WallpaperManager.makeTempURL(for: withExtension).path)555 }556557 @Test("setDesktopImage without screens throws instead of force-unwrapping")558 func setDesktopImageWithoutScreensThrows() {559 let manager = WallpaperManager(560 desktopSetter: { _, _ in },561 mainScreen: { nil }, firstScreen: { nil })562 let url = URL(fileURLWithPath: "/tmp/no-screens.jpg")563564 #expect(throws: WallhavenError.self) {565 try manager.setDesktopImage(from: url)566 }567 }568569 @Test("setDesktopImage prefers main over first screen when both are available")570 func setDesktopImagePrefersMain() throws {571 let mainScreen = NSScreen.main ?? NSScreen.screens.first572 guard let mainScreen else { return }573 let otherScreen = mainScreen574 var captured: [NSScreen] = []575 let manager = WallpaperManager(576 desktopSetter: { _, target in captured.append(target) },577 mainScreen: { mainScreen }, firstScreen: { otherScreen })578 let url = URL(fileURLWithPath: "/tmp/main-preferred.jpg")579580 try manager.setDesktopImage(from: url)581582 #expect(captured == [mainScreen])583 }584585 @Test("setDesktopImage falls back to first screen when main is missing")586 func setDesktopImageFallsBackToFirst() throws {587 guard let first = NSScreen.screens.first else { return }588 var captured: [NSScreen] = []589 let manager = WallpaperManager(590 desktopSetter: { _, target in captured.append(target) },591 mainScreen: { nil }, firstScreen: { first })592 let url = URL(fileURLWithPath: "/tmp/fallback.jpg")593594 try manager.setDesktopImage(from: url)595596 #expect(captured == [first])597 }598599 @Test("setDesktopImage default firstScreen closure calls NSScreen.screens.first")600 func setDesktopImageDefaultFirstScreenClosure() throws {601 guard let first = NSScreen.screens.first else { return }602 var captured: [NSScreen] = []603 604 605 let manager = WallpaperManager(606 desktopSetter: { _, target in captured.append(target) },607 mainScreen: { nil })608 let url = URL(fileURLWithPath: "/tmp/default-first.jpg")609610 try manager.setDesktopImage(from: url)611612 #expect(captured == [first])613 }614}615616@Suite("Autostart persistence", .serialized)617@MainActor618struct AutostartManagerTests {619 @Test("Enable writes a launch agent and disable removes it")620 func enableAndDisable() throws {621 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)622 defer { try? FileManager.default.removeItem(at: root) }623 let manager = AutostartManager(624 libraryDirectory: root, executablePath: { "/Applications/Haven.app/Contents/MacOS/Haven" })625626 #expect(!manager.isEnabled)627 manager.isEnabled = true628 #expect(manager.isEnabled)629 manager.isEnabled = true630631 let plist = root.appendingPathComponent("LaunchAgents/com.june.Haven.plist")632 let contents =633 try PropertyListSerialization.propertyList(634 from: Data(contentsOf: plist), format: nil) as? [String: Any]635 #expect(contents?["Label"] as? String == "com.june.Haven")636 #expect(637 contents?["ProgramArguments"] as? [String]638 == ["/Applications/Haven.app/Contents/MacOS/Haven"])639 #expect(contents?["RunAtLoad"] as? Bool == true)640641 manager.isEnabled = false642 #expect(!manager.isEnabled)643 manager.isEnabled = false644 }645646 @Test("Missing executable leaves autostart disabled")647 func missingExecutable() {648 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)649 defer { try? FileManager.default.removeItem(at: root) }650 let manager = AutostartManager(libraryDirectory: root, executablePath: { nil })651652 manager.isEnabled = true653654 #expect(!manager.isEnabled)655 }656657 @Test("Default executable provider writes only inside the injected library")658 func defaultExecutableProvider() {659 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)660 defer { try? FileManager.default.removeItem(at: root) }661 let manager = AutostartManager(libraryDirectory: root)662 manager.isEnabled = true663 #expect(manager.isEnabled)664 manager.isEnabled = false665 }666667 @Test("Unwritable launch-agent roots do not enable autostart")668 func writeFailure() throws {669 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)670 defer { try? FileManager.default.removeItem(at: file) }671 try Data("not a directory".utf8).write(to: file)672 let manager = AutostartManager(libraryDirectory: file, executablePath: { "/tmp/Haven" })673 manager.isEnabled = true674 #expect(!manager.isEnabled)675 }676677 @Test("Removal failures leave the launch agent enabled")678 func removalFailure() {679 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)680 defer { try? FileManager.default.removeItem(at: root) }681 let manager = AutostartManager(682 libraryDirectory: root, executablePath: { "/tmp/Haven" },683 removePlist: { _ in throw CocoaError(.fileWriteNoPermission) })684 manager.isEnabled = true685 manager.isEnabled = false686 #expect(manager.isEnabled)687 }688689 @Test("LaunchAgents directory-creation failures surface through lastError")690 func launchAgentsDirectoryFailureIsSurfaced() {691 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)692 defer { try? FileManager.default.removeItem(at: file) }693 try? Data("not a directory".utf8).write(to: file)694 let manager = AutostartManager(695 libraryDirectory: file, executablePath: { "/tmp/Haven" })696697 698 #expect(manager.lastError?.contains("LaunchAgents") == true)699 }700701 @Test("lastError is cleared by a successful enable/disable cycle")702 func lastErrorClearsOnSuccess() throws {703 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)704 defer { try? FileManager.default.removeItem(at: root) }705 let manager = AutostartManager(706 libraryDirectory: root, executablePath: { "/tmp/Haven" },707 writePlist: { _, _ in throw CocoaError(.fileWriteNoPermission) })708709 manager.isEnabled = true710 #expect(manager.lastError?.contains("Failed to enable autostart") == true)711712 713 let successManager = AutostartManager(714 libraryDirectory: root, executablePath: { "/tmp/Haven" })715 successManager.isEnabled = true716 successManager.isEnabled = false717 #expect(successManager.lastError == nil)718 }719}720721@Suite("Local wallpaper library", .serialized)722@MainActor723struct LocalWallpapersManagerTests {724 private func waitForScan(_ manager: LocalWallpapersManager) async {725 for _ in 0..<100 where manager.isLoading {726 try? await Task.sleep(nanoseconds: 10_000_000)727 }728 }729730 @Test("Scan filters files, reads metadata, and deletes wallpapers")731 func scanAndDelete() async throws {732 let defaults = UserDefaults.standard733 let oldPath = defaults.string(forKey: "saveFolderPath")734 defaults.removeObject(forKey: "saveFolderPath")735 defer {736 if let oldPath {737 defaults.set(oldPath, forKey: "saveFolderPath")738 } else {739 defaults.removeObject(forKey: "saveFolderPath")740 }741 }742 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)743 defer { try? FileManager.default.removeItem(at: root) }744 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)745 try onePixelPNG.write(to: root.appendingPathComponent("wallpaper.png"))746 try Data("broken".utf8).write(to: root.appendingPathComponent("broken.png"))747 try Data("ignored".utf8).write(to: root.appendingPathComponent("notes.txt"))748 let manager = LocalWallpapersManager()749750 manager.scanFolder(path: root.path)751 manager.scanFolder(path: root.path)752 await waitForScan(manager)753754 let wallpaper = try #require(manager.wallpapers.first)755 #expect(manager.wallpapers.count == 1)756 #expect(wallpaper.thumbnailURL == wallpaper.url)757 #expect(wallpaper.filename == "wallpaper.png")758 #expect(wallpaper.resolution == "1x1")759 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("broken.png")) == nil)760 #expect(LocalWallpapersManager.wallpaper(at: root.appendingPathComponent("missing.png")) == nil)761762 manager.deleteWallpaper(wallpaper)763 #expect(manager.wallpapers.isEmpty)764 manager.deleteWallpaper(wallpaper)765 #expect(manager.errorMessage?.hasPrefix("Failed to delete") == true)766 }767768 @Test("Invalid scan roots surface an error")769 func scanError() async throws {770 let defaults = UserDefaults.standard771 defaults.removeObject(forKey: "saveFolderPath")772 let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)773 defer { try? FileManager.default.removeItem(at: file) }774 try Data("file".utf8).write(to: file)775 let manager = LocalWallpapersManager()776777 manager.scanFolder(path: file.path)778 await waitForScan(manager)779780 #expect(manager.errorMessage?.hasPrefix("Failed to scan folder") == true)781 #expect(!manager.isLoading)782 }783784 @Test("Concurrent scan requests are dispatched, not silently dropped")785 func concurrentScansAreDispatched() async throws {786 let defaults = UserDefaults.standard787 let oldPath = defaults.string(forKey: "saveFolderPath")788 defaults.removeObject(forKey: "saveFolderPath")789 defer {790 if let oldPath {791 defaults.set(oldPath, forKey: "saveFolderPath")792 } else {793 defaults.removeObject(forKey: "saveFolderPath")794 }795 }796 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)797 defer { try? FileManager.default.removeItem(at: root) }798 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)799800 let manager = LocalWallpapersManager()801 802 manager.scanFolder(path: root.path)803 await waitForScan(manager)804 #expect(manager.wallpapers.isEmpty)805806 807 808 809 try onePixelPNG.write(to: root.appendingPathComponent("first.png"))810 try onePixelPNG.write(to: root.appendingPathComponent("second.png"))811 manager.scanFolder(path: root.path)812 manager.scanFolder(path: root.path)813 await waitForScan(manager)814 #expect(manager.wallpapers.contains(where: { $0.filename == "first.png" }))815 #expect(manager.wallpapers.contains(where: { $0.filename == "second.png" }))816 #expect(!manager.isLoading)817 #expect(manager.errorMessage == nil)818 }819820 @Test("Reissuing a scan during an in-flight scan observes later state via generation")821 func scanDuringFlightWinsForLatestGeneration() async throws {822 let defaults = UserDefaults.standard823 let oldPath = defaults.string(forKey: "saveFolderPath")824 defaults.removeObject(forKey: "saveFolderPath")825 defer {826 if let oldPath {827 defaults.set(oldPath, forKey: "saveFolderPath")828 } else {829 defaults.removeObject(forKey: "saveFolderPath")830 }831 }832 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)833 defer { try? FileManager.default.removeItem(at: root) }834 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)835 let manager = LocalWallpapersManager()836837 838 839 840 841 manager.scanFolder(path: root.path)842 manager.scanFolder(path: root.path)843 manager.scanFolder(path: root.path)844 await waitForScan(manager)845 #expect(!manager.isLoading)846 #expect(manager.wallpapers.isEmpty)847 #expect(manager.errorMessage == nil)848849 850 try onePixelPNG.write(to: root.appendingPathComponent("added.png"))851 manager.scanFolder(path: root.path)852 await waitForScan(manager)853 #expect(manager.wallpapers.contains(where: { $0.filename == "added.png" }))854 }855856 @Test("Monitoring configured folders observes filesystem changes and stops")857 func monitoring() async throws {858 let defaults = UserDefaults.standard859 let oldPath = defaults.string(forKey: "saveFolderPath")860 let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)861 try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)862 defaults.set(root.path, forKey: "saveFolderPath")863 defer {864 try? FileManager.default.removeItem(at: root)865 if let oldPath {866 defaults.set(oldPath, forKey: "saveFolderPath")867 } else {868 defaults.removeObject(forKey: "saveFolderPath")869 }870 }871 let manager = LocalWallpapersManager()872 await waitForScan(manager)873874 try onePixelPNG.write(to: root.appendingPathComponent("observed.png"))875 try? await Task.sleep(nanoseconds: 100_000_000)876 await waitForScan(manager)877 #expect(manager.wallpapers.contains(where: { $0.filename == "observed.png" }))878879 manager.stopMonitoring()880 try? await Task.sleep(nanoseconds: 500_000_000)881882 let invalidMonitor = FolderMonitor(folderPath: root.appendingPathComponent("missing").path) {}883 #expect(invalidMonitor.start() == false)884 #expect(invalidMonitor.openFailureMessage?.isEmpty == false)885 invalidMonitor.closeFileDescriptor()886887 888 889 890 let broken = FolderMonitor(891 folderPath: root.path, callback: {},892 openPath: { _, _ in -1 })893 #expect(broken.start() == false)894 #expect(broken.openFailureMessage?.localizedCaseInsensitiveContains("could not watch") == true)895896 let descriptor = open("/dev/null", O_RDONLY)897 let descriptorMonitor = FolderMonitor(898 folderPath: "/dev/null", fileDescriptor: descriptor, callback: {})899 descriptorMonitor.closeFileDescriptor()900 #expect(fcntl(descriptor, F_GETFD) == -1)901 }902903 @Test("startMonitoring surfaces folder-monitor open failures through errorMessage")904 func startMonitoringSurfacesMonitorFailures() async {905 906 907 908 909 let defaults = UserDefaults.standard910 let oldPath = defaults.string(forKey: "saveFolderPath")911 defaults.removeObject(forKey: "saveFolderPath")912 defer {913 if let oldPath {914 defaults.set(oldPath, forKey: "saveFolderPath")915 } else {916 defaults.removeObject(forKey: "saveFolderPath")917 }918 }919 920 921 let missing = FileManager.default.temporaryDirectory922 .appendingPathComponent("haven-missing-\(UUID().uuidString)").path923 let manager = LocalWallpapersManager()924 manager.startMonitoring(folderPath: missing)925 #expect(manager.errorMessage?.isEmpty == false)926 manager.stopMonitoring()927 }928}