Skip to content

[lexical-y] Chore: Add undo run-end, own-run restore end and run-sequence cases, with the ones main fails as expected failures - #9605

Merged
zurfyx merged 4 commits into
mainfrom
zurfyx/y-undo-run-end-tests
Oct 30, 2026
Merged

zurfyx merged 4 commits into
mainfrom
zurfyx/y-undo-run-end-tests

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Description

Tests only, no change to the binding. They come from #9561 (an undo keeps
the run ends of the text it removes), parked as a draft, and from the
randomized run-sequence sessions, so the cases stay in main whatever
lands later.

RunSequenceUndo.test.ts, the 200-seed run-sequence fuzz, is in #9562.

Test plan

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/UndoRunEnds.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceCases.test.ts
      Tests  103 passed | 81 expected fail (184)

On #9561's branch (fbb792b) the 81 expected failures pass and the unseen
variants of the seed-8500123 case fail. Without its seed-175 fix (282d1ecf)
the seed-175 case fails. pnpm run ci-check passes.

Follow-up: #9562's cases

#9562 is parked as well; its cases are added in a second commit: OwnRunRestoreEnd.test.ts (the own-run cases main fails as test.fails, tracked in #9609; the cases #9562 regressed as plain tests), OwnCommentGapCollapse.test.ts (one write inside the author's comment undone while a collaborator deletes past its end across another comment; main passes) and RunSequenceUndo.test.ts (200-seed run-sequence fuzz, only #9599's seed 87 fails, as on main).

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/OwnRunRestoreEnd.test.ts packages/lexical-y/src/__tests__/unit/OwnCommentGapCollapse.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceUndo.test.ts packages/lexical-y/src/__tests__/unit/UndoRunEnds.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceCases.test.ts
      Tests  124 passed | 97 expected fail (221)

…ones main fails as expected failures

## Description

Tests only, no change to the binding. They come from #9561 (an undo keeps
the run ends of the text it removes), parked as a draft, and from the
randomized run-sequence sessions, so the cases stay in main whatever
lands later.

- `UndoRunEnds.test.ts`: undos of text typed inside a bold, italic,
  underlined, styled or commented run while a collaborator types next to
  it; a paragraph inserted at an Enter split; type, delete, undo, undo;
  the undo of the run such run ends restate; restarts; several inserts;
  the skipped-step path; token and segmented nodes; long pastes (link-read
  counts). The cases main fails are `test.fails`: they pass on #9561's
  branch. The concurrent-undo case that #9561 gets wrong (#9593) passes on
  main and is a plain test here.
