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

35 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks
## 1. Роль локального файла вместо состояния индекса
- [x] 1.1 Добавить в `LocalAsset` (`data/sync/SyncPlan.kt`) признаки метки превью (`stampedOriginalHash`, `stampedAssetKey`), а в `DeviceLocalAssetProvider` читать `ProvenanceStamp` в момент пересчёта хеша файла; решение «локальный файл — созданное приложением превью» вынести в чистую функцию с юнит-тестом (`cd android && ./gradlew :app:testDebugUnitTest --tests "*ProvenanceStampTest" --tests "*SyncPlannerTest"`).
- [x] 1.2 В `SyncPlanner.plan` не загружать превью и не удалять из-за него серверную копию: использовать метку как признак роли и идентичность оригинала, отдавать новое действие `SyncAction.MarkPreview`; покрыть `SyncPlannerTest` случаями «состояние индекса `LOCAL`, файл — превью с меткой» (ожидается `MarkPreview`), «другое содержимое без метки» (ожидается загрузка) и «превью потерявшего копию элемента» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest"`).
- [x] 1.3 В `SyncCoordinator` применять `MarkPreview` (состояние `PREVIEW`) и не разгружать элементы с незавершённой загрузкой (`SyncUploadQueue.isPending`) в `offloadUploaded`; покрыть `SyncCoordinatorTest` случаями «расхождение состояния выправляется» и «загрузка в очереди — разгрузки нет» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncCoordinatorTest"`).
- [x] 1.4 Распознавать постер разгруженного видео как превью исходного элемента по метке (`asset` указывает на видео, а не на постер) и не удалять серверную копию видео, уступившего место постеру; покрыть `SyncPlannerTest` случаями «постер связывается с видео, не загружается и не удаляется» и «видео без постера удаляется как раньше» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest"`).
## 2. Замена версии с подтверждением
- [x] 2.1 Добавить `replacesPhotoId` в `UploadJobEntity`, миграцию `MIGRATION_8_9` (версия 9) и ветку в `MigrationTest`; проверить `cd android && ./gradlew :app:testDebugUnitTest --tests "*MigrationTest" --tests "*UploadRepositoryTest"`.
- [x] 2.2 В `SyncPlanner` заменить пару «`Upload` + `DeleteOnServer`» для изменённого содержимого на `SyncAction.ReplaceUpload(asset, stalePhotoId)`, а в `SyncCoordinator` писать надгробие для прежней копии и ставить задание с полем замены; покрыть `SyncPlannerTest` (ожидается `ReplaceUpload`) и `SyncCoordinatorTest` (надгробие и задание с заменой) (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest" --tests "*SyncCoordinatorTest"`).
- [x] 2.3 Вынести сверку принятой контрольной суммы в чистую функцию и подключить её в `UploadWorker`: совпадение `original_checksum` с хешем задания удаляет прежнюю копию и записывает провенанс, расхождение или ошибка — не удаляет, не разгружает и оставляет задание с ошибкой; покрыть `SyncUploadTest` (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest"`).
- [x] 2.4 Сервер: заполнять `p.OriginalChecksum` в `Service.Upload` до ответа и публикации события (существующее требование `photos` «Контрольная сумма оригинала» распространяется на данные созданного фото, но ответ на загрузку её не отдавал — живая проверка показала, что из-за этого отклонялись все синхронные загрузки); покрыть тестом ответа на загрузку (`cd backend && go test ./internal/app/ -run TestUploadResponseExposesOriginalChecksum -v`).
- [x] 2.5 Клиент остаётся устойчивым к инстансу без контрольной суммы: «не сообщено» SHALL NOT считаться расхождением — загрузка принимается, а прежняя копия не удаляется; покрыть `SyncUploadTest` случаями «не сообщено» и «нет заявки на содержимое» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest"`).
## 3. Состояние после возврата оригинала
- [x] 3.1 В `RestoreManager` помечать восстановленный элемент `UPLOADED` (оригинал и серверная копия) вместо `LOCAL`; покрыть `RestoreManagerTest` и `GallerySyncPolicyTest` случаем «после возврата повторной разгрузки нет» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*RestoreManagerTest" --tests "*GallerySyncPolicyTest"`).
## 5. Интеграционная проверка
- [x] 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`: они лежат там же.
- Заархивировать изменение после прохождения ревью проекта.
- Проверить результат архивации.