fix(metadata): stop hardcover sync colliding on reading attempt read ids

Read selection was positional: prefer any open remote read, else a cached book-level id, else the
first read returned, while the primary attempt was re-derived each sync from book dates. The two
could disagree, assigning one Hardcover read to two attempts and violating
reading_attempts_external_uidx, which aborted the sync and persisted the failure.

Treat reading_attempts.external_id as the mapping of record: an attempt keeps the read it owns, an
unlinked attempt adopts only an unclaimed read and claims it locally before the remote write, and a
fresh read is minted when nothing is adoptable. Never merge or delete attempts to resolve a
conflict, and keep soft-deleted ids reserved so importExternalRead tombstones still hold.

Also skip the link write when an attempt already owns its read, and map the aggregated attempt
timestamp to a Date so change detection stops comparing a driver string against lastSyncedAt.

Closes #907
This commit is contained in:
neon
2026-08-21 18:18:29 +00:00
parent 9d116c3e97
commit cbd5f56778
8 changed files with 1136 additions and 46 deletions
+21
View File
@@ -285,6 +285,27 @@ export const E2E_SUITES = Object.freeze({
...SHARED_DB_AND_HELPER_PATHS,
],
},
"reading-attempts": {
id: "reading-attempts",
name: "Reading Attempts",
timeout: 120,
lane: "full",
description: "Reading attempt lifecycle, external read identity, and Hardcover read ownership suite",
vitestTarget: "test/reading-attempts.e2e-spec.ts",
junitOutput: `${TEST_RESULTS_DIR}/reading-attempts-e2e-junit.xml`,
prepareDedicatedDatabase: true,
useDedicatedDatabase: true,
changedPaths: [
"server/src/modules/user-book-status/**",
"server/src/modules/hardcover/**",
"server/src/modules/dashboard/**",
"server/test/reading-attempts.e2e-spec.ts",
"server/test/e2e/**",
".github/workflows/e2e-runner.yml",
"packages/types/src/book.ts",
...SHARED_DB_AND_HELPER_PATHS,
],
},
"book-api-contract": {
id: "book-api-contract",
name: "Book API Contract",