Skip to content

test(sync): add the P2 sync integration scenarios - #321

Merged
mohamede1945 merged 20 commits into
test/sync-p1-scenariosfrom
test/sync-p2-scenarios
Oct 14, 2026
Merged

mohamede1945 merged 20 commits into
test/sync-p1-scenariosfrom
test/sync-p2-scenarios

Conversation

@mohamede1945

@mohamede1945 mohamede1945 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #320 (P1), then #319 (P0), #318 (harness) and #317. This is the last PR in the integration-test stack.

Summary

Adds the P2 end-to-end sync scenarios: 28 of 29 catalog cases. The rules are the same as P0 and P1. Tests use only the public API plus server state, and every known issue is pinned to its exact current failure.

Area Pass Known issue
Lifecycle 2 0
Notes 1 3 (S7→S4, S13, S12)
Text 2 1 (S13)
Collections 1 2 (S26, S9)
Ayah bookmarks / Favorites 2 2 (S23 across pages, S6)
Highlights 2 0
Reading bookmarks 0 1 (S13)
Reading sessions 1 0
Import 1 (by design) 2 (S20, S14)
Failure / protocol 3 1 (S27)
Observability 1 0

Skipped (not reachable through the public API):

  • OBS-04: no public API lists held items.
  • The highlight half of COL-19: highlight collection ids aren't exposed.

New issues

  • S26: a remote rename restores a collection this device deleted, but the delete's membership removals are still pushed. Both sides end up with an empty collection, so the verses are silently lost.
  • S27 (low): a {} reading-bookmark entry clears the local slot. The backend sends {} for a row whose entity vanished.

Harness changes

  • restart() waits for launch recovery. A logout left unfinished by a crash no longer hangs it.
  • The duplicate check reports each duplicate on its own:
    • knownDuplicates takes a matcher and fails on any other duplicate;
    • allowDuplicates(reason, match) covers duplicates that are expected by design, such as legacy-import edits.
  • The fake sends a vanished membership as its {collectionId, bookmarkId} key, as the backend does, not as {}.
  • ERR-14 to ERR-17 moved into ProtocolScenariosTest with no code change.

Tests

./gradlew :sync-pipelines:jvmTest --rerun passed 3 runs in a row after the review fixes, and 5 before them. Each run had 285 tests with 0 failures, and the integration tests take about 22 s.

Reviewed by a separate reviewer. S26 and S27 were confirmed against the backend code, and all findings are addressed.

🤖 Generated with Claude Code

Cleanup (rebased 2026-10-14)

  • Removed redundant comments.
  • Documented multi-issue knownIssue, allowDuplicates and KnownDuplicates.

@mohamede1945
mohamede1945 added this pull request to stack #326 October 14, 2026 14:05
mohamede1945 and others added 20 commits October 14, 2026 11:17
restart() decided whether the device was still signed in before the
new graph's launch recovery ran. A logout that the previous launch left
unfinished completes during that recovery and signs the device out, so
restart() then waited for a sync that never starts.

restart() now awaits initialization, then reads the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add LIFE-13 and LIFE-16. Both pass, as predicted.

- LIFE-13 holds a logout after its reset began (the flag is persisted)
  and before any data was deleted, then relaunches. Token capture runs
  non-cancellable, so the held logout is left suspended outside the
  test's scope, as a killed process never runs again. The relaunch
  finishes the reset and signs out: the data is empty before the first
  write is accepted, and after a new login the only POSTed mutation is
  that write.
- LIFE-16: a write while a logout is held throws
  SessionResetInProgressException, writes nothing, and never reaches
  the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add NOTE-13, NOTE-14, NOTE-15 and NOTE-18.

- NOTE-13 pins S7: the range edit's UPDATE carries ranges and gets 422.
  Behind it, the multi-issue form accepts S4's divergence, with the
  phone on the new range and the server on the old one.
- NOTE-14 pins S13: the archived note still shows on the phone. The
  public Note has no archived state, so "hidden" is the only product
  decision the test can express. The server keeps the note with
  archivedAt.
- NOTE-15 passes: the GET's {} entry leaves the note unchanged and
  pushes nothing. The fault clears when the web saves the note again,
  and the next sync applies the web's body. Nothing resends a row the
  client has already passed, so the re-save is what brings it back.
- NOTE-18 pins S12: two identical pending notes plus a lost response
  leave 4 server notes, not 2. The catalog makes the pair with two
  imports, but an import matches a note with the same body and range
  and skips it, so the test calls addNote twice with different
  timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add TXT-09, TXT-10 and TXT-11.

- TXT-09 was "unknown" and passes: "étude" links to the web's "ÉTUDE",
  "STRASSE" stays apart from "Straße", no 409 repeats, and both devices
  converge. The client's key uses Kotlin's locale-free lowercase(), and
  the fake models Postgres lower() with the root locale. That matches a
  UTF-8 collation. It is unverified against the real database: a C
  collation folds only ASCII.
