Skip to content

test(sync): add the P1 sync integration scenarios - #320

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

mohamede1945 merged 25 commits into
test/sync-p0-scenariosfrom
test/sync-p1-scenarios

Conversation

@mohamede1945

@mohamede1945 mohamede1945 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #319 (P0 scenarios), then #318 (harness) and #317.

Summary

Adds the 61 P1 end-to-end sync scenarios: 37 pass, 24 are knownIssue. They use the same rules as P0: only the public API plus server state, and each known issue is pinned to its exact current failure.

Area Pass Known issue
Lifecycle 4 1 new variant (S25)
Notes 3 4 (S7+S17, S15, S4+S7, S12+S7)
Text 5 0
Collections 1 7 (S2, S9 ×2, S16, S17, S15, S11)
Ayah bookmarks / Favorites 5 2 (S23, S15)
Highlights 1 1 (S24)
Reading bookmarks 3 1 (S18)
Reading sessions 5 1 (S8), plus an RS-13 variant (S8, with S19 accepted)
Import 2 1 (S11)
Failure / protocol 3 5 (S11 ×3, S10, S21), plus an ERR-06 variant pinning S10 alone
Convergence 3 2 (S6+S7+S2, S2+S7)
Observability 2 0

New issues found

  • S23 (critical): removing a verse from Favorites on the web wedges every device's sync.
    • When the phone applies the server's bookmark delete, it also marks its own Favorites membership as a pending delete.
    • That delete gets 404 "bookmark is not found" on every attempt.
    • It has the same mechanism as S6, but S6's ACK fix wouldn't cover it.
  • S24: if two devices change a verse's highlight color at the same time, the server ends up with the verse in two color collections. Devices show only one color, so the extra one is invisible.
  • S25: when the server rejects a token before it has expired on the device (a revocation, or a device clock running behind), sync fails with [401 ×6] on every run. The client only refreshes by local expiry time, and nothing reacts to a 401.

Issues these tests confirm by running, previously only read from code

  • S9: a remote rename onto a name already used locally makes sync report success while the device and server diverge.
  • S10: a remote DELETE is skipped when the user edits the item mid-sync, and the next push gets a 404.
  • S15: a remote delete beats a local edit.
  • S18: a local reading-bookmark edit made during a sync is lost.

Harness changes

  • knownIssue(description, "S7" to …, "S17" to …) reports which interacting issue matched.
  • SyncDevice.syncWhileHeld(gate) { … }, plus poll helpers.
  • syncUntilQuiescent errors carry the failing run.
  • The fake login layer refreshes a token that has expired by the device's clock, as the real one does.

Tests

./gradlew :sync-pipelines:jvmTest --rerun passed 3 runs in a row after the review fixes, and 7 runs before them. Each run had 255 tests, 169 of them integration tests, with 0 failures. The integration tests take about 20 s.

Reviewed by a separate reviewer, who confirmed S23 and S24 against the backend code. All findings are addressed.

🤖 Generated with Claude Code

Cleanup (rebased 2026-10-14)

  • Removed redundant comments, plus 2 path tests covered by LIFE-15 and ERR-15.

mohamede1945 and others added 25 commits October 14, 2026 11:16
Add LIFE-08, LIFE-10, LIFE-14 and LIFE-15. All four pass today.

- LIFE-08 holds the POST at the server and the logout at token capture,
  so it can check that only the held POST reaches the server once the
  reset has begun, and that logout finishes before the POST is released.
- LIFE-14 was "unknown". It passes: the engine retries after the 401,
  and the test refreshes the token between attempts, as a refresh
  would. The device uses a 100 ms retry base to leave room for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add NOTE-05, NOTE-08, NOTE-09, NOTE-10, NOTE-11, NOTE-12 and NOTE-17.

- Passing: NOTE-05, NOTE-08, NOTE-11.
- NOTE-09 pins S7's 422 on the phone's UPDATE. Once S7 is fixed, it
  also accepts S17 keeping the phone's older edit.
