Review hardening for Task 8 (test-server workflow: testers install older
APKs over a newer DB):
- Old APK opening a v2 database hits drift's downgrade path, which leaves
the force column in place but writes user_version back to 1. Reinstalling
the new APK then re-runs onUpgrade(1,2) and addColumn crashes with
"duplicate column name: force", failing the DB open.
- onUpgrade now checks PRAGMA table_info(upload_tasks) and only adds the
force column when absent; the normal v1 -> v2 path is unchanged.
- Migration test simulates the downgrade-reupgrade cycle on a file-backed
database (v2 open -> user_version forced back to 1 -> re-upgrade),
asserting the open succeeds, legacy rows survive, and the column stays
writable. RED before the guard (SqliteException: duplicate column name).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Task 8: close the gap between server-side duplicate detection (90dcc58)
and the app.
- ApiClient.mapDioException: normalize 409 wire body reusing
VALIDATION_FAILED with message=PHOTO_DUPLICATE to
ErrorCodes.photoDuplicate (message-based; auto-compat if the contract
later adds a dedicated code)
- l10n zh/en errorPhotoDuplicate; error_l10n maps the code (queue page
inherits via fallback)
- UploadEngine._classifyFailure: PHOTO_DUPLICATE joins the no-auto-retry
bucket (retrying the same init body would 409 again); force to skip
dedup travels only through the enqueue path
- UploadTasks drift table +force column (schema v2, addColumn migration;
legacy rows default false); UploadTaskDraft/UploadTask/prepare carry it
- enqueueForUpload +forcePaths; duplicate dialog "全部上传" (capture page
+ album detail page) now returns DuplicateDialogChoice so confirmed
duplicates enqueue with force=true (server adjudicates owner/admin)
- engine init body carries force only when true (absent otherwise)
Co-Authored-By: Claude Code <noreply@anthropic.com>