- TXT-10 passes: " Study" ending in an ideographic space and "Study"
  starting with a byte order mark are the same local collection, linked
  to the web's "Study".
- TXT-11 pins S13: the server and the tablet show "Juz 30", but the
  phone keeps "  Juz 30  ".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add COL-15, COL-16 and COL-19.

- COL-15 was "unknown" and finds a new issue, S26. The web's rename
  wins over the phone's delete, and "Memorize" comes back on the phone.
  But the delete's removal of 2:255 from the collection is still pushed
  as a COLLECTION_BOOKMARK DELETE, so the phone and the server both end
  with an empty Memorize. The test pins S26.
- COL-16 pins S9, as predicted: the sync succeeds, and the web's user
  collection named "Favorites" with 4:1 never shows on the phone.
- COL-19 passes: deleting or renaming the default collection throws
  IllegalArgumentException and pushes nothing. The highlight half of
  the case can't be written: the public API never hands out a highlight
  collection's id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add BMK-15, BMK-17, BMK-18 and BMK-19.

- BMK-15 passes: the phone's removal of 2:255 and the tablet's add of
  3:18 leave "Study" = {3:18} on both devices and the server after two
  sync rounds.
- BMK-17 was "unknown" and pins S23. It seeds 999 web changes, as
  ERR-14 does, instead of a page cap, so the web's BOOKMARK DELETE ends
  page 1 and its Favorites membership DELETE starts page 2; the test
  checks that split outside the known issue. The client fetches every
  page before applying any, so the split changes nothing: the phone
  pushes the membership DELETE and gets 404, as in BMK-11.
- BMK-18 passes: the web's page and surah bookmarks never show on the
  phone, and the phone's later edits never touch them on the server.
  The web actor has no juz bookmark, so the case covers page and surah,
  as its Given does.
- BMK-19 pins S6, as predicted: the phone shows the web's IndoPak
  bookmark on 2:255, and removing it from Favorites gets
  [200, 404 x5].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add HL-08 and HL-09. Both pass.

- HL-08: deleting the only blue highlight pushes no COLLECTION DELETE.
  The empty blue collection stays on the server, and both devices show
  no highlight.
- HL-09 was "unknown": an older QDC app's "blue" collection with 2:255
  shows as a normal collection and adds no highlight. The test also
  sets a blue highlight on the phone, which goes to
  system:highlights:blue and leaves the legacy collection alone.
  Read-only isn't visible in the public model; COL-17 covers renaming
  and deleting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RB-06, which pins S13, as predicted. The web sets slot 1 to page
300 of the 16-line IndoPak (pages_548), and the phone shows Madani page
300. The public model has no mushaf, so the test can only require that
the phone does not show page 300.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RS-14, which was "unknown" and passes. The tablet's older offline
visit to 2:255 loses last-write-wins to the phone's newer synced one,
so the tablet pushes nothing, and both devices show the newer time. The
server's CREATE upsert, which would take an older time, is never
reached, so the server keeps the newer time too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add IMP-08, IMP-09 and IMP-10.

- IMP-08: the documented design holds. The edited legacy note imports
  as a second note, and the phone and the server both hold two notes.
  The catalog predicted a pass, but the edit keeps the legacy note's
  creation date, as the iOS legacy reader passes it. So the server
  holds two notes created at the same time on the same range, which is
  the scenario duplicate check's key. The test passes that check to
  knownDuplicates under S14, whose "edited legacy notes come back as
  new notes" is this design choice.
- IMP-09 pins S20 (catalog N6), with the end state checked outside the
  known issue: the server and the phone end with the newest 20
  sessions. But the phone pushes all 25 CREATEs, then 5 DELETEs.
- IMP-10 pins S14: logout clears the import history, so re-running the
  tracked import and logging in pushes the note the user deleted while
  signed in, and it is back on the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move ERR-14, ERR-15, ERR-16 and ERR-17 unchanged into
ProtocolScenariosTest: GET paging and chained POST chunks.
FailureScenariosTest keeps the injected failures. The P2 ERR cases
would have taken it past ~500 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add ERR-13, ERR-19 and ERR-20 to ProtocolScenariosTest, and ERR-18 to
FailureScenariosTest.

- ERR-13 passes: an invalid-JSON GET and a 200 success:false POST
  apply nothing. The run retries to Done, and both notes are on each
  side once.
- ERR-18 was "unknown" and passes. Without the note scope, the POST
  that carries the note gets 403 on every attempt. The run ends
  Failed, with 403 ForbiddenError, which is the surfacing OBS-02
  defines. The collection, pushed in an earlier phase, syncs, and the
  note stays on the phone. Once the scope is back, the next sync
  pushes the note.
- ERR-19 was "unknown" and finds a new issue, S27. The GET sends {}
  for a collection, a membership, a session and a reading bookmark.
  Nothing is pushed, and the collection, membership and session are
  unchanged. But slot 1 disappears from the phone: a READING_BOOKMARK
  entry without a slot falls back to the local slot with no location,
  as a cleared slot is read. The test pins S27. Once the web saves
  each row again, the next sync converges.
