fix(sync): let content-equal echoes refresh drifted locators
The LWW skip path compared only annotation content (text, color, note, percentages) — locator columns were not part of 'changed'. A device echo with identical content therefore resolved to skip, and the freshly re-derived canonical CFIs were discarded in the same request that computed them: a highlight whose stored start anchor had been corrupted by the old percentage-exact bug could never heal, because every subsequent echo carried the same text and was skipped before the locator columns were written. Observed live: a push converted the cross-block highlight's anchors correctly (structural, heading to paragraph) yet the row kept its garbage start CFI and the highlight stayed unpaintable on the web. Echo saves now treat a non-empty incoming locator that differs from the stored one as a change (locatorRefresher): empty locators still coalesce (no drift), and once healed the echo produces identical CFIs, so the steady state remains skip — no write churn. Highlights check epubcfi_start/end; bookmarks check cfi_position/position. applyBookmarkLWW also gains the empty-locator coalescing the highlight path already had: web bookmark edits carry no device locators, and a title/note edit must not wipe the stored device-native position.
This commit is contained in:
@@ -369,3 +369,82 @@ func TestIncomingNewerThanTombstone(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Content-equal device echoes must still count as changed when they carry
|
||||
// a locator that differs from the stored one: echoes re-derive canonical
|
||||
// CFIs on every push, and a better conversion (or a repair of a corrupted
|
||||
// anchor) has to reach the row — otherwise the skip path discards it and
|
||||
// the bad locator can never heal.
|
||||
func TestCompareIncomingLocatorDriftRefreshes(t *testing.T) {
|
||||
svc := &AnnotationService{}
|
||||
existing := database.MediaHighlights{
|
||||
SelectionText: "CHAPTER IV. “What a pity it is, Elinor,”",
|
||||
Color: pgtype.Text{String: "#90caf9", Valid: true},
|
||||
EpubcfiStart: pgtype.Text{String: "epubcfi(/6/12!/4/2[x]/32/3:576)", Valid: true},
|
||||
EpubcfiEnd: pgtype.Text{String: "epubcfi(/6/12!/4/2[x]/4/1:93)", Valid: true},
|
||||
}
|
||||
base := SaveHighlightRequest{
|
||||
SelectionText: existing.SelectionText,
|
||||
Color: "#90caf9",
|
||||
}
|
||||
|
||||
t.Run("identical content and locators skip", func(t *testing.T) {
|
||||
req := base
|
||||
req.EpubcfiStart = existing.EpubcfiStart.String
|
||||
req.EpubcfiEnd = existing.EpubcfiEnd.String
|
||||
_, changed := svc.compareIncoming(req, existing)
|
||||
if changed {
|
||||
t.Error("identical echo must not rewrite the row")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("better start locator refreshes despite identical content", func(t *testing.T) {
|
||||
req := base
|
||||
req.EpubcfiStart = "epubcfi(/6/12!/4/2[x]/2/3:0)"
|
||||
req.EpubcfiEnd = existing.EpubcfiEnd.String
|
||||
_, changed := svc.compareIncoming(req, existing)
|
||||
if !changed {
|
||||
t.Error("locator drift on a content-equal echo must trigger an update")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty incoming locators leave the row alone", func(t *testing.T) {
|
||||
req := base // no CFIs: applyLWW coalesces empty to stored
|
||||
_, changed := svc.compareIncoming(req, existing)
|
||||
if changed {
|
||||
t.Error("empty locators coalesce; must not count as drift")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bookmarks share the echo-refresh rule for their locators.
|
||||
func TestCompareIncomingBookmarkLocatorDrift(t *testing.T) {
|
||||
svc := &AnnotationService{}
|
||||
existing := database.MediaBookmarks{
|
||||
Title: "in CHAPTER IV.",
|
||||
CfiPosition: pgtype.Text{String: "epubcfi(/6/12!/4/2[x]/2/3:0)", Valid: true},
|
||||
Position: pgtype.Text{String: "/body/DocFragment[6]/body/div[1]/h2[1]/text().0", Valid: true},
|
||||
}
|
||||
|
||||
t.Run("same locators skip", func(t *testing.T) {
|
||||
req := SaveBookmarkRequest{
|
||||
Title: existing.Title,
|
||||
CFIPosition: existing.CfiPosition.String,
|
||||
Position: existing.Position.String,
|
||||
}
|
||||
if _, changed := svc.compareIncomingBookmark(req, existing); changed {
|
||||
t.Error("identical bookmark echo must not rewrite the row")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new CFI refreshes despite same title", func(t *testing.T) {
|
||||
req := SaveBookmarkRequest{
|
||||
Title: existing.Title,
|
||||
CFIPosition: "epubcfi(/6/12!/4/2[x]/2/3:5)",
|
||||
Position: existing.Position.String,
|
||||
}
|
||||
if _, changed := svc.compareIncomingBookmark(req, existing); !changed {
|
||||
t.Error("bookmark locator drift must trigger an update")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user