- NOTE-10 pins S15: the web's delete drops the phone's edit.
- NOTE-12 pins S4: the device shows only the first range. Once it keeps
  every range, the body edit also accepts S7's 422.
- NOTE-17 pins S12: the phone and the server each end with the
  original and the edited note. The scenario's duplicate check carries
  S12 too. The device makes no retries, so the run ends at the lost
  response and the edit lands before the next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add TXT-02, TXT-04, TXT-05, TXT-07 and TXT-08. All five pass today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add COL-04, COL-10, COL-11, COL-12, COL-13, COL-14, COL-17 and COL-18.

- Passing: COL-18.
- COL-04 pins S2: the echo of the membership's collection bump reverts
  the local rename.
- COL-10 pins S9: the rename chain fails the apply with a SQLite unique
  violation.
- COL-11 pins S9's third scenario. The sync succeeds, but the phone
  keeps its unsent "Memorize" and never shows the web's Memorize or its
  verse, while the server still has "Notes".
- COL-12 pins S16: the rename throws org.sqlite.SQLiteException. The
  collections stay unchanged and nothing is pushed.
- COL-13 pins S17: the phone takes the tablet's older name.
- COL-14 pins S15: the web's delete drops the phone's rename.
- COL-17 pins S11: the rename gets 403 on every attempt. The engine
  reports a 403 ForbiddenError as AUTHENTICATION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add BMK-03, BMK-06, BMK-11, BMK-12, BMK-13, BMK-14 and BMK-16.