- ERR-20 passes: setting slot 1 three times offline sends one
  READING_BOOKMARK per slot per POST, and the server ends on the last
  location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add OBS-03 and leave OBS-04 unwritten.

- OBS-03 was "needs API". QuranDataService.syncStatus now provides it,
  and the case passes: after a sync, the public status is the run's
  Succeeded result, stamped after the sync started and later than the
  login's.
- OBS-04 can't be written yet: no public API lists held items or their
  reasons. IMP-06 and IMP-07 hold notes only locally. The class
  comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The final duplicate check was all or nothing. knownDuplicates put the
whole check inside knownIssue, so it also accepted duplicates the issue
doesn't cause. Nothing could say that a duplicate is expected by
design.

- ServerState.duplicates() returns structured ServerDuplicate values
  (account, kind, key, ids). assertNoDuplicates() is built on it, and
  its lines read as before.
- knownDuplicates takes KnownDuplicates(id, description, match). Only
  the duplicates match accepts are checked inside knownIssue. Any other
  duplicate fails the scenario.
- SyncScenario.allowDuplicates(reason, match) skips duplicates expected
  by design. It isn't strict, and match runs after the block, so it can
  read the server's final state.
- NOTE-17 and NOTE-18 now match only the duplicate notes S12 leaves on
  2:255. IMP-08 keeps its S14 pin with a NOTE matcher until the next
  commit.
- KnownIssueTest covers other duplicates failing under knownDuplicates,
  and allowDuplicates skipping without being required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two notes is the documented import design, not an open issue.
knownDuplicates under S14 would have failed as "appears fixed" if the
design never changed. IMP-08 now uses allowDuplicates for exactly that
pair: a NOTE duplicate whose bodies are the original and the edited
legacy note. It also asserts that both notes keep the legacy creation
date, which is why they share the duplicate check's key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fake's "vanished resource" fault sent {} for every type. The
backend never does for COLLECTION_BOOKMARK: hasResourceId is false, so
it always fills the split {collectionId, bookmarkId}, with no
timestamps when the membership row is gone. The fault now sends that
key-only body for memberships.

ERR-19 changes with it, in this commit, because the old expectation no
longer holds:

- The GET sends three {} entries (collection, session, reading
  bookmark) and one key-only membership, which the test checks.
- The key-only membership reads like any other membership. The phone
  applies it in one attempt, pushing nothing. 3:18 comes from the
  BOOKMARK CREATE in the same GET, and goes into the collection the
  phone still calls Study, because the collection's {} is ignored.
- S27 (slot 1 cleared) and the converged second sync are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- NOTE-13 also asserts that the agreed range is 3:18. The S4 pin
  accepts both of S4's shapes behind S7: the phone on 3:18 while the
  server keeps 2:255, or the echo reverting both to 2:255.
- RB-06 syncs twice, pushes nothing, and asserts outside the known
  issue that the server's slot stays page 300 of pages_548. The
  assertIs<PageReadingBookmark> moved inside the known issue.
- COL-16 accepts the catalog's alternative outcomes: some user
  collection holds 4:1, under any name. It no longer requires
  convergence, which a documented rename would break. It asserts the
  server side outside the known issue: the web's Favorites keeps 4:1,
  and the default keeps 1:1.
- LIFE-13 waits for the held logout within 10 s, and selects on the
  logout's Deferred too. A logout that fails or finishes before the
  hold surfaces instead of hanging. After the relaunch, it asserts that
  the dead launch's logout never resumed.
- TXT-09 says it assumes production Postgres uses a UTF-8 lc_ctype.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ERR-13 answered the first GET page with invalid JSON, so nothing had
arrived that the client could apply early. "No partial apply" could
not fail.

ERR-13 now seeds 1050 web notes, as ERR-15 does, and breaks page 2
after page 1 has arrived. The phone runs with no retries, so each
malformed answer ends its run and the test can look at what is left:

- After the broken page 2: the GETs were pages 1 and 2, nothing was
  POSTed, and the phone shows only its own note, with nothing from
  page 1.
- After a 200 success:false POST: the server applied nothing, and the
  phone still shows only its own note.
- The next sync succeeds: the phone's note is on the server once, the
  phone has all 1051 notes, and it converges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop references to the private test catalog from the P2 scenario
comments: "Catalog section 2.x" prefixes, "the harness note for" prefixes,
"as OBS-02 defines it", and narration of what the catalog does, keeping
the explanations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The integration tests section now covers knownIssue with several
issues that hide one another, allowDuplicates for duplicates a scenario
expects by design, and the KnownDuplicates matcher for an open issue's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mohamede1945
mohamede1945 force-pushed the test/sync-p2-scenarios branch from 7aacd8f to 36f95d4 Compare October 14, 2026 15:36
@mohamede1945
mohamede1945 merged commit d2ff7dd into main Oct 14, 2026
1 check passed
@mohamede1945
mohamede1945 deleted the test/sync-p2-scenarios branch October 14, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant