Repository navigation
test(sync): add the P2 sync integration scenarios - #321
Merged
Merged
Conversation
mohamede1945
added this pull request to stack #326
October 14, 2026 14:05
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
force-pushed
the
test/sync-p2-scenarios
branch
from
October 14, 2026 15:36
7aacd8f to
36f95d4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Skipped (not reachable through the public API):
New issues
{}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.knownDuplicatestakes a matcher and fails on any other duplicate;allowDuplicates(reason, match)covers duplicates that are expected by design, such as legacy-import edits.{collectionId, bookmarkId}key, as the backend does, not as{}.ProtocolScenariosTestwith no code change.Tests
./gradlew :sync-pipelines:jvmTest --rerunpassed 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)
knownIssue,allowDuplicatesandKnownDuplicates.