// 🤖 The composer serialization contract (issue #890): the vectors in // test/event-vectors/composer-serialize.json pin the wire bytes the WEB // serializer (composer-md.js, the authority) emits for each composer doc // shape, so the native twin (ComposerSerialize.swift, run by // ComposerSerializeTests over the byte-identical fixture) can never drift. // node test/composer-vectors.test.mjs import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { composerSchema as schema, serializeDoc } from "../web/static/composer-md.js"; import { parseMessage } from "../web/static/markdown.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); let failures = 0; async function test(name, fn) { try { await fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } // The tree vocabulary (test/event-vectors/markdown.json's) → a ProseMirror // doc over the composer schema. Inline nesting becomes mark arrays; a br // inside a styled wrapper carries the wrapper's marks (the marked-break case). function inlineNodes(children, marks = []) { const out = []; for (const n of children) { if (n.t === "text") out.push(schema.text(n.text, marks)); else if (n.t === "br") out.push(schema.nodes.hard_break.create(null, null, marks)); else if (n.t === "code") out.push(schema.text(n.text, [...marks, schema.marks.code.create()])); else if (n.t === "strong" || n.t === "em") out.push(...inlineNodes(n.children, [...marks, schema.marks[n.t].create()])); else if (n.t === "strike") out.push(...inlineNodes(n.children, [...marks, schema.marks.strike.create()])); else if (n.t === "link") out.push(...inlineNodes(n.children, [...marks, schema.marks.link.create({ href: n.href })])); else throw new Error("unknown inline " + n.t); } return out; } function blockNode(b) { if (b.t === "p") return schema.nodes.paragraph.create(null, inlineNodes(b.children)); if (b.t === "quote") return schema.nodes.blockquote.create(null, b.children.map(blockNode)); if (b.t === "ul") return schema.nodes.bullet_list.create(null, b.items.map((item) => schema.nodes.list_item.create(null, item.map(blockNode)))); if (b.t === "ol") return schema.nodes.ordered_list.create({ order: b.start }, b.items.map((item) => schema.nodes.list_item.create(null, item.map(blockNode)))); if (b.t === "codeblock") return schema.nodes.code_block.create({ params: b.lang }, b.text ? [schema.text(b.text)] : []); throw new Error("unknown block " + b.t); } export function docFromTree(tree) { return schema.nodes.doc.create(null, tree.map(blockNode)); } const canonical = readFileSync(join(__dirname, "event-vectors", "composer-serialize.json"), "utf8"); const { vectors } = JSON.parse(canonical); await test("vector file has substantial coverage", () => { assert.ok(vectors.length >= 40, `only ${vectors.length} vectors`); }); for (const v of vectors) { await test(`serialize: ${v.name}`, () => { assert.equal(serializeDoc(docFromTree(v.tree)), v.md, v.note || ""); }); await test(`reparse: ${v.name}`, () => { assert.deepEqual(parseMessage(v.md), v.reparse ?? v.tree); }); } await test("ios fixture copy is byte-identical to canonical", () => { const bundled = readFileSync( join(__dirname, "..", "ios", "LChatCore", "Tests", "LChatCoreTests", "Fixtures", "composer-serialize-vector.json"), "utf8" ); assert.strictEqual( bundled, canonical, "run cp test/event-vectors/composer-serialize.json ios/LChatCore/Tests/LChatCoreTests/Fixtures/composer-serialize-vector.json" ); }); if (failures) { console.error(`\ncomposer-vectors: ${failures} test(s) failed`); process.exit(1); } console.log("\ncomposer-vectors: all tests passed");