- `RunSequenceCases.test.ts`: two reduced run-sequence sessions. Seed 175
  (one undo step that underlines a run, types and underlines what it typed,
  undone while a collaborator types inside the run) and seed 8500123 (a
  collaborator's undo of its text inside a run together with the writer's
  undo of the run's format, seen or not). Main passes both; #9561's branch
  fails the unseen variant of the second (#9593).

`RunSequenceUndo.test.ts`, the 200-seed run-sequence fuzz, is in #9562.

## Test plan

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/UndoRunEnds.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceCases.test.ts
      Tests  103 passed | 81 expected fail (184)
```

On #9561's branch (fbb792b) the 81 expected failures pass and the unseen
variants of the seed-8500123 case fail. Without its seed-175 fix (282d1ecf)
the seed-175 case fails. `pnpm run ci-check` passes.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 30, 2026 6:54pm UTC
lexical-playground Ready Ready Preview Oct 30, 2026 6:54pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 30, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9605

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9605

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9605

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9605

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9605

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9605

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9605

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9605

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9605

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9605

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9605

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9605

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9605

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9605

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9605

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9605

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9605

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9605

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9605

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9605

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9605

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9605

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9605

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9605

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9605

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9605

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9605

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9605

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9605

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9605

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9605

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9605

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9605

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9605

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9605

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9605

commit: 66cfd53

@meta-code-review

meta-code-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 30, 2026, 7:41 PM UTC

… from #9562, with the ones main fails as expected failures

## Description

Tests only. #9562 (a restore inside a run its own client opened gets a null
of its own after the run's end) is parked; its cases move to main here, so
they stay whatever lands later.

- `OwnRunRestoreEnd.test.ts`: the cases where the writer opened the run
  itself (a comment's restore whose end a collaborator's delete drops; a
  comment written with a hole in it; the same after the writer wrote the
  value again; 64 comment ids on the run; 1, 16, 64 and 128 comment ids
  dropped with the word) fail on main and are `test.fails`, tracked in
  #9609. The cases #9562 regressed and main handles are plain tests: S9-12
  and S9-12b, the review's cases of a restore in another client's run after
  that client wrote the value again, a later write's hole (comment and
  bold), and the #9589 tie where the author opened the run (a collaborator's
  bold starting at, or running past, the author's bold run's end stays).
- `OwnCommentGapCollapse.test.ts`: one write inside a comment its author
  opened, undone while a collaborator who has seen it deletes past the
  comment's end across another comment; the comment ends where it ended.
  Main passes; #9562 fails it in both id orders.
- `RunSequenceUndo.test.ts`: the 200-seed two-client run-sequence fuzz;
  only seed 87 fails, as on main (#9599).

## Test plan

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/OwnRunRestoreEnd.test.ts packages/lexical-y/src/__tests__/unit/OwnCommentGapCollapse.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceUndo.test.ts packages/lexical-y/src/__tests__/unit/UndoRunEnds.test.ts packages/lexical-y/src/__tests__/unit/RunSequenceCases.test.ts
      Tests  124 passed | 97 expected fail (221)
```

The same on main at 2fe8056 (the three new files: 21 passed, 16 expected
fail). On #9562's branch `OwnCommentGapCollapse` fails 4 of 4:
`expected 'aa Kil[c1] bb[c1,c2] cc[c1] dd[c1]' not to match /(bb|cc|dd)\[c1/`.
`pnpm run ci-check` passes.
@zurfyx zurfyx changed the title [lexical-y] Chore: Add undo run-end and run-sequence cases, with the ones main fails as expected failures [lexical-y] Chore: Add undo run-end, own-run restore end and run-sequence cases, with the ones main fails as expected failures Oct 30, 2026
…lapse case

## Description

`OwnCommentGapCollapse.test.ts` checked only that `bb`, `cc` and `dd` carry
no `c1` and that the second line is untouched: losing the comment on `Kil`
(or `c2` on `bb`) passed too, so a mutation that skips A's comment of
`Kilo` passed all four cells. It now asserts the whole first line as main
gives it: `aa Kil[c1] bb[c2] cc dd` when B deletes `o` and the space,
`aa Kil[c1] cc dd` when it deletes `o` and ` bb`. Reported in review.

## Test plan

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/OwnCommentGapCollapse.test.ts
      Tests  4 passed (4)
```

With A's comment of `Kilo` removed from the test, all four cells fail now:

```
AssertionError: expected 'aa Kil bb[c2] cc dd' to be 'aa Kil[c1] bb[c2] cc dd' // Object.is equality
AssertionError: expected 'aa Kil cc dd' to be 'aa Kil[c1] cc dd' // Object.is equality
```

On #9562's branch all four still fail. `pnpm run ci-check` passes.
…llapses spaces

## Description

The word model these tests compare (`words()`, `line()`) splits text on
spaces and joins the words with one space, so it cannot tell `aa Kil bb`
from `aa Kilbb`: in `OwnCommentGapCollapse`, leaving out B's second delete
(the space) still passed. The final checks now also assert the first
line's text exactly, as main gives it: `aa Kilbb cc dd` and `aa Kil cc dd`
there, and `aa Xbb cc`, `aa Ybb cc` and `aa Kilome cc dd` for the four
`OwnRunRestoreEnd` cases whose result is checked with `words()` after the
merge. Reported in review.

## Test plan

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/OwnRunRestoreEnd.test.ts packages/lexical-y/src/__tests__/unit/OwnCommentGapCollapse.test.ts
      Tests  20 passed | 16 expected fail (36)
```

With B's second delete left out of the `o then space` rows, those two
cells fail now:

```
AssertionError: expected 'aa Kil bb cc dd' to be 'aa Kilbb cc dd' // Object.is equality
```

`pnpm run tsc-test`, `pnpm run prettier` and `pnpm run lint` pass.

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added cases look good to me: they converge on every peer, pass at this head (124 passed, 97 expected failures), and test.fails is a sensible way to keep the parked work's cases on main. A few optional, non-blocking suggestions:

  1. UndoRunEnds.test.ts:663. The six cases of “the end a kept restore relies on stays ended when a collaborator deletes the run” fail on main only at expect(hNulls.length).toBe(2), which counts the nulls #9561's undo keeps (main's undo deletes both). Without that line, the user-visible assertion on line 667 ('one three') already passes on main in all six id orders, but as test.fails that outcome is not checked on main at all. Dropping the count, or leaving it to #9561, would let this run as a plain test.each now.
  2. UndoRunEnds.test.ts:528-547. In boldAfterUndoneWord, neither the undo nor the redo writes a transaction with the binding's run-end origin on main, so the throwing listener never fires and the case passes on its content checks alone; the follow-up writes named at lines 549-568 do not happen on main either. A note that both depend on #9561, or a scenario where main's closeRestoredRuns writes a follow-up, would make them exercise what their names say.
  3. Comments that describe a parked branch. UndoRunEnds.test.ts:1300-1304 describes #9561's outcome ( two turns bold, #9593) above an assertion that two ends plain, and OwnRunRestoreEnd.test.ts:250-256 (S9-12) describes #9562's rule 5, which main does not apply to a run the writer opened (lines 48-50 and S9-12b say so). Prefixing them with “With #9561” and “With #9562” would remove the contradiction.
  4. Run-sequence agreement. RunSequenceCases.test.ts:149-153 and RunSequenceUndo.test.ts:199-213 compare only per-word format maps between the clients, which run without selfCheck. Comparing both editors' JSON (or using the harness's expectConverged) would also catch a text or structure divergence; all 200 seeds already agree on the full JSON on main.
  5. Test plan. AGENTS.md asks for the real Before and After output in the Test plan; the results on #9561's and #9562's branches are described rather than pasted.

No changeset is needed: CONTRIBUTING.md and AGENTS.md exempt test-only changes.

hNulls.push(it);
}
}
expect(hNulls.length).toBe(2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: on main all six id orders fail here (main's undo deletes both nulls, so the count is 0). Without this line, the assertion on line 667 ('one three') passes on main, but as test.fails that outcome is not checked on main at all. Dropping the count, or leaving it to #9561, would let this case run as a plain test.each.

test('a listener of the follow-up write that throws leaves it undone with its step', async () => {
const t = await boldAfterUndoneWord();
const listener = (tr: Y.Transaction) => {
if ((tr.origin as {runEnd?: boolean} | null)?.runEnd === true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: in boldAfterUndoneWord on main, neither a.undo() nor the redo writes a transaction with the run-end origin, so this listener never throws and the case passes on its content checks alone. A note that it depends on #9561, or a scenario where main's closeRestoredRuns writes a follow-up, would make it exercise what its name says.

Comment on lines +1300 to +1304
// F's undo keeps its null and the restore after " mid" (text typed
// next to " mid" would rely on them); A's undo, which has not seen
// F's, removes the bold's opener and end, so the restore gives " two"
// bold for everyone. Settling it needs a write after both are seen,
// which no client may make while it applies an update.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this describes #9561's outcome (#9593). On main, F's undo deletes the null and the restore, and the test asserts that two ends plain; “With #9561, …” would match the assertion.

Comment on lines +250 to +256
// S9-12: one write of A types uncommented text at two places inside A's
// own comment, so both of its restores end at the comment's end. Rule 5
// gives that end one null of A's own (not one per restore: the undo
// removes them together, and a second one would push the comment's end
// out of the last items a collaborator's collapse keeps). A undoes the
// write while B, which has not seen the undo, deletes the rest of the
// comment and then the text after it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: on main, rule 5 writes no null for a run its own client opened (lines 48-50, and S9-12b below), so this describes #9562's rule; “With #9562, rule 5 gives …” would match.

Comment on lines +208 to +213
if (
changed.length > 0 ||
JSON.stringify(after) !== JSON.stringify(afterB)
) {
failing.push(seed);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: agreement here is the per-word format maps only, and the clients run without selfCheck. Also comparing both editors' getEditorState().toJSON() would catch a text or structure divergence; all 200 seeds agree on the full JSON on main.

This branch was successfully deployed

2 active deployments
Preview – lexical — 66cfd530 Deployed Oct 30, 2026 by vercel[bot]
Preview – lexical-playground — 66cfd530 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant