35f2c06f42
DO-1 (regression): the orderedList start hardening `Number(x)||1` let a
negative/fractional start through into the marker ("-3.", "2.5.") — marked
can't tokenize it, so the whole list re-imported as a paragraph (structure
corruption); start=0 churned. Both the markdown and raw-HTML export paths
now use `Number.isInteger(raw) && raw > 1 ? raw : 1`, so any degenerate
start collapses to the default "1." markers / bare <ol> (list always valid).
New ordered-list-start-normalization test pins {0,-3,2.5} → valid start=1
list, byte-stable; the round-trip fuzz keeps to integers >=2 (num 2,3,5,42).
DO-2 (nightly was non-functional): NUM_RUNS=5000 OOM'd the worker and the
crash was misreported as a "counterexample" issue whose prefix-dedup then
locked out all future issues. Reworked to shard 8 fresh vitest processes
(600 runs each, distinct seeds, --max-old-space-size) so deep fuzzing
never OOMs; a failing shard's output is preserved, and issue creation
discriminates a real fast-check counterexample from an infra/OOM failure
(distinct titles + scoped dedup). The two issue steps use `always() &&`
so they actually run on the failure path.
DO-3: envInt extracted to test/generative/env-int.ts + unit-tested.
DO-4: nightly dispatch inputs go through env: (no ${{ }} in run:).
DO-5: attr-arbitraries.ts docblock synced (column.width/orderedList.start
are fixed+fuzzed, not pinned it.fails).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { convertProseMirrorToMarkdown } from '../src/lib/markdown-converter.js';
|
|
import { markdownToProseMirror } from '../src/lib/markdown-to-prosemirror.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// #351 regression — DEGENERATE orderedList `start` normalization (DO-1).
|
|
//
|
|
// Only an INTEGER >= 2 is a valid explicit start. A degenerate start (0,
|
|
// negative, fractional, non-number) MUST collapse to the default 1 on export:
|
|
// emitting the raw value would produce an untokenizable marker (e.g. "-3.",
|
|
// "2.5.") that `marked` cannot parse, causing the WHOLE orderedList (and its
|
|
// listItems) to re-import as a PARAGRAPH — structural corruption. This test
|
|
// pins that each degenerate start:
|
|
// (1) exports to "1."/"2." markers (default numbering),
|
|
// (2) re-imports as a VALID `orderedList` (not a paragraph) with `start`
|
|
// defaulting, structure preserved,
|
|
// (3) is byte-stable (md1 === md2).
|
|
//
|
|
// Mutation check: revert the converter guard to `Number(raw) || 1` and this
|
|
// test goes RED (a fractional/negative start leaks into the marker).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const makeDoc = (start: unknown) => ({
|
|
type: 'doc',
|
|
content: [
|
|
{
|
|
type: 'orderedList',
|
|
attrs: { type: null, start },
|
|
content: [
|
|
{
|
|
type: 'listItem',
|
|
content: [
|
|
{ type: 'paragraph', content: [{ type: 'text', text: 'alpha' }] },
|
|
],
|
|
},
|
|
{
|
|
type: 'listItem',
|
|
content: [
|
|
{ type: 'paragraph', content: [{ type: 'text', text: 'beta' }] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
describe('#351 orderedList degenerate start normalization', () => {
|
|
for (const start of [0, -3, 2.5]) {
|
|
it(`start=${start} exports as default "1."/"2." markers`, () => {
|
|
const md = convertProseMirrorToMarkdown(makeDoc(start));
|
|
expect(md).toContain('1. alpha');
|
|
expect(md).toContain('2. beta');
|
|
// The degenerate value must NOT leak into any marker.
|
|
expect(md).not.toMatch(/-?\d*\.\d+\.\s/); // no fractional marker like "2.5."
|
|
expect(md).not.toContain('-3.');
|
|
});
|
|
|
|
it(`start=${start} re-imports as a valid orderedList (not a paragraph)`, async () => {
|
|
const md = convertProseMirrorToMarkdown(makeDoc(start));
|
|
const doc = (await markdownToProseMirror(md)) as any;
|
|
const top = doc.content?.[0];
|
|
expect(top?.type).toBe('orderedList');
|
|
// start defaults (never the degenerate value); tiptap materializes 1.
|
|
expect(top?.attrs?.start ?? 1).toBe(1);
|
|
expect(top?.content?.length).toBe(2);
|
|
});
|
|
|
|
it(`start=${start} is byte-stable across a re-export`, async () => {
|
|
const md1 = convertProseMirrorToMarkdown(makeDoc(start));
|
|
const doc = await markdownToProseMirror(md1);
|
|
const md2 = convertProseMirrorToMarkdown(doc);
|
|
expect(md2).toBe(md1);
|
|
});
|
|
}
|
|
});
|