Files
gitmost/packages/prosemirror-markdown/test/generative/counterexamples.test.ts
T
agent_coder 54f0ba681e fix(converter): preserve orderedList start; fuzz column.width numerically (#351)
Item 3 of the #351 remainder — resolve the two pinned converter
counterexamples, fixtures-first.

orderedList.start (genuine P1 loss): the converter always emitted "1."
and dropped attrs.start. Now the markdown path emits `${start + index}.`
(Number-coerced, guards a stray non-numeric start) and the raw-HTML path
emits `<ol start="N">` when start>1; a default (start=1) list is
byte-unchanged. tiptap StarterKit reads both forms back. Un-pinned as a
passing regression test; orderedList.start is now value-fuzzed.

column.width: the "50% churn" counterexample rested on a false premise —
the canonical editor (editor-ext column.ts) stores width as a unitless
flex-grow NUMBER (parseFloat, `flex:${width}`), never a "%" string, and
docmost-schema.ts is a vendored mirror that MUST match it. The original
parseFloat was correct parity and already byte-stable for numeric widths.
Reverted the mirror to parseFloat, deleted the fabricated counterexample
fixture, and now value-fuzz column.width as a number (real type). No src
behaviour change for column.width — parity with editor-ext preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:51:53 +03:00

47 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { convertProseMirrorToMarkdown } from '../../src/lib/markdown-converter.js';
import { markdownToProseMirror } from '../../src/lib/markdown-to-prosemirror.js';
import { docsCanonicallyEqual } from '../../src/lib/canonicalize.js';
// ---------------------------------------------------------------------------
// #351 committed counterexample — a REAL round-trip bug surfaced by the flat
// generative probing (attribute level). The bug below is now FIXED in src/
// (maintainer-approved), so this case is a permanent PASSING regression pin:
// the exact document that once lost data now round-trips cleanly, and the
// fixture stays in test/fixtures/counterexamples/ forever (per the epic
// guardrail) to guard against a re-regression. The case carries a loud
// `// BUG #351 (FIXED):` note recording the defect and its fix site.
//
// With the bug fixed, the offending attribute is now VALUE-FUZZED by the
// generator (see attr-arbitraries.ts: orderedList.start), not held out — this
// committed document is the minimal, human-readable pin.
// ---------------------------------------------------------------------------
const here = path.dirname(fileURLToPath(import.meta.url));
const fixtureDir = path.resolve(here, '../fixtures/counterexamples');
function loadDoc(file: string): any {
return JSON.parse(readFileSync(path.join(fixtureDir, file), 'utf8')).doc;
}
describe('#351 counterexamples (round-trip bugs, now FIXED — permanent regression pins)', () => {
// BUG #351 (FIXED): an `orderedList` with a non-1 `start` lost its start
// number. CommonMark CAN express this ("5." starts the list at 5), but the
// converter always emitted "1." and ignored `attrs.start`:
// doc.start = 5 -> md1 = "1. alpha" (start dropped on export)
// re-import stored start = 1 => docsCanonicallyEqual(rt, doc) === false
// A P1 (semantic round-trip) loss of the SAME class as column.width. FIXED in
// this PR in src/lib/markdown-converter.ts (the orderedList markdown path emits
// `${start + index}.`; the raw-HTML path emits `<ol start="N">`). tiptap's
// StarterKit / marked parse the start back, so it round-trips. Permanent P1 pin.
it('ordered list start number is preserved (P1)', async () => {
const doc = loadDoc('ordered-list-start.json');
const md1 = convertProseMirrorToMarkdown(doc);
const doc2 = await markdownToProseMirror(md1);
expect(docsCanonicallyEqual(doc2, doc)).toBe(true);
});
});