test(git-sync): exhaustive converter coverage + fix 3 round-trip data-loss bugs
Coder↔reviewer design loop (9 rounds, reviewer verdict: exhaustive) produced 92 specs; implemented +123 tests (465 -> 588 passing). The new round-trip coverage exposed three genuine data-loss bugs in the Markdown<->ProseMirror converter, all now FIXED (round-trip is lossless for these): 1. pageBreak was lost on export (no converter case -> rendered to "" and the node vanished). Now emits <div data-type="pageBreak"></div>, which the schema parses back -> round-trips. 2. A block image between blocks left an empty <p> artifact after import-hoisting, producing a phantom blank-gap diff on every sync. markdownToProseMirror now strips content-less paragraphs after generateJSON — with a schema-validity guard that keeps the obligatory single empty paragraph in `content: "block+"` containers (tableCell/tableHeader/blockquote/column/callout/doc), so empty cells/quotes never become an invalid `content: []`. 3. The `code` mark combined with another mark was not byte-stable (emitted nested HTML that the schema's `code` `excludes:"_"` collapsed on import). The converter now emits code-only when `code` co-occurs, matching the editor. New coverage spans media/diagram/details/columns/math/mention attribute round-trips, converter emission branches, git error paths, and engine decision branches. A dedicated test pins the empty-container schema validity (the review catch on the bug-2 fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getSchema } from "@tiptap/core";
|
||||
|
||||
import { markdownToProseMirror } from "../src/lib/markdown-to-prosemirror";
|
||||
import { docmostExtensions } from "../src/lib/docmost-schema";
|
||||
|
||||
// REGRESSION LOCK for the stripEmptyParagraphs schema-validity guard.
|
||||
//
|
||||
// markdownToProseMirror removes empty `paragraph` nodes that the import leaves
|
||||
// behind when a block atom (e.g. a block image) is hoisted out of marked's
|
||||
// wrapping <p> — they cause phantom blank-gap diffs on every sync. But several
|
||||
// schema nodes REQUIRE non-empty block content (`content: "block+"`): tableCell,
|
||||
// tableHeader, blockquote, column, callout, and the doc root. For an empty one of
|
||||
// those, generateJSON materializes a single empty paragraph as its OBLIGATORY
|
||||
// content. Stripping that would produce a schema-INVALID doc (`content: []`),
|
||||
// which crashes any consumer that validates the public markdownToProseMirror
|
||||
// output via ProseMirror's Node.check() / nodeFromJSON. The guard keeps one empty
|
||||
// paragraph when removal would empty such a container; these tests pin that.
|
||||
|
||||
const schema = getSchema(docmostExtensions as any);
|
||||
|
||||
/** Throws if the JSON doc is not valid against the Docmost schema. */
|
||||
function assertSchemaValid(doc: unknown): void {
|
||||
schema.nodeFromJSON(doc).check();
|
||||
}
|
||||
|
||||
describe("stripEmptyParagraphs keeps the import schema-valid", () => {
|
||||
it("an empty GFM table cell round-trips to a schema-valid doc", async () => {
|
||||
const doc = await markdownToProseMirror(
|
||||
"| a | |\n|---|---|\n| x | y |\n",
|
||||
);
|
||||
expect(() => assertSchemaValid(doc)).not.toThrow();
|
||||
});
|
||||
|
||||
it("an empty blockquote stays schema-valid", async () => {
|
||||
const doc = await markdownToProseMirror("> \n");
|
||||
expect(() => assertSchemaValid(doc)).not.toThrow();
|
||||
});
|
||||
|
||||
it("an empty document stays schema-valid", async () => {
|
||||
const doc = await markdownToProseMirror("\n\n");
|
||||
expect(() => assertSchemaValid(doc)).not.toThrow();
|
||||
});
|
||||
|
||||
it("still removes the empty hoist-artifact paragraph beside a block image", async () => {
|
||||
const doc = await markdownToProseMirror("p\n\n\n\nq\n");
|
||||
const emptyParas = ((doc as { content?: any[] }).content ?? []).filter(
|
||||
(n: any) =>
|
||||
n.type === "paragraph" &&
|
||||
(!Array.isArray(n.content) || n.content.length === 0),
|
||||
);
|
||||
// The artifact paragraph must be gone (no phantom blank-gap on re-export)...
|
||||
expect(emptyParas).toHaveLength(0);
|
||||
// ...and the result is still a valid doc.
|
||||
expect(() => assertSchemaValid(doc)).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user