fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123) - #3287
Conversation
…ge writes (SD-3123) Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed through editor.commands.updateStructuredContentById, which dispatches tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The structured-content lock plugin's filterTransaction reads that as wrapper damage and silently filters the transaction on sdtLocked controls - producing false-success no-ops on operations the API allows (text.setValue, replaceContent, appendContent, prependContent, setLockMode). Two engine changes: - applyAttrsUpdate (sdt-properties-write.ts) - switch from editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute. AttrSteps have no from/to range and are explicitly skipped by the lock plugin's filterTransaction. This is the path that patch, setLockMode, patchRawProperties, setType, and the date/binding/multiline family use. - replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps as wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked / sdtContentLocked (both per spec). The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from target-resolution.ts) so it resolves the same nodes the upstream resolveSdtByTarget would resolve. documentSection / documentPartObject are intentionally excluded - they have their own write paths and could collide on id. No changes to API-level lock guards (assertNotSdtLocked on patch, setType, setBinding, etc.) - relaxing those is a separate behavior decision and out of scope here. Tests: - 4 new regression tests for sdtLocked (setLockMode, text.setValue, text.clearValue, replaceContent) that assert the actual inner-range positions (pos+1, pos+nodeSize-1), not just call counts. - 11 existing tests migrated from "expect updateStructuredContentById called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete". - 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in conformance (no synthetic-failure mode after this refactor, matching the existing precedent for wrap/unwrap/etc.). All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398 document-api. Zero new TypeScript errors. Related: SD-3131, SD-3139, IT-1046.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
* fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123) Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed through editor.commands.updateStructuredContentById, which dispatches tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The structured-content lock plugin's filterTransaction reads that as wrapper damage and silently filters the transaction on sdtLocked controls - producing false-success no-ops on operations the API allows (text.setValue, replaceContent, appendContent, prependContent, setLockMode). Two engine changes: - applyAttrsUpdate (sdt-properties-write.ts) - switch from editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute. AttrSteps have no from/to range and are explicitly skipped by the lock plugin's filterTransaction. This is the path that patch, setLockMode, patchRawProperties, setType, and the date/binding/multiline family use. - replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps as wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked / sdtContentLocked (both per spec). The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from target-resolution.ts) so it resolves the same nodes the upstream resolveSdtByTarget would resolve. documentSection / documentPartObject are intentionally excluded - they have their own write paths and could collide on id. No changes to API-level lock guards (assertNotSdtLocked on patch, setType, setBinding, etc.) - relaxing those is a separate behavior decision and out of scope here. Tests: - 4 new regression tests for sdtLocked (setLockMode, text.setValue, text.clearValue, replaceContent) that assert the actual inner-range positions (pos+1, pos+nodeSize-1), not just call counts. - 11 existing tests migrated from "expect updateStructuredContentById called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete". - 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in conformance (no synthetic-failure mode after this refactor, matching the existing precedent for wrap/unwrap/etc.). All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398 document-api. Zero new TypeScript errors. Related: SD-3131, SD-3139, IT-1046. * test(content-controls): add clearContent sdtLocked regression test (SD-3123) * test(behavior): document-api locked SDT mutations (SD-3144) Real-editor regression coverage for SD-3123 (#3287). The engine fix was verified at the unit-test level (mock transaction shapes: AttrSteps for metadata, inner-range ReplaceSteps for content), but mocks don't run the real structured-content lock plugin's filterTransaction. This spec closes that gap by exercising the customer surface (editor.doc.contentControls.*) in a real browser with the real lock plugin loaded. Five tests: - text.setValue updates an sdtLocked inline plain-text control - replaceContent updates an sdtLocked block rich-text control - clearContent empties an sdtLocked block control (wrapper preserved) - round-trip sdtLocked → unlocked → text.setValue (lockMode AttrStep writes) - contentLocked + text.setValue returns LOCK_VIOLATION (API guard fires) Lives in tests/behavior/tests/sdt/document-api-locked-mutations.spec.ts. Does NOT un-fixme the older sdt-lock-modes.spec.ts (different concern — that one exercises editor.commands.*, SD-3123 is about the Document API). 5/5 pass on chromium. Stacked on SD-3123 (#3287).
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.93 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.137 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.139 |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.108 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.30.0-next.88 The release is available on GitHub release |
…ge writes (SD-3123) (superdoc-dev#3287) * fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123) Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed through editor.commands.updateStructuredContentById, which dispatches tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The structured-content lock plugin's filterTransaction reads that as wrapper damage and silently filters the transaction on sdtLocked controls - producing false-success no-ops on operations the API allows (text.setValue, replaceContent, appendContent, prependContent, setLockMode). Two engine changes: - applyAttrsUpdate (sdt-properties-write.ts) - switch from editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute. AttrSteps have no from/to range and are explicitly skipped by the lock plugin's filterTransaction. This is the path that patch, setLockMode, patchRawProperties, setType, and the date/binding/multiline family use. - replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps as wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked / sdtContentLocked (both per spec). The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from target-resolution.ts) so it resolves the same nodes the upstream resolveSdtByTarget would resolve. documentSection / documentPartObject are intentionally excluded - they have their own write paths and could collide on id. No changes to API-level lock guards (assertNotSdtLocked on patch, setType, setBinding, etc.) - relaxing those is a separate behavior decision and out of scope here. Tests: - 4 new regression tests for sdtLocked (setLockMode, text.setValue, text.clearValue, replaceContent) that assert the actual inner-range positions (pos+1, pos+nodeSize-1), not just call counts. - 11 existing tests migrated from "expect updateStructuredContentById called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete". - 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in conformance (no synthetic-failure mode after this refactor, matching the existing precedent for wrap/unwrap/etc.). All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398 document-api. Zero new TypeScript errors. Related: SD-3131, SD-3139, IT-1046. * test(content-controls): add clearContent sdtLocked regression test (SD-3123)
…-dev#3291) * fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123) Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed through editor.commands.updateStructuredContentById, which dispatches tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The structured-content lock plugin's filterTransaction reads that as wrapper damage and silently filters the transaction on sdtLocked controls - producing false-success no-ops on operations the API allows (text.setValue, replaceContent, appendContent, prependContent, setLockMode). Two engine changes: - applyAttrsUpdate (sdt-properties-write.ts) - switch from editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute. AttrSteps have no from/to range and are explicitly skipped by the lock plugin's filterTransaction. This is the path that patch, setLockMode, patchRawProperties, setType, and the date/binding/multiline family use. - replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps as wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked / sdtContentLocked (both per spec). The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from target-resolution.ts) so it resolves the same nodes the upstream resolveSdtByTarget would resolve. documentSection / documentPartObject are intentionally excluded - they have their own write paths and could collide on id. No changes to API-level lock guards (assertNotSdtLocked on patch, setType, setBinding, etc.) - relaxing those is a separate behavior decision and out of scope here. Tests: - 4 new regression tests for sdtLocked (setLockMode, text.setValue, text.clearValue, replaceContent) that assert the actual inner-range positions (pos+1, pos+nodeSize-1), not just call counts. - 11 existing tests migrated from "expect updateStructuredContentById called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete". - 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in conformance (no synthetic-failure mode after this refactor, matching the existing precedent for wrap/unwrap/etc.). All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398 document-api. Zero new TypeScript errors. Related: SD-3131, SD-3139, IT-1046. * test(content-controls): add clearContent sdtLocked regression test (SD-3123) * test(behavior): document-api locked SDT mutations (SD-3144) Real-editor regression coverage for SD-3123 (superdoc-dev#3287). The engine fix was verified at the unit-test level (mock transaction shapes: AttrSteps for metadata, inner-range ReplaceSteps for content), but mocks don't run the real structured-content lock plugin's filterTransaction. This spec closes that gap by exercising the customer surface (editor.doc.contentControls.*) in a real browser with the real lock plugin loaded. Five tests: - text.setValue updates an sdtLocked inline plain-text control - replaceContent updates an sdtLocked block rich-text control - clearContent empties an sdtLocked block control (wrapper preserved) - round-trip sdtLocked → unlocked → text.setValue (lockMode AttrStep writes) - contentLocked + text.setValue returns LOCK_VIOLATION (API guard fires) Lives in tests/behavior/tests/sdt/document-api-locked-mutations.spec.ts. Does NOT un-fixme the older sdt-lock-modes.spec.ts (different concern — that one exercises editor.commands.*, SD-3123 is about the Document API). 5/5 pass on chromium. Stacked on SD-3123 (superdoc-dev#3287).
|
🎉 This PR is included in superdoc-sdk v1.10.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.6.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.34.0 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.5.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.6.0 |
Per ECMA-376,
sdtLockedprotects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed througheditor.commands.updateStructuredContentById, which dispatchestr.replaceWith(pos, pos + node.nodeSize, ...)spanning the entire SDT. The structured-content lock plugin'sfilterTransactionreads that as wrapper damage and silently filters the transaction onsdtLockedcontrols — producing false-success no-ops on operations the API allows (text.setValue,replaceContent,appendContent,prependContent,setLockMode).Two engine changes:
applyAttrsUpdate(sdt-properties-write.ts) — switch fromeditor.commands.updateStructuredContentByIdto per-keytr.setNodeAttribute. AttrSteps have nofrom/torange and are explicitly skipped by the lock plugin'sfilterTransaction. This is the path thatpatch,setLockMode,patchRawProperties,setType, and thedate/setBinding/text.setMultilinefamily use.replaceSdtTextContent(content-controls-wrappers.ts) — replace the SDT's inner content range (pos+1topos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps aswouldModifyContent: allowed onsdtLocked(per spec, the wrapper is protected but content is editable).contentLockedandsdtContentLockedcontinue to reject content edits at the wrapper API boundary via the existingassertNotContentLockedguards — adapter-level guard coverage for those is a follow-up.The search in
applyAttrsUpdateusesSDT_NODE_NAMESimported fromtarget-resolution.tsso it resolves the same nodes the upstreamresolveSdtByTargetwould resolve.documentSection/documentPartObjectare intentionally excluded — they have their own write paths and could collide onidif both an SDT and a section share an identifier.No changes to API-level lock guards (
assertNotSdtLockedonpatch,setType,setBinding, etc.) — relaxing those is a separate behavior decision and out of scope here.Tests:
sdtLocked(setLockMode,text.setValue,text.clearValue,replaceContent,clearContent) that assert the actual inner-range positions (pos+1,pos+nodeSize-1), not just call counts. A full-wrapperreplaceWithwould still fail these.updateStructuredContentByIdcalled" to "expecttr.setNodeAttribute/tr.replaceWith/tr.deleteon inner range".CC_DIRECT_DISPATCH_OPSin conformance (no synthetic-failure mode after this refactor, matching the existing precedent forwrap/unwrap/etc.).Related: SD-3131, SD-3139, IT-1046.