import Testing import Foundation import MusicShared import Network @testable import Music @MainActor struct RemoteLibraryDisplayTests { // Reproduces the reported remote-mode bug: "connect + DB download seem fine, // but no tracks show up". This exercises the exact step enterRemoteMode performs // after the download — pointing a LibraryViewModel at the downloaded DB — and // asserts the view model's `tracks` array actually populates. @Test(.timeLimit(.minutes(1))) func remoteLibraryViewModelPopulatesTracksAfterDownload() async throws { // 1. Create a host database and insert three tracks (the "host library"). let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: tempDir) } let hostDBPath = tempDir.appendingPathComponent("host.sqlite").path let hostDB = try DatabaseService(path: hostDBPath) for i in 1...3 { var t = Track.fixture(fileURL: "/song\(i).mp3", title: "Song \(i)") try hostDB.insert(&t) } // 2. Start the HostServer configured with the DB so GET /db serves a backup copy. let server = HostServer(dbPath: hostDBPath) server.configure(player: nil, db: hostDB) try server.start() try await Task.sleep(for: .milliseconds(200)) let port = server.actualPort! defer { server.stop() } // 3. Download the DB over HTTP and write the body to disk, exactly as // RemoteClient.handleDBData does when a remote connects to a host. let body = try await httpGet(host: "127.0.0.1", port: port, path: "/db") let downloadedPath = tempDir.appendingPathComponent("remote_db.sqlite").path try body.write(to: URL(fileURLWithPath: downloadedPath)) // 4. Point a LibraryViewModel at the downloaded DB — this is precisely what // MusicApp.enterRemoteMode() does after a successful connect. let remoteDB = try DatabaseService(path: downloadedPath) let library = LibraryViewModel(db: remoteDB) // 5. The LibraryViewModel loads via an async GRDB ValueObservation, so poll // briefly for the observation to deliver its initial value. try await waitUntil { library.tracks.count == 3 } // 6. The remote library MUST display all three downloaded tracks. If this // fails with 0 tracks, it reproduces the "no tracks after connect" bug. #expect(library.tracks.count == 3) } // MARK: - Helpers /// Polls `condition` on the main actor until it becomes true or the timeout elapses. private func waitUntil( timeout: Duration = .seconds(3), _ condition: @MainActor () -> Bool ) async throws { let deadline = ContinuousClock.now + timeout while ContinuousClock.now < deadline { if condition() { return } try await Task.sleep(for: .milliseconds(50)) } } /// Performs a simple HTTP GET using NWConnection and returns the response body. private func httpGet(host: String, port: UInt16, path: String) async throws -> Data { try await withCheckedThrowingContinuation { continuation in let connection = NWConnection( host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!, using: .tcp ) connection.stateUpdateHandler = { state in if case .ready = state { let request = "GET \(path) HTTP/1.1\r\nHost: \(host)\r\nConnection: close\r\n\r\n" connection.send(content: Data(request.utf8), completion: .contentProcessed { _ in }) connection.receiveMessage { data, _, _, error in if let error { continuation.resume(throwing: error) } else if let data, let range = data.range(of: Data("\r\n\r\n".utf8)) { continuation.resume(returning: Data(data[range.upperBound...])) } else { continuation.resume(returning: data ?? Data()) } connection.cancel() } } else if case .failed(let error) = state { continuation.resume(throwing: error) } } connection.start(queue: .main) } } }