Repository navigation
test(loro.js): add a multi-peer differential fuzz harness - #1141
Merged
Merged
Conversation
Random multi-peer edits, update exchanges in several orders, and checkouts, checked against replay, checkout-at-version, snapshot, shallow-snapshot, and event-mirror oracles, and optionally step by step against a loro-crdt WASM build. Failures are grouped by signature, minimized, and printed as loro.js calls. The default run is a fixed 120-seed Map/List/Text/Counter set that needs no WASM build (~2 s). Seeds that fail on main are listed with the fix that makes them pass. Usage and findings: context/loro-js-differential-fuzz.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
WASM Size Report
|
This was referenced Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seeds 0-119 with nested children and snapshot imports pass on main now that #1131 is in. See context/loro-js-differential-fuzz.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
#1166) * test(loro.js): add a Rust differential suite for MovableList 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> * test(loro.js): compare event effects and replicate unordered reverts 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> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow 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> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust 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> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing 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> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas 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> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
* test(loro.js): add a Rust differential suite for MovableList 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> * test(loro.js): compare event effects and replicate unordered reverts 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> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow 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> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust 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> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): make import and importBatch atomic A failed import no longer leaves history, pending changes, version, containers, state or events half applied. In-place history changes are journaled and undone, state is rebuilt only when it had changed, events are emitted after success. Details: context/import-batch-atomicity.md ("loro.js"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing 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> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas 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> * perf(loro.js): journal snapshot-sequence writes instead of copying per import Each import copied the whole snapshot-sequence table for rollback, so its cost grew with the number of snapshot-hydrated containers. Record each write in the import journal instead (context/import-batch-atomicity.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
zxch3n
added a commit
that referenced
this pull request
Sep 29, 2026
…) (#1168) * test(loro.js): add a Rust differential suite for MovableList 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> * test(loro.js): compare event effects and replicate unordered reverts 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> * test(loro.js): join shallow snapshots through Rust and keep replayed undo unchained Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): accept no-op sequence events and replicate reverts whose diff shape differs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): skip shallow editors and start range exports at the shallow 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> * test(loro.js): measure revert container depths at the revert target Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(loro.js): run the MovableList differential suite on the shared Rust 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> * fix(loro.js): model MovableList like Rust (positions and LWW elements) MovableList keeps a Fugue sequence of list items and elements whose position and value are last-writer-wins registers, as Rust does. Moves of concurrently deleted elements, deletes after moves, snapshots (list item, element and last-set IDs, dead items), metadata getters, version transitions, revertTo/applyDiff (Rust's apply_delta), undo and events now match Rust. Snapshot-hydrated documents replay history before concurrent imports, and a few shallow-snapshot incompatibilities are fixed. The differential suite passes every profile. Design and remaining gaps: context/loro-js-movable-list.md. Rebuilt on current main: MovableList joins #1126's per-container snapshot completion (its snapshot state names Rust IDs), applyDiff/revertTo/undo use main's port of Rust #1138's apply_delta on the new model, and undo transforms only over changes the UndoManager did not make. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip final syncs with a peer that left the run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): keep pending changes and follow Rust's shallow import rules Decoding deferred snapshot history dropped pending changes. Shallow imports now check the unknown part of each change like is_before_shallow_root, and an import that only adds pending changes creates their root containers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): pin diff() of a MovableList child created in the range main's diff() reports such a child by its ops, including map keys set and deleted again, as Rust does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): resolve concurrent MovableList op indices through a tracker A causal view per concurrent op made importing concurrent moves O(n^2). Like Rust's Tracker::checkout, keep a tracker version and a per-position delta metric that moves incrementally between op versions; see context/loro-js-movable-list.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(loro.js): describe the merged MovableList fixes in the changeset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): make import and importBatch atomic A failed import no longer leaves history, pending changes, version, containers, state or events half applied. In-place history changes are journaled and undone, state is rebuilt only when it had changed, events are emitted after success. Details: context/import-batch-atomicity.md ("loro.js"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): reject MovableList moves and sets of elements outside their history Port of #1125; see context/loro-js-movable-list.md (Validation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): never walk a container's fields when comparing event values A MovableList replaced by a plain value made the comparison enumerate the container and hit its guarded element sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fuzz MovableList in the multi-peer differential CI run The Rust-model MovableList passes seeds 0-599 of the #1141 fuzz with all oracles in lockstep with Rust; main failed 144 of seeds 0-299. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loro.js): apply MovableList diffs deletes first, like Rust's apply_delta Without moved children the delta was walked in order, which gave the ops other IDs than Rust's and later deletes by ID missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): skip map entries that only delete keys when comparing 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> * docs(loro.js): record snapshot completion on import and its cost The performance context still described main's MovableList snapshot special case and #1163 as open. Measurements are in context/loro-js-performance.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(loro.js): check MovableList transition inserts in one tree walk canTransition walked the position tree once per insert op, which made checkout, diff and revertTo O(insert ops * positions). Collect the runs and check them in one containsIdRuns call, as the List branch does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(loro.js): fix the suite's changeset and ignore empty diff() deltas 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> * perf(loro.js): journal snapshot-sequence writes instead of copying per import Each import copied the whole snapshot-sequence table for rollback, so its cost grew with the number of snapshot-hydrated containers. Record each write in the import journal instead (context/import-batch-atomicity.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 a randomized differential harness for
loro.jsand keeps a CI-sized fixed seed set in the test suite.loro-js/tests/fuzz/differential.ts: 2–4 peers make random edits (Map, List, Text, Tree, Counter, MovableList, marks, child containers), exchange updates (update, full, per-change blobs in shuffled order viaimportorimportBatch, optionally snapshots), and check out random versions.checkout(F)equals a fresh import of exactly the changes inF; a full snapshot and shallow snapshots equal the full-history document, including checkouts into the retained range; an event mirror keyed by container ID must reproducetoJSON()after every commit, import, and checkout. The mirror follows the Rust fuzzer'sContainerTrackerrules.FUZZ_WASM=1): each peer gets aloro-crdttwin, every concrete edit/sync/checkout runs on both, and the first diverging action is reported. It also compares a Rust replay, Rust checkouts, both shallow-snapshot directions, and the chosen shallow root.loro.jsAPI calls.context/loro-js-differential-fuzz.md: how to run it, the oracle semantics, and the divergences found onmain, linked from the rootAGENTS.md.Rationale
The runtime had no multi-peer oracle beyond hand-written cases. This harness found the bugs fixed in #1139, #1140, and the other
fix/loro-js-*PRs from this session. It also found the still-open Tree, nested-container event, and shallow classes listed in the context doc. The default run needs no WASM build, so it can run with the rest ofpnpm test. It overlaps little with #1132, which pairs every peer with Rust for MovableList only.Validation
mainat238988f5.FUZZ_VALIDATE_HARNESS=1with the WASM reference found no case where Rust's own events disagree with the mirror: 300 Map/List/Text/Counter seeds with child containers and 100CORE_FEATURESseeds (which include trees). A tree-only validation run could not finish becauseloro.jsloops forever on a tree cycle created by concurrent moves (listed in the context doc).pnpm --dir loro-js format:check,lint,typecheck,test(249 passed, 1 skipped manual block),build.🤖 Generated with Claude Code