Skip to content

test(loro.js): add a Rust differential suite for MovableList - #1132

Merged
zxch3n merged 11 commits into
mainfrom
test/loro-js-movable-list-differential
Sep 29, 2026
Merged

zxch3n merged 11 commits into
mainfrom
test/loro-js-movable-list-differential

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Summary

First PR of the stack that makes the loro.js MovableList behave like Rust. This PR only adds tests and the design; the published package does not change.

  • loro-js/tests/differential/ is a randomized differential suite. Each simulated peer is a pair of docs with the same peer ID, the Rust WASM build and loro.js. The scenarios cover:

    • random multi-peer insert/delete/move/set;
    • nested child containers (Map, Text, MovableList);
    • syncs by update, updates-in-range, snapshot and multi-blob importBatch;
    • snapshot and shallow-snapshot joins;
    • checkout, diff, revertTo, and single-peer undo/redo.

    Syncs randomly cross engines: Rust imports loro.js bytes and loro.js imports Rust bytes. After every step both halves must agree on the value, oplog version, frontiers, reachable container IDs, getCreatorAt/getLastEditorAt/getLastMoverAt, and canonicalized events. diff() results must match too.

  • A failing seed writes its trace to the temp dir. LORO_JS_DIFF_REPLAY=<file> replays it and LORO_JS_DIFF_MINIMIZE=1 shrinks it.

  • The suite loads Rust through the shared tests/support/rust-reference.ts, like the other differential suites: crates/loro-wasm/nodejs, or LORO_WASM_NODEJS. It runs inside pnpm test, skips without a WASM build, and runs in CI through pnpm test-loro-js after release-wasm. No separate workflow or script.

  • context/loro-js-movable-list.md covers:

    • the Rust MovableList contract, anchored to code;
    • why the old loro.js model diverges;
    • the replacement model, implemented in the next PR;
    • how to use the suite;
    • the divergences that are not MovableList-specific.

Findings on main

MovableList, fixed by the follow-up PRs:

  1. A concurrent delete + move gives ["a","c"] in loro.js and ["a","c","B"] in Rust.
  2. Rust's "move, then delete" imported into loro.js gives ["b","a"] instead of ["b"].
  3. loro.js snapshots write position IDs and last-set IDs as the element ID.
  4. getLastEditorAt/getLastMoverAt return the creator.
  5. Checkout, diff and revertTo select different winners than Rust.
  6. A same-value set is skipped (Rust records it).

The insert/delete-only profile converges on main since the Fugue fixes in #1136/#1139, so the other seven profiles are marked KNOWN_DIVERGENT. Each asserts that it does diverge, and the model PR empties the set.

The suite normalizes a few differences of form and works around two Rust issues; each is commented in harness.ts/fuzz.ts and listed in the context doc:

  • Event deltas that differ only in shape, or are a no-op where Rust reports nothing, are checked by their effect.
  • A revertTo whose op order Rust leaves to FxHashMap order, or whose diff() shape differs, is checked by value, and Rust's ops are replicated.
  • Rust reports a wrong getLastEditorAt on shallow documents after a diff across the root, so editors are not compared there.
  • Rust's updates-in-range from a shallow doc exports nothing for a span that starts before the root, so spans start at the root.

Validation

  • pnpm -C loro-js check: passes (511 tests; 3 Rust-engine variants of loro.js-only tests skipped). Against a reference built from current main, the suite's 8 profiles all pass: one converges, seven diverge as expected.
  • CI on this PR runs the suite in pnpm test-loro-js.

Merge order

  1. This PR.
  2. fix(loro.js): model MovableList like Rust (positions and LWW elements) #1166 MovableList model and Rust-compatible encoding (empties KNOWN_DIVERGENT).
  3. fix(loro.js): make import and importBatch atomic #1167 atomic import/importBatch.
  4. fix(loro.js): reject invalid MovableList move/set refs (port Rust #1125) #1168 MovableList move/set validation (Rust fix: reject malformed movable list move/set ops on import instead of aborting #1125).

Each later PR targets the previous PR's branch.

🤖 Generated with Claude Code

Drive the Rust WASM build and loro.js with the same random multi-peer
edits, crossing engines on every sync, and require equal values, versions,
per-element metadata and events. Profiles that diverge on the current
MovableList model are recorded as known divergences. Design and findings:
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

WASM Size Report

  • Original size: 3277.59 KB
  • Gzipped size: 1089.33 KB
  • Brotli size: 762.83 KB

Event deltas that differ only in shape are checked by effect; reverts whose
op order Rust leaves to hash order are checked by value and replicated from
Rust; peers that both engines reject are retired; peer IDs are never reused;
undo stays unchained; the minimizer keeps those rules. See
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n and others added 8 commits September 30, 2026 00:51
…undo unchained

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…se diff shape differs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…allow root

Both follow Rust issues listed in context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ust reference

Load Rust through tests/support/rust-reference.ts like the other differential
suites, so it runs in `pnpm test` (and in CI's test-loro-js) and skips without
a WASM build. Drop the separate workflow and scripts. insert/delete now
converges on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…diff()

A child created in the range whose keys were all set and deleted again is
listed by Rust and omitted by loro.js, also on main and for any parent; see
context/loro-js-movable-list.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changeset named a test:differential script that does not exist. Rust's
diff() can list a sequence with an empty delta where loro.js omits it
(context/loro-js-movable-list.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zxch3n
zxch3n merged commit 86cc484 into main Sep 29, 2026
2 checks passed
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