close the test gaps a QA pass found, and de-flake the flaky ones

Verify the Subsonic token hash and salt freshness, and fall back to per-album
songs when the empty-search probe fails with an I/O error instead of
aborting the whole sync.

Add a real-server sync test that mirrors navidrome and gonic into Room and
reads the rows back, plus SyncManager coverage for the success, coalescing
and mid-sync-failure paths.

Cover the sort mapping and delete/ping ViewModels, the download engine's
request flags, the queue helpers, download prefs and migration data
preservation.

Drop the leaked collectors in the ViewModel/queue tests, document the
screenshot tests as render smoke tests, bind a real search term in the
trigram plan test, and assert a positive in-flight invariant alongside the
concurrency timing checks.
This commit is contained in:
austinried
2026-10-04 00:30:04 +00:00
parent 5cbe4c6839
commit 684cc5a287
27 changed files with 989 additions and 25 deletions
@@ -14,6 +14,7 @@ import com.subtracks.data.repo.DownloadRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
@@ -42,6 +43,10 @@ class DownloadNotifier(
}
}
internal fun close() {
scope.cancel()
}
private fun build(content: DownloadNotification): Notification =
NotificationCompat
.Builder(context, CHANNEL_ID)
@@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@@ -78,7 +79,9 @@ class DownloadRepository(
}
fun close() {
val job = scope.coroutineContext[Job]
scope.cancel()
runBlocking { job?.join() }
}
fun states(): StateFlow<Map<String, SongDownload>> = statesFlow
@@ -27,6 +27,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import org.xml.sax.Attributes
import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class)
class SubsonicSource(
@@ -272,6 +273,8 @@ class SubsonicSource(
) { input -> SubsonicXml.containsEntity(input, "song") }
} catch (_: SubsonicException) {
false
} catch (_: IOException) {
false
}
emptyQuerySearchSupported = supported
return supported
@@ -159,8 +159,8 @@ class LibraryViewModel(
fun sync() = syncManager.requestSync()
}
private fun ListQuery.albumSort(): AlbumSort = AlbumSort.entries.firstOrNull { it.name == sort } ?: AlbumSort.Name
internal fun ListQuery.albumSort(): AlbumSort = AlbumSort.entries.firstOrNull { it.name == sort } ?: AlbumSort.Name
private fun ListQuery.artistSort(): ArtistSort = ArtistSort.entries.firstOrNull { it.name == sort } ?: ArtistSort.Name
internal fun ListQuery.artistSort(): ArtistSort = ArtistSort.entries.firstOrNull { it.name == sort } ?: ArtistSort.Name
private fun ListQuery.playlistSort(): PlaylistSort = PlaylistSort.entries.firstOrNull { it.name == sort } ?: PlaylistSort.Name
internal fun ListQuery.playlistSort(): PlaylistSort = PlaylistSort.entries.firstOrNull { it.name == sort } ?: PlaylistSort.Name