test(converter): value-fuzz the deferred media attributes (#351)
Item 4 of the #351 remainder — implement value-fuzz for the media dimension/family attributes previously parked in the flat property suite's ATTR_VALUE_FUZZ_ALLOWLIST as "round-trip candidates deferred to a later PR": width/height/align/aspectRatio/size/caption/title/alt across image, video, youtube, pdf, drawio, excalidraw, embed. Each gets a non-default-value arbitrary in attr-arbitraries.ts and is removed from the allowlist; the P1 (semantic) + P2 (byte-stability) property gate now exercises them. All round-trip green — the comment-JSON serialization (stable key order, String()-stringified) carries every one faithfully, so no new pins or accepted-limitations were needed. embed.width/height are fuzzed as numeric STRINGS (not numbers): a non-default embed dimension round-trips through the comment JSON as a string (the numeric 800/600 default is still omitted/re-materialized), so authoring a number diverged under P1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,11 @@ interface AttrPolicy {
|
||||
const num = (...xs: number[]) => fc.constantFrom(...xs);
|
||||
const str = (...xs: string[]) => fc.constantFrom(...xs);
|
||||
const widthStr = str('120', '320', '640');
|
||||
// Media `aspectRatio`/`size` are stringified numerics too (converter emits
|
||||
// String(value)); the schema parseHTML reads them back as strings. Fuzz as
|
||||
// plausible numeric strings so they round-trip byte-stably like widthStr.
|
||||
const aspectRatioStr = str('1.5', '0.75', '1');
|
||||
const sizeStr = str('320', '640');
|
||||
|
||||
// The documented override table, keyed `type.attr`. Every entry is grounded in
|
||||
// the empirical converter probe (see flat-roundtrip.property.test.ts header).
|
||||
@@ -149,16 +154,57 @@ const OVERRIDES: Record<string, AttrPolicy> = {
|
||||
'image.title': { arb: letterPhraseArb },
|
||||
'image.width': { arb: widthStr, degen: '' },
|
||||
'image.height': { arb: widthStr, degen: '' },
|
||||
// #351 (this PR): media sizing/family attrs ride in the `<!--img {…}-->`
|
||||
// comment JSON via String(value); parseHTML reads them back as strings, so a
|
||||
// numeric string round-trips byte-stably (mirrors image.width).
|
||||
'image.size': { arb: sizeStr, degen: '' },
|
||||
'image.aspectRatio': { arb: aspectRatioStr },
|
||||
// caption is text carried verbatim in the comment JSON (mirrors image.alt).
|
||||
'image.caption': { arb: letterPhraseArb, degen: '' },
|
||||
'video.src': { noDefault: true, arb: urlArb, degen: '' },
|
||||
'video.alt': { arb: letterPhraseArb },
|
||||
'video.width': { arb: widthStr },
|
||||
'video.height': { arb: widthStr },
|
||||
// #351 (this PR): video sizing/align ride in the `<!--video {…}-->` comment.
|
||||
'video.size': { arb: sizeStr },
|
||||
'video.aspectRatio': { arb: aspectRatioStr },
|
||||
// align default "center" is dropped on export; fuzz non-center only.
|
||||
'video.align': { arb: str('left', 'right') },
|
||||
'audio.src': { noDefault: true, arb: urlArb, degen: '' },
|
||||
'youtube.src': { noDefault: true, arb: urlArb },
|
||||
// #351 (this PR): youtube width/height/align ride in the `<!--youtube {…}-->`
|
||||
// comment JSON (String()-coerced dimensions, non-center align only).
|
||||
'youtube.width': { arb: widthStr },
|
||||
'youtube.height': { arb: widthStr },
|
||||
'youtube.align': { arb: str('left', 'right') },
|
||||
'pdf.src': { noDefault: true, arb: urlArb },
|
||||
'pdf.name': { arb: phraseArb },
|
||||
// #351 (this PR): pdf size/width/height ride in the `<!--pdf {…}-->` comment
|
||||
// (String()-coerced dimensions, read back as strings).
|
||||
'pdf.size': { arb: sizeStr },
|
||||
'pdf.width': { arb: widthStr },
|
||||
'pdf.height': { arb: widthStr },
|
||||
'drawio.src': { noDefault: true, arb: urlArb },
|
||||
// #351 (this PR): drawio family attrs ride in the `<!--drawio {…}-->` comment.
|
||||
// Dimensions/size/aspectRatio are String()-coerced numeric strings; title/alt
|
||||
// are text carried verbatim; align fuzzed non-center only.
|
||||
'drawio.width': { arb: widthStr },
|
||||
'drawio.height': { arb: widthStr },
|
||||
'drawio.size': { arb: sizeStr },
|
||||
'drawio.aspectRatio': { arb: aspectRatioStr },
|
||||
'drawio.align': { arb: str('left', 'right') },
|
||||
'drawio.title': { arb: letterPhraseArb },
|
||||
'drawio.alt': { arb: letterPhraseArb },
|
||||
'excalidraw.src': { noDefault: true, arb: urlArb },
|
||||
// #351 (this PR): excalidraw family attrs ride in the `<!--excalidraw {…}-->`
|
||||
// comment (same shape as the drawio family above).
|
||||
'excalidraw.width': { arb: widthStr },
|
||||
'excalidraw.height': { arb: widthStr },
|
||||
'excalidraw.size': { arb: sizeStr },
|
||||
'excalidraw.aspectRatio': { arb: aspectRatioStr },
|
||||
'excalidraw.align': { arb: str('left', 'right') },
|
||||
'excalidraw.title': { arb: letterPhraseArb },
|
||||
'excalidraw.alt': { arb: letterPhraseArb },
|
||||
'attachment.url': { noDefault: true, arb: urlArb },
|
||||
'attachment.name': { arb: phraseArb },
|
||||
// ── callout / status ─────────────────────────────────────────────────────
|
||||
@@ -201,8 +247,19 @@ const OVERRIDES: Record<string, AttrPolicy> = {
|
||||
// ── embed (schema keeps width/height NUMERIC, not string-coerced) ─────────
|
||||
'embed.src': { noDefault: true, arb: urlArb, degen: '' },
|
||||
'embed.provider': { noDefault: true, arb: str('iframe', 'youtube', 'vimeo') },
|
||||
'embed.width': { always: true, frozen: true },
|
||||
'embed.height': { always: true, frozen: true },
|
||||
// #351 (this PR): the embed schema defaults width/height to the NUMBERS 800/600
|
||||
// and the converter only emits them when they differ. But the value round-trips
|
||||
// as a STRING: export stringifies into the comment JSON (String(width)) and the
|
||||
// import path (embedToHtml -> data-width -> embed parseHTML) reads it back as a
|
||||
// string, so an authored NUMBER 400 would diverge under P1 (400 vs "400"). Fuzz
|
||||
// as numeric STRINGS avoiding "800"/"600" so they round-trip byte-stably. The
|
||||
// 800/600 numeric default state still round-trips (omitted on export, re-
|
||||
// materialized as the numeric default). `always` stays because these are
|
||||
// materialized on import but absent from canonicalize's KNOWN_DEFAULTS.
|
||||
'embed.width': { always: true, arb: str('400', '1000', '1200') },
|
||||
'embed.height': { always: true, arb: str('300', '500', '900') },
|
||||
// align default "center" is dropped on export; fuzz non-center only.
|
||||
'embed.align': { arb: str('left', 'right') },
|
||||
// ── subpages / math / htmlEmbed ──────────────────────────────────────────
|
||||
'subpages.recursive': { always: true, arb: fc.constant(true) }, // boolean, default false
|
||||
'mathBlock.text': { noDefault: true, arb: str('x^2', 'a < b', '\\frac{1}{2}'), degen: '' },
|
||||
|
||||
@@ -28,32 +28,32 @@ import {
|
||||
// a NEW attribute (or a newly-frozen one) that lands in this bucket flips the
|
||||
// snapshot test red and forces a reviewer to classify it. Each belongs to one of:
|
||||
// - internal/opaque ids & placeholders (attachmentId, slugId, placeholder,
|
||||
// creatorId, anchorId) — no meaningful non-default to assert;
|
||||
// - dimensions/among the media family with no standalone md form here
|
||||
// (aspectRatio, size, caption, drawio/excalidraw/pdf/video/youtube w/h/align)
|
||||
// — round-trip candidates deferred to a later PR, not silently dropped;
|
||||
// creatorId, anchorId, mime) — no meaningful non-default to assert. These stay
|
||||
// frozen: their value is an opaque token carried verbatim, not a round-trip
|
||||
// shape worth fuzzing;
|
||||
// - ACCEPTED limitations with no md representation (indent, callout.icon,
|
||||
// orderedList.type, table spans/bg/colwidth).
|
||||
// The media dimension/family attrs (image/video/youtube/drawio/excalidraw/pdf/
|
||||
// embed width/height/align/size/aspectRatio/caption/title/alt) that were
|
||||
// previously "deferred to a later PR" are IMPLEMENTED (value-fuzzed) in THIS PR
|
||||
// via the OVERRIDES table in attr-arbitraries.ts — they ride in the discriminator
|
||||
// comment JSON and round-trip byte-stably, so they are no longer allowlisted.
|
||||
const ATTR_VALUE_FUZZ_ALLOWLIST = new Set<string>([
|
||||
'attachment.attachmentId', 'attachment.mime', 'attachment.placeholder', 'attachment.size',
|
||||
'audio.attachmentId', 'audio.placeholder', 'audio.size',
|
||||
'callout.icon',
|
||||
'drawio.align', 'drawio.alt', 'drawio.aspectRatio', 'drawio.attachmentId',
|
||||
'drawio.height', 'drawio.size', 'drawio.title', 'drawio.width',
|
||||
'embed.align', 'embed.height', 'embed.width',
|
||||
'excalidraw.align', 'excalidraw.alt', 'excalidraw.aspectRatio', 'excalidraw.attachmentId',
|
||||
'excalidraw.height', 'excalidraw.size', 'excalidraw.title', 'excalidraw.width',
|
||||
'drawio.attachmentId',
|
||||
'excalidraw.attachmentId',
|
||||
'heading.indent',
|
||||
'image.aspectRatio', 'image.attachmentId', 'image.caption', 'image.placeholder', 'image.size',
|
||||
'image.attachmentId', 'image.placeholder',
|
||||
'mention.anchorId', 'mention.creatorId', 'mention.slugId',
|
||||
'orderedList.type', 'paragraph.indent',
|
||||
'pdf.attachmentId', 'pdf.height', 'pdf.placeholder', 'pdf.size', 'pdf.width',
|
||||
'pdf.attachmentId', 'pdf.placeholder',
|
||||
'tableCell.backgroundColor', 'tableCell.backgroundColorName', 'tableCell.colspan',
|
||||
'tableCell.colwidth', 'tableCell.rowspan',
|
||||
'tableHeader.backgroundColor', 'tableHeader.backgroundColorName', 'tableHeader.colspan',
|
||||
'tableHeader.colwidth', 'tableHeader.rowspan',
|
||||
'video.align', 'video.aspectRatio', 'video.attachmentId', 'video.placeholder', 'video.size',
|
||||
'youtube.align', 'youtube.height', 'youtube.width',
|
||||
'video.attachmentId', 'video.placeholder',
|
||||
]);
|
||||
|
||||
// ── MARK attribute-value coverage ───────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user