// fixtures/poison.mjs — documents no op can produce: the malformed // shapes a peer could put into the CRDT by hand (box.x = "a", // background.kind = 5, fonts.body = [], an asset that is a string), // built directly with yjs. view.test.mjs runs the view over them and // core/src/view.rs's tests build the same shapes with yrs; run as a // script this writes each one as a Yjs v1 update into // internal/slidestest/testdata/poison/, which // TestPoisonUpdateDoesNotBrickTheDeck posts at the real server. // // Every document has a fixed guid and client id, so a re-run writes the // same bytes and the committed files only move when this file does. // Each update adds a slide of its own (or touches meta/assets), so it // applies cleanly to any deck. import * as Y from 'yjs' import { mkdirSync, writeFileSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) export const poisonDir = join(here, '..', 'internal', 'slidestest', 'testdata', 'poison') function slideWith (doc, id, fill) { const slide = new Y.Map() doc.getArray('slides').push([slide]) slide.set('id', id) slide.set('layout', 'canvas') slide.set('notes', new Y.XmlFragment()) const blocks = new Y.Array() slide.set('blocks', blocks) fill(slide, blocks) } function blockWith (blocks, id, fill) { const block = new Y.Map() blocks.push([block]) block.set('id', id) block.set('type', 'text') block.set('props', new Y.Map()) block.set('phone', new Y.Map()) block.set('z', 0) block.set('text', new Y.XmlFragment()) fill(block) } /** The poison shapes, name → a fill function over a fresh Y.Doc. */ export const shapes = { // A canvas block whose box is not four numbers. box: doc => slideWith(doc, 'poisonboxsld', (slide, blocks) => { blockWith(blocks, 'poisonboxblk', block => block.set('box', { x: 'a', y: 0, w: 1, h: 1 })) }), // A slide whose background kind is a number. background: doc => slideWith(doc, 'poisonbgsld1', slide => { const bg = new Y.Map() slide.set('background', bg) bg.set('kind', 5) bg.set('value', '') }), // meta.fonts with a body that is an array. fonts: doc => { doc.getMap('meta').set('fonts', { heading: 'Serif', body: [] }) }, // An assets entry that is a string, one with no ref, one with a // mistyped name. assets: doc => { const assets = doc.getMap('assets') assets.set('poisonstr', 'not a map') assets.set('poisonnoref', { name: 'x.png' }) assets.set('poisonbadnm', { ref: 'poisonref', name: 3, width: 'w', height: 9.9, alt: null }) }, // A phone order the view passes (it is a number) and the server's // typed reader cannot hold: what the parse gate in appendUpdate // exists for, over and above the view's own tolerance. magnitude: doc => slideWith(doc, 'poisonmagsld', (slide, blocks) => { blockWith(blocks, 'poisonmagblk', block => { block.set('region', 'body') block.get('phone').set('order', 1e300) }) }), // A run under a formatting attribute the schema does not define // (codex 3). The view reads the words fine, so the typed parse // passes; y-prosemirror 1.3.7 deletes the XmlText when it binds. The // gate refuses it by checking the shared text, not the projection // (core/src/check.rs). Last in this list, so the client ids of the // shapes before it — and their committed bytes — do not move. unknownmark: doc => slideWith(doc, 'poisonmrksld', (slide, blocks) => { blockWith(blocks, 'poisonmrkblk', block => { block.set('region', 'body') const p = new Y.XmlElement('paragraph') const t = new Y.XmlText() p.insert(0, [t]) block.get('text').insert(0, [p]) t.applyDelta([{ insert: 'keep ' }, { insert: 'these words', attributes: { unknown_mark: true } }]) }) }) } /** A fresh document holding one poison shape. */ export function poisonDoc (name) { const fill = shapes[name] if (!fill) throw new Error(`no poison shape ${name}`) const doc = new Y.Doc({ guid: `poison-${name}` }) doc.clientID = 900 + Object.keys(shapes).indexOf(name) doc.transact(() => fill(doc)) return doc } /** Every shape as a Yjs v1 update, name → bytes. */ export function poisonUpdates () { const out = {} for (const name of Object.keys(shapes)) out[name] = Y.encodeStateAsUpdate(poisonDoc(name)) return out } export function writePoison () { mkdirSync(poisonDir, { recursive: true }) for (const [name, bytes] of Object.entries(poisonUpdates())) { writeFileSync(join(poisonDir, `${name}.yjs`), Buffer.from(bytes)) console.log(`${name}: ${bytes.length} bytes`) } } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) writePoison()