@@ -442,3 +442,32 @@ export const testToDeltaDeepEmitsNoDeleteOpsForSoftDeletedParent = _tc => {
442442 `toDeltaDeep(am) emitted ${ offenders . length } forbidden delete op(s) for a soft-deleted parent (issue #247 / y-prosemirror)`
443443 )
444444}
445+
446+ /**
447+ * Companion to the cascade case above: an explicit `deleteAttr` under a diff
448+ * AM must also surface as a positive `SetAttrOp` carrying the prior value and
449+ * `{ delete: [] }` attribution, never as a `DeleteAttrOp`. The same contract
450+ * applies whether the attribute deletion came from a parent-cascade or from
451+ * a direct `ytype.deleteAttr(key)` call.
452+ *
453+ * @param {t.TestCase } _tc
454+ */
455+ export const testToDeltaDeepRendersExplicitDeleteAttrAsSetAttrWithAttribution = _tc => {
456+ const ydocV1 = new Y . Doc ( { gc : false } )
457+ ydocV1 . get ( 'p' ) . setAttr ( 'id' , 'C' )
458+ const ydoc = new Y . Doc ( { gc : false } )
459+ Y . applyUpdate ( ydoc , Y . encodeStateAsUpdate ( ydocV1 ) )
460+ ydoc . transact ( ( ) => {
461+ ydoc . get ( 'p' ) . deleteAttr ( 'id' )
462+ } )
463+ const am = Y . createAttributionManagerFromDiff ( ydocV1 , ydoc )
464+ const rendered = ydoc . get ( 'p' ) . toDeltaDeep ( am )
465+
466+ const offenders = collectForbiddenOps ( rendered , [ 'DeleteAttrOp' , 'DeleteOp' ] )
467+ t . assert ( offenders . length === 0 , 'no DeleteAttrOp / DeleteOp from explicit deleteAttr under diff AM' )
468+ t . compare (
469+ rendered . toJSON ( ) . attrs ,
470+ { id : { type : 'insert' , value : 'C' , attribution : { delete : [ ] } } } ,
471+ 'explicit deleteAttr surfaces as SetAttrOp with prior value and delete attribution'
472+ )
473+ }
0 commit comments