2025-05-21 16:04:55 +02:00
/ * *
* Testing if encoding / decoding compatibility and integration compatibility is given .
* We expect that the document always looks the same , even if we upgrade the integration algorithm , or add additional encoding approaches .
*
* The v1 documents were generated with Yjs v13 . 2.0 based on the randomisized tests .
* /
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
2025-10-20 02:14:02 +02:00
import * as delta from 'lib0/delta'
2026-06-30 10:19:20 +02:00
import * as prng from 'lib0/prng'
import * as math from 'lib0/math'
import { bind , $rdt } from 'lib0/delta/rdt'
2026-01-10 22:04:30 +01:00
import { init } from './testHelper.js' // eslint-disable-line
2025-05-21 16:04:55 +02:00
2025-05-29 21:47:29 +02:00
/ * *
* @ param { t . TestCase } _tc
* /
export const testRelativePositions = _tc => {
const ydoc = new Y . Doc ( )
2025-12-29 11:37:01 +01:00
const ytext = ydoc . get ( )
2025-05-29 21:47:29 +02:00
ytext . insert ( 0 , 'hello world' )
const v1 = Y . cloneDoc ( ydoc )
ytext . delete ( 1 , 6 )
2025-06-05 14:52:55 +02:00
ytext . insert ( 1 , 'x' )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( v1 , ydoc )
const rel = Y . createRelativePositionFromTypeIndex ( ytext , 9 , 1 , renderer ) // pos after "hello wo"
const abs1 = Y . createAbsolutePositionFromRelativePosition ( rel , ydoc , true , renderer )
2025-05-29 21:47:29 +02:00
const abs2 = Y . createAbsolutePositionFromRelativePosition ( rel , ydoc , true )
t . assert ( abs1 ? . index === 9 )
t . assert ( abs2 ? . index === 3 )
}
2025-05-21 16:04:55 +02:00
/ * *
* @ param { t . TestCase } _tc
* /
export const testAttributedEvents = _tc => {
const ydoc = new Y . Doc ( )
2025-12-29 11:37:01 +01:00
const ytext = ydoc . get ( )
2025-05-21 16:04:55 +02:00
ytext . insert ( 0 , 'hello world' )
const v1 = Y . cloneDoc ( ydoc )
ydoc . transact ( ( ) => {
ytext . delete ( 6 , 5 )
} )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( v1 , ydoc )
const c1 = ytext . toDelta ( { renderer } )
2026-02-25 15:55:42 +01:00
t . compare ( c1 , delta . create ( ) . insert ( 'hello ' ) . insert ( 'world' , null , { delete : [ ] } ) . done ( ) )
2025-05-21 16:04:55 +02:00
let calledObserver = false
ytext . observe ( event => {
2026-06-16 21:29:45 +02:00
const d = event . getDelta ( { renderer } )
2026-02-25 15:55:42 +01:00
t . compare ( d , delta . create ( ) . retain ( 11 ) . insert ( '!' , null , { insert : [ ] } ) . done ( ) )
2025-05-21 16:04:55 +02:00
calledObserver = true
} )
2026-07-02 18:44:46 +02:00
ytext . applyDelta ( delta . create ( ) . retain ( 11 ) . insert ( '!' ) . done ( ) , null , { renderer } )
2025-05-21 16:04:55 +02:00
t . assert ( calledObserver )
}
2025-05-21 22:11:28 +02:00
/ * *
* @ param { t . TestCase } _tc
* /
export const testInsertionsMindingAttributedContent = _tc => {
const ydoc = new Y . Doc ( )
2025-12-29 11:37:01 +01:00
const ytext = ydoc . get ( )
2025-05-21 22:11:28 +02:00
ytext . insert ( 0 , 'hello world' )
const v1 = Y . cloneDoc ( ydoc )
ydoc . transact ( ( ) => {
ytext . delete ( 6 , 5 )
} )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( v1 , ydoc )
const c1 = ytext . toDelta ( { renderer } )
2026-02-25 15:55:42 +01:00
t . compare ( c1 , delta . create ( ) . insert ( 'hello ' ) . insert ( 'world' , null , { delete : [ ] } ) . done ( ) )
2026-07-02 18:44:46 +02:00
ytext . applyDelta ( delta . create ( ) . retain ( 11 ) . insert ( 'content' ) . done ( ) , null , { renderer } )
2025-05-21 22:11:28 +02:00
t . assert ( ytext . toString ( ) === 'hello content' )
}
/ * *
* @ param { t . TestCase } _tc
* /
export const testInsertionsIntoAttributedContent = _tc => {
const ydoc = new Y . Doc ( )
2025-12-29 11:37:01 +01:00
const ytext = ydoc . get ( )
2025-05-21 22:11:28 +02:00
ytext . insert ( 0 , 'hello ' )
const v1 = Y . cloneDoc ( ydoc )
ydoc . transact ( ( ) => {
ytext . insert ( 6 , 'word' )
} )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( v1 , ydoc )
const c1 = ytext . toDelta ( { renderer } )
2026-02-25 15:55:42 +01:00
t . compare ( c1 , delta . create ( ) . insert ( 'hello ' ) . insert ( 'word' , null , { insert : [ ] } ) . done ( ) )
2026-07-02 18:44:46 +02:00
ytext . applyDelta ( delta . create ( ) . retain ( 9 ) . insert ( 'l' ) . done ( ) , null , { renderer } )
2025-05-21 22:11:28 +02:00
t . assert ( ytext . toString ( ) === 'hello world' )
}
2025-11-27 19:59:26 +01:00
export const testYdocDiff = ( ) => {
const ydocStart = new Y . Doc ( )
2025-12-29 11:37:01 +01:00
ydocStart . get ( 'text' ) . insert ( 0 , 'hello' )
ydocStart . get ( 'array' ) . insert ( 0 , [ 1 , 2 , 3 ] )
ydocStart . get ( 'map' ) . setAttr ( 'k' , 42 )
ydocStart . get ( 'map' ) . setAttr ( 'nested' , new Y . Type ( ) )
2025-11-27 19:59:26 +01:00
const ydocUpdated = Y . cloneDoc ( ydocStart )
2025-12-29 11:37:01 +01:00
ydocUpdated . get ( 'text' ) . insert ( 5 , ' world' )
ydocUpdated . get ( 'array' ) . insert ( 1 , [ 'x' ] )
ydocUpdated . get ( 'map' ) . setAttr ( 'newk' , 42 )
ydocUpdated . get ( 'map' ) . getAttr ( 'nested' ) . insert ( 0 , [ 1 ] )
2025-11-27 19:59:26 +01:00
// @todo add custom attribution
2026-02-23 20:22:40 +01:00
const d = Y . diffDocsToDelta ( ydocStart , ydocUpdated ) . done ( )
2025-12-07 01:21:19 +01:00
console . log ( 'calculated diff' , d . toJSON ( ) )
2026-02-23 20:22:40 +01:00
const expected = delta . create ( )
2025-12-24 09:18:38 +01:00
. modifyAttr ( 'text' , delta . create ( ) . retain ( 5 ) . insert ( ' world' , null , { insert : [ ] } ) )
. modifyAttr ( 'array' , delta . create ( ) . retain ( 1 ) . insert ( [ 'x' ] , null , { insert : [ ] } ) )
. modifyAttr ( 'map' , delta . create ( ) . setAttr ( 'newk' , 42 , { insert : [ ] } ) . modifyAttr ( 'nested' , delta . create ( ) . insert ( [ 1 ] , null , { insert : [ ] } ) ) )
2026-02-23 20:22:40 +01:00
const expectedDone = expected . done ( )
2026-02-25 15:55:42 +01:00
t . compare ( d , expectedDone )
2025-11-27 19:59:26 +01:00
}
2025-12-07 02:13:04 +01:00
export const testChildListContent = ( ) => {
const ydocStart = new Y . Doc ( )
const ydocUpdated = Y . cloneDoc ( ydocStart )
2025-12-29 11:37:01 +01:00
const yf = new Y . Type ( 'test' )
2025-12-07 02:13:04 +01:00
let calledEvent = 0
2026-02-25 15:55:42 +01:00
yf . applyDelta ( delta . create ( ) . insert ( 'test content' ) . setAttr ( 'k' , 'v' ) . done ( ) )
2025-12-07 02:13:04 +01:00
2025-12-29 11:37:01 +01:00
const yarray = ydocUpdated . get ( 'array' )
yarray . observeDeep ( event => {
2025-12-07 02:13:04 +01:00
calledEvent ++
const d = event . deltaDeep
2025-12-24 09:18:38 +01:00
const expectedD = delta . create ( ) . insert ( [ delta . create ( 'test' ) . insert ( 'test content' ) . setAttr ( 'k' , 'v' ) ] )
2025-12-07 02:13:04 +01:00
t . compare ( d , expectedD )
} )
2025-12-29 11:37:01 +01:00
ydocUpdated . get ( 'array' ) . insert ( 0 , [ yf ] )
2025-12-07 02:13:04 +01:00
t . assert ( calledEvent === 1 )
const d = Y . diffDocsToDelta ( ydocStart , ydocUpdated )
console . log ( 'calculated diff' , d . toJSON ( ) )
2025-12-24 09:18:38 +01:00
const expected = delta . create ( )
2026-02-25 15:55:42 +01:00
. modifyAttr ( 'array' , delta . create ( ) . insert ( [ delta . create ( 'test' ) . insert ( 'test content' , null , { insert : [ ] } ) . setAttr ( 'k' , 'v' , { insert : [ ] } ) ] , null , { insert : [ ] } ) . done ( ) )
2025-12-24 09:18:38 +01:00
t . compare ( d . done ( ) , expected . done ( ) )
2025-12-07 02:13:04 +01:00
}
2026-01-10 22:04:30 +01:00
/ * *
* @ param { t . TestCase } tc
* /
export const testAttributionSession1 = tc => {
const { testConnector , users , text0 , text1 } = init ( tc , { users : 3 } )
2026-01-10 23:53:38 +01:00
users [ 0 ] . gc = false
2026-07-11 23:59:37 +02:00
const globalAttributions = Y . createContentMap ( )
2026-01-10 22:04:30 +01:00
const v1 = Y . cloneDoc ( users [ 0 ] )
users . forEach ( user => user . on ( 'update' , ( update , _ , ydoc , tr ) => {
if ( ! tr . local ) return
const userid = ydoc . clientID . toString ( )
2026-01-13 04:11:23 +01:00
const contentIds = Y . createContentIdsFromUpdate ( update )
2026-01-10 22:04:30 +01:00
Y . insertIntoIdMap ( globalAttributions . inserts , Y . createIdMapFromIdSet ( contentIds . inserts , [ Y . createContentAttribute ( 'insert' , userid ) ] ) )
Y . insertIntoIdMap ( globalAttributions . deletes , Y . createIdMapFromIdSet ( contentIds . deletes , [ Y . createContentAttribute ( 'delete' , userid ) ] ) )
} ) )
text0 . insert ( 0 , 'a' )
text1 . insert ( 0 , 'b' )
testConnector . flushAllMessages ( )
2026-07-11 23:59:37 +02:00
const d1 = text0 . toDelta ( { renderer : Y . createDiffRenderer ( v1 , users [ 0 ] , { attributions : globalAttributions } ) } )
2026-02-25 15:55:42 +01:00
t . compare ( d1 , delta . create ( ) . insert ( 'a' , null , { insert : [ '0' ] } ) . insert ( 'b' , null , { insert : [ '1' ] } ) . done ( ) )
2026-01-10 22:04:30 +01:00
const v2 = Y . cloneDoc ( users [ 0 ] )
2026-01-10 23:53:38 +01:00
text0 . delete ( 1 , 1 )
text1 . insert ( 2 , 'c' )
2026-01-10 22:04:30 +01:00
testConnector . flushAllMessages ( )
2026-07-11 23:59:37 +02:00
const d2 = text0 . toDelta ( { renderer : Y . createDiffRenderer ( v2 , users [ 0 ] , { attributions : globalAttributions } ) } )
2026-02-25 15:55:42 +01:00
t . compare ( d2 , delta . create ( ) . insert ( 'a' ) . insert ( 'b' , null , { delete : [ '0' ] } ) . insert ( 'c' , null , { insert : [ '1' ] } ) . done ( ) )
2026-01-10 23:53:38 +01:00
2026-07-11 23:59:37 +02:00
const onlyUser0ChangesAttributed = Y . createContentMap (
Y . filterIdMap ( globalAttributions . inserts , attrs => attrs . some ( attr => attr . name === 'insert' && attr . val === '0' ) ) ,
Y . filterIdMap ( globalAttributions . deletes , attrs => attrs . some ( attr => attr . name === 'delete' && attr . val === '0' ) )
)
const rendererUser0 = new Y . AttributionsRenderer ( onlyUser0ChangesAttributed )
2026-06-16 21:29:45 +02:00
const d3 = text0 . toDelta ( { renderer : rendererUser0 } )
2026-02-25 15:55:42 +01:00
t . compare ( d3 , delta . create ( ) . insert ( 'a' , null , { insert : [ '0' ] } ) . insert ( 'b' , null , { delete : [ '0' ] } ) . insert ( 'c' ) . done ( ) )
2026-01-10 23:53:38 +01:00
Y . undoContentIds ( users [ 0 ] , Y . createContentIdsFromContentMap ( onlyUser0ChangesAttributed ) )
const d4 = text0 . toDelta ( )
2026-02-25 15:55:42 +01:00
t . compare ( d4 , delta . create ( ) . insert ( 'bc' ) . done ( ) )
2026-01-10 22:04:30 +01:00
}
2026-04-11 18:27:01 +02:00
export const testAttributionEvent = ( ) => {
const ydoc = new Y . Doc ( )
const ytype = ydoc . get ( )
// <p>hi</p>
ytype . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'p' ) . insert ( 'hi' ) . done ( ) ] ) . done ( ) )
const ydocBase = Y . cloneDoc ( ydoc )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( ydocBase , ydoc )
2026-04-11 18:27:01 +02:00
let called = false
ytype . observeDeep ( event => {
2026-06-16 21:29:45 +02:00
const change = event . getDelta ( { renderer } )
2026-04-11 18:27:01 +02:00
const expectedChange = delta . create ( ) . modify ( delta . create ( 'p' ) . retain ( 2 , null , { delete : [ ] } ) , null , { delete : [ ] } ) . done ( )
t . compare (
change ,
expectedChange
)
called = true
} )
// delete <p>
// we expect that the children get attributions as well
ytype . delete ( 0 , 1 )
t . assert ( called )
}
2026-04-15 01:24:15 +02:00
export const testAttributionChange = ( ) => {
const ydoc = new Y . Doc ( )
const ytype = ydoc . get ( )
ytype . applyDelta ( delta . create ( ) . insert ( 'hi' ) . done ( ) )
const ydocClone = Y . cloneDoc ( ydoc )
2026-06-16 21:29:45 +02:00
const renderer = Y . createDiffRenderer ( ydocClone , ydoc )
2026-04-15 01:24:15 +02:00
ytype . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( '!' ) . done ( ) )
let calledHandler = false
2026-06-16 21:29:45 +02:00
renderer . on ( 'change' , changes => {
2026-04-15 01:24:15 +02:00
calledHandler = true
2026-06-16 21:29:45 +02:00
const changeUpdate = ytype . toDelta ( { renderer , deep : true , itemsToRender : changes , retainInserts : true , retainDeletes : true } )
2026-06-30 10:19:20 +02:00
// the '!' lost its `{ insert: [] }` suggestion attribution → the change clears it (tri-state `null`)
const expectedUpdate = delta . create ( ) . retain ( 2 ) . retain ( 1 , undefined , null )
2026-04-15 01:24:15 +02:00
t . compare ( changeUpdate , expectedUpdate )
} )
Y . applyUpdate ( ydocClone , Y . encodeStateAsUpdate ( ydoc ) )
t . assert ( calledHandler )
}
2026-06-30 10:19:20 +02:00
/ * *
* A YType implements the lib0 ` RDT ` interface , so two types can be kept in sync with ` bind ` .
* /
export const testRdtBinding = ( ) => {
const docA = new Y . Doc ( )
const docB = new Y . Doc ( )
const a = docA . get ( 'text' )
const b = docB . get ( 'text' )
const binding = bind ( a , b )
// edit A -> propagates to B
a . insert ( 0 , 'hello' )
t . assert ( b . toString ( ) === 'hello' )
// edit B -> propagates back to A (no echo loop)
b . insert ( 5 , ' world' )
t . assert ( a . toString ( ) === 'hello world' )
t . assert ( b . toString ( ) === 'hello world' )
// after the binding is destroyed, changes no longer propagate
binding . destroy ( )
a . insert ( 0 , 'x' )
t . assert ( a . toString ( ) === 'xhello world' )
t . assert ( b . toString ( ) === 'hello world' )
}
/ * *
* Local changes are emitted on the ` 'delta' ` channel as the deep delta .
* /
export const testRdtDeltaEvent = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( )
/ * *
* @ type { any }
* /
let captured = null
ytext . on ( 'delta' , d => { captured = d } )
ytext . insert ( 0 , 'hello' )
t . compare ( captured , delta . create ( ) . insert ( 'hello' ) . done ( ) )
}
2026-07-02 00:59:03 +02:00
/ * *
* The ` 'delta' ` event carries the transaction origin as its second argument .
* /
export const testRdtDeltaEventOrigin = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( )
/ * *
* @ type { any }
* /
let capturedOrigin = null
ytext . on ( 'delta' , ( _d , origin ) => { capturedOrigin = origin } )
const myOrigin = { }
ydoc . transact ( ( ) => {
ytext . insert ( 0 , 'hello' )
} , myOrigin )
t . assert ( capturedOrigin === myOrigin )
// without an explicit origin, `null` is emitted
capturedOrigin = myOrigin
ytext . insert ( 5 , ' world' )
t . assert ( capturedOrigin === null )
2026-07-02 18:44:46 +02:00
// the origin passed to `applyDelta` becomes the transaction origin and is forwarded on the event
const applyOrigin = { }
ytext . applyDelta ( delta . create ( ) . retain ( 11 ) . insert ( '!' ) . done ( ) , applyOrigin )
t . assert ( capturedOrigin === applyOrigin )
// `applyDelta` without an explicit origin emits `null`
capturedOrigin = applyOrigin
ytext . applyDelta ( delta . create ( ) . retain ( 12 ) . insert ( '?' ) . done ( ) )
t . assert ( capturedOrigin === null )
2026-07-02 00:59:03 +02:00
}
2026-06-30 10:19:20 +02:00
/ * *
* ` useRenderer ` changes the default renderer used by ` toDelta ` ( and friends ) . Calling ` toDelta() `
* with no argument afterwards is equivalent to passing the renderer explicitly .
* /
export const testUseRenderer = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( )
ytext . insert ( 0 , 'hello world' )
const v1 = Y . cloneDoc ( ydoc )
ydoc . transact ( ( ) => {
ytext . delete ( 6 , 5 )
} )
const renderer = Y . createDiffRenderer ( v1 , ydoc )
const explicit = ytext . toDelta ( { renderer } )
// change the default renderer; toDelta() with no arg now matches the explicit form
ytext . useRenderer ( renderer )
const viaDefault = ytext . toDelta ( )
t . compare ( viaDefault , explicit )
t . compare ( viaDefault , delta . create ( ) . insert ( 'hello ' ) . insert ( 'world' , null , { delete : [ ] } ) . done ( ) )
}
/ * *
* ` destroy() ` emits the RDT ` 'destroy' ` event , and top - level types are destroyed with their Doc .
* /
export const testRdtDestroy = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( 'text' )
let destroyed = 0
ytext . on ( 'destroy' , ( ) => { destroyed ++ } )
ytext . destroy ( )
t . assert ( destroyed === 1 )
// a top-level type is torn down when its Doc is destroyed
const ydoc2 = new Y . Doc ( )
const ytext2 = ydoc2 . get ( 'text' )
let destroyed2 = 0
ytext2 . on ( 'destroy' , ( ) => { destroyed2 ++ } )
ydoc2 . destroy ( )
t . assert ( destroyed2 === 1 )
}
/ * *
* The ` 'delta' ` event bubbles to ancestors on nested changes , like ` observeDeep ` . A listener on a
* container fires ( with the container - rooted delta ) when a nested child is edited .
* /
export const testRdtDeltaBubblesLikeObserveDeep = ( ) => {
const ydoc = new Y . Doc ( )
const yarray = ydoc . get ( 'arr' )
const child = new Y . Type ( )
yarray . insert ( 0 , [ child ] )
let containerFired = 0
let childFired = 0
/ * *
* @ type { any }
* /
let captured = null
yarray . on ( 'delta' , d => { containerFired ++ ; captured = d } )
child . on ( 'delta' , ( ) => { childFired ++ } )
child . insert ( 0 , 'hi' )
// both the edited child and its ancestor container received a 'delta'
t . assert ( childFired === 1 )
t . assert ( containerFired === 1 )
// the container-rooted delta is a non-empty (nested modify) change
t . assert ( captured !== null && ! captured . isEmpty ( ) )
}
/ * *
* ` get delta() ` returns the deep delta and keeps it current on every event of this type , including
* nested - child edits ( which apply as a nested ` modify ` ) . The returned value is the live cache .
* /
export const testRdtDeltaCacheMaintenance = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( 'text' )
ytext . insert ( 0 , 'hello' )
// first access materializes the cache
t . assert ( ytext . delta . equals ( delta . create ( ) . insert ( 'hello' ) . done ( ) ) )
// a later edit updates the live cache in place
const live = ytext . delta
ytext . insert ( 5 , ' world' )
t . assert ( live === ytext . delta ) // same maintained object
t . assert ( ytext . delta . equals ( delta . create ( ) . insert ( 'hello world' ) . done ( ) ) )
t . assert ( ytext . delta . equals ( ytext . toDeltaDeep ( ) ) ) // matches a fresh deep render
// nested: editing a child updates the container's cached deep delta via a nested modify apply
const yarray = ydoc . get ( 'arr' )
const child = new Y . Type ( )
yarray . insert ( 0 , [ child ] )
child . insert ( 0 , 'a' )
const before = yarray . delta // materialize under base renderer
child . insert ( 1 , 'b' ) // nested edit after materialization
t . assert ( before === yarray . delta )
t . assert ( yarray . delta . equals ( yarray . toDeltaDeep ( ) ) )
}
/ * *
* ` clearCache() ` drops the maintained deep delta ; the next ` delta ` access re - materializes it .
* /
export const testRdtClearCache = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( 'text' )
ytext . insert ( 0 , 'hello' )
const d1 = ytext . delta
t . assert ( ytext . _delta !== null )
ytext . clearCache ( )
t . assert ( ytext . _delta === null )
const d2 = ytext . delta // re-materialized, a fresh builder
t . assert ( d2 !== d1 )
t . assert ( d2 . equals ( delta . create ( ) . insert ( 'hello' ) . done ( ) ) )
}
/ * *
* ` useRenderer ` re - renders the maintained delta with the new renderer , emits the difference on the
* ` 'delta' ` channel , and updates the cache .
* /
export const testRdtUseRendererEmitsDiff = ( ) => {
const ydoc = new Y . Doc ( )
const ytext = ydoc . get ( 'text' )
ytext . insert ( 0 , 'hello world' )
const v1 = Y . cloneDoc ( ydoc )
ydoc . transact ( ( ) => { ytext . delete ( 6 , 5 ) } )
// materialize the cache under the base renderer
t . assert ( ytext . delta . equals ( delta . create ( ) . insert ( 'hello ' ) . done ( ) ) )
/ * *
* @ type { any }
* /
let captured = null
ytext . on ( 'delta' , d => { captured = d } )
ytext . useRenderer ( Y . createDiffRenderer ( v1 , ydoc ) )
// a non-empty rendering diff was emitted only on the 'delta' channel
t . assert ( captured !== null && ! captured . isEmpty ( ) )
// and the cache now reflects the diff-rendered state
t . assert ( ytext . delta . equals ( delta . create ( ) . insert ( 'hello ' ) . insert ( 'world' , null , { delete : [ ] } ) . done ( ) ) )
}
/ * *
* ` YType ` conforms to the lib0 ` RDT ` interface — verified at runtime with ` $ rdt.check ` ( replaces the
* old compile - time ` _assertYTypeIsRdt ` ) .
* /
export const testRdtConformsToRdtSchema = ( ) => {
t . assert ( $rdt . check ( new Y . Doc ( ) . get ( ) ) )
t . assert ( $rdt . check ( new Y . Type ( ) ) )
t . assert ( ! $rdt . check ( { } ) )
t . assert ( ! $rdt . check ( null ) )
}
/ * *
* Collect a type and all of its ( non - deleted ) nested ` YType ` descendants .
*
* @ param { Y . Type < any > } root
* @ return { Array < Y . Type < any >> }
* /
const collectTypes = root => {
/ * *
* @ type { Array < Y . Type < any >> }
* /
const out = [ root ]
for ( let i = 0 ; i < out . length ; i ++ ) {
out [ i ] . forEach ( c => { if ( c instanceof Y . Type ) out . push ( c ) } )
out [ i ] . forEachAttr ( v => { if ( v instanceof Y . Type ) out . push ( v ) } )
}
return out
}
/ * *
* Apply one random mutation to a random type in the tree rooted at ` root ` .
*
* @ param { prng . PRNG } gen
* @ param { Y . Type < any > } root
* /
2026-07-01 01:14:26 +02:00
const applyRandomYTypeOp = ( gen , root ) => {
2026-06-30 10:19:20 +02:00
const target = prng . oneOf ( gen , collectTypes ( root ) )
2026-07-01 11:45:55 +02:00
switch ( prng . int32 ( gen , 0 , 4 ) ) {
2026-06-30 10:19:20 +02:00
case 0 : // insert text
target . insert ( prng . int32 ( gen , 0 , target . length ) , prng . word ( gen ) )
break
case 1 : // insert a nested type
target . insert ( prng . int32 ( gen , 0 , target . length ) , [ new Y . Type ( ) ] )
break
case 2 : // delete a range
if ( target . length > 0 ) {
const p = prng . int32 ( gen , 0 , target . length - 1 )
target . delete ( p , prng . int32 ( gen , 1 , math . min ( 3 , target . length - p ) ) )
}
break
2026-07-01 01:14:26 +02:00
case 3 : // format a range (add or remove bold)
if ( target . length > 0 ) {
2026-06-30 10:19:20 +02:00
const p = prng . int32 ( gen , 0 , target . length - 1 )
target . format ( p , prng . int32 ( gen , 1 , math . min ( 3 , target . length - p ) ) , { bold : prng . bool ( gen ) ? true : null } )
}
break
case 4 : // set / delete a map attribute
if ( prng . bool ( gen ) ) {
target . setAttr ( prng . oneOf ( gen , [ 'a' , 'b' , 'c' ] ) , prng . word ( gen ) )
} else {
target . deleteAttr ( prng . oneOf ( gen , [ 'a' , 'b' , 'c' ] ) )
}
break
}
}
/ * *
* Fuzz : after each random mutation , every type ' s maintained ` delta ` cache ( at every nesting level )
* must equal a fresh deep render ` toDelta({ deep: true }) ` .
*
* @ param { t . TestCase } tc
* /
export const testRdtDeltaFuzz = tc => {
const ydoc = new Y . Doc ( )
const root = ydoc . get ( 'root' )
for ( let i = 0 ; i < 300 ; i ++ ) {
applyRandomYTypeOp ( tc . prng , root )
collectTypes ( root ) . forEach ( type =>
t . assert ( type . delta . equals ( type . toDelta ( { deep : true } ) ) , ` iter ${ i } ` ) )
}
}
/ * *
* Fuzz under a diffing renderer , across two synced replicas . Each replica is a "suggestion doc" that
* diffs against its own fixed baseline clone ( taken after some shared initial content ) . With the plain
* diff renderer ( no ` attrs ` ) , suggestion inserts render ` { insert: [] } ` and deletes render
* ` { delete: [] } ` — identical on every replica — so the maintained , diff - attributed ` delta ` must
* converge across replicas ( and match a fresh deep render ) . The cache is kept current purely by the
* ` 'delta' ` event ( no recompute ) .
*
* @ param { t . TestCase } tc
* /
export const testRdtDeltaSuggestionConvergence = tc => {
const { testConnector , users } = init ( tc , { users : 2 } )
const [ d0 , d1 ] = users
d0 . get ( 'root' ) . insert ( 0 , 'shared baseline content' )
testConnector . flushAllMessages ( )
// each replica diffs against its own fixed baseline clone (plain diff renderer => {insert:[]}/{delete:[]})
d0 . get ( 'root' ) . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( d0 ) , d0 ) )
d1 . get ( 'root' ) . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( d1 ) , d1 ) )
for ( let i = 0 ; i < 300 ; i ++ ) {
2026-07-01 01:14:26 +02:00
applyRandomYTypeOp ( tc . prng , prng . oneOf ( tc . prng , users ) . get ( 'root' ) ) // includes format add/remove
2026-06-30 10:19:20 +02:00
testConnector . flushAllMessages ( )
const a = d0 . get ( 'root' ) . delta
const b = d1 . get ( 'root' ) . delta
t . assert ( a . equals ( b ) , ` converge iter ${ i } ` ) // the suggestion view is replica-independent
t . assert ( a . equals ( d0 . get ( 'root' ) . toDelta ( { deep : true } ) ) , ` canonical iter ${ i } ` ) // and matches a fresh render
}
}
2026-07-01 01:14:26 +02:00
/ * *
* Regression ( deterministic , seed 1 ) : removing a format under a diffing renderer must keep the
* incrementally - maintained ` .delta ` equal to a fresh ` toDelta({ deep: true }) ` . The bug was in the
* ` ContentFormat ` change - mode block of ` toDelta ` ( src / ytype . js ) : un - formatting cleared the format
* * value * but emitted only a context - skip for the format - * attribution * , so the maintained cache kept
* a stale ` {attribution:{format:{bold:[]}}} ` on the un - formatted range and drifted ( at iter 35 ) . The
* fix emits an explicit ` attribution:{format:{<key>:null}} ` clear on the retained range ( only in a
* change / diff render ) . This test pins that behavior ; if it regresses , the drift reappears at iter 35.
* /
export const testRdtFormatRemovalDrift = ( ) => {
const gen = prng . create ( 1 ) // fixed seed → deterministic
const docs = [ new Y . Doc ( ) , new Y . Doc ( ) ]
const [ d0 , d1 ] = docs
const sync = ( ) => {
Y . applyUpdate ( d1 , Y . encodeStateAsUpdate ( d0 , Y . encodeStateVector ( d1 ) ) )
Y . applyUpdate ( d0 , Y . encodeStateAsUpdate ( d1 , Y . encodeStateVector ( d0 ) ) )
}
d0 . get ( 'root' ) . insert ( 0 , 'shared baseline content' )
sync ( )
// each replica diffs against its own fixed baseline clone
d0 . get ( 'root' ) . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( d0 ) , d0 ) )
d1 . get ( 'root' ) . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( d1 ) , d1 ) )
for ( let i = 0 ; i < 40 ; i ++ ) {
applyRandomYTypeOp ( gen , prng . oneOf ( gen , docs ) . get ( 'root' ) )
sync ( )
// read `.delta` every step so it is maintained incrementally (a single read at the end would
// recompute fresh and hide the drift). The maintained delta MUST equal a fresh deep render.
const cached = d0 . get ( 'root' ) . delta
const fresh = d0 . get ( 'root' ) . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'iter ' + i + ' cached :' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'iter ' + i + ' toDelta:' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , ` iter ${ i } : maintained .delta drifted from toDelta({ deep: true }) ` )
}
}
/ * *
* Regression ( minimal , deterministic , no prng ) : re - bolding content by deleting a transient ` bold:null `
* marker under a diffing renderer must keep the maintained ` delta ` equal to a fresh deep render .
*
* Steps : bold all of "abcdef" , un - bold "cd" ( inserts a ` bold:null ` marker ) , then re - bold "cd" ( which
* DELETES that marker ) . The deleted marker surfaces ` attrs == null ` in the change render , so the
* attribution context must be * preserved * ( not cleared ) for the re - bolded run ; a fresh render sees the
* resulting attributed ` bold:true ` marker and renders ` {format:{bold:[]}} ` , so the cache must match :
*
* . delta == toDelta ( { deep } ) == "abcdef" { bold , attr : { format : { bold : [ ] } } }
* /
export const testRdtFormatRebold = ( ) => {
const doc = new Y . Doc ( )
const root = doc . get ( 'root' )
root . insert ( 0 , 'abcdef' )
// diff against a baseline taken BEFORE formatting => every format change is an attributed suggestion
root . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( doc ) , doc ) )
2026-07-01 12:00:42 +02:00
// first access starts maintaining the incremental cache (baseline == current, so no suggestions yet)
t . assert ( root . delta . equals ( delta . create ( ) . insert ( 'abcdef' ) . done ( ) ) )
2026-07-01 01:14:26 +02:00
root . format ( 0 , 6 , { bold : true } ) // all bold
root . format ( 2 , 2 , { bold : null } ) // un-bold "cd" (inserts a transient bold:null marker)
root . format ( 2 , 2 , { bold : true } ) // re-bold "cd" => DELETES that transient marker
const cached = root . delta
const fresh = root . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'rebold cached :' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'rebold toDelta:' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta drifted from toDelta({ deep: true }) after re-bold' )
}
/ * *
2026-07-01 11:45:55 +02:00
* Regression ( was a known bug — minimal , deterministic , no prng ) : inserting an embed ( nested ` Y.Type ` )
* into a bold run used to leave a spurious ` attribution:{format:{bold:null}} ` null - leaf on the embed in
* the maintained ` delta ` , where a fresh deep render has none . Now fixed ; this pins it .
2026-07-01 01:14:26 +02:00
*
* Two ops : bold "ab" , then insert an embed between "a" and "b" . Inserting into a formatted run makes
* Yjs surround the embed with NEGATED markers ( ` [bold:null] <embed> [bold:true] ` ) so the embed is not
* bold . In the change render of that insert , the new ` bold:null ` negation marker triggers the
* format - attribution clear ( a ` null ` leaf ) , and because the embed is a FRESH renderContent insert it
* inherits that leaf — but unlike a text insert the null - leaf does NOT resolve away for a ` ContentType `
* ( embed ) insert , so it sticks in the cache . A full render ( insert mode ) never emits the leaf :
*
* maintained . delta : "a" { bold , attr } | < embed > { attr : { format : { bold : null } , insert : [ ] } } | "b" { bold , attr }
* toDelta ( { deep } ) : "a" { bold , attr } | < embed > { attr : { insert : [ ] } } | "b" { bold , attr }
*
* Root cause : the single ` usedAttribution ` context can ' t distinguish inserts ( need absolute attribution ,
* no null - leaves ) from retains ( need the null - leaf clear ) — the value dimension already splits these
2026-07-01 11:45:55 +02:00
* ( ` currentFormats ` for inserts vs ` changedFormats ` for retains ) ; the attribution dimension does
2026-07-01 01:14:26 +02:00
* not . ( Note : a * third * op ` format(1,1,{bold:null}) ` to un - bold the embed is a no - op — the embed is
* already not bold — so it produces an empty transaction and fires no ` 'delta' ` event . )
* /
export const testRdtFormatEmbedInBold = ( ) => {
const doc = new Y . Doc ( )
const root = doc . get ( 'root' )
root . insert ( 0 , 'ab' )
root . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( doc ) , doc ) )
2026-07-01 12:00:42 +02:00
// first access starts maintaining the incremental cache (baseline == current, so no suggestions yet)
t . assert ( root . delta . equals ( delta . create ( ) . insert ( 'ab' ) . done ( ) ) )
2026-07-01 01:14:26 +02:00
root . format ( 0 , 2 , { bold : true } ) // bold "ab"
root . insert ( 1 , [ new Y . Type ( ) ] ) // insert an embed inside the bold run: "a<T>b"
const cached = root . delta
const fresh = root . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'embed-in-bold cached :' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'embed-in-bold toDelta:' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta drifted from toDelta({ deep: true }) after embed-in-bold insert' )
}
/ * *
2026-07-01 01:36:43 +02:00
* Regression ( minimal , deterministic , no prng ) : formatting a char and then deleting it under a diffing
* renderer used to leave the maintained ` delta ` with a stale bold value + ` {format:{bold:[]}} `
* attribution on the deleted char , where a fresh deep render keeps only the ` {delete:[]} ` suggestion .
2026-07-01 01:14:26 +02:00
*
2026-07-01 01:36:43 +02:00
* maintained . delta : "a" { format : { bold : true } , attribution : { format : { bold : [ ] } , delete : [ ] } } ( was )
* toDelta ( { deep } ) : "a" { attribution : { delete : [ ] } } ( correct )
2026-07-01 01:14:26 +02:00
*
2026-07-01 01:36:43 +02:00
* On delete , the format markers around the char are cleaned up ( deleted ) too ; their insert + delete
* suggestion nets to no attribution , so the change render skipped them and never undid the value +
* attribution the format step had written to the cache . The fix ( in ` toDelta ` ) : a retain emits the
2026-07-01 11:45:55 +02:00
* format * diff * ( ` changedFormats ` , which carries the ` bold→null ` clear ) , and a deleted format marker
2026-07-01 01:36:43 +02:00
* that actually removes a format under an attributing renderer emits an explicit ` {format:{<key>:null}} `
* attribution clear . This was the stale - ` {format:{bold:[]}} ` - on - deleted - content ( re - assert ) class .
2026-07-01 01:14:26 +02:00
* /
export const testRdtFormatDeleteFormatted = ( ) => {
const doc = new Y . Doc ( )
const root = doc . get ( 'root' )
root . insert ( 0 , 'a' )
root . useRenderer ( Y . createDiffRenderer ( Y . cloneDoc ( doc ) , doc ) ) // baseline before formatting
2026-07-01 12:00:42 +02:00
// start maintaining the incremental cache (baseline == current, so no suggestions yet)
t . assert ( root . delta . equals ( delta . create ( ) . insert ( 'a' ) . done ( ) ) )
2026-07-01 01:14:26 +02:00
root . format ( 0 , 1 , { bold : true } ) // bold "a"
root . delete ( 0 , 1 ) // delete "a"
const cached = root . delta
const fresh = root . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'delete-formatted cached :' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'delete-formatted toDelta:' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta drifted from toDelta({ deep: true }) after format+delete' )
}
2026-06-30 10:19:20 +02:00
/ * *
* Sanity : the maintained ` delta ` equals both an explicit expected delta and a fresh deep render —
* for flat content , nested children , and ongoing edits .
* /
export const testRdtDeltaSanity = ( ) => {
const ydoc = new Y . Doc ( )
const root = ydoc . get ( 'root' )
root . insert ( 0 , 'hello' )
root . setAttr ( 'k' , 'v' )
t . assert ( root . delta . equals ( delta . create ( ) . insert ( 'hello' ) . setAttr ( 'k' , 'v' ) . done ( ) ) )
t . assert ( root . delta . equals ( root . toDelta ( { deep : true } ) ) )
// nested child + ongoing edits keep delta == fresh deep render
const child = new Y . Type ( )
root . insert ( 5 , [ child ] )
child . insert ( 0 , 'world' )
t . assert ( root . delta . equals ( root . toDelta ( { deep : true } ) ) )
child . insert ( 5 , '!' )
root . delete ( 0 , 1 )
t . assert ( root . delta . equals ( root . toDelta ( { deep : true } ) ) )
// the nested child's own cache is consistent too
t . assert ( child . delta . equals ( child . toDelta ( { deep : true } ) ) )
t . assert ( child . delta . equals ( delta . create ( ) . insert ( 'world!' ) . done ( ) ) )
}
/ * *
* Sanity : under a diffing - attribution renderer the maintained ` delta ` carries the expected
* attribution markers and equals a fresh attributed deep render .
* /
export const testRdtDeltaAttributionSanity = ( ) => {
const ydoc = new Y . Doc ( )
const root = ydoc . get ( 'root' )
const v1 = Y . cloneDoc ( ydoc )
2026-07-11 23:59:37 +02:00
const attributions = Y . createContentMap ( )
2026-06-30 10:19:20 +02:00
ydoc . on ( 'update' , ( update , _origin , doc , tr ) => {
if ( ! tr . local ) return
const uid = doc . clientID . toString ( )
const cids = Y . createContentIdsFromUpdate ( update )
2026-07-11 23:59:37 +02:00
Y . insertIntoIdMap ( attributions . inserts , Y . createIdMapFromIdSet ( cids . inserts , [ Y . createContentAttribute ( 'insert' , uid ) ] ) )
Y . insertIntoIdMap ( attributions . deletes , Y . createIdMapFromIdSet ( cids . deletes , [ Y . createContentAttribute ( 'delete' , uid ) ] ) )
2026-06-30 10:19:20 +02:00
} )
root . insert ( 0 , 'hello' ) // a suggestion relative to v1
const uid = ydoc . clientID . toString ( )
2026-07-11 23:59:37 +02:00
root . useRenderer ( Y . createDiffRenderer ( v1 , ydoc , { attributions } ) )
2026-06-30 10:19:20 +02:00
t . assert ( root . delta . equals ( delta . create ( ) . insert ( 'hello' , null , { insert : [ uid ] } ) . done ( ) ) )
t . assert ( root . delta . equals ( root . toDelta ( { deep : true } ) ) )
}
2026-07-03 00:37:32 +02:00
/ * *
2026-07-05 23:33:40 +02:00
* Regression pin ( originally a failing repro ) : formatting across a suggestion - deleted range with an
2026-07-03 00:37:32 +02:00
* accepting renderer ( suggestionMode = false ) drifts the maintained ` .delta `
* cache — the change render attributes the formatted runs
* ( ` {format:{code:[]}} ` ) while a fresh deep render nets no format attribution
* ( the format committed to base ; it is not a suggestion ) . Also reproduces with
* two independent renderers on separate suggestion docs ( fixed suggestionMode
* flags ) , i . e . without flipping the flag : a suggestion - mode peer deletes , a
* view - suggestions peer formats across the deleted range .
* /
export const testRdtFormatAcrossSuggestionDeletedDrift = ( ) => {
const doc = new Y . Doc ( { gc : false } )
const suggestionDoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , suggestionDoc , { attributions : Y . createContentMap ( ) } )
2026-07-03 00:37:32 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'hello world' ) ] ) . done ( )
)
const ytype = suggestionDoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
2026-07-03 19:35:43 +02:00
t . assert ( ytype . delta != null ) // materialize the maintained cache
2026-07-03 00:37:32 +02:00
// suggestion-delete "llo " (stays a suggestion; still rendered, attributed)
renderer . suggestionMode = true
ytype . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . delete ( 4 ) ) . done ( ) )
// as an accepting user, format across the still-rendered deleted range
renderer . suggestionMode = false
ytype . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 6 , { code : { } } ) ) . done ( ) )
const cached = ytype . delta
const fresh = ytype . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
2026-07-03 19:35:43 +02:00
/ * *
* Fixture for the delivery - through - deleted - parents tests : base doc + suggestion doc with pinned
* clientIDs ( item / marker order at equal positions depends on clientID comparison — always pin , and
* exercise both orderings where it matters ) , a ` paragraph('hello world') ` created on the base doc
* ( flows into the suggestion doc through the renderer ) , and optionally the diff renderer attached
* to the suggestion doc ' s root .
*
* @ param { number } baseClientID
* @ param { number } sdocClientID
* @ param { boolean } useRootRenderer
* /
const createSuggestionPair = ( baseClientID , sdocClientID , useRootRenderer = true ) => {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-03 19:35:43 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'hello world' ) ] ) . done ( )
)
const ytype = sdoc . get ( 'prosemirror' )
if ( useRootRenderer ) ytype . useRenderer ( renderer )
return { doc , sdoc , renderer , ytype }
}
/ * *
* A remote base - doc insert into a suggestion - deleted paragraph must reach the root ' s RDT surface :
2026-07-04 16:03:01 +02:00
* the tombstone is still rendered by the diff renderer , so the change is visible . The event
2026-07-03 19:35:43 +02:00
* bubbles through the deleted parent ( tracked unconditionally in ` changedParentTypes ` , fired on
2026-07-04 16:03:01 +02:00
* live ancestors ) , and the freshly inserted content — auto - deleted with its parent in the same
* transaction ( ` insertSet ∩ deleteSet ` ) yet attributed by the renderer — renders as a fresh
* insert carrying its delete attribution ( ` itemsToRender ` includes ` I∩D ∩ renderer.attributed ` ) ,
* so the maintained cache stays equal to a fresh deep render .
2026-07-03 19:35:43 +02:00
* /
export const testRdtDeltaThroughDeletedParent = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { doc , ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null ) // materialize the maintained cache
let fired = 0
/ * *
* @ type { any }
* /
let captured = null
ytype . on ( 'delta' , d => { fired ++ ; captured = d } )
// suggestion-delete the whole paragraph (stays rendered as an attributed tombstone)
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
t . assert ( fired === 1 )
fired = 0
// remote base edit inside the tombstone: integrates under the deleted paragraph in the
// suggestion doc (and is auto-deleted with it) — must still fire the root's 'delta'
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) ) . done ( ) )
t . assert ( fired === 1 , 'root delta fires for a change inside a suggestion-deleted paragraph' )
t . assert ( captured !== null && ! captured . isEmpty ( ) )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( JSON . stringify ( fresh . toJSON ( ) ) . includes ( 'XY' ) , 'fresh render shows the text inside the tombstone' )
const cached = ytype . delta
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
2026-07-04 16:03:01 +02:00
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* A remote base - doc insert of a whole * nested type * into a suggestion - deleted paragraph : the
* fresh paragraph and all of its content are auto - deleted on integration yet attributed by the
* renderer , so the change renders it as a deep fresh insert ( mode - 3 through the ContentType
* branch ) and the maintained cache stays equal to a fresh deep render .
* /
export const testRdtDeltaFreshTypeThroughDeletedParent = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { doc , ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null ) // materialize the maintained cache
let fired = 0
ytype . on ( 'delta' , ( ) => { fired ++ } )
// suggestion-delete the whole paragraph (stays rendered as an attributed tombstone)
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
fired = 0
// remote base edit: insert a fresh nested paragraph inside the tombstone
const freshParagraph = /** @type {any} */ ( delta . create ( 'paragraph' , { } , 'fresh' ) )
const inner = /** @type {any} */ ( delta . create ( ) . retain ( 2 ) . insert ( [ freshParagraph ] ) )
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( inner ) . done ( ) )
t . assert ( fired === 1 , 'root delta fires for a fresh nested type inside a suggestion-deleted paragraph' )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( JSON . stringify ( fresh . toJSON ( ) ) . includes ( 'fresh' ) , 'fresh render shows the nested type inside the tombstone' )
const cached = ytype . delta
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* Content inserted AND suggestion - deleted within the same transaction renders as nothing ( a
* suggested insert that was taken back is invisible ) , and the maintained cache stays consistent —
* the ` insertSet ∩ deleteSet ` handling must not leak invisible content into the rendered state .
* /
export const testRdtDeltaSuggestedInsertThenDeleteInvisible = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { sdoc , ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null ) // materialize the maintained cache
// insert and delete the same content in ONE transaction on the suggestion doc
sdoc . transact ( ( ) => {
const par = /** @type {Y.Type} */ ( ytype . get ( 0 ) )
par . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( 'zz' ) . done ( ) )
par . applyDelta ( delta . create ( ) . retain ( 2 ) . delete ( 2 ) . done ( ) )
} )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( ! JSON . stringify ( fresh . toJSON ( ) ) . includes ( 'zz' ) , 'insert-then-deleted suggestion is invisible' )
const cached = ytype . delta
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
2026-07-03 19:35:43 +02:00
}
}
2026-07-04 16:03:01 +02:00
/ * *
* Freshness ( mode 3 ) must be decided per id range , not per item : a nested transaction ( created
* from a 'delta' observer during another transaction ' s emit loop ) has its freshly
* inserted + deleted item merged into an older left neighbor by the OUTER transaction ' s cleanup
* ( ` tryToMergeWithLefts ` ) before the nested transaction ' s events render . With a whole - item check
* ( ` insertedItems.hasId(item.id) ` — first id only ) the fresh range would render as a spurious
* ` delete ` op , removing content the cache legitimately holds .
* /
export const testRdtDeltaFreshRangeAfterItemMerge = ( ) => {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = 1
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = 2
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-04 16:03:01 +02:00
doc . get ( 't' ) . applyDelta ( delta . create ( ) . insert ( 'XY' ) . done ( ) )
const ytype = sdoc . get ( 't' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null ) // materialize the maintained cache
let reacted = false
ytype . on ( 'delta' , change => {
if ( reacted || ! JSON . stringify ( change . toJSON ( ) ) . includes ( '"a"' ) ) return
reacted = true
// nested transaction, cleaned up after the outer one: insert 'b' right after the suggested
// 'a' (adjacent clock, same client), then delete both — the outer cleanup merges the two
// deleted items into one before this transaction's events render
sdoc . transact ( ( ) => {
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( 'b' ) . done ( ) )
ytype . applyDelta ( delta . create ( ) . delete ( 2 ) . done ( ) )
} )
} )
// outer transaction: suggested insert 'a' at position 0
ytype . applyDelta ( delta . create ( ) . insert ( 'a' ) . done ( ) )
t . assert ( reacted )
const cached = ytype . delta
const fresh = ytype . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
2026-07-05 16:39:34 +02:00
/ * *
* The lib0 ` RDT ` fix contract for a * fully reverted * apply : nothing landed on the doc , the
* maintained cache stays consistent , and ` before.apply(d).apply(fix) ` round - trips back to the
* actual ( unchanged ) rendered state — the fix is the inverse of the unapplied change .
*
* @ param { any } ytype the type whose maintained ` .delta ` cache to check ( must be materialized )
* @ param { any } before deep render of ` ytype ` captured before the apply
* @ param { any } d the change that was ( not ) applied
* @ param { any } fix the fix ` applyDelta ` returned
* /
const assertRevertedApply = ( ytype , before , d , fix ) => {
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( delta . diff ( before , fresh ) . isEmpty ( ) , 'nothing was applied to the doc' )
t . assert ( ytype . delta . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
const roundTrip = delta . cloneDeep ( before )
roundTrip . apply ( delta . cloneDeep ( d ) , { final : true , move : true } )
if ( fix !== null ) {
roundTrip . apply ( delta . cloneDeep ( fix ) , { final : true , move : true } )
}
t . assert ( delta . diff ( roundTrip , fresh ) . isEmpty ( ) , 'the fix round-trips the expected state back to the actual state' )
}
/ * *
* Modifying a suggestion - deleted ( rendered ) node must not apply anything — ` applyDelta ` returns
* the reverted operation ( the inverse of the nested change ) as the RDT fix , emits no 'delta'
* event ( nothing changed ) , and leaves doc + cache untouched .
* /
export const testRdtApplyDeltaModifyIntoTombstoneReturnsInverse = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null ) // materialize the maintained cache
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
const before = ytype . toDelta ( { deep : true } )
let fired = 0
ytype . on ( 'delta' , ( ) => { fired ++ } )
const d = delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null , 'the reverted operation is returned' )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . delete ( 2 ) ) . done ( ) . toJSON ( ) )
t . assert ( fired === 0 , 'a fully reverted apply emits no delta event' )
t . assert ( ! JSON . stringify ( ytype . toDelta ( { deep : true } ) . toJSON ( ) ) . includes ( 'XY' ) , 'the insert was not applied' )
assertRevertedApply ( ytype , before , d , fix )
}
}
/ * *
* Deleting content inside a tombstone : the fix re - inserts the deleted range from the rendered
* base state , restoring its stored attribution ( the caller ' s view shows the content
* delete - attributed — the revert must bring exactly that back ) .
* /
export const testRdtApplyDeltaDeleteInsideTombstoneInverse = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null )
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
const before = ytype . toDelta ( { deep : true } )
const d = delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . delete ( 4 ) ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null )
const fixJson = JSON . stringify ( /** @type {any} */ ( fix ) . toJSON ( ) )
t . assert ( fixJson . includes ( 'llo ' ) , 'the fix re-inserts the deleted range' )
t . assert ( fixJson . includes ( '"delete"' ) , 'the re-insert restores the stored delete attribution' )
assertRevertedApply ( ytype , before , d , fix )
}
}
/ * *
* Formatting content inside a tombstone : not applied ( no markers created ) ; the fix clears the
* format keys back to the base values ( ` { bold: null } ` for a previously unformatted range ) .
* /
export const testRdtApplyDeltaFormatInsideTombstoneInverse = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null )
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
const before = ytype . toDelta ( { deep : true } )
const d = delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 4 , { bold : { } } ) ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 4 , { bold : null } ) ) . done ( ) . toJSON ( ) )
assertRevertedApply ( ytype , before , d , fix )
}
}
/ * *
* A ` modify ` carrying node formats on a tombstone : ` op.format ` is not applied either , and the fix
* restores the * previous * format value — read from the cursor ' s format context AFTER stepping to
* the node , so an alive format marker between the walk ' s start and the node is accounted for .
* /
export const testRdtApplyDeltaNodeFormatOnTombstoneInverse = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'aa' ) , delta . create ( 'paragraph' , { } , 'bb' ) ] ) . done ( )
)
// base-doc node format over both paragraphs: an alive `align` marker sits before the first one
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . retain ( 2 , { align : 'x' } ) . done ( ) )
const ytype = sdoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null )
// suggestion-delete the second paragraph
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . delete ( 1 ) . done ( ) )
const before = ytype . toDelta ( { deep : true } )
const d = delta . create ( ) . retain ( 1 ) . modify ( delta . create ( ) , { align : 'y' } ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . retain ( 1 ) . modify ( delta . create ( ) , { align : 'x' } ) . done ( ) . toJSON ( ) )
assertRevertedApply ( ytype , before , d , fix )
}
}
/ * *
* Applied and reverted ops coexist in one delta : ops on live content land ( one 'delta' event ) ,
* only the tombstone - targeting modify reverts — and the fix ' s retain pad is measured in the
* caller 's *expected* space (including `d`' s own earlier insert ) .
* /
export const testRdtApplyDeltaMixedFixCoordinates = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [
delta . create ( 'paragraph' , { } , 'aa' ) , delta . create ( 'paragraph' , { } , 'hello world' ) , delta . create ( 'paragraph' , { } , 'cc' )
] ) . done ( )
)
const ytype = sdoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null )
// suggestion-delete the middle paragraph
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . delete ( 1 ) . done ( ) )
let fired = 0
ytype . on ( 'delta' , ( ) => { fired ++ } )
const innerXY = /** @type {any} */ ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) )
const innerZZ = /** @type {any} */ ( delta . create ( ) . retain ( 2 ) . insert ( 'ZZ' ) )
const pNew = /** @type {any} */ ( delta . create ( 'paragraph' , { } , 'nn' ) )
const d = /** @type {any} */ ( delta . create ( ) ) . insert ( [ pNew ] ) . retain ( 1 ) . modify ( innerXY ) . modify ( innerZZ ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fired === 1 , 'the applied part of the change emits exactly one delta event' )
t . assert ( fix !== null )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . retain ( 2 ) . modify ( delta . create ( ) . retain ( 2 ) . delete ( 2 ) ) . done ( ) . toJSON ( ) )
const fresh = JSON . stringify ( ytype . toDelta ( { deep : true } ) . toJSON ( ) )
t . assert ( fresh . includes ( 'nn' ) && fresh . includes ( 'ZZ' ) , 'ops on live content were applied' )
t . assert ( ! fresh . includes ( 'XY' ) , 'the tombstone-targeting modify was not applied' )
t . assert ( ytype . delta . equals ( ytype . toDelta ( { deep : true } ) ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* ` modifyAttr ` addressing a suggestion - deleted ( still rendered ) map value : the renderer - aware
* lookup finds the tombstone , nothing is applied , and the fix wraps the inverse in a
* ` modifyAttr ` — previously this hit ` unexpectedCase ` ( ` typeMapGet ` is blind to deleted items ) .
* /
export const testRdtApplyDeltaModifyAttrOnDeletedMapValue = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
const title = doc . get ( 'm' ) . setAttr ( 'title' , new Y . Type ( ) )
title . insert ( 0 , 'hi' )
const m = sdoc . get ( 'm' )
m . useRenderer ( renderer )
t . assert ( m . delta != null )
// suggestion-delete the attribute (stays rendered, delete-attributed)
m . applyDelta ( delta . create ( ) . deleteAttr ( 'title' ) . done ( ) )
const before = m . toDelta ( { deep : true } )
const d = delta . create ( ) . modifyAttr ( 'title' , delta . create ( ) . insert ( 'X' ) ) . done ( )
const fix = m . applyDelta ( d )
t . assert ( fix !== null , 'no throw — the reverted operation is returned' )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . modifyAttr ( 'title' , delta . create ( ) . delete ( 1 ) ) . done ( ) . toJSON ( ) )
assertRevertedApply ( m , before , d , fix )
}
}
/ * *
* A plainly deleted ( invisible — no renderer claims it ) type keeps today ' s semantics : the apply
* is silently dropped and ` applyDelta ` returns ` null ` ( the caller ' s view shows nothing there , so
* there is nothing to revert ) .
* /
export const testRdtApplyDeltaInvisibleDeletedSilentNull = ( ) => {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = 1
const root = doc . get ( 'prosemirror' )
root . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'hello' ) ] ) . done ( ) )
const par = /** @type {Y.Type} */ ( root . get ( 0 ) )
root . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
const res = par . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) . done ( ) )
t . assert ( res === null , 'invisible deleted type: silent drop, no fix' )
t . assert ( root . toDelta ( { deep : true } ) . isEmpty ( ) , 'nothing was applied' )
}
/ * *
* ` applyDelta ` called directly on a deleted - but - rendered type ( the top - level guard ) : nothing is
* applied and the inverse against the rendered state is returned . Without a renderer the same
* call stays a silent ` null ` drop .
* /
export const testRdtApplyDeltaDirectGuardOnDeletedType = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { ytype , renderer } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null )
const par = /** @type {Y.Type} */ ( ytype . get ( 0 ) )
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
const rootBefore = ytype . toDelta ( { deep : true } )
const d = /** @type {any} */ ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) . done ( ) )
// children do not inherit the root's renderer — without one the node is invisible
t . assert ( par . applyDelta ( d ) === null , 'no renderer: silent drop' )
const fix = par . applyDelta ( d , null , { renderer } )
t . assert ( fix !== null )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . retain ( 2 ) . delete ( 2 ) . done ( ) . toJSON ( ) )
t . assert ( delta . diff ( rootBefore , ytype . toDelta ( { deep : true } ) ) . isEmpty ( ) , 'nothing was applied to the doc' )
t . assert ( ytype . delta . equals ( ytype . toDelta ( { deep : true } ) ) , 'maintained .delta must equal a fresh deep render' )
// fix round-trip at the node level, against its rendered state
const parBefore = /** @type {any} */ ( par . toDelta ( { deep : true , renderer } ) )
const roundTrip = /** @type {any} */ ( delta . cloneDeep ( parBefore ) )
roundTrip . apply ( delta . cloneDeep ( d ) , { final : true , move : true } )
roundTrip . apply ( delta . cloneDeep ( /** @type {any} */ ( fix ) ) , { final : true , move : true } )
t . assert ( delta . diff ( roundTrip , par . toDelta ( { deep : true , renderer } ) ) . isEmpty ( ) , 'the fix round-trips at the node level' )
}
}
/ * *
* A tombstone grandchild behind an alive child : the alive child ' s applyDelta bubbles the nested
* fix , and the parent wraps it positionally — ` modify(modify(inverse)) ` .
* /
export const testRdtApplyDeltaNestedTombstoneFixBubbles = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , [ delta . create ( 'nested' , { } , 'ww' ) ] ) ] ) . done ( )
)
const ytype = sdoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null )
// suggestion-delete only the nested node inside the (alive) paragraph
const par = /** @type {Y.Type} */ ( ytype . get ( 0 ) )
par . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) , null , { renderer } )
const before = ytype . toDelta ( { deep : true } )
const innerX = /** @type {any} */ ( delta . create ( ) . insert ( 'X' ) )
const midModify = /** @type {any} */ ( delta . create ( ) . modify ( innerX ) )
const d = delta . create ( ) . modify ( midModify ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null )
const innerDel = /** @type {any} */ ( delta . create ( ) . delete ( 1 ) )
const midModifyDel = /** @type {any} */ ( delta . create ( ) . modify ( innerDel ) )
t . compare ( /** @type {any} */ ( fix ) . toJSON ( ) , delta . create ( ) . modify ( midModifyDel ) . done ( ) . toJSON ( ) )
assertRevertedApply ( ytype , before , d , fix )
}
}
/ * *
* A plain ` delete ` op spanning a tombstone keeps its existing semantics ( suggestion - deletes the
* alive content , records the attributed range ) and returns no fix — the known , downstream - healed
* gap ; pinned here so a change of behavior is a conscious one .
* /
export const testRdtApplyDeltaPureDeleteOverTombstoneNoFix = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [
delta . create ( 'paragraph' , { } , 'aa' ) , delta . create ( 'paragraph' , { } , 'hello world' ) , delta . create ( 'paragraph' , { } , 'cc' )
] ) . done ( )
)
const ytype = sdoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null )
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . delete ( 1 ) . done ( ) )
const res = ytype . applyDelta ( delta . create ( ) . delete ( 3 ) . done ( ) )
t . assert ( res === null , 'a plain delete over a tombstone range returns no fix' )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( ytype . delta . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* A ` delete ` op ending mid - way through a struck ( attributed - deleted ) chunk must split the item at
* the consumption boundary — otherwise the cursor advances past the whole chunk and every
* following op of the same delta targets too far right ( the modify walk then reverts the WRONG
* node , returning a fix that carries another node ' s content ) .
* /
export const testRdtApplyDeltaDeleteMidStruckChunkKeepsCursorSync = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc . get ( 't' ) . applyDelta ( delta . create ( ) . insert ( 'abc' ) . done ( ) )
doc . get ( 't' ) . applyDelta ( delta . create ( ) . retain ( 3 ) . insert ( [ delta . create ( 'nA' , { } , 'kk' ) , delta . create ( 'nB' , { } , 'qqq' ) ] ) . done ( ) )
const ytype = sdoc . get ( 't' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null )
// strike 'bc' (one 2-unit chunk) and both nodes
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . delete ( 2 ) . done ( ) )
ytype . applyDelta ( delta . create ( ) . retain ( 3 ) . delete ( 2 ) . done ( ) )
const before = ytype . toDelta ( { deep : true } )
// delete struck 'b' (ends MID-chunk), retain struck 'c', revert-modify tombstone nA
const inner = /** @type {any} */ ( delta . create ( ) . delete ( 2 ) )
const d = delta . create ( ) . retain ( 1 ) . delete ( 1 ) . retain ( 1 ) . modify ( inner ) . done ( )
const fix = ytype . applyDelta ( d )
t . assert ( fix !== null )
const fixJson = JSON . stringify ( /** @type {any} */ ( fix ) . toJSON ( ) )
t . assert ( fixJson . includes ( 'kk' ) && ! fixJson . includes ( 'qq' ) , "the fix restores nA's content, not nB's" )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( delta . diff ( before , fresh ) . isEmpty ( ) , 'nothing was applied (struck delete is meta-only, modify reverted)' )
t . assert ( ytype . delta . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* A change * inside * a suggestion - deleted ( still rendered ) attr value must re - emit : the deleted
* value has no modifyAttr path , so the change render re - emits the full - state ` setAttr ` ( an
* idempotent replace ) whenever the value type is in ` modified ` — else the maintained cache and
* every RDT consumer go permanently stale .
* /
export const testRdtDeltaThroughDeletedAttrValue = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const doc = new Y . Doc ( { gc : false } )
doc . clientID = baseClientID
const sdoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sdoc . clientID = sdocClientID
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , sdoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
const title = doc . get ( 'm' ) . setAttr ( 'title' , new Y . Type ( ) )
title . insert ( 0 , 'hi' )
const m = sdoc . get ( 'm' )
m . useRenderer ( renderer )
t . assert ( m . delta != null )
m . applyDelta ( delta . create ( ) . deleteAttr ( 'title' ) . done ( ) )
t . assert ( m . delta . equals ( m . toDelta ( { deep : true } ) ) , 'cache consistent after the suggestion deleteAttr' )
let fired = 0
m . on ( 'delta' , ( ) => { fired ++ } )
// base-doc edit INSIDE the tombstone attr value
doc . get ( 'm' ) . getAttr ( 'title' ) . insert ( 2 , 'XY' )
t . assert ( fired === 1 , "'delta' fires for a change inside the tombstone attr value" )
const fresh = m . toDelta ( { deep : true } )
t . assert ( JSON . stringify ( fresh . toJSON ( ) ) . includes ( 'XY' ) , 'fresh render shows the edit' )
t . assert ( m . delta . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
2026-07-03 19:35:43 +02:00
/ * *
* A remote base - doc format inside a suggestion - deleted paragraph : full contract — the root ' s
2026-07-04 16:03:01 +02:00
* 'delta' fires and the maintained cache equals a fresh deep render ( fresh - deleted format markers
* stay on the retained - marker path of the format state machine ) .
2026-07-03 19:35:43 +02:00
* /
export const testRdtDeltaFormatThroughDeletedParent = ( ) => {
for ( const [ baseClientID , sdocClientID ] of [ [ 1 , 2 ] , [ 2 , 1 ] ] ) {
const { doc , ytype } = createSuggestionPair ( baseClientID , sdocClientID )
t . assert ( ytype . delta != null ) // materialize the maintained cache
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) ) // suggestion-delete the paragraph
let fired = 0
ytype . on ( 'delta' , ( ) => { fired ++ } )
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 4 , { bold : { } } ) ) . done ( ) )
t . assert ( fired === 1 , 'root delta fires for a format inside a suggestion-deleted paragraph' )
const cached = ytype . delta
const fresh = ytype . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
}
}
/ * *
* Plain docs ( base renderer ) : deleted content is invisible , so a remote change inside a deleted
* paragraph emits no 'delta' ( the change renders to an empty delta , which is suppressed ) and v1
* ` observe ` semantics are unchanged . Deep listeners on live ancestors ARE notified now ( the event
* is tracked through the deleted parent ) — that is the chosen semantics .
* /
export const testRdtNoDeltaThroughDeletedParentPlainDoc = ( ) => {
const docP = new Y . Doc ( { gc : false } )
docP . clientID = 3
const docQ = new Y . Doc ( { gc : false } )
docQ . clientID = 4
docP . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'hello world' ) ] ) . done ( )
)
Y . applyUpdate ( docQ , Y . encodeStateAsUpdate ( docP ) )
const rootQ = docQ . get ( 'prosemirror' )
t . assert ( rootQ . delta != null ) // materialize the maintained cache
let deltaFired = 0
let deepFired = 0
let observeFired = 0
rootQ . on ( 'delta' , ( ) => { deltaFired ++ } )
rootQ . observeDeep ( ( ) => { deepFired ++ } )
rootQ . observe ( ( ) => { observeFired ++ } )
// plain-delete the paragraph on Q — a visible change, fires normally
rootQ . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
t . assert ( deltaFired === 1 && observeFired === 1 )
deltaFired = 0
deepFired = 0
observeFired = 0
// remote edit inside the (invisible) deleted paragraph
docP . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) ) . done ( ) )
Y . applyUpdate ( docQ , Y . encodeStateAsUpdate ( docP ) )
t . assert ( deltaFired === 0 , 'no delta emission for an invisible change' )
t . assert ( observeFired === 0 , 'v1 observe on the root is unaffected' )
t . assert ( deepFired === 1 , 'deep listeners on live ancestors are notified' )
t . assert ( rootQ . delta . equals ( rootQ . toDelta ( { deep : true } ) ) , 'maintained cache stays equal to a fresh render' )
}
/ * *
* A deleted type with its OWN custom renderer keeps firing ( its content is still rendered ) : the
* deletion transaction and later remote edits inside the tombstone reach its ` observe ` / 'delta'
* and keep its maintained cache current — while a base - renderer root above it stays silent for
* changes it cannot see .
* /
export const testRdtDeletedTypeWithOwnRendererFires = ( ) => {
const { doc , renderer , ytype } = createSuggestionPair ( 1 , 2 , false )
const parB = /** @type {any} */ ( ytype . get ( 0 ) )
parB . useRenderer ( renderer )
t . assert ( parB . delta != null ) // materialize the maintained cache
t . assert ( ytype . delta != null ) // root cache, maintained under the base renderer
let parFired = 0
let parObserved = 0
let rootFired = 0
parB . on ( 'delta' , ( ) => { parFired ++ } )
parB . observe ( ( ) => { parObserved ++ } )
ytype . on ( 'delta' , ( ) => { rootFired ++ } )
// delete the paragraph (a visible change on the root; the paragraph itself becomes a tombstone
// that its own diff renderer still renders)
ytype . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
t . assert ( rootFired === 1 , 'root fires for its own visible delete' )
t . assert ( parB . delta . equals ( parB . toDelta ( { deep : true } ) ) , 'deleted type cache current after the deletion' )
const parFiredAfterDelete = parFired
// remote format inside the tombstone: the deleted type fires; the base-renderer root renders
// nothing and must not emit an empty delta
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 4 , { bold : { } } ) ) . done ( ) )
t . assert ( parFired === parFiredAfterDelete + 1 , 'deleted type with its own renderer fires delta' )
t . assert ( parObserved >= 1 , 'deleted type with its own renderer fires observe' )
t . assert ( rootFired === 1 , 'base-renderer root does not emit empty deltas' )
t . assert ( parB . delta . equals ( parB . toDelta ( { deep : true } ) ) , 'deleted type cache current after the remote format' )
t . assert ( ytype . delta . equals ( ytype . toDelta ( { deep : true } ) ) , 'root cache stays equal to a fresh render' )
}
/ * *
* Type - scoped UndoManager : tombstone - subtree transactions now reach ` changedParentTypes ` ( the
* scope check ) , but origin gating still decides capture — an untracked - origin remote change is
* not captured .
* /
export const testRdtDeletedSubtreeUndoScope = ( ) => {
const docP = new Y . Doc ( { gc : false } )
docP . clientID = 5
const docQ = new Y . Doc ( { gc : false } )
docQ . clientID = 6
docP . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'hello world' ) ] ) . done ( )
)
Y . applyUpdate ( docQ , Y . encodeStateAsUpdate ( docP ) )
const rootQ = docQ . get ( 'prosemirror' )
const um = new Y . UndoManager ( rootQ )
rootQ . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) ) // local delete → captured
t . assert ( um . undoStack . length === 1 )
// remote change inside the deleted paragraph with an untracked origin: in scope via the
// deleted-parent bubble, but not captured
docP . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 2 ) . insert ( 'XY' ) ) . done ( ) )
Y . applyUpdate ( docQ , Y . encodeStateAsUpdate ( docP ) , 'remote-origin' )
t . assert ( um . undoStack . length === 1 , 'untracked-origin remote change is not captured' )
}
2026-07-05 16:39:34 +02:00
/ * *
2026-07-05 23:33:40 +02:00
* Regression pin ( originally a failing repro ) : an * accepting * ( suggestionMode = false ) child - node insert with no
2026-07-05 16:39:34 +02:00
* suggested content anywhere drifts the maintained ` .delta ` cache . The inserted node reaches the
* base doc ( asserted below — it is committed content , NOT a suggestion , so the
* "inserted-adjacent-to-suggested-is-suggested" rule does not apply ) , and a fresh deep render
* correctly shows no attribution — but the cache keeps the change render ' s transient
* ` {insert: []} ` attribution . Suggestion - mode inserts and accepting inserts adjacent to suggested
* content are consistent ( both sides attributed ) ; only this committed - insert case drifts .
* Delete - tail + insert - node is the CRDT shape of a ProseMirror block split , so editor workloads
* hit this constantly .
* /
export const testRdtAcceptingNodeInsertCacheDrift = ( ) => {
const doc = new Y . Doc ( { gc : false } )
const suggestionDoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , suggestionDoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
renderer . suggestionMode = false
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'base para' ) ] ) . done ( )
)
const ytype = suggestionDoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null ) // materialize the maintained cache
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( [ delta . create ( 'paragraph' , { } , 'plain' ) ] ) . done ( ) )
// the insert is committed content: it reached the base doc
t . assert ( JSON . stringify ( doc . get ( 'prosemirror' ) . toDeltaDeep ( ) . toJSON ( ) ) . includes ( 'plain' ) , 'the insert committed to base' )
const cached = ytype . delta
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( ! JSON . stringify ( fresh . toJSON ( ) ) . includes ( '"attribution"' ) , 'fresh render shows committed (unattributed) content' )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render' )
// same class, second entry point: ACCEPTING a suggested node-insert. The suggested insert
// itself is consistent (both sides attributed), but the accept's de-attribution correction
// does not descend into the nested node's content — the cache keeps `{insert: []}`.
{
const doc2 = new Y . Doc ( { gc : false } )
const suggestionDoc2 = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
2026-07-11 23:59:37 +02:00
const renderer2 = Y . createDiffRenderer ( doc2 , suggestionDoc2 , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
doc2 . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'base para' ) ] ) . done ( ) )
const ytype2 = suggestionDoc2 . get ( 'prosemirror' )
ytype2 . useRenderer ( renderer2 )
t . assert ( ytype2 . delta != null ) // materialize the maintained cache
renderer2 . suggestionMode = true
ytype2 . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( [ delta . create ( 'paragraph' , { } , 'sugg' ) ] ) . done ( ) )
t . assert ( ytype2 . delta . equals ( ytype2 . toDelta ( { deep : true } ) ) , 'suggested insert itself is consistent' )
renderer2 . acceptAllChanges ( )
t . assert ( ytype2 . delta . equals ( ytype2 . toDelta ( { deep : true } ) ) , 'maintained .delta must equal a fresh render after accepting the node insert' )
}
}
/ * *
2026-07-05 23:33:40 +02:00
* Regression pin ( originally a failing repro ) — the consumer - visible framing of the cache drift above : inserting a
2026-07-05 16:39:34 +02:00
* node through an * accepting * renderer ( ` suggestionMode = false ` ) must not leave it presented as
* a suggestion . The write currently emits two ` 'delta' ` events : first the insert render , fully
* attributed ( ` {insert: []} ` on the inserted node AND its nested content ) , then a de - attribution
* correction ` retain(1).retain(1, {attribution: null}) ` once the content commits to base — but
* the correction only clears the attribution on the node itself and never descends into the
* node ' s children . Composing the event stream ( which is exactly how the maintained ` .delta `
* cache is built ) therefore leaves the nested text attributed as a suggested insert forever ,
* while ground truth ( a fresh deep render ) shows committed , unattributed content . The test does
* not prescribe the fix : it passes if the insert render arrives unattributed OR if the
* correction descends — it only requires the settled event stream to converge to the truth .
* /
export const testRdtAcceptingNodeInsertRenderedAsSuggestion = ( ) => {
const doc = new Y . Doc ( { gc : false } )
const suggestionDoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , suggestionDoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 16:39:34 +02:00
renderer . suggestionMode = false
doc . get ( 'prosemirror' ) . applyDelta (
delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'base para' ) ] ) . done ( )
)
const ytype = suggestionDoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
// composed = pre-write state + every emitted change: what any consumer of the `'delta'`
// channel (a remote binding, the maintained cache) believes the document looks like
const composed = delta . cloneDeep ( /** @type {any} */ ( ytype . toDelta ( { deep : true } ) ) )
ytype . on ( 'delta' , d => {
composed . apply ( /** @type {any} */ ( delta . cloneDeep ( /** @type {any} */ ( d ) ) ) , { final : true , move : true } )
} )
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( [ delta . create ( 'paragraph' , { } , 'plain' ) ] ) . done ( ) )
const fresh = ytype . toDelta ( { deep : true } )
t . assert ( ! JSON . stringify ( fresh . toJSON ( ) ) . includes ( '"attribution"' ) , 'ground truth: the insert committed to base, nothing is suggested' )
if ( ! composed . equals ( fresh ) ) {
console . error ( 'composed:' , JSON . stringify ( composed . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( ! JSON . stringify ( composed . toJSON ( ) ) . includes ( '"attribution"' ) , 'the settled event stream must not present the committed insert as a suggestion' )
t . assert ( composed . equals ( fresh ) , 'composing the emitted changes converges to a fresh render' )
2026-07-05 17:33:39 +02:00
// deeper nesting must heal at *every* level below the top, not just the first
ytype . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( [ delta . create ( 'blockquote' , { } , [ delta . create ( 'paragraph' , { } , 'deep' ) ] ) ] ) . done ( ) )
const fresh2 = ytype . toDelta ( { deep : true } )
t . assert ( ! JSON . stringify ( fresh2 . toJSON ( ) ) . includes ( '"attribution"' ) , 'ground truth: the multi-level insert committed to base' )
t . assert ( ! JSON . stringify ( composed . toJSON ( ) ) . includes ( '"attribution"' ) , 'no nesting level is left presented as a suggestion' )
t . assert ( composed . equals ( fresh2 ) , 'the event stream converges for multi-level nesting' )
}
/ * *
* Guards the id - scoped heal design : unattributing an accepted node must never cascade into a
* blanket subtree clear , because children may still be attributed . Here only the node ' s OWN
* insert - suggestion is accepted ( ` acceptChanges ` with a range covering just the node id ) — the
* node commits to base as an empty paragraph while its text children remain pending
* suggestions . The heal must clear the node 's attribution, keep the children' s ` {insert: []} ` ,
* and keep the maintained cache equal to a fresh render .
* /
export const testRdtPartialAcceptKeepsPendingChildSuggestions = ( ) => {
const doc = new Y . Doc ( { gc : false } )
const suggestionDoc = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( doc , suggestionDoc , { attributions : Y . createContentMap ( ) } )
2026-07-05 17:33:39 +02:00
doc . get ( 'prosemirror' ) . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'base para' ) ] ) . done ( ) )
const ytype = suggestionDoc . get ( 'prosemirror' )
ytype . useRenderer ( renderer )
t . assert ( ytype . delta != null ) // materialize the maintained cache
renderer . suggestionMode = true
const client = suggestionDoc . clientID
// suggestion 1: node insert — the paragraph node takes clock 0, its text 'x' clock 1
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( [ delta . create ( 'paragraph' , { } , 'x' ) ] ) . done ( ) )
// suggestion 2: more text inside the suggested node
ytype . applyDelta ( delta . create ( ) . retain ( 1 ) . modify ( delta . create ( ) . retain ( 1 ) . insert ( 'Q' ) ) . done ( ) )
t . assert ( ytype . delta . equals ( ytype . toDelta ( { deep : true } ) ) , 'consistent before the partial accept' )
// accept ONLY the node's own id (clock 0); every child stays a pending suggestion
renderer . acceptChanges ( Y . createID ( client , 0 ) )
const cached = ytype . delta
const fresh = ytype . toDelta ( { deep : true } )
const freshJson = /** @type {any} */ ( fresh . toJSON ( ) )
// ground truth: the node committed (as an empty paragraph) …
t . assert ( JSON . stringify ( doc . get ( 'prosemirror' ) . toDeltaDeep ( ) . toJSON ( ) ) . split ( '"paragraph"' ) . length === 3 , 'the accepted node reached the base doc' )
t . assert ( freshJson . children [ 0 ] . attribution === undefined , 'the accepted node itself is no longer attributed' )
// … while its children are still suggested
const acceptedPara = freshJson . children [ 0 ] . insert [ 1 ]
t . assert ( acceptedPara . name === 'paragraph' && JSON . stringify ( acceptedPara . children ) . includes ( '"attribution":{"insert":[]}' ) , 'pending child suggestions keep their attribution' )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after the partial accept' )
2026-07-05 16:39:34 +02:00
}
2026-07-05 19:17:19 +02:00
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by ` isRangeEndClear ` in the ytype format walk ( a rendered unattributed
* marker closing an attributed same - key range must emit the per - key attribution clear ) .
* Minimized from y - prosemirror ' s ` .dbg-fuzz.mjs ` seed 54321 , op # 8 ; 14 / 25 fuzz seeds hit this
* class : a * base - doc * format arrives over an overlapping same - key * suggested * format , then a
* plain base - doc insert lands inside the formatted range ( past the suggested span , with
* formatted content still following ) — the change render emitted for the insert failed to close
* the attributed - format range , so the maintained ` .delta ` cache kept a stale
* ` {format: {strong: []}} ` on the trailing content while a fresh render showed it committed :
2026-07-05 19:17:19 +02:00
*
* cached : a ( strong ) | bx ( { format : { strong : [ ] } } ) | c ( strong , { format : { strong : [ ] } } ← STALE )
* fresh : a ( strong ) | bx ( { format : { strong : [ ] } } ) | c ( strong )
*
* The drift appeared alongside the ` usedFormats ` / ` useFormats() ` rework ( yjs 804e4 d34 +
* lib0 7 c1d44e ) — it reproduces with the previous lib0 as well , so the yjs - side walk is
* sufficient to trigger it . It needs the insert strictly * inside * the base - format range and
* * after * the suggested span ; inserting at the suggested span ' s boundary or at the range end
* renders correctly .
* /
export const testRdtBaseInsertIntoOverlappingFormatRangeCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:17:19 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'abc' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// suggested strong on 'a'
st . applyDelta ( delta . create ( ) . retain ( 1 , { strong : { } } ) . done ( ) )
// base strong over all of 'abc' — a base-doc format arriving under an overlapping same-key
// format suggestion
bt . applyDelta ( delta . create ( ) . retain ( 3 , { strong : { } } ) . done ( ) )
// plain base insert between 'b' and 'c' → the emitted change must end the attributed range,
// but leaves the trailing 'c' attributed in the cache
bt . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( 'x' ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a base insert into the overlapping format range' )
}
2026-07-05 19:52:11 +02:00
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by installing the all - null - leaves context copy in the attribution
* install gate . Sibling of the class above , found by grid - enumerating the repro skeleton
* ( 192 same - class combos in the suggested - insert pass , 496 more with formatted base inserts ) :
* with TWO overlapping suggested format keys , the change - render copy of the ambient attributed
* format context carries per - key null - clear leaves ( which fresh contexts never hold ) , so the
* emptiness test at the install gate failed where the fresh walk reaches its empty - copy
* ` useAttribution(null) ` closure — an unattributed close marker ' s key deletion was discarded .
* The stale ` {format: {strong: []}} ` then surfaced in the cache on the next change render that
* walks the span : a suggested insert * inside * the em range ( an insert past it renders clean ) :
2026-07-05 19:52:11 +02:00
*
* cached : a ( em + strong , both attributed ) | x ( { insert : [ ] } ) | b ( … ) | cd ( { format : { strong : [ ] } } ← STALE )
* fresh : a ( em + strong , both attributed ) | x ( { insert : [ ] } ) | b ( … ) | cd
*
* The marker integration order is load - bearing : it reproduces with base . clientID > sugg . clientID
2026-07-05 22:47:19 +02:00
* and renders clean with the order flipped .
2026-07-05 19:52:11 +02:00
* /
export const testRdtSuggestedInsertUnderTwoKeySuggestedFormatsCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 1
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 0
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'abcd' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// two overlapping suggested formats → a two-key attributed format context over 'ab'
st . applyDelta ( delta . create ( ) . retain ( 2 , { em : { } } ) . done ( ) )
st . applyDelta ( delta . create ( ) . retain ( 4 , { strong : { } } ) . done ( ) )
// base strong over 'ab' — same key/value as the suggestion; the cache is still clean here
bt . applyDelta ( delta . create ( ) . retain ( 2 , { strong : { } } ) . done ( ) )
// suggested insert inside the em range → the change render re-stamps the trailing spans and
// flushes the stale strong governance into the cache
st . applyDelta ( delta . create ( ) . retain ( 1 ) . insert ( 'x' ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a suggested insert under a two-key suggested format context' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by the same install - gate change as the two - key class above ( the
* all - null - leaves context copy must be installed ) . Formatted - base - insert sibling ( grid class B ,
* 496 combos in the formatted - base - insert fuzzing pass ) : under a two - key
2026-07-05 19:52:11 +02:00
* suggested format context ( em over 'ab' , strong over 'a' ) , a base - doc em arrives on 'a'
* only — strictly undercovering the suggested em range — so the em governance over the
* trailing 'b' stays attributed - stale in the cache . A base insert * formatted with the other
* suggested key * ( { strong : { } } ) before the context then flushes the stale em into the cache :
*
* cached : x ( strong + em , attributed ) | a ( em , attributed ) | b ( { format : { em : [ ] } } ← STALE )
* fresh : x ( strong + em , attributed ) | a ( em , attributed ) | b
*
* Every knob is load - bearing : a plain base insert , an em - or third - key - formatted insert ,
* inserting at position 1 or 2 , base em covering the full suggested em range , suggested
* strong covering the full em range , or dropping any of the three format ops all render
* clean — so this does not collapse into the plain - insert siblings above . Like the sibling ,
* it needs base . clientID > sugg . clientID and renders clean with the order flipped .
* /
export const testRdtFormattedBaseInsertUnderTwoKeySuggestedFormatsCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 1
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 0
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'ab' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// two overlapping suggested formats → a two-key attributed format context, em past strong
st . applyDelta ( delta . create ( ) . retain ( 2 , { em : { } } ) . done ( ) )
st . applyDelta ( delta . create ( ) . retain ( 1 , { strong : { } } ) . done ( ) )
// base em on 'a' only — same key/value as the suggestion but undercovering its range, so
// the em governance over the trailing 'b' stays stale; the cache is still clean here
bt . applyDelta ( delta . create ( ) . retain ( 1 , { em : { } } ) . done ( ) )
// base insert formatted with the OTHER suggested key, before the context → the change
// render re-stamps the trailing spans and flushes the stale em governance into the cache
bt . applyDelta ( delta . create ( ) . insert ( 'x' , { strong : { } } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a formatted base insert under a two-key suggested format context' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by the ` previousFormats ` cache - reference rework in ` # Update Formats `
* ( a marker deleted by the change whose value equals the walk ' s current value dropped out of
* the tracking , so a later marker was misread as a restore - to - previous and the needed format
* diff was swallowed ) . FORMAT - VALUE divergence , minimized from fuzz - core seeds 711 and 4935 ,
* which both collapse to this one 4 - op skeleton — the two seeds are mirror directions of it ,
* ` em:{} ` leaking format IN vs ` em:null ` leaking format OUT :
2026-07-05 19:52:11 +02:00
* when a base - doc format op ' s range END lands strictly * inside * a same - key suggested - format
2026-07-05 22:47:19 +02:00
* span , the change render stamped the base op ' s format one char PAST its range end . The cache
2026-07-05 19:52:11 +02:00
* then disagrees with a fresh deep render about the EFFECTIVE FORMAT of that boundary char —
* not merely its attribution ( no attribution differs ; none is even present ) :
*
* cached : abxx ( em ) ← trailing 'x' wrongly formatted
* fresh : abx ( em ) | x ( no format )
*
* Every ingredient is load - bearing : the pre - existing em on 'b' , the TWO appended chars ( with a
* single 'x' there is no boundary char inside the suggested span ) , the suggestion straddling
* the base range end ( covering only one of the two 'x' s renders clean ) , the base op ending
* strictly inside the span ( retain ( 4 ) renders clean ) , same key on both sides ( a ` strong `
* suggestion renders clean ) , and the order suggestion - before - base - format ( flipped renders
* clean ) . Reproduces identically with both clientID orders ( 0 / 1 and 1 / 0 ) .
* /
export const testRdtBaseFormatEndingInsideSuggestedFormatRangeFormatValueCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'ab' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// pre-existing base em on 'b'
bt . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : { } } ) . done ( ) )
// plain base append after the em run
bt . applyDelta ( delta . create ( ) . retain ( 2 ) . insert ( 'xx' ) . done ( ) )
// suggested em over the appended 'xx' — same key as the base format below
st . applyDelta ( delta . create ( ) . retain ( 2 ) . retain ( 2 , { em : { } } ) . done ( ) )
// base em over 'abx' — the range end lands strictly inside the suggested span; the cache
// bleeds the em onto the trailing 'x' as its *format* (fresh leaves it unformatted)
bt . applyDelta ( delta . create ( ) . retain ( 3 , { em : { } } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a base format ending inside a same-key suggested format range' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by the open - attributed - range guard on ` isDeletedFormatClear `
* ( ` previousUnattributedFormats ` has the key → the fresh render skips the deleted marker , so
* the change render must not emit a clear there ; mirrors ` isAcceptedFormatClear ` ' s guard ) .
* MISSING - attribution class ( inverse direction of the stale classes above — there the cache
* keeps an attribution the fresh render has dropped ; here the cache DROPPED one the fresh
* render keeps ) . Minimized from fuzz - core seed 2 ( drift at opIndex 24 , 25 ops → 3 ) :
* a * suggested format removal * ( key → null ) of a base - doc format was applied to the maintained
2026-07-05 19:52:11 +02:00
* cache without the attributed - removal marker that the fresh render produces :
*
* cached : a ( em ) | b ← removal applied , attribution LOST
* fresh : a ( em ) | b ( { format : { em : [ ] } } ) ← attributed suggested removal
*
* Load - bearing shape : the base format must extend * beyond * the removed char ( em over 'ab' ,
* removal on 'b' only — base - formatting 'b' alone renders clean ) , and the suggestion must
* first * override * the base format value on that char with a different value ( { x : 1 } vs { } ;
* without the override , or with an equal - value override , or with override + removal spanning
* the whole base range , the removal renders clean ) . Both clientID orders reproduce ; no
* accept / reject involved ( deterministic , 100 / 100 without seeding Math . random ) .
* /
export const testRdtSuggestedRemovalOfOverriddenBaseFormatCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'ab' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// base em over all of 'ab' — must extend beyond the char the suggestion touches
bt . applyDelta ( delta . create ( ) . retain ( 2 , { em : { } } ) . done ( ) )
// suggested override of the base format value on 'b' (different value is load-bearing)
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : { x : 1 } } ) . done ( ) )
// suggested removal of em on 'b' → cache applies the removal but loses the attribution
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : null } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a suggested removal of an overridden base format' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by the same open - attributed - range guard on ` isDeletedFormatClear ` as
* the class above . Adjudicated core of lifecycle - fuzz class P5 ( "post-clearCache re-drift" ,
* seeds 657 / 1131 / 2401 ) : clearCache is NOT load - bearing — all three seeds drift identically with
* the clearCache op deleted , and a FRESH doc / renderer pair built via
* encodeStateAsUpdate / applyUpdate to the pre - clearCache state drifts identically on the same
* final op . The class collapses into the core no - lifecycle overlapping same - key format drift
* family ( missing - attribution direction ) : a suggested unformat of a base - doc format , staged
* left - to - right in two steps , lost the second step ' s unformat attribution in the maintained
* cache :
2026-07-05 19:52:11 +02:00
*
* cached : a ( { format : { em : [ ] } } ) | b ( no attribution ← MISSING )
* fresh : ab ( { format : { em : [ ] } } )
*
* Load - bearing : the two - step left - to - right staging ( a single merged retain ( 2 , { em : null } ) renders
* clean , and unformatting 'b' before 'a' renders clean ) and the base format arriving as ONE op
* over both chars ( two 1 - char base formats + a merged suggested unformat renders clean ) .
* Key ( em / strong ) , base format value ( { } vs { x : 1 } ) and the clientID order are NOT load - bearing
* ( drifts identically with base = 0 / sugg = 1 and base = 1 / sugg = 0 ) . With a 3 - char doc the same ops
* additionally leave a STALE { format : { em : [ ] } } on the still - formatted trailing char , i . e . both
* catalogued drift directions come from this walk defect .
* /
export const testRdtStagedSuggestedUnformatOfBaseFormatCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'ab' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// base em over all of 'ab' — in suggestion mode a base-doc format is not itself a suggestion
bt . applyDelta ( delta . create ( ) . retain ( 2 , { em : { } } ) . done ( ) )
// suggested unformat of the same key, staged left-to-right: first 'a' — the cache is still
// clean here, 'a' carries {format:{em:[]}}
st . applyDelta ( delta . create ( ) . retain ( 1 , { em : null } ) . done ( ) )
// ... then 'b' — the second step drops the {format:{em:[]}} attribution for 'b' from the
// cache while the fresh render keeps it
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : null } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a staged suggested unformat of a base format' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — fixed by provenance tracking ( ` currentFormatsAttributed ` ) widening
* ` isDeletedFormatClear ` to committed enclosing values : the accept deletes the committed run ' s
* close marker , whose removal re - exposes the committed ` strong={} ` , and the old
* ` currFormatVal == null ` guard blocked the needed clear . ( Lifecycle - fuzz class P2 , 23 / 3000
* seeds ; minimized from seed 259 ' s 9 ops to 3 ) : ACCEPT - triggered cache drift — every step
* before the accept is consistent , and ` acceptAllChanges() ` itself corrupted the maintained
* ` .delta ` . Shape : a committed ` strong ` run
2026-07-05 19:52:11 +02:00
* ( 'a' , present since the very first render ) with a plain char ( 'b' ) right after it ; a suggested
* ` strong ` on 'b' with the SAME key AND the SAME value as the committed run ; accept - all . The
* accept commits the suggestion to base , and a fresh deep render shows one fully - committed
* unattributed run — but the accept 's change render never clears the suggestion' s format
* attribution on 'b' , leaving the stale ` {format:{strong:[]}} ` in the cache forever :
*
* cached : a ( strong ) | b ( strong , { format : { strong : [ ] } } ← STALE )
* fresh : ab ( strong )
*
* Everything is load - bearing : same key ( strong / em → clean ) , same value ( { } vs { x : 1 } or null →
* clean ) , adjacency ( a gap between the runs → clean ) , and order ( committed run must precede the
* suggested char ; suggesting on the char * before * the run , or on the run itself , renders clean ) .
* Both clientID orders reproduce . Related but distinct from
* testRdtAcceptingNodeInsertCacheDrift ( that class leaks ` {insert:[]} ` into nested node content ;
* this one leaks a ` format ` attribution on plain text , with a clean pre - accept state ) .
* /
export const testRdtAcceptAllOfSameValueSuggestedFormatCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
// committed strong on 'a', plain 'b' — present since the very first render
bt . applyDelta ( delta . create ( ) . insert ( 'a' , { strong : { } } ) . insert ( 'b' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// suggested strong on the adjacent 'b' — same key AND same value as the committed run
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { strong : { } } ) . done ( ) )
// the suggestion itself renders consistently; the drift is accept-triggered
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'suggested format itself is consistent' )
renderer . acceptAllChanges ( )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after accepting a same-value suggested format extension' )
}
/ * *
2026-07-05 22:47:19 +02:00
* Regression pin — the long - documented reject - side provenance residual , fixed by the same
* ` currentFormatsAttributed ` provenance tracking as the accept class above ( the failing render
* is the sugg - doc EVENT render — the reject ' s heal render receives an empty change set — and
* the deleted suggestion marker re - exposes the enclosing * committed * base strong , which the old
* ` currFormatVal == null ` guard could not see ) . Lifecycle - fuzzing class P1 , 45 seeds ; minimized
* from the 9 - op seed - 132 representative to 4 ops : a suggested strong on 'a' under a base strong
* over all of 'ab' — same key , with the base range extending strictly * past * the suggested
* span — then rejectAllChanges . The reject clears the suggestion , but the change render left
* the trailing 'b' still carrying the suggested - format attribution in the cache ; the fresh
* render shows it unattributed :
2026-07-05 19:52:11 +02:00
*
* cached : a ( strong ) | b ( strong , { format : { strong : [ ] } } ← STALE )
* fresh : ab ( strong )
*
* Everything here is load - bearing : without the suggested format , without the base format , with
* a different suggested key ( em ) , with the base range ending where the suggested span ends
* ( full overlap ) , or with the sugg span at the end of the base range , it renders clean . The
* pre - reject cache is consistent — the residual appears only at the reject . The marker
* integration order is load - bearing in the opposite direction of the two - key siblings above :
* it reproduces with base . clientID < sugg . clientID and renders clean flipped . The reject flow
* does not echo structs back here ( both clientIDs survive unreassigned ) , so the repro is
* Math . random - free : 100 / 100 identical failing runs without seeding .
* /
export const testRdtRejectAllOverlappingSameKeyFormatProvenanceResidual = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 19:52:11 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'ab' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// suggested strong on 'a'
st . applyDelta ( delta . create ( ) . retain ( 1 , { strong : { } } ) . done ( ) )
// base strong over all of 'ab' — same key, extending strictly past the suggested span
bt . applyDelta ( delta . create ( ) . retain ( 2 , { strong : { } } ) . done ( ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'consistent before the reject' )
// reject the suggestion → the trailing 'b' keeps the stale suggested-format attribution
renderer . rejectAllChanges ( )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after rejecting overlapping same-key formats' )
}
2026-07-05 23:33:40 +02:00
/ * *
* Regression pin — fixed by re - adding the reset key as an explicit clear when the ambient - copy
* install is forced by the all - null - leaves disjunct ( installing after silently dropping the key
* would close its governance without the clear the fresh render ' s context - close implies ) :
* partial - accept cache drift when the accepted id range SPLITS a suggested format ' s
* open / close ContentFormat marker pair . Lifecycle - fuzz seed 648 ( failIndex 14 , 15 ops incl . an
* accept and a reject ) minimized to 5 ops with no inserts / deletes at all .
*
* Shape : two different - key suggested formats on the same char 'A' of base text 'AB' . The sugg
* client ' s structs are exactly four ContentFormat markers — clock 0 = em open ( { em : { } } ) ,
* clock 1 = em close ( { em : null } ) , clock 2 = strong open , clock 3 = strong close .
* ` acceptChanges(createID(1,1), createID(1,1)) ` accepts ONLY the em CLOSE marker ( clock 1 ) ,
* splitting the em pair . The accept itself still renders consistently — the residual is armed ,
* not visible . The next suggested unformat of 'A' then corrupts the maintained cache : the
* incremental render lets the strong suggestion ' s format attribution run past its close marker
* onto the trailing 'B' :
*
* cached : A ( strong , { format : { strong : [ ] } } ) | B ( { format : { strong : [ ] } } < - STALE )
* fresh : A ( strong , { format : { strong : [ ] } } ) | B
*
* Load - bearing ( each verified ) : the accept range must contain the em close marker and NOT the
* em open marker ( 1 - 1 , 1 - 2 , 1 - 3 all drift ; 0 - 0 , 0 - 1 whole - pair , 2 - 2 , 3 - 3 all clean ) — the
* marker - pair split IS the trigger ; a second suggested format with a different key and non - null
* value ( same key em - > clean , strong : null - > clean ) ; a trailing char after the formatted one
* ( init 'A' alone - > clean , formatting the last char - > clean ) ; the final op must be an
* unformat — em : { } reformat is clean , while em : null and strong : null both drift identically .
* Every step before the final unformat is consistent , including the accept . Both clientID
* orders fail byte - identically ; the accept does not echo structs back ( no Math . random clientID
* reassignment ) , so the repro is seed - free : 100 / 100 identical failing runs . Same stale -
* { format : { strong : [ ] } } - on - trailing - char symptom as the ( fixed )
* testRdtRejectAllOverlappingSameKeyFormatProvenanceResidual pin , but a distinct class : no base
* format involved , the trigger is a marker - splitting PARTIAL accept , and the drift is deferred
* to a later unformat .
* /
export const testRdtSplitFormatMarkerPairPartialAcceptThenUnformatCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 23:33:40 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'AB' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// two different-key suggested formats on 'A' — sugg structs: clock 0/1 = em open/close,
// clock 2/3 = strong open/close
st . applyDelta ( delta . create ( ) . retain ( 1 , { em : { } } ) . done ( ) )
st . applyDelta ( delta . create ( ) . retain ( 1 , { strong : { } } ) . done ( ) )
// accept ONLY the em close marker (clock 1), splitting the em marker pair — the accept
// itself still renders consistently
renderer . acceptChanges ( Y . createID ( 1 , 1 ) , Y . createID ( 1 , 1 ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'consistent immediately after the marker-splitting accept' )
// suggested unformat of 'A' → the cache leaks {format:{strong:[]}} onto the trailing 'B'
st . applyDelta ( delta . create ( ) . retain ( 1 , { em : null } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after unformatting under a split-accepted format marker pair' )
}
/ * *
* Regression pin — fixed by the same reset - branch clear re - add as the split - marker - pair class
* above ( both are the marker - pair - splitting - accept walk shape ) . Minimized from lifecycle - fuzz
* seed 1034 ( failIndex 15 , 16 ops → 6 ) :
* PARTIAL - ACCEPT - triggered LATENT cache drift , stale - attribution direction . A committed base
* ` strong ` over 'abc' ; a suggested unformat ( strong → null ) of 'b' ( sugg - client markers :
* clock 0 = open strong : null , clock 1 = close / restore strong : { } ) ; a second suggested format
* with a DIFFERENT key ( em : { } ) on the same 'b' ( clocks 2 / 3 ) ; then a partial
* ` acceptChanges(id(1,1), id(1,1)) ` that accepts ONLY the unformat ' s range - END restore marker ,
* skipping its open marker at clock 0. The accept itself still renders consistently — the
* corruption is latent — but the next suggested format edit over the region ( removing the em )
* re - renders 'c' with a stale ` {format:{strong:[]}} ` the fresh deep render does not have :
*
* cached : a ( strong ) | b ( { format : { strong : [ ] } } ) | c ( strong , { format : { strong : [ ] } } ← STALE )
* fresh : a ( strong ) | b ( { format : { strong : [ ] } } ) | c ( strong )
*
* Load - bearing : the accept range must EXCLUDE clock 0 and include at least one later sugg
* marker — accept ( 1 , 1 ) , accept ( 2 , 2 ) and accept ( 3 , 3 ) all drift identically , while accept ( 0 , 0 ) ,
* accept ( 0 , 3 ) , acceptAll and reject ( 1 , 1 ) render clean ; the second suggested format must exist
* BEFORE the accept and use a different key ( dropping it , or using strong for both , renders
* clean — which op carries which key does not matter , swapped keys drift too ) ; the final op
* must be a format CHANGE re - rendering the region ( em : null on 'b' or 'c' , or a base em : { } on
* 'b' — re - applying the same em : { } , inserts , deletes and clearCache all render clean ) ; and the
* committed strong must extend both strictly before and strictly past 'b' ( 2 - char docs 'ab' / 'bc'
* and base ranges not covering 'a' or 'c' render clean ) . Both clientID orders drift identically ;
* the accept does not echo structs back ( clientIDs survive unreassigned ) , so the repro is
* Math . random - free : 100 / 100 identical failing runs without seeding , on both trees . Related to
* ( but distinct from ) the fixed testRdtAcceptAllOfSameValueSuggestedFormatCacheDrift : here
* acceptAll is CLEAN , the partial id range is essential , and the drift needs a second op after
* the accept .
* /
export const testRdtPartialAcceptSkippingUnformatOpenMarkerCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 23:33:40 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( 'abc' ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// committed base strong over all of 'abc' — must extend both before and past 'b'
bt . applyDelta ( delta . create ( ) . retain ( 3 , { strong : { } } ) . done ( ) )
// suggested unformat of the base strong on 'b' → sugg-client markers
// clock 0 (open, strong:null) and clock 1 (close/restore, strong:{})
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { strong : null } ) . done ( ) )
// second suggested format (different key) on the same 'b' → clocks 2 (em:{}) and 3 (em:null)
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : { } } ) . done ( ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'consistent before the accept' )
// PARTIAL accept of only clock 1 — the unformat's range-END restore marker — skipping the
// open marker at clock 0. The accept itself still renders consistently (latent corruption).
renderer . acceptChanges ( Y . createID ( 1 , 1 ) , Y . createID ( 1 , 1 ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'consistent after the partial accept' )
// suggested removal of the em → the re-render leaves a stale {format:{strong:[]}} on 'c'
st . applyDelta ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : null } ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a format edit following a partial accept that skipped the unformat open marker' )
}
/ * *
* Regression pin — fixed by threading ` fresh ` onto fresh - format pieces ( mode - 0 retained path )
* so a fresh reset - to - previous marker emits the per - key attribution clear like a rendered
* marker . The removeMark class from y - prosemirror cohort - fuzz seeds 143
* ( drift at op # 35 ) , 2 ( op # 46 ) and 18 ( op # 63 ) , which all collapse to this one 3 - op
* skeleton ( cross - checked : each seed 's failing op is a no-suggestions user' s removeMark ,
* i . e . a BASE - doc unformat , over text inside a suggested - block - deleted paragraph , and each
* seed ' s drift is the same stale format - attribution shape ) . STALE - attribution class in the
* nested deleted - children render path : a base - doc partial unformat ( strong : null ) of a base
* format on text inside a paragraph that is block - deleted as a suggestion never lands in the
* maintained cache — the cached delta after the op is byte - identical to the pre - op state ,
* while the fresh deep render drops the format attribution on the covered chars :
*
* cached : paragraph [ ab ( { format : { strong : [ ] } , delete : [ ] } ) ] ← stale strong : [ ] on 'b'
* fresh : paragraph [ a ( { format : { strong : [ ] } , delete : [ ] } ) | b ( { delete : [ ] } ) ]
*
* Load - bearing : the suggested BLOCK delete of the paragraph embed ( a flat - YText suggested
* delete renders clean , and a text - level delete inside the paragraph renders clean ) , the
* base - side provenance of format + unformat ( a sugg - side format / unformat pair inside the
* deleted paragraph renders clean ) , the unformat being a REMOVAL ( re - formatting 'b' to a
* different value { x : 1 } renders clean ; a partial format with no prior format renders clean ) ,
* and the partial unformat clearing the TRAILING part of the formatted run ( a full - range
* unformat renders clean , unformatting only the leading 'a' renders clean , and a 1 - char
* format with the unformat extending past its end renders clean — hence the two chars ) .
* NOT load - bearing : the format key ( em drifts identically ) , the order of the block delete
* vs the base format ( format - before - delete drifts identically ) , and the clientID order
* ( both 0 / 1 and 1 / 0 drift identically ) . No accept / reject is involved , so no Math . random
* clientID reassignment can occur : 100 / 100 byte - identical failing runs without seeding .
* Fails identically on the unpatched baseline — a pre - existing class , disjoint from the
* seven flat - text pins above ( this one needs the deleted - ContentType modify render path ;
* likely the known " modify targeting a tombstoned child is skipped by the incremental
* change render " defect ) .
* /
export const testRdtBaseUnformatInsideBlockDeletedParagraphCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 23:33:40 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
bt . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'paragraph' , { } , 'ab' ) ] ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// suggested block delete of the whole paragraph
st . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
// base strong over both chars inside the (suggested-deleted) paragraph — renders as an
// attributed format on the deleted text; still consistent here
bt . applyDelta ( delta . create ( ) . modify ( delta . create ( 'paragraph' ) . retain ( 2 , { strong : { } } ) . done ( ) ) . done ( ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'consistent before the unformat' )
// base unformat of the TRAILING char only — fresh drops the strong:[] attribution on 'b',
// the maintained cache keeps it on all of 'ab' (the change never lands in the cache)
bt . applyDelta ( delta . create ( ) . modify ( delta . create ( 'paragraph' ) . retain ( 1 ) . retain ( 1 , { strong : null } ) . done ( ) ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a base partial unformat inside a block-deleted paragraph' )
}
/ * *
* Regression pin — fixed by the same fresh - format ` c.fresh ` threading as the class above .
* Minimized from y - prosemirror fuzz seed 103 ( cache
* drift at op # 68 : a no - suggestions user ' s deleteRange whose PM diff carried a ` {em:null} `
* format clear into the base doc , ` retain(2).delete(2).insert('tl',{strong}).retain(1,{em:null})
* . delete ( 4 ) ` inside a modify). The 6-user cohort, the ProseMirror layer and the 68-op history
* are NOT load - bearing — fresh docs + a fresh renderer rebuilt from the pre - op state snapshot
* drift identically , and the class reduces to 2 ops on a 1 - paragraph doc :
* a base - doc format CLEAR ( key → null ) ending strictly * inside * the same - key base format run ,
* applied INSIDE a suggestion - deleted paragraph ( a whole - node tombstone , still rendered
* delete - attributed ) , never reaches the maintained cache . The change render re - stamps the
* ambient ` {format:{em:[]}} ` attribution over the run instead of emitting the clear ( and fires
* 'delta' twice for the one base transaction ) ; a fresh deep render is correct :
*
* cached : ab ( { format : { em : [ ] } , delete : [ ] } ) ← clear LOST
* fresh : a ( { format : { em : [ ] } , delete : [ ] } ) | b ( { delete : [ ] } )
*
* Load - bearing shape : the WHOLE - NODE suggestion - delete ( a text - level suggestion - delete of the
* same chars renders clean , as does every flat - text variant ) , the format CLEAR direction ( a
* format add inside the tombstone renders clean — the green
* testRdtDeltaFormatThroughDeletedParent contract ) , and the run extending before the cleared
* char ( em on 'b' alone , clearing the leading char , or a whole - run single - char clear all render
* clean ; clearing a middle char of 'abc' drifts too ) . Key ( em / strong ) and format value ( { } vs
* true ) are not load - bearing . Both clientID orders drift identically ( 100 / 100 unseeded , no
* accept / reject involved ) .
* /
export const testRdtBaseFormatClearInsideSuggestionDeletedParagraphCacheDrift = ( ) => {
const base = new Y . Doc ( { gc : false } )
base . clientID = 0
const sugg = new Y . Doc ( { isSuggestionDoc : true , gc : false } )
sugg . clientID = 1
2026-07-11 23:59:37 +02:00
const renderer = Y . createDiffRenderer ( base , sugg , { attributions : Y . createContentMap ( ) } )
2026-07-05 23:33:40 +02:00
renderer . suggestionMode = true
const bt = base . get ( 't' )
const st = sugg . get ( 't' )
// base paragraph with an em run over 'ab'
bt . applyDelta ( delta . create ( ) . insert ( [ delta . create ( 'paragraph' ) . insert ( 'ab' , { em : { } } ) ] ) . done ( ) )
st . useRenderer ( renderer )
t . assert ( st . delta != null ) // materialize the maintained cache
// suggestion-delete the whole paragraph (stays rendered as an attributed tombstone)
st . applyDelta ( delta . create ( ) . delete ( 1 ) . done ( ) )
t . assert ( st . delta . equals ( st . toDelta ( { deep : true } ) ) , 'cache consistent after the suggestion delete' )
// base clears em on 'b' only — a clear ending strictly inside the em run, inside the tombstone
bt . applyDelta ( delta . create ( ) . modify ( delta . create ( ) . retain ( 1 ) . retain ( 1 , { em : null } ) ) . done ( ) )
const cached = st . delta
const fresh = st . toDelta ( { deep : true } )
if ( ! cached . equals ( fresh ) ) {
console . error ( 'cached:' , JSON . stringify ( cached . toJSON ( ) ) )
console . error ( 'fresh :' , JSON . stringify ( fresh . toJSON ( ) ) )
}
t . assert ( cached . equals ( fresh ) , 'maintained .delta must equal a fresh deep render after a base format clear inside a suggestion-deleted paragraph' )
}
2026-07-15 16:18:07 +02:00
/ * *
*
*
* @ param { t . TestCase } _tc
* /
export const testAttributionRendererActiveChangesInDiff = _tc => {
const ydoc = new Y . Doc ( { gc : false } )
const ytext = ydoc . get ( )
ytext . insert ( 0 , 'hella' )
const state1 = Y . createContentIdsFromDoc ( ydoc , true )
ytext . applyDelta ( delta . create ( ) . retain ( 4 ) . delete ( 1 ) . insert ( 'o!' ) . done ( ) )
const state2 = Y . createContentIdsFromDoc ( ydoc , true )
ytext . applyDelta ( delta . create ( ) . retain ( 5 ) . delete ( 1 ) . insert ( ' world' ) . done ( ) )
// state3 captures the *visible* content (`insertsContainDeletes: false` → inserts already exclude
// deletes). Using it both as the intersection target and as `renderedContent` means: a change that
// was inserted-then-deleted by state3 (the '!' at clock 6) is no longer an active insert and drops
// out, and state3's deletions (the 'a' at clock 4) aren't "restored" so they keep their delete
// attribution instead of re-rendering as inserts.
const state3 = Y . createContentIdsFromDoc ( ydoc , false )
ytext . applyDelta ( delta . create ( ) . delete ( 11 ) . insert ( '42' ) . done ( ) )
// render the active changes that happened between state 1 and 2 within state 3.
const attrs = Y . createContentMapFromContentIds ( Y . intersectContentIds ( Y . excludeContentIds ( state2 , state1 ) , state3 ) , [ ] , [ ] )
const renderer = Y . createAttributionsRenderer ( attrs , { renderedContent : state3 . inserts } )
const attrDelta = ytext . toDelta ( { renderer } )
console . log ( attrDelta . toJSON ( ) )
t . compare ( attrDelta , delta . create ( ) . insert ( 'hell' ) . insert ( 'a' , null , { delete : [ ] } ) . insert ( 'o' , null , { insert : [ ] } ) . insert ( ' world' ) . done ( ) )
}