fix(app-phone): звук при watch (диагностика+подстраховка mirrorByItem) + качка не воскресает после удаления

- onWatch: явные логи в каждой точке отказа (watch result / audio source / уже играет / не скачано / mediaDir пуст), свежий mirrorByItem если files не догрузился, звук стартует с позиции очков при уже идущем видео
- resolveAudioSource → resolveAudioSourceDetailed (AudioSourceResult с причиной null)
- DownloadManager: поколения generations — delete() инвалидирует старую корутину (не воскрешает качку), ensureActive перед .part/rename, jobs.remove(itemId, job)
- Тесты 62/0 (в т.ч. поколения — поймали null != 0L)

Инфраструктурный блокер (не код): SeaweedFS на Server3 76.66 в IO-шторме (load ~184, wa 20%, чужой yt-dlp на хосте) — большие GET с s3.binom.pw висят/рвутся с ~14:00; качка на очки FAILED по этой причине.
This commit is contained in:
Hermes Agent
2026-08-19 17:19:53 +03:00
parent fafdddc121
commit 49dac8b33f
29 changed files with 2237 additions and 173 deletions
@@ -0,0 +1,119 @@
package pw.binom.viewmate.glasses
import java.io.File
import java.nio.file.Files
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class GlassesDownloadManagerTest {
private fun tempDir(): File = Files.createTempDirectory("glasses-dm-test").toFile()
private fun newBaseDir(): File {
val base = tempDir()
base.deleteOnExit()
return base
}
@Test
fun isFullyOnDiskRequiresAllFilesNoParts() {
val dir = newBaseDir()
val names = listOf("video.mkv", "audio-0.ogg", "audio-1.ogg")
assertFalse(isFullyOnDisk(dir, names), "пустой каталог — не полный")
File(dir, "video.mkv").writeBytes(ByteArray(10))
assertFalse(isFullyOnDisk(dir, names), "только видео — не полный")
File(dir, "audio-0.ogg").writeBytes(ByteArray(5))
assertFalse(isFullyOnDisk(dir, names), "не все аудио — не полный")
File(dir, "audio-1.ogg").writeBytes(ByteArray(5))
assertTrue(isFullyOnDisk(dir, names), "все файлы — полный")
}
@Test
fun isFullyOnDiskRejectsPartFiles() {
val dir = newBaseDir()
File(dir, "video.mkv").writeBytes(ByteArray(10))
File(dir, "audio-0.ogg").writeBytes(ByteArray(5))
File(dir, "audio-0.ogg.part").writeBytes(ByteArray(2))
assertFalse(isFullyOnDisk(dir, listOf("video.mkv", "audio-0.ogg")), ".part не даёт полного")
}
@Test
fun isFullyOnDiskEmptyListIsFalse() {
assertFalse(isFullyOnDisk(newBaseDir(), emptyList()))
}
@Test
fun partNameAppendsSuffix() {
assertEquals("video.mkv.part", partName("video.mkv"))
assertEquals("audio-0.ogg.part", partName("audio-0.ogg"))
}
@Test
fun requestForAddsRangeFromExistingPart() {
val noResume = glassesRequestFor("https://s3/mirror/video.mkv?sig", 0)
assertNull(noResume.header("Range"), "без .part Range-заголовка быть не должно")
val resume = glassesRequestFor("https://s3/mirror/video.mkv?sig", 1_863_946_240)
assertEquals("bytes=1863946240-", resume.header("Range"))
}
@Test
fun filePercentClampsToRange() {
assertEquals(0, filePercent(0, 0))
assertEquals(50, filePercent(500, 1000))
assertEquals(100, filePercent(1000, 1000))
assertEquals(99, filePercent(999, 1000))
}
@Test
fun overallPercentSumsAcrossFiles() {
val done = mapOf("video.mkv" to 100L, "audio-0.ogg" to 25L, "audio-1.ogg" to 0L)
val total = mapOf("video.mkv" to 100L, "audio-0.ogg" to 50L, "audio-1.ogg" to 50L)
assertEquals(62, overallPercent(done, total)) // 125/200
}
@Test
fun overallPercentZeroWithoutTotals() {
assertEquals(0, overallPercent(mapOf("video.mkv" to 5L), emptyMap()))
}
@Test
fun stateStoreWritesAndReads() {
val file = File(tempDir(), "status.json")
val store = GlassesStateStore(file)
val state = GlassesState(
expected = mapOf("i1" to listOf("video.mkv", "audio-0.ogg")),
downloaded = listOf("i1"),
)
store.write(state)
val read = store.read()
assertEquals(state, read)
assertTrue(file.exists())
}
@Test
fun stateStoreMissingReadsEmpty() {
val store = GlassesStateStore(File(tempDir(), "status.json"))
assertEquals(GlassesState(), store.read())
}
@Test
fun localVideoPathNullWhenNotDownloaded() {
val base = newBaseDir()
val dir = File(base, "i1").apply { mkdirs() }
File(dir, "video.mkv").writeBytes(ByteArray(10))
// без ожидаемого списка файлов isFullyDownloaded = false
assertFalse(isFullyOnDisk(dir, listOf("video.mkv", "audio-0.ogg")))
}
}