Files
ds-galo/openspec/changes/archive/2026-10-20-fix-album-sync-churn-and-degradation/tasks.md
T
iu 3a44844ff9 chore(openspec): архивировать fix-album-sync-churn-and-degradation и синхронизировать спеки
В спеку phone-album-sync добавлены требования о роли локального файла,
подтверждении новой версии до удаления устаревшей и разгрузке без
незавершённой загрузки; уточнены синхронизация изменений, провенанс и
возврат оригиналов.
2026-10-20 20:35:52 +03:00

7.7 KiB
Raw Blame History

Tasks

1. Роль локального файла вместо состояния индекса

  • 1.1 Добавить в LocalAsset (data/sync/SyncPlan.kt) признаки метки превью (stampedOriginalHash, stampedAssetKey), а в DeviceLocalAssetProvider читать ProvenanceStamp в момент пересчёта хеша файла; решение «локальный файл — созданное приложением превью» вынести в чистую функцию с юнит-тестом (cd android && ./gradlew :app:testDebugUnitTest --tests "*ProvenanceStampTest" --tests "*SyncPlannerTest").
  • 1.2 В SyncPlanner.plan не загружать превью и не удалять из-за него серверную копию: использовать метку как признак роли и идентичность оригинала, отдавать новое действие SyncAction.MarkPreview; покрыть SyncPlannerTest случаями «состояние индекса LOCAL, файл — превью с меткой» (ожидается MarkPreview), «другое содержимое без метки» (ожидается загрузка) и «превью потерявшего копию элемента» (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest").
  • 1.3 В SyncCoordinator применять MarkPreview (состояние PREVIEW) и не разгружать элементы с незавершённой загрузкой (SyncUploadQueue.isPending) в offloadUploaded; покрыть SyncCoordinatorTest случаями «расхождение состояния выправляется» и «загрузка в очереди — разгрузки нет» (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncCoordinatorTest").
  • 1.4 Распознавать постер разгруженного видео как превью исходного элемента по метке (asset указывает на видео, а не на постер) и не удалять серверную копию видео, уступившего место постеру; покрыть SyncPlannerTest случаями «постер связывается с видео, не загружается и не удаляется» и «видео без постера удаляется как раньше» (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest").

2. Замена версии с подтверждением

  • 2.1 Добавить replacesPhotoId в UploadJobEntity, миграцию MIGRATION_8_9 (версия 9) и ветку в MigrationTest; проверить cd android && ./gradlew :app:testDebugUnitTest --tests "*MigrationTest" --tests "*UploadRepositoryTest".
  • 2.2 В SyncPlanner заменить пару «Upload + DeleteOnServer» для изменённого содержимого на SyncAction.ReplaceUpload(asset, stalePhotoId), а в SyncCoordinator писать надгробие для прежней копии и ставить задание с полем замены; покрыть SyncPlannerTest (ожидается ReplaceUpload) и SyncCoordinatorTest (надгробие и задание с заменой) (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest" --tests "*SyncCoordinatorTest").
  • 2.3 Вынести сверку принятой контрольной суммы в чистую функцию и подключить её в UploadWorker: совпадение original_checksum с хешем задания удаляет прежнюю копию и записывает провенанс, расхождение или ошибка — не удаляет, не разгружает и оставляет задание с ошибкой; покрыть SyncUploadTest (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest").
  • 2.4 Сервер: заполнять p.OriginalChecksum в Service.Upload до ответа и публикации события (существующее требование photos «Контрольная сумма оригинала» распространяется на данные созданного фото, но ответ на загрузку её не отдавал — живая проверка показала, что из-за этого отклонялись все синхронные загрузки); покрыть тестом ответа на загрузку (cd backend && go test ./internal/app/ -run TestUploadResponseExposesOriginalChecksum -v).
  • 2.5 Клиент остаётся устойчивым к инстансу без контрольной суммы: «не сообщено» SHALL NOT считаться расхождением — загрузка принимается, а прежняя копия не удаляется; покрыть SyncUploadTest случаями «не сообщено» и «нет заявки на содержимое» (cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest").

3. Состояние после возврата оригинала

  • 3.1 В RestoreManager помечать восстановленный элемент UPLOADED (оригинал и серверная копия) вместо LOCAL; покрыть RestoreManagerTest и GallerySyncPolicyTest случаем «после возврата повторной разгрузки нет» (cd android && ./gradlew :app:testDebugUnitTest --tests "*RestoreManagerTest" --tests "*GallerySyncPolicyTest").

5. Интеграционная проверка

  • 5.1 Прогнать весь Android-набор и собрать debug-APK (cd android && ./gradlew test :app:assembleDebug).

Workflow follow-up

  • Восстановление пострадавших оригиналов (бывшая задача 4) — ручная процедура владельца инстанса, выполняется вне change, креды в репозиторий и артефакты не попадают:
    • вернуть из корзины 6d4e1897-3a7e-4cba-9d85-5bbb4b06f5a4 (IMG_20261004_163151.jpg, 3 437 694 Б, 3072x4096) и 991ea4ee-debb-4ecb-bc26-e4d1a2140085 (IMG_20261009_153136.jpg, 2 226 043 Б, 3072x4096) в альбом N2 (POST /api/photos/{id}/restore);
    • удалить деградировавшие копии b0b29905-712b-4143-9d8c-0b74aa567ce5 и 1f49e8fb-ea9c-4764-8daa-7b185be107e9 (DELETE /api/photos/{id});
    • проверить, что в альбоме осталось по одной записи с каждым именем и оригинал у обеих 3072x4096.
  • Уборка после регрессии первой версии сверки контрольной суммы (живая проверка 2026-10-16): в новом альбоме U2 (e7ae31c9-805c-42f5-8fd2-ea2304f0c485) лежат 19 мягко удалённых копий десяти файлов, которые приложение само же и отклонило. Их список (id и имена) — в отчёте; удаляются точечно DELETE /api/photos/{id}/permanent. Корзину целиком очищать нельзя, пока не восстановлены оригиналы N2: они лежат там же.
  • Заархивировать изменение после прохождения ревью проекта.
  • Проверить результат архивации.