Fixed
- CloudKit 401 / persistent 421 no longer loops under Docker restart. Removed the 0.8.0 cache-fallback that accepted cached session data when
/validateand/accountLoginboth returned 421 — stale cached tokens would then produce a CloudKit 401, the process exited, and Docker restarted with the same cache forever. Auth now falls through to SRP in every stale-session case (trust_token is preserved, so 2FA is skipped in the common path). CloudKit 401 maps toICloudError::SessionExpiredand CloudKit 421 (after one pool reset) maps toICloudError::MisdirectedRequest; the sync loop handles both by invalidating the validation cache, forcing SRP, and retrying init. A second failure surfaces cleanly. (#217, #199) - Final error output carries a timestamp. The top-level
anyhow::Erroris now routed throughtracing::error!in addition to stderr, so crash messages indocker logs/journalctlcarry the sameYYYY-MM-DDTHH:MM:SS INFO kei::...prefix as the rest of the output. Makes the log timeline correlate cleanly instead of jumping from "kei::sync_loop: ..." lines to an unprefixed "Error: ..." line. - Duplicate
--albumnames no longer error -sync --album X --album Xpreviously failed with "Album 'X' not found" because the album map was drained on first match. Duplicate names are now deduplicated before resolution. (#219)
Added
- Live download-integrity tests - end-to-end coverage for data-sacred invariants: deleted files get re-downloaded on next sync, and truncated files don't cause data loss. (#220)
- Live gap tests -
sync_watch_runs_multiple_cyclesverifies watch mode completes 2+ cycles,sync_report_json_writes_valid_schemaparses--report-jsonoutput against the schema, and new password subcommand edge cases cover clear-without-stored-credential and backend-with-empty-data-dir. (#219)
Changed
- Simplified 421 recovery.
init_photos_serviceno longer ladders through pool reset → 10/30/60s backoff → full re-auth (~150 lines). It resets the HTTP pool once and, on a second 421, surfaces the typed error through the same SRP re-auth path that handles 401. Per-endpoint pool-reset retries invalidate_token/authenticate_with_tokenwere hoisted to a single reset atauth::authenticatelevel, removing duplication. - Typed
ICloudError::MisdirectedRequest. Replaces theConnection(String).contains("421")stringly-typed check inis_misdirected_request.check_apple_rscdno longer treatsX-Apple-I-Rscd: 421as an auth failure (never observed in the wild; only fed the removed cache-fallback). - CloudKit 421 response body logged at WARN. Issue #199's breakthrough was seeing
Missing X-APPLE-WEBAUTH-USER cookiein the 421 body — the single most useful signal for distinguishing ADP-class 421 from session-class 421. Surfaced by default so the next reporter doesn't needRUST_LOG=debug. - Live tests portable across accounts - test setup reads
ICLOUD_USERNAME,ICLOUD_PASSWORD, andKEI_TEST_ALBUMfrom env with sane defaults, no code edits needed. Shared bash helpers intests/lib.sh;run-gap-tests.sh,run-deep-validation.sh, andrun-docker-live.share now committed. (#222) - Consolidated 9 deprecation tests behind an
assert_deprecated(args, should_succeed, hint)helper. Test names preserved so CI output still pinpoints the failing case. (#219)
Full changelog: CHANGELOG.md