- Passing: BMK-03 and BMK-06 (each heals within one run's retries, the
  catalog's "eventual"), BMK-12, BMK-13, BMK-14.
- BMK-11 was predicted to pass but pins a new issue, S23. A web removal
  from Favorites wedges the device: applying the remote BOOKMARK DELETE
  marks the phone's Favorites membership as a pending DELETE. The
  membership phase can no longer match it to the remote DELETE, so the
  phone pushes it and gets 404 "bookmark is not found" on every
  attempt.
- BMK-16 was "unknown". It pins S15: the sync completes, but the web's
  delete drops the phone's new membership and the collection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add HL-04 and HL-07.

- HL-04 passes.
- HL-07 was "unknown" and pins a new issue, S24. Both devices end up
  showing yellow, because the highlights flow shows one color per verse.
  But the tablet stores the phone's green membership next to its own
  yellow and never removes it, so the server keeps 2:255 in both color
  collections. The test asserts the devices' shared color outside the
  known issue, then the single server color inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RB-02, RB-03, RB-05 and RB-07.

- Passing: RB-02, RB-03, RB-05.
- RB-07 confirms the suspected S18. The phone's POST is held after a GET
  that carries the web's slot 1. A newer local slot 1 set during the
  hold is lost: once the POST is released, applying the web's slot
  clears the phone's pending edit, and both sides end on the web's
  location. A pending note gives the run a POST to hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RS-03, RS-04, RS-05, RS-07, RS-12 and RS-13.

- Passing: RS-03, RS-04, RS-05.
- RS-07 was "unknown". It passes: both devices and the server keep the
  same newest 20 sessions.
- RS-12 pins S8. The phone deletes the session after the tablet's
  delete has synced, so its DELETE is newer by the wall clock. It wins
  last-write-wins and gets 404 on every attempt.
- RS-13 was suspected as S19, but it passes. The server logs its DELETE
  of the replaced session before the moved session's UPDATE, and the
  phone applies them in that order. The move comes from the web,
  because a device merges a move onto a verse it already holds into a
  DELETE plus an UPDATE, which never reaches this path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add IMP-04, IMP-05 and IMP-07.

- IMP-04 passes: after a restore with deleteExisting, the phone, the
  server and the tablet all hold exactly the backup.
- IMP-05 was predicted as S1, but it passes. The second restore turns
  the first restore's Study into a tombstone, but the restored Study is
  still active and links the web's study first. The test also checks
  that the web's study survives later syncs and that the phone shows it
  once.
- IMP-07 pins S11: the note on 2:300 gets 500 on every attempt and
  blocks the valid note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move NOTE_UPDATE_REJECTED and FAVORITES_DELETE_REJECTED into
ScenarioSupport. The P1 failure and convergence scenarios meet the same
two failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add ERR-04, ERR-05, ERR-06, ERR-08, ERR-10, ERR-12, ERR-15 and ERR-17
in FailureScenariosP1Test, which keeps FailureScenariosTest under
~500 lines.

- Passing: ERR-15. ERR-04 and ERR-17 were "unknown" and pass: lost
  delete responses cause no 404 loop, and a 409 between chunks doesn't
  duplicate the committed chunk.
- ERR-05, ERR-08 and ERR-10 pin S11: the 409 UniqueViolationError,
  the BE1-indexed 422, and the 500 each repeat on every attempt.
- ERR-06 pins S10. In the catalog's note form, S7's 422 rejects the
  UPDATE before the server can answer 404, so the test accepts either
  signature. A collection-rename variant pins S10's 404 on its own,
  which confirms S10 (previously "from code").
- ERR-12 pins S21: once the retries run out during the outage, the
  note never reaches the server. ERR-12 uses short local-change and
  periodic timings, and waits three periodic intervals.
- ERR-15 seeds more than 1000 rows instead of capping the page size,
  like ERR-14, because today's client pages by its own limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add CONV-04, CONV-05, CONV-06, CONV-08 and CONV-09 in
ConvergenceScenariosP1Test.

- Passing: CONV-05 and CONV-06. CONV-09 was "unknown" and passes: with
  no retries, the commits of all three phases each lose their response,
  the app relaunches each time, and nothing is duplicated.
- CONV-04 pins S6, which d3's unbookmark hits first. Behind it, the
  test accepts S7's 422 on the note edits, and S2 reverting a rename
  made right after a push. Every run must succeed, so the failing run
  carries its signature.
- CONV-08 was predicted as S7, but it pins S2. The edit made while the
  CREATE is in flight survives the ACK, but the next GET echoes the
  phone's own CREATE and reverts it before it is pushed. S7's 422 is
  accepted behind it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add OBS-01 and OBS-02 in a new ObservabilityScenariosTest. Both pass
today.

- OBS-01 checks each TextLengthLimitException's field, bound, limit and
  escaped length for adds and edits of notes and collection names, and
  that none of them changes state.
- OBS-02 was "needs API". The public syncStatus flow, added in
  feat/sync-status, now provides it. The test collects that flow during
  a sync that fails for good: the flow reports exactly one Failed,
  carrying 422 ValidationError, not one per adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes take no timestamp. If the tablet's delete and the phone's later
delete land in the same millisecond, the phone's delete is not newer,
and S8 does not reproduce. Setting the fake server's clock back a
minute keeps the tablet's delete strictly older.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
knownIssue now also takes a description plus several (id, matcher)
pairs. Each pair is a known issue, and they are tried in order. The
first one that accepts the failure is reported as the one that
reproduced. When the block passes, the failure names every id ("S2, S7
appear fixed"). When none accepts the failure, it is an error, as
before.

The single-issue form delegates to it, and its messages are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the last round has a failed or skipped run, syncUntilQuiescent
now throws a SyncRunFailure carrying that run. A knownIssue can then
pin it with syncRejectedWith or syncFailedLocally, as it can for
assertSucceeded. The message is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The harness gains three helpers:

- SyncDevice.syncWhileHeld(gate) starts a sync, waits until its POST
  reaches the gate, runs the block, then releases the gate and returns
  the run. It replaces the async, awaitArrived and release boilerplate
  in LIFE-08, RB-07, both ERR-06 tests, CONV-08 and the P0 CONV-07.
- pollUntil polls a condition and returns whether it held, for checks
  the test then asserts itself (ERR-12).
- awaitUntil polls a condition and raises HarnessTimeoutException when
  it never holds (LIFE-08, LIFE-14 and the P0 ERR-11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FakeAuthRepository.refreshTokensIfNeeded now replaces the token once a
test calls expireLocally(), the way OidcAuthRepository refreshes inside
its expiry safety margin. It counts each refresh in `refreshes`. Tokens
still never expire locally unless a test asks.

getAuthHeaders now refreshes before reading the token, as
OidcAuthRepository does. It used to read the token first, so a refresh
in the same call sent the stale token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog's LIFE-14 passed only because the test itself rotated the
token after the 401. The client has no refresh triggered by a 401.

- Renamed LIFE-14 to "retries re-read the token store after a 401",
  which is what it proves.
- Added a variant where the token has expired on both the device and
  the server. The client's own refresh path (refreshTokensIfNeeded)
  replaces it before the first request, and the sync passes.
- Added a variant where the server rejects the token before its local
  expiry: a revocation, or a device clock running behind. Only the
  client's refresh path could recover, and it never runs. Every
  attempt gets 401 TokenExpiredError, and nothing is refreshed. This
  is pinned as a new issue, S25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move NOTE-03, NOTE-09, NOTE-12, NOTE-17, CONV-04, CONV-08 and the note
form of ERR-06 from one id with an OR'd matcher to one (id, matcher)
pair per issue. The log now names the issue that reproduced. A fix to
the visible issue no longer passes silently under its id while the
issue behind it keeps the test failing. Today:

- NOTE-03 and CONV-08 show S2.
- NOTE-09 and ERR-06 show S7.
- NOTE-12 shows S4.
- NOTE-17 shows S12.
- CONV-04 shows S6.

Two review nits are in the same file:

- NOTE-05 no longer counts POSTs. It checks that the session is among
  the accepted mutations.
- NOTE-17 syncs until quiescent, as the catalog says.
  syncUntilQuiescent's failure now carries the failed run, so S7's 422
  is still pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CONV-09 relaunches after every failed run, up to six times, instead
  of expecting exactly three lost phases. It asserts that some
  response was lost. ERR-04 likewise asserts that a delete's response
  was lost at least once. Neither hard-codes today's three-phase POST
  structure.
- OBS-02 asserts the whole status sequence: the login's replayed
  result, then Syncing, then exactly one Failed. A comment notes what
  StateFlow conflation still can't prove.
- ERR-12's S21 pin also requires "but was:<[]>".
- RS-12 freezes the server clock an hour back, instead of offsetting
  it, and its comment explains why the phone's later delete is newer.
- COL-12 documents that it accepts IllegalArgumentException because no
  duplicate-name type exists yet, and that S16's fix should tighten it.
- CONV-04 explains why only one device changes a color (S24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Merge FailureScenariosP1Test into FailureScenariosTest (435 lines)
  and ConvergenceScenariosP1Test into ConvergenceScenariosTest (309
  lines), with tests in case-id order, like the other areas. The
  failure file's companion now holds DROPPED, PHASES and PERIODIC as
  well as POISON.
- Rename FAVORITES_DELETE_REJECTED to FAVORITES_DELETE_404. S6 and S23
  share that signature.
- Fix the import order in ImportScenariosTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The phone revisits 3:7 with a newer timestamp, so its 3:7 session is
pending when the web moves 2:255 onto 3:7.

S19 still doesn't surface. The revisit wins last-write-wins over the
server's DELETE of the replaced session, and its UPDATE gets 404 on
every attempt (S8), so the POST fails before the move is applied.
The test pins S8 and accepts S19's local SQLite failure behind it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop references to the private test catalog ("the harness note for",
"the catalog predicted", "the catalog's LIFE-14") and a transient
"today" from the P1 scenario comments, keeping the explanations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove two harness path tests that a P1 scenario now repeats:

- logout clears local data, keeps the server, and reports a failed
  revocation (LIFE-15, with LIFE-06)
- a head change between GET pages is retried (ERR-15)

Drop the seedNotes helper they were the last users of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mohamede1945
mohamede1945 merged commit bc5f02b into main Oct 14, 2026
1 check passed
@mohamede1945
mohamede1945 deleted the test/sync-